mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-10 12:46:46 +00:00
Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 072ce54fe1 | |||
| 1f31df3a94 | |||
| 9ee30e4923 | |||
| 7b44f12ab1 | |||
| 874882b554 | |||
| 945bdf4c60 | |||
| 6e1e5a2afe | |||
| b709b51790 | |||
| d380485bb6 | |||
| 87c8228cd0 | |||
| 152046bf97 | |||
| a0fbed5859 | |||
| 89e9e17d26 | |||
| b595247392 | |||
| 9d50a8cef2 | |||
| f888176485 | |||
| 2f9876ad74 | |||
| 53404122cc | |||
| ba65494fce | |||
| 70645c1732 | |||
| 1055e60d27 | |||
| e04196f8a0 | |||
| 11a0dec047 | |||
| b9740e1b7d | |||
| f0695eb870 | |||
| a40018cdf9 | |||
| 5b7eabe21a | |||
| d5d60aa11b | |||
| a62fa132e6 | |||
| 052395f917 | |||
| 9a77650004 | |||
| f1d70361c9 | |||
| 3496599723 | |||
| 9e0635a6a4 | |||
| 10509621ce | |||
| 3727ecdfe5 | |||
| ac71d22e86 | |||
| bc81356d53 |
2
.gitattributes
vendored
2
.gitattributes
vendored
@ -1 +1 @@
|
||||
Changelog.md merge=union
|
||||
unreleased.md merge=union
|
||||
|
||||
@ -27,6 +27,9 @@ Issue tracker is ONLY used for reporting bugs with technical details. "It doesn'
|
||||
3.
|
||||
4.
|
||||
|
||||
## Version Information
|
||||
<!--- Which version of the app(s) were you using when you experienced this issue? -->
|
||||
|
||||
## Context (Environment)
|
||||
<!--- How has this issue affected you? What are you trying to accomplish? -->
|
||||
<!--- Providing context helps us come up with a solution that is most useful in the real world -->
|
||||
|
||||
56
Changelog.md
56
Changelog.md
@ -1,8 +1,55 @@
|
||||
# ProtonMail Bridge Changelog
|
||||
# ProtonMail Bridge and Import-Export app Changelog
|
||||
|
||||
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
|
||||
## Unreleased
|
||||
## [Bridge 1.5.2] Golden Gate
|
||||
|
||||
### Changed
|
||||
* GODT-883 Use `ClearPacket` for `text/plain` with signature
|
||||
|
||||
|
||||
## [Bridge 1.5.1] Golden Gate
|
||||
|
||||
### Added
|
||||
* GODT-701 Try load messages one-by-one if IMAP server errors with batch load
|
||||
and not interrupt the transfer.
|
||||
* GODT-878 Tests for send packet creation logic.
|
||||
|
||||
### Changed
|
||||
* GODT-180 Updated Sentry client.
|
||||
* GODT-651 Build creates proper binary names.
|
||||
* GODT-878 Fix an issue where the random session key is inadvertently sent to
|
||||
the Proton server. The data payload is always encrypted within TLS, but this
|
||||
is still a potential privacy problem. Discovered by Proton's internal
|
||||
security audit team.
|
||||
* GODT-878 Refactor and move the send packet creation logic to `pmapi.SendMessageReq`.
|
||||
* GODT-878 Encryption of session keys moved to pmapi.
|
||||
|
||||
|
||||
## [IE 1.2.1] Elbe
|
||||
|
||||
### Added
|
||||
* GODT-799 Skipped messages do not change total counts but shows as separate number.
|
||||
|
||||
## Fixed
|
||||
* GODT-799 Fix skipping unwanted folders importing from mbox files.
|
||||
* GODT-769 Close connection before deleting labels to prevent panics accessing deleted bucket.
|
||||
|
||||
### Removed
|
||||
* GODT-766 Remove GUI popup for IMAP TLS error.
|
||||
|
||||
|
||||
## [Bridge 1.5.0] Golden Gate
|
||||
|
||||
### Changed
|
||||
* Updated go-mbox dependency back to upstream.
|
||||
|
||||
### Fixed
|
||||
* GODT-847 Waiting for unilateral update during deleting the message.
|
||||
* GODT-849 Show in error counts in the end also lost messages.
|
||||
* GODT-835 Do not include conversation ID in references to show properly conversation threads in clients.
|
||||
* GODT-685 Improve deb packaging regarding dejavu font
|
||||
|
||||
|
||||
## [IE 1.2.0] Elbe
|
||||
|
||||
@ -26,6 +73,11 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* GODT-827 Do not spam sentry with bad ID by integration test.
|
||||
* GODT-700 Fix UTF-7 incompatibility.
|
||||
* GODT-837 Fix flaky TestFailUnpauseAndStops.
|
||||
* GODT-782 Don't use TLS pinning when checking connectivity status.
|
||||
|
||||
### Changed
|
||||
* TLS pins conform to official list.
|
||||
|
||||
|
||||
## [Bridge 1.4.5] Forth
|
||||
|
||||
|
||||
30
Makefile
30
Makefile
@ -10,19 +10,21 @@ 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.4.5-git
|
||||
IE_APP_VERSION?=1.2.0-git
|
||||
BRIDGE_APP_VERSION?=1.5.2-git
|
||||
IE_APP_VERSION?=1.2.1-git
|
||||
APP_VERSION:=${BRIDGE_APP_VERSION}
|
||||
SRC_ICO:=logo.ico
|
||||
SRC_ICNS:=Bridge.icns
|
||||
SRC_SVG:=logo.svg
|
||||
TGT_ICNS:=Bridge.icns
|
||||
EXE_NAME:=proton-bridge
|
||||
ifeq "${TARGET_CMD}" "Import-Export"
|
||||
APP_VERSION:=${IE_APP_VERSION}
|
||||
SRC_ICO:=ie.ico
|
||||
SRC_ICNS:=ie.icns
|
||||
SRC_SVG:=ie.svg
|
||||
TGT_ICNS:=ImportExport.icns
|
||||
EXE_NAME:=proton-ie
|
||||
endif
|
||||
REVISION:=$(shell git rev-parse --short=10 HEAD)
|
||||
BUILD_TIME:=$(shell date +%FT%T%z)
|
||||
@ -40,30 +42,34 @@ BUILD_FLAGS_NOGUI+= ${GO_LDFLAGS}
|
||||
|
||||
DEPLOY_DIR:=cmd/${TARGET_CMD}/deploy
|
||||
ICO_FILES:=
|
||||
EXE:=$(shell basename ${CURDIR})
|
||||
|
||||
DIRNAME:=$(shell basename ${CURDIR})
|
||||
EXE:=${EXE_NAME}
|
||||
EXE_QT:=${DIRNAME}
|
||||
ifeq "${TARGET_OS}" "windows"
|
||||
EXE:=${EXE}.exe
|
||||
EXE_QT:=${EXE_QT}.exe
|
||||
ICO_FILES:=${SRC_ICO} icon.rc icon_windows.syso
|
||||
endif
|
||||
ifeq "${TARGET_OS}" "darwin"
|
||||
DARWINAPP_CONTENTS:=${DEPLOY_DIR}/darwin/${EXE}.app/Contents
|
||||
EXE:=${EXE}.app/Contents/MacOS/${EXE}
|
||||
EXE:=${EXE}.app
|
||||
EXE_QT:=${EXE_QT}.app
|
||||
EXE_BINARY_DARWIN:=/Contents/MacOS/${EXE_NAME}
|
||||
endif
|
||||
EXE_TARGET:=${DEPLOY_DIR}/${TARGET_OS}/${EXE}
|
||||
EXE_QT_TARGET:=${DEPLOY_DIR}/${TARGET_OS}/${EXE_QT}
|
||||
|
||||
TGZ_TARGET:=bridge_${TARGET_OS}_${REVISION}.tgz
|
||||
ifeq "${TARGET_CMD}" "Import-Export"
|
||||
TGZ_TARGET:=ie_${TARGET_OS}_${REVISION}.tgz
|
||||
endif
|
||||
|
||||
|
||||
build: ${TGZ_TARGET}
|
||||
build-ie:
|
||||
TARGET_CMD=Import-Export $(MAKE) build
|
||||
|
||||
build-nogui:
|
||||
go build ${BUILD_FLAGS_NOGUI} -o ${TARGET_CMD} cmd/${TARGET_CMD}/main.go
|
||||
go build ${BUILD_FLAGS_NOGUI} -o ${EXE_NAME} cmd/${TARGET_CMD}/main.go
|
||||
|
||||
build-ie-nogui:
|
||||
TARGET_CMD=Import-Export $(MAKE) build-nogui
|
||||
@ -78,12 +84,16 @@ ${DEPLOY_DIR}/linux: ${EXE_TARGET}
|
||||
cp -pf ./Changelog.md ${DEPLOY_DIR}/linux/
|
||||
|
||||
${DEPLOY_DIR}/darwin: ${EXE_TARGET}
|
||||
if [ "${DIRNAME}" != "${EXE_NAME}" ]; then \
|
||||
mv ${EXE_TARGET}/Contents/MacOS/{${DIRNAME},${EXE_NAME}}; \
|
||||
perl -i -pe"s/>${DIRNAME}/>${EXE_NAME}/g" ${EXE_TARGET}/Contents/Info.plist; \
|
||||
fi
|
||||
cp ./internal/frontend/share/icons/${SRC_ICNS} ${DARWINAPP_CONTENTS}/Resources/${TGT_ICNS}
|
||||
cp LICENSE ${DARWINAPP_CONTENTS}/Resources/
|
||||
rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebEngine.framework"
|
||||
rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebView.framework"
|
||||
rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebEngineCore.framework"
|
||||
./utils/remove_non_relative_links_darwin.sh "${EXE_TARGET}"
|
||||
./utils/remove_non_relative_links_darwin.sh "${EXE_TARGET}${EXE_BINARY_DARWIN}"
|
||||
|
||||
${DEPLOY_DIR}/windows: ${EXE_TARGET}
|
||||
cp ./internal/frontend/share/icons/${SRC_ICO} ${DEPLOY_DIR}/windows/logo.ico
|
||||
@ -101,6 +111,7 @@ ${EXE_TARGET}: check-has-go gofiles ${ICO_FILES} update-vendor
|
||||
cp cmd/${TARGET_CMD}/main.go .
|
||||
qtdeploy ${BUILD_FLAGS} ${QT_BUILD_TARGET}
|
||||
mv deploy cmd/${TARGET_CMD}
|
||||
if [ "${EXE_QT_TARGET}" != "${EXE_TARGET}" ]; then mv ${EXE_QT_TARGET} ${EXE_TARGET}; fi
|
||||
rm -rf ${TARGET_OS} main.go
|
||||
|
||||
logo.ico ie.ico: ./internal/frontend/share/icons/${SRC_ICO}
|
||||
@ -197,7 +208,7 @@ coverage: test
|
||||
|
||||
mocks:
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/users Configer,PanicHandler,ClientManager,CredentialsStorer,StoreMaker > internal/users/mocks/mocks.go
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/transfer PanicHandler,ClientManager > internal/transfer/mocks/mocks.go
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/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/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
|
||||
@ -265,7 +276,6 @@ run-ie-qt:
|
||||
run-ie-nogui:
|
||||
TARGET_CMD=Import-Export $(MAKE) run-nogui
|
||||
|
||||
|
||||
clean-frontend-qt:
|
||||
$(MAKE) -C internal/frontend/qt -f Makefile.local clean
|
||||
clean-frontend-qt-ie:
|
||||
|
||||
11
go.mod
11
go.mod
@ -18,13 +18,13 @@ 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-vcard v0.0.0-20180326232728-33aaa0a0c8a5
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.0.1
|
||||
github.com/PuerkitoBio/goquery v1.5.1
|
||||
github.com/abiosoft/ishell v2.0.0+incompatible
|
||||
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect
|
||||
github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc
|
||||
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 // indirect
|
||||
github.com/chzyer/logex v1.1.10 // indirect
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
|
||||
github.com/cucumber/godog v0.8.1
|
||||
@ -35,14 +35,14 @@ require (
|
||||
github.com/emersion/go-imap-quota v0.0.0-20200423100218-dcfd1b7d2b41
|
||||
github.com/emersion/go-imap-specialuse v0.0.0-20200722111535-598ff00e4075
|
||||
github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26
|
||||
github.com/emersion/go-mbox v1.0.0
|
||||
github.com/emersion/go-mbox v1.0.2
|
||||
github.com/emersion/go-message v0.12.1-0.20200903165315-e1abe21f389a
|
||||
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21
|
||||
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe
|
||||
github.com/emersion/go-vcard v0.0.0-20190105225839-8856043f13c5 // indirect
|
||||
github.com/fatih/color v1.9.0
|
||||
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
|
||||
github.com/getsentry/raven-go v0.2.0
|
||||
github.com/getsentry/sentry-go v0.8.0
|
||||
github.com/go-resty/resty/v2 v2.3.0
|
||||
github.com/golang/mock v1.4.4
|
||||
github.com/google/go-cmp v0.5.1
|
||||
@ -59,7 +59,7 @@ require (
|
||||
github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce
|
||||
github.com/olekukonko/tablewriter v0.0.4 // indirect
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/sirupsen/logrus v1.6.0
|
||||
github.com/sirupsen/logrus v1.7.0
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||
github.com/stretchr/testify v1.6.1
|
||||
@ -74,8 +74,7 @@ require (
|
||||
|
||||
replace (
|
||||
github.com/docker/docker-credential-helpers => github.com/ProtonMail/docker-credential-helpers v1.1.0
|
||||
github.com/emersion/go-imap => github.com/ProtonMail/go-imap v0.0.0-20201016095853-a7520cc904d3
|
||||
github.com/emersion/go-mbox => github.com/ProtonMail/mbox v0.0.0-20200918064939-909a18c9af45
|
||||
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
|
||||
|
||||
167
go.sum
167
go.sum
@ -1,6 +1,10 @@
|
||||
github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1 h1:j9HaafapDbPbGRDku6e/HRs6KBMcKHiWcm1/9Sbxnl4=
|
||||
github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1/go.mod h1:NtXa9WwQsukMHZpjNakTTz0LArxvGYdPA9CjIcUSZ6s=
|
||||
github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno=
|
||||
github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMdUywE7VMo=
|
||||
github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY=
|
||||
github.com/Masterminds/semver/v3 v3.1.0 h1:Y2lUDsFKVRSYGojLJ1yLxSXdMmMYTYls0rCvoqmMUQk=
|
||||
github.com/Masterminds/semver/v3 v3.1.0/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998 h1:YT2uVwQiRQZxCaaahwfcgTq2j3j66w00n/27gb/zubs=
|
||||
@ -15,36 +19,46 @@ github.com/ProtonMail/go-apple-mobileconfig v0.0.0-20160701194735-7ea9927a11f6 h
|
||||
github.com/ProtonMail/go-apple-mobileconfig v0.0.0-20160701194735-7ea9927a11f6/go.mod h1:EtDfBMIDWmVe4viZCuBTEfe3OIIo0ghbpOaAZVO+hVg=
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a h1:fXK2KsfnkBV9Nh+9SKzHchYjuE9s0vI20JG1mbtEAcc=
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
|
||||
github.com/ProtonMail/go-imap v0.0.0-20201016095853-a7520cc904d3 h1:Jvv9t3rSg/ID3Fh+uYsxgmvNI9fYnlab4vtBsbPtmq8=
|
||||
github.com/ProtonMail/go-imap v0.0.0-20201016095853-a7520cc904d3/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU=
|
||||
github.com/ProtonMail/go-imap v0.0.0-20201102134601-418cd74e9474 h1:D0RwDtkBw0Gt7hmbb1ivdEulplJAwu1i2jzh4HM45fo=
|
||||
github.com/ProtonMail/go-imap v0.0.0-20201102134601-418cd74e9474/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU=
|
||||
github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde h1:5koQozTDELymYOyFbQ/VSubexAEXzDR8qGM5mO8GRdw=
|
||||
github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde/go.mod h1:795VPXcRUIQ9JyMNHP4el582VokQfippgjkQP3Gk0r0=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a h1:W6RrgN/sTxg1msqzFFb+G80MFmpjMw61IU+slm+wln4=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4=
|
||||
github.com/ProtonMail/go-rfc5322 v0.2.0 h1:tndoDGFtiCvESta9KLUeMksojz8qf76PefnkoQ+fqeg=
|
||||
github.com/ProtonMail/go-rfc5322 v0.2.0/go.mod h1:mzZWlMWnQJuYLL7JpzuPF5+FimV2lZ9f0jeq24kJjpU=
|
||||
github.com/ProtonMail/go-smtp v0.0.0-20181206232543-8261df20d309 h1:2pzfKjhBjSnw3BgmfTYRFQr1rFGxhfhUY0KKkg+RYxE=
|
||||
github.com/ProtonMail/go-smtp v0.0.0-20181206232543-8261df20d309/go.mod h1:6UoBvDAMA/cTBwS3Y7tGpKnY5RH1F1uYHschT6eqAkI=
|
||||
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5 h1:Uga1DHFN4GUxuDQr0F71tpi8I9HqPIlZodZAI1lR6VQ=
|
||||
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5/go.mod h1:oeP9CMN+ajWp5jKp1kue5daJNwMMxLF+ujPaUIoJWlA=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.0.1 h1:x0uvDhry5WzoHeJO4J3dgMLhG4Z9PeBJ2O+sDOY0LcU=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.0.1/go.mod h1:wQQCJo7DURO6S9VwH+kSDEYs/B63yZnAEfGlOg8YNBY=
|
||||
github.com/ProtonMail/mbox v0.0.0-20200918064939-909a18c9af45 h1:GDh55hDI2sNiirDqEWV8b6EB729u78Qxu3nKF970n6g=
|
||||
github.com/ProtonMail/mbox v0.0.0-20200918064939-909a18c9af45/go.mod h1:Yp9IVuuOYLEuMv4yjgDHvhb5mHOcYH6x92Oas3QqEZI=
|
||||
github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE=
|
||||
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||
github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0=
|
||||
github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw=
|
||||
github.com/abiosoft/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzgHf/aS6+zVXRj14cVk9qg=
|
||||
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8=
|
||||
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db/go.mod h1:rB3B4rKii8V21ydCbIzH5hZiCQE7f5E9SzUb/ZZx530=
|
||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||
github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc h1:mZca0/HZ/XWXP9txkfdl2GH6mUzBqAlyJz3u5Lg8fuA=
|
||||
github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc/go.mod h1:qqsTQiwdyqxU05iDCsi0oN3P4nrVxAmn8xCtODDSf/U=
|
||||
github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo=
|
||||
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 h1:JLaf/iINcLyjwbtTsCJjc6rtlASgHeIJPrB6QmwURnA=
|
||||
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
|
||||
github.com/antlr/antlr4 v0.0.0-20201029161626-9a95f0cc3d7c h1:j/C2kxPfyE0d87/ggAjIsCV5Cdkqmjb+O0W8W+1J+IY=
|
||||
github.com/antlr/antlr4 v0.0.0-20201029161626-9a95f0cc3d7c/go.mod h1:T7PbCXFs94rrTttyxjbyT5+/1V8T2TYDejxUfHJjw1Y=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g=
|
||||
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
@ -55,6 +69,11 @@ github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7h
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM=
|
||||
github.com/emersion/go-imap-appendlimit v0.0.0-20190308131241-25671c986a6a h1:bMdSPm6sssuOFpIaveu3XGAijMS3Tq2S3EqFZmZxidc=
|
||||
github.com/emersion/go-imap-appendlimit v0.0.0-20190308131241-25671c986a6a/go.mod h1:ikgISoP7pRAolqsVP64yMteJa2FIpS6ju88eBT6K1yQ=
|
||||
github.com/emersion/go-imap-idle v0.0.0-20200601154248-f05f54664cc4 h1:/JIALzmCduf5o8TWJSiOBzTb9+R0SChwElUrJLlp2po=
|
||||
@ -67,6 +86,8 @@ github.com/emersion/go-imap-specialuse v0.0.0-20200722111535-598ff00e4075 h1:z8T
|
||||
github.com/emersion/go-imap-specialuse v0.0.0-20200722111535-598ff00e4075/go.mod h1:/nybxhI8kXom8Tw6BrHMl42usALvka6meORflnnYwe4=
|
||||
github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26 h1:FiSb8+XBQQSkcX3ubr+1tAtlRJBYaFmRZqOAweZ9Wy8=
|
||||
github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26/go.mod h1:+gnnZx3Mg3MnCzZrv0eZdp5puxXQUgGT/6N6L7ShKfM=
|
||||
github.com/emersion/go-mbox v1.0.2 h1:tE/rT+lEugK9y0myEymCCHnwlZN04hlXPrbKkxRBA5I=
|
||||
github.com/emersion/go-mbox v1.0.2/go.mod h1:Yp9IVuuOYLEuMv4yjgDHvhb5mHOcYH6x92Oas3QqEZI=
|
||||
github.com/emersion/go-message v0.11.1/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY=
|
||||
github.com/emersion/go-message v0.12.1-0.20200903165315-e1abe21f389a h1:3C6qIGgPr1qAT0ikRD5NbyKpME/iHCDeXhpv/JJsFsE=
|
||||
github.com/emersion/go-message v0.12.1-0.20200903165315-e1abe21f389a/go.mod h1:kYIioST9GDHte9/BRWgi93rpqbDuFftMjKSMaXS8ABo=
|
||||
@ -77,79 +98,161 @@ github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe h1:40SWqY0
|
||||
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
|
||||
github.com/emersion/go-vcard v0.0.0-20190105225839-8856043f13c5 h1:n9qx98xiS5V4x2WIpPC2rr9mUM5ri9r/YhCEKbhCHro=
|
||||
github.com/emersion/go-vcard v0.0.0-20190105225839-8856043f13c5/go.mod h1:WIi9g8OKJQHXtQbx7GExlo6UAFaui9WDMYabJ+Be4WI=
|
||||
github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw=
|
||||
github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8=
|
||||
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BMXYYRWTLOJKlh+lOBt6nUQgXAfB7oVIQt5cNreqSLI=
|
||||
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:rZfgFAXFS/z/lEd6LJmf9HVZ1LkgYiHx5pHhV5DR16M=
|
||||
github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs=
|
||||
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
|
||||
github.com/getsentry/sentry-go v0.8.0 h1:F52cjBVLuiTfdW6p4JFuxlt3pOjKfWYT/aka7cdJ7v0=
|
||||
github.com/getsentry/sentry-go v0.8.0/go.mod h1:kELm/9iCblqUYh+ZRML7PNdCvEuw24wBvJPYyi86cws=
|
||||
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
|
||||
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
|
||||
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
|
||||
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
|
||||
github.com/go-resty/resty/v2 v2.3.0 h1:JOOeAvjSlapTT92p8xiS19Zxev1neGikoHsXJeOq8So=
|
||||
github.com/go-resty/resty/v2 v2.3.0/go.mod h1:UpN9CgLZNsv4e9XG50UU8xdI0F43UQ4HmxLBDwaroHU=
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20190411002643-bd77b112433e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI=
|
||||
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
|
||||
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI=
|
||||
github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0=
|
||||
github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk=
|
||||
github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0GqwkjqxNd0u65g=
|
||||
github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw=
|
||||
github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7 h1:g0fAGBisHaEQ0TRq1iBvemFRf+8AEWEmBESSiWB3Vsc=
|
||||
github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA=
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
|
||||
github.com/kataras/golog v0.0.10/go.mod h1:yJ8YKCmyL+nWjERB90Qwn+bdyBZsaQwU3bTVFgkFIp8=
|
||||
github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYbq3UhfoFmE=
|
||||
github.com/kataras/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE=
|
||||
github.com/kataras/pio v0.0.2/go.mod h1:hAoW0t9UmXi4R5Oyq5Z4irTbaTsOemSrDGUtaTl7Dro=
|
||||
github.com/kataras/sitemap v0.0.5/go.mod h1:KY2eugMKiPwsJgx7+U103YZehfvNGOXURubcGyk0Bz8=
|
||||
github.com/keybase/go-keychain v0.0.0-20200502122510-cda31fe0c86d h1:gVjhBCfVGl32RIBooOANzfw+0UqX8HU+yPlMv8vypcg=
|
||||
github.com/keybase/go-keychain v0.0.0-20200502122510-cda31fe0c86d/go.mod h1:W6EbaYmb4RldPn0N3gvVHjY1wmU59kbymhW9NATWhwY=
|
||||
github.com/keybase/go.dbus v0.0.0-20200324223359-a94be52c0b03/go.mod h1:a8clEhrrGV/d76/f9r2I41BwANMihfZYV9C223vaxqE=
|
||||
github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g=
|
||||
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
|
||||
github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
|
||||
github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/martinlindhe/base36 v1.0.0 h1:eYsumTah144C0A8P1T/AVSUk5ZoLnhfYFM3OGQxB52A=
|
||||
github.com/martinlindhe/base36 v1.0.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
|
||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
|
||||
github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8=
|
||||
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
|
||||
github.com/miekg/dns v1.1.30 h1:Qww6FseFn8PRfw07jueqIXqodm0JKiiKuK0DeXSqfyo=
|
||||
github.com/miekg/dns v1.1.30/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
|
||||
github.com/myesui/uuid v1.0.0 h1:xCBmH4l5KuvLYc5L7AS7SZg9/jKdIFubM7OVoLqaQUI=
|
||||
github.com/myesui/uuid v1.0.0/go.mod h1:2CDfNgU0LR8mIdO8vdWd8i9gWWxLlcoIGGpSNgafq84=
|
||||
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
|
||||
github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
|
||||
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce h1:RPclfga2SEJmgMmz2k+Mg7cowZ8yv4Trqw9UsJby758=
|
||||
github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce/go.mod h1:uFMI8w+ref4v2r9jz+c9i1IfIttS/OkmLfrk1jne5hs=
|
||||
github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8=
|
||||
github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
|
||||
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
@ -166,29 +269,58 @@ github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e h1:G0DQ/TRQyrEZjtLlLw
|
||||
github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e/go.mod h1:SUUR2j3aE1z6/g76SdD6NwACEpvCxb3fvG82eKbD6us=
|
||||
github.com/twinj/uuid v1.0.0 h1:fzz7COZnDrXGTAOHGuUGYd6sG+JMq+AoE7+Jlu0przk=
|
||||
github.com/twinj/uuid v1.0.0/go.mod h1:mMgcE1RHFUFqe5AfiwlINXisXfDGro23fWdPUfOMjRY=
|
||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA=
|
||||
github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w=
|
||||
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
||||
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
|
||||
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
|
||||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
|
||||
github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
|
||||
go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
|
||||
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190420063019-afa5a82059c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@ -199,6 +331,9 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
@ -208,9 +343,17 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
||||
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
|
||||
gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
|
||||
gopkg.in/stretchr/testify.v1 v1.2.2 h1:yhQC6Uy5CqibAIlk1wlusa/MJ3iAN49/BsR/dCCKz3M=
|
||||
gopkg.in/stretchr/testify.v1 v1.2.2/go.mod h1:QI5V/q6UbPmuhtm10CaFZxED9NreB8PnFYN9JcR6TxU=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@ -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 Sep 29 14:56:25 CEST 2020. DO NOT EDIT.
|
||||
// Code generated by ./credits.sh at Tue Nov 24 08:56:01 AM CET 2020. DO NOT EDIT.
|
||||
|
||||
package bridge
|
||||
|
||||
const Credits = "github.com/0xAX/notificator;github.com/Masterminds/semver/v3;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/go-smtp;github.com/ProtonMail/go-vcard;github.com/ProtonMail/gopenpgp/v2;github.com/ProtonMail/mbox;github.com/PuerkitoBio/goquery;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/certifi/gocertifi;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-specialuse;github.com/emersion/go-imap-unselect;github.com/emersion/go-mbox;github.com/emersion/go-message;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/raven-go;github.com/go-resty/resty/v2;github.com/golang/mock;github.com/google/go-cmp;github.com/google/uuid;github.com/gopherjs/gopherjs;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/mattn/go-runewidth;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/olekukonko/tablewriter;github.com/pkg/errors;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/ssor/bom;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"
|
||||
const Credits = "github.com/0xAX/notificator;github.com/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;"
|
||||
|
||||
@ -15,21 +15,18 @@
|
||||
// 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 Sep 21 01:29:10 PM CEST 2020'. DO NOT EDIT.
|
||||
// Code generated by ./release-notes.sh at 'Mon Nov 23 07:38:53 AM CET 2020'. DO NOT EDIT.
|
||||
|
||||
package bridge
|
||||
|
||||
const ReleaseNotes = `• Bulletproofing against any potential data loss and/or duplication
|
||||
• Performance improvements for handling attachments and non-standard formatting
|
||||
• Better stability of the message parser
|
||||
• Additional foreign encoding support for outgoing messages
|
||||
• Complete refactor of the way messages are parsed to simplify code maintenance
|
||||
• Improved User-Agent detection
|
||||
• Added MacOS Big Sur compatibility
|
||||
• Added persistent anonymous API cookies
|
||||
const 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 ReleaseFixedBugs = `• Fixed rare mail loss when moving from Spam folder
|
||||
• Limited log size
|
||||
• Fixed Linux font issues (mouse hover).
|
||||
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)
|
||||
`
|
||||
|
||||
@ -22,7 +22,7 @@ import (
|
||||
"runtime"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/pkg/constants"
|
||||
"github.com/getsentry/raven-go"
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
@ -51,10 +51,18 @@ var (
|
||||
|
||||
// Main sets up Sentry, filters out unwanted args, creates app and runs it.
|
||||
func Main(appName, usage string, extraFlags []cli.Flag, run func(*cli.Context) error) {
|
||||
if err := raven.SetDSN(constants.DSNSentry); err != nil {
|
||||
err := sentry.Init(sentry.ClientOptions{
|
||||
Dsn: constants.DSNSentry,
|
||||
Release: constants.Revision,
|
||||
})
|
||||
|
||||
sentry.ConfigureScope(func(scope *sentry.Scope) {
|
||||
scope.SetFingerprint([]string{"{{ default }}"})
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.WithError(err).Errorln("Can not setup sentry DSN")
|
||||
}
|
||||
raven.SetRelease(constants.Revision)
|
||||
|
||||
filterProcessSerialNumberFromArgs()
|
||||
filterRestartNumberFromArgs()
|
||||
|
||||
@ -40,7 +40,6 @@ const (
|
||||
NoActiveKeyForRecipientEvent = "noActiveKeyForRecipient"
|
||||
UpgradeApplicationEvent = "upgradeApplication"
|
||||
TLSCertIssue = "tlsCertPinningIssue"
|
||||
IMAPTLSBadCert = "imapTLSBadCert"
|
||||
|
||||
// LogoutEventTimeout is the minimum time to permit between logout events being sent.
|
||||
LogoutEventTimeout = 3 * time.Minute
|
||||
|
||||
@ -184,9 +184,17 @@ func (f *frontendCLI) setTransferRules(t *transfer.Transfer) bool {
|
||||
}
|
||||
|
||||
func (f *frontendCLI) printTransferProgress(progress *transfer.Progress) {
|
||||
failed, imported, exported, added, total := progress.GetCounts()
|
||||
if total != 0 {
|
||||
f.Println(fmt.Sprintf("Progress update: %d (%d / %d) / %d, failed: %d", imported, exported, added, total, failed))
|
||||
counts := progress.GetCounts()
|
||||
if counts.Total != 0 {
|
||||
f.Println(fmt.Sprintf(
|
||||
"Progress update: %d (%d / %d) / %d, skipped: %d, failed: %d",
|
||||
counts.Imported,
|
||||
counts.Exported,
|
||||
counts.Added,
|
||||
counts.Total,
|
||||
counts.Skipped,
|
||||
counts.Failed,
|
||||
))
|
||||
}
|
||||
|
||||
if progress.IsPaused() {
|
||||
|
||||
@ -237,13 +237,6 @@ Item {
|
||||
winMain.tlsBarState="notOK"
|
||||
}
|
||||
|
||||
onShowIMAPCertTroubleshoot : {
|
||||
go.notifyBubble(1, qsTr(
|
||||
"Bridge was unable to establish a connection with your Email client. <br> <a href=\"https://protonmail.com/support/knowledge-base/bridge-ssl-connection-issue\">Learn more</a> <br>",
|
||||
"notification message"
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -599,7 +599,7 @@ Dialog {
|
||||
}
|
||||
|
||||
Text {
|
||||
text: qsTr("<b>Import summary:</b><br>Total number of emails: %1<br>Imported emails: %2<br>Errors: %3").arg(go.total).arg(finalReport.imported).arg(go.progressFails)
|
||||
text: qsTr("<b>Import summary:</b><br>Total number of emails: %1<br>Imported emails: %2<br>Filtered out emails: %3<br>Errors: %4").arg(go.total).arg(go.progressImported).arg(go.progressSkipped).arg(go.progressFails)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
textFormat: Text.RichText
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
|
||||
@ -45,7 +45,7 @@ Row {
|
||||
}
|
||||
|
||||
InfoToolTip {
|
||||
info: qsTr( "When master import lablel is selected then all imported email will have this label.", "Tooltip text for master import label")
|
||||
info: qsTr( "When master import label is selected then all imported emails will have this label.", "Tooltip text for master import label")
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
|
||||
@ -122,7 +122,6 @@ Window {
|
||||
ListElement { title: "Minimize this" }
|
||||
ListElement { title: "SendAlertPopup" }
|
||||
ListElement { title: "TLSCertError" }
|
||||
ListElement { title: "IMAPCertError" }
|
||||
}
|
||||
|
||||
ListView {
|
||||
@ -209,9 +208,6 @@ Window {
|
||||
case "TLSCertError" :
|
||||
go.showCertIssue()
|
||||
break;
|
||||
case "IMAPCertError" :
|
||||
go.showIMAPCertTroubleshoot()
|
||||
break;
|
||||
default :
|
||||
console.log("Not implemented " + data)
|
||||
}
|
||||
@ -314,7 +310,6 @@ Window {
|
||||
signal failedAutostartCode(string code)
|
||||
|
||||
signal showCertIssue()
|
||||
signal showIMAPCertTroubleshoot()
|
||||
|
||||
signal updateFinished(bool hasError)
|
||||
|
||||
|
||||
@ -840,6 +840,8 @@ Window {
|
||||
|
||||
property real progress: 0.0
|
||||
property int progressFails: 0
|
||||
property int progressImported: 0
|
||||
property int progressSkipped: 0
|
||||
property string progressDescription: "nothing"
|
||||
property string progressInit: "init"
|
||||
property int total: 42
|
||||
@ -1011,6 +1013,8 @@ Window {
|
||||
property SequentialAnimation animateProgressBar : SequentialAnimation {
|
||||
id: apb
|
||||
property real speedup : 1.0;
|
||||
PropertyAnimation{ target: go; properties: "progressSkipped"; to: 0; duration: 1; }
|
||||
PropertyAnimation{ target: go; properties: "progressImported"; to: 0; duration: 1; }
|
||||
PropertyAnimation{ target: go; properties: "importLogFileName"; to: ""; duration: 1; }
|
||||
PropertyAnimation{ target: go; properties: "progressDescription"; to: go.progressInit; duration: 1; }
|
||||
PropertyAnimation{ duration: 2000/apb.speedup; }
|
||||
@ -1024,6 +1028,8 @@ Window {
|
||||
PropertyAnimation{ target: go; properties: "progress"; to: 0.01; duration: 1; }
|
||||
PropertyAnimation{ duration: 1000/apb.speedup; }
|
||||
PropertyAnimation{ target: go; properties: "progress"; to: 0.1; duration: 1; }
|
||||
PropertyAnimation{ target: go; properties: "progressSkipped"; to: 12; duration: 1; }
|
||||
PropertyAnimation{ target: go; properties: "progressImported"; to: 13.1; duration: 1; }
|
||||
PropertyAnimation{ duration: 1000/apb.speedup; }
|
||||
PropertyAnimation{ target: go; properties: "progress"; to: 0.3; duration: 1; }
|
||||
PropertyAnimation{ target: go; properties: "progressFails"; to: 1; duration: 1; }
|
||||
|
||||
@ -21,6 +21,7 @@ package qtcommon
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/therecipe/qt/core"
|
||||
)
|
||||
|
||||
|
||||
@ -337,18 +337,25 @@ func (f *FrontendQt) setProgressManager(progress *transfer.Progress) {
|
||||
if progress.IsStopped() {
|
||||
break
|
||||
}
|
||||
failed, imported, _, _, total := progress.GetCounts()
|
||||
f.Qml.SetTotal(int(total))
|
||||
f.Qml.SetProgressFails(int(failed))
|
||||
counts := progress.GetCounts()
|
||||
f.Qml.SetTotal(int(counts.Total))
|
||||
f.Qml.SetProgressImported(int(counts.Imported))
|
||||
f.Qml.SetProgressSkipped(int(counts.Skipped))
|
||||
f.Qml.SetProgressFails(int(counts.Failed))
|
||||
f.Qml.SetProgressDescription(progress.PauseReason())
|
||||
if total > 0 {
|
||||
newProgress := float32(imported+failed) / float32(total)
|
||||
if counts.Total > 0 {
|
||||
newProgress := counts.Progress()
|
||||
if newProgress >= 0 && newProgress != f.Qml.Progress() {
|
||||
f.Qml.SetProgress(newProgress)
|
||||
f.Qml.ProgressChanged(newProgress)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Counts will add lost messages only once the progress is completeled.
|
||||
counts := progress.GetCounts()
|
||||
f.Qml.SetProgressImported(int(counts.Imported))
|
||||
f.Qml.SetProgressSkipped(int(counts.Skipped))
|
||||
f.Qml.SetProgressFails(int(counts.Failed))
|
||||
|
||||
if err := progress.GetFatalError(); err != nil {
|
||||
f.Qml.SetProgressDescription(err.Error())
|
||||
|
||||
@ -77,6 +77,8 @@ func (f *FrontendQt) StartImport(email string) { // TODO email not needed
|
||||
log.Trace("Starting import")
|
||||
|
||||
f.Qml.SetProgressDescription("init") // TODO use const
|
||||
f.Qml.SetProgressImported(0)
|
||||
f.Qml.SetProgressSkipped(0)
|
||||
f.Qml.SetProgressFails(0)
|
||||
f.Qml.SetProgress(0.0)
|
||||
f.Qml.SetTotal(1)
|
||||
|
||||
@ -43,6 +43,8 @@ type GoQMLInterface struct {
|
||||
_ string `property:lastError`
|
||||
_ float32 `property:progress`
|
||||
_ string `property:progressDescription`
|
||||
_ int `property:progressImported`
|
||||
_ int `property:progressSkipped`
|
||||
_ int `property:progressFails`
|
||||
_ int `property:total`
|
||||
_ string `property:importLogFileName`
|
||||
|
||||
@ -40,17 +40,17 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend/autoconfig"
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend/qt-common"
|
||||
qtcommon "github.com/ProtonMail/proton-bridge/internal/frontend/qt-common"
|
||||
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
|
||||
"github.com/ProtonMail/proton-bridge/internal/preferences"
|
||||
"github.com/ProtonMail/proton-bridge/internal/updates"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/config"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/ports"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/useragent"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/ports"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/useragent"
|
||||
"github.com/kardianos/osext"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/skratchdot/open-golang/open"
|
||||
"github.com/therecipe/qt/core"
|
||||
"github.com/therecipe/qt/gui"
|
||||
@ -187,7 +187,6 @@ func (s *FrontendQt) watchEvents() {
|
||||
updateApplicationCh := s.getEventChannel(events.UpgradeApplicationEvent)
|
||||
newUserCh := s.getEventChannel(events.UserRefreshEvent)
|
||||
certIssue := s.getEventChannel(events.TLSCertIssue)
|
||||
imapCertIssue := s.getEventChannel(events.IMAPTLSBadCert)
|
||||
for {
|
||||
select {
|
||||
case errorDetails := <-errorCh:
|
||||
@ -227,8 +226,6 @@ func (s *FrontendQt) watchEvents() {
|
||||
s.Qml.LoadAccounts()
|
||||
case <-certIssue:
|
||||
s.Qml.ShowCertIssue()
|
||||
case <-imapCertIssue:
|
||||
s.Qml.ShowIMAPCertTroubleshoot()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -135,7 +135,6 @@ type GoQMLInterface struct {
|
||||
_ func(x, y float32) `slot:"saveOutgoingNoEncPopupCoord"`
|
||||
_ func(recipient string) `signal:"showNoActiveKeyForRecipient"`
|
||||
_ func() `signal:"showCertIssue"`
|
||||
_ func() `signal:"ShowIMAPCertTroubleshoot"`
|
||||
|
||||
_ func() `slot:"startUpdate"`
|
||||
_ func(hasError bool) `signal:"updateFinished"`
|
||||
|
||||
@ -28,7 +28,6 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||
"github.com/emersion/go-imap"
|
||||
goIMAPBackend "github.com/emersion/go-imap/backend"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type panicHandler interface {
|
||||
@ -198,11 +197,3 @@ func (ib *imapBackend) monitorDisconnectedUsers() {
|
||||
ib.deleteUser(address)
|
||||
}
|
||||
}
|
||||
|
||||
func (ib *imapBackend) upgradeError(err error) {
|
||||
logrus.WithError(err).Error("IMAP connection couldn't be upgraded to TLS during STARTTLS")
|
||||
|
||||
if strings.Contains(err.Error(), "remote error: tls: bad certificate") {
|
||||
ib.eventListener.Emit(events.IMAPTLSBadCert, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@ -58,7 +58,6 @@ func NewIMAPServer(debugClient, debugServer bool, port int, tls *tls.Config, ima
|
||||
s.AllowInsecureAuth = true
|
||||
s.ErrorLog = newServerErrorLogger("server-imap")
|
||||
s.AutoLogout = 30 * time.Minute
|
||||
s.UpgradeError = imapBackend.upgradeError
|
||||
|
||||
serverID := imapid.ID{
|
||||
imapid.FieldName: "ProtonMail Bridge",
|
||||
|
||||
@ -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 Sep 29 14:56:25 CEST 2020. DO NOT EDIT.
|
||||
// Code generated by ./credits.sh at Tue Nov 24 08:56:01 AM CET 2020. DO NOT EDIT.
|
||||
|
||||
package importexport
|
||||
|
||||
const Credits = "github.com/0xAX/notificator;github.com/Masterminds/semver/v3;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/go-smtp;github.com/ProtonMail/go-vcard;github.com/ProtonMail/gopenpgp/v2;github.com/ProtonMail/mbox;github.com/PuerkitoBio/goquery;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/certifi/gocertifi;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-specialuse;github.com/emersion/go-imap-unselect;github.com/emersion/go-mbox;github.com/emersion/go-message;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/raven-go;github.com/go-resty/resty/v2;github.com/golang/mock;github.com/google/go-cmp;github.com/google/uuid;github.com/gopherjs/gopherjs;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/mattn/go-runewidth;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/olekukonko/tablewriter;github.com/pkg/errors;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/ssor/bom;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"
|
||||
const Credits = "github.com/0xAX/notificator;github.com/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;"
|
||||
|
||||
@ -15,18 +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 'Thu Oct 29 12:57:32 PM CET 2020'. DO NOT EDIT.
|
||||
// Code generated by ./release-notes.sh at 'Wed Nov 11 01:57:14 PM CET 2020'. DO NOT EDIT.
|
||||
|
||||
package importexport
|
||||
|
||||
const ReleaseNotes = `• Improvements to the import from large mbox files with multiple labels
|
||||
• Not allow to run multiple instances of the app or transfers at the same time
|
||||
• Various enhancements of the import process related to parsing
|
||||
• Cosmetic GUI changes
|
||||
• Better error handling
|
||||
const ReleaseNotes = `• Further improvements to address and date parsing
|
||||
• Better handling and displaying of skipped messages
|
||||
• Improved error reporting
|
||||
`
|
||||
|
||||
const ReleaseFixedBugs = `• Linux font issues - Fedora specific
|
||||
• App response to the user pausing and canceling import or export
|
||||
• Handling errors during update
|
||||
const ReleaseFixedBugs = `
|
||||
`
|
||||
|
||||
@ -45,7 +45,7 @@ type SendPreferences struct {
|
||||
// internal emails (including the so-called encrypted-to-outside emails,
|
||||
// which even though meant for external users, they don't really get out of
|
||||
// our platform). If the email is sent unencrypted, no PGP scheme is needed.
|
||||
Scheme int
|
||||
Scheme pmapi.PackageFlag
|
||||
|
||||
// MIMEType is the MIME type to use for formatting the body of the email
|
||||
// (before encryption/after decryption). The standard possibilities are the
|
||||
@ -191,8 +191,12 @@ func (b *sendPreferencesBuilder) build() (p SendPreferences) {
|
||||
p.Scheme = pmapi.PGPMIMEPackage
|
||||
}
|
||||
|
||||
case b.shouldSign() && !b.shouldEncrypt() && b.getScheme() == pgpMIME:
|
||||
p.Scheme = pmapi.ClearMIMEPackage
|
||||
case b.shouldSign() && !b.shouldEncrypt():
|
||||
if b.getScheme() == pgpInline {
|
||||
p.Scheme = pmapi.ClearPackage
|
||||
} else {
|
||||
p.Scheme = pmapi.ClearMIMEPackage
|
||||
}
|
||||
|
||||
default:
|
||||
p.Scheme = pmapi.ClearPackage
|
||||
|
||||
@ -41,7 +41,7 @@ func TestPreferencesBuilder(t *testing.T) {
|
||||
|
||||
wantEncrypt bool
|
||||
wantSign bool
|
||||
wantScheme int
|
||||
wantScheme pmapi.PackageFlag
|
||||
wantMIMEType string
|
||||
wantPublicKey string
|
||||
}{
|
||||
@ -254,6 +254,20 @@ func TestPreferencesBuilder(t *testing.T) {
|
||||
wantMIMEType: "multipart/mixed",
|
||||
},
|
||||
|
||||
{
|
||||
name: "external with contact sign enabled and plain text",
|
||||
|
||||
contactMeta: &ContactMetadata{MIMEType: "text/plain", Scheme: pgpInline, Sign: true, SignIsSet: true},
|
||||
receivedKeys: []pmapi.PublicKey{},
|
||||
isInternal: false,
|
||||
mailSettings: pmapi.MailSettings{PGPScheme: pmapi.PGPMIMEPackage, DraftMIMEType: "text/html"},
|
||||
|
||||
wantEncrypt: false,
|
||||
wantSign: true,
|
||||
wantScheme: pmapi.ClearPackage,
|
||||
wantMIMEType: "text/plain",
|
||||
},
|
||||
|
||||
{
|
||||
name: "external with sign enabled, sending plaintext, should still send as ClearMIME",
|
||||
|
||||
|
||||
@ -187,7 +187,7 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err
|
||||
log.WithError(err).Error("Failed to parse message")
|
||||
return
|
||||
}
|
||||
clearBody := message.Body
|
||||
richBody := message.Body
|
||||
|
||||
externalID := message.Header.Get("Message-Id")
|
||||
externalID = strings.Trim(externalID, "<>")
|
||||
@ -256,7 +256,6 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err
|
||||
atts = append(atts, message.Attachments...)
|
||||
// Decrypt attachment keys, because we will need to re-encrypt them with the recipients' public keys.
|
||||
attkeys := make(map[string]*crypto.SessionKey)
|
||||
attkeysEncoded := make(map[string]pmapi.AlgoKey)
|
||||
|
||||
for _, att := range atts {
|
||||
var keyPackets []byte
|
||||
@ -266,23 +265,9 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err
|
||||
if attkeys[att.ID], err = kr.DecryptSessionKey(keyPackets); err != nil {
|
||||
return errors.Wrap(err, "decrypting attachment session key")
|
||||
}
|
||||
attkeysEncoded[att.ID] = pmapi.AlgoKey{
|
||||
Key: attkeys[att.ID].GetBase64Key(),
|
||||
Algorithm: attkeys[att.ID].Algo,
|
||||
}
|
||||
}
|
||||
|
||||
plainSharedScheme := 0
|
||||
htmlSharedScheme := 0
|
||||
mimeSharedType := 0
|
||||
|
||||
plainAddressMap := make(map[string]*pmapi.MessageAddress)
|
||||
htmlAddressMap := make(map[string]*pmapi.MessageAddress)
|
||||
mimeAddressMap := make(map[string]*pmapi.MessageAddress)
|
||||
|
||||
var plainKey, htmlKey, mimeKey *crypto.SessionKey
|
||||
var plainData, htmlData, mimeData []byte
|
||||
|
||||
req := pmapi.NewSendMessageReq(kr, mimeBody, plainBody, richBody, attkeys)
|
||||
containsUnencryptedRecipients := false
|
||||
|
||||
for _, email := range to {
|
||||
@ -298,61 +283,15 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err
|
||||
return err
|
||||
}
|
||||
|
||||
var signature int
|
||||
var signature pmapi.SignatureFlag
|
||||
if sendPreferences.Sign {
|
||||
signature = pmapi.YesSignature
|
||||
signature = pmapi.SignatureDetached
|
||||
} else {
|
||||
signature = pmapi.NoSignature
|
||||
signature = pmapi.SignatureNone
|
||||
}
|
||||
if sendPreferences.Scheme == pmapi.PGPMIMEPackage || sendPreferences.Scheme == pmapi.ClearMIMEPackage {
|
||||
if mimeKey == nil {
|
||||
if mimeKey, mimeData, err = encryptSymmetric(kr, mimeBody, true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if sendPreferences.Scheme == pmapi.PGPMIMEPackage {
|
||||
mimeBodyPacket, _, err := createPackets(sendPreferences.PublicKey, mimeKey, map[string]*crypto.SessionKey{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mimeAddressMap[email] = &pmapi.MessageAddress{Type: sendPreferences.Scheme, BodyKeyPacket: mimeBodyPacket, Signature: signature}
|
||||
} else {
|
||||
mimeAddressMap[email] = &pmapi.MessageAddress{Type: sendPreferences.Scheme, Signature: signature}
|
||||
}
|
||||
mimeSharedType |= sendPreferences.Scheme
|
||||
} else {
|
||||
switch sendPreferences.MIMEType {
|
||||
case pmapi.ContentTypePlainText:
|
||||
if plainKey == nil {
|
||||
if plainKey, plainData, err = encryptSymmetric(kr, plainBody, true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
newAddress := &pmapi.MessageAddress{Type: sendPreferences.Scheme, Signature: signature}
|
||||
if sendPreferences.Encrypt && sendPreferences.PublicKey != nil {
|
||||
newAddress.BodyKeyPacket, newAddress.AttachmentKeyPackets, err = createPackets(sendPreferences.PublicKey, plainKey, attkeys)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
plainAddressMap[email] = newAddress
|
||||
plainSharedScheme |= sendPreferences.Scheme
|
||||
case pmapi.ContentTypeHTML:
|
||||
if htmlKey == nil {
|
||||
if htmlKey, htmlData, err = encryptSymmetric(kr, clearBody, true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
newAddress := &pmapi.MessageAddress{Type: sendPreferences.Scheme, Signature: signature}
|
||||
if sendPreferences.Encrypt && sendPreferences.PublicKey != nil {
|
||||
newAddress.BodyKeyPacket, newAddress.AttachmentKeyPackets, err = createPackets(sendPreferences.PublicKey, htmlKey, attkeys)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
htmlAddressMap[email] = newAddress
|
||||
htmlSharedScheme |= sendPreferences.Scheme
|
||||
}
|
||||
|
||||
if err := req.AddRecipient(email, sendPreferences.Scheme, sendPreferences.PublicKey, signature, sendPreferences.MIMEType, sendPreferences.Encrypt); err != nil {
|
||||
return errors.Wrap(err, "failed to add recipient")
|
||||
}
|
||||
}
|
||||
|
||||
@ -370,31 +309,7 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err
|
||||
}
|
||||
}
|
||||
|
||||
req := &pmapi.SendMessageReq{}
|
||||
|
||||
plainPkg := buildPackage(plainAddressMap, plainSharedScheme, pmapi.ContentTypePlainText, plainData, plainKey, attkeysEncoded)
|
||||
if plainPkg != nil {
|
||||
req.Packages = append(req.Packages, plainPkg)
|
||||
}
|
||||
|
||||
htmlPkg := buildPackage(htmlAddressMap, htmlSharedScheme, pmapi.ContentTypeHTML, htmlData, htmlKey, attkeysEncoded)
|
||||
if htmlPkg != nil {
|
||||
req.Packages = append(req.Packages, htmlPkg)
|
||||
}
|
||||
|
||||
if len(mimeAddressMap) > 0 {
|
||||
pkg := &pmapi.MessagePackage{
|
||||
Body: base64.StdEncoding.EncodeToString(mimeData),
|
||||
Addresses: mimeAddressMap,
|
||||
MIMEType: pmapi.ContentTypeMultipartMixed,
|
||||
Type: mimeSharedType,
|
||||
BodyKey: pmapi.AlgoKey{
|
||||
Key: mimeKey.GetBase64Key(),
|
||||
Algorithm: mimeKey.Algo,
|
||||
},
|
||||
}
|
||||
req.Packages = append(req.Packages, pkg)
|
||||
}
|
||||
req.PreparePackages()
|
||||
|
||||
return su.storeUser.SendMessage(message.ID, req)
|
||||
}
|
||||
|
||||
@ -18,11 +18,7 @@
|
||||
package smtp
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"regexp"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
)
|
||||
|
||||
//nolint:gochecknoglobals // Used like a constant
|
||||
@ -35,85 +31,3 @@ var mailFormat = regexp.MustCompile(`.+@.+\..+`)
|
||||
func looksLikeEmail(e string) bool {
|
||||
return mailFormat.MatchString(e)
|
||||
}
|
||||
|
||||
func createPackets(
|
||||
pubkey *crypto.KeyRing,
|
||||
bodyKey *crypto.SessionKey,
|
||||
attkeys map[string]*crypto.SessionKey,
|
||||
) (bodyPacket string, attachmentPackets map[string]string, err error) {
|
||||
// Encrypt message body keys.
|
||||
packetBytes, err := pubkey.EncryptSessionKey(bodyKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
bodyPacket = base64.StdEncoding.EncodeToString(packetBytes)
|
||||
|
||||
// Encrypt attachment keys.
|
||||
attachmentPackets = make(map[string]string)
|
||||
for id, attkey := range attkeys {
|
||||
var packets []byte
|
||||
if packets, err = pubkey.EncryptSessionKey(attkey); err != nil {
|
||||
return
|
||||
}
|
||||
attachmentPackets[id] = base64.StdEncoding.EncodeToString(packets)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func encryptSymmetric(
|
||||
kr *crypto.KeyRing,
|
||||
textToEncrypt string,
|
||||
canonicalizeText bool, // nolint[unparam]
|
||||
) (key *crypto.SessionKey, symEncryptedData []byte, err error) {
|
||||
// We use only primary key to encrypt the message. Our keyring contains all keys (primary, old and deacivated ones).
|
||||
firstKey, err := kr.FirstKey()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
pgpMessage, err := firstKey.Encrypt(crypto.NewPlainMessageFromString(textToEncrypt), kr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
pgpSplitMessage, err := pgpMessage.SeparateKeyAndData(len(textToEncrypt), 0)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
key, err = kr.DecryptSessionKey(pgpSplitMessage.GetBinaryKeyPacket())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
symEncryptedData = pgpSplitMessage.GetBinaryDataPacket()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func buildPackage(
|
||||
addressMap map[string]*pmapi.MessageAddress,
|
||||
sharedScheme int,
|
||||
mimeType string,
|
||||
bodyData []byte,
|
||||
bodyKey *crypto.SessionKey,
|
||||
attKeys map[string]pmapi.AlgoKey,
|
||||
) (pkg *pmapi.MessagePackage) {
|
||||
if len(addressMap) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
pkg = &pmapi.MessagePackage{
|
||||
Body: base64.StdEncoding.EncodeToString(bodyData),
|
||||
Addresses: addressMap,
|
||||
MIMEType: mimeType,
|
||||
Type: sharedScheme,
|
||||
}
|
||||
|
||||
if sharedScheme|pmapi.ClearPackage > 0 {
|
||||
pkg.BodyKey.Key = bodyKey.GetBase64Key()
|
||||
pkg.BodyKey.Algorithm = bodyKey.Algo
|
||||
pkg.AttachmentKeys = attKeys
|
||||
}
|
||||
return pkg
|
||||
}
|
||||
|
||||
@ -37,7 +37,7 @@ func (store *Store) SetIMAPUpdateChannel(updates chan imapBackend.Update) {
|
||||
}
|
||||
}
|
||||
|
||||
func (store *Store) imapNotice(address, notice string) {
|
||||
func (store *Store) imapNotice(address, notice string) *imapBackend.StatusUpdate {
|
||||
update := new(imapBackend.StatusUpdate)
|
||||
update.Update = imapBackend.NewUpdate(address, "")
|
||||
update.StatusResp = &imap.StatusResp{
|
||||
@ -46,13 +46,14 @@ func (store *Store) imapNotice(address, notice string) {
|
||||
Info: notice,
|
||||
}
|
||||
store.imapSendUpdate(update)
|
||||
return update
|
||||
}
|
||||
|
||||
func (store *Store) imapUpdateMessage(
|
||||
address, mailboxName string,
|
||||
uid, sequenceNumber uint32,
|
||||
msg *pmapi.Message, hasDeletedFlag bool,
|
||||
) {
|
||||
) *imapBackend.MessageUpdate {
|
||||
store.log.WithFields(logrus.Fields{
|
||||
"address": address,
|
||||
"mailbox": mailboxName,
|
||||
@ -70,9 +71,10 @@ func (store *Store) imapUpdateMessage(
|
||||
}
|
||||
update.Message.Uid = uid
|
||||
store.imapSendUpdate(update)
|
||||
return update
|
||||
}
|
||||
|
||||
func (store *Store) imapDeleteMessage(address, mailboxName string, sequenceNumber uint32) {
|
||||
func (store *Store) imapDeleteMessage(address, mailboxName string, sequenceNumber uint32) *imapBackend.ExpungeUpdate {
|
||||
store.log.WithFields(logrus.Fields{
|
||||
"address": address,
|
||||
"mailbox": mailboxName,
|
||||
@ -82,9 +84,10 @@ func (store *Store) imapDeleteMessage(address, mailboxName string, sequenceNumbe
|
||||
update.Update = imapBackend.NewUpdate(address, mailboxName)
|
||||
update.SeqNum = sequenceNumber
|
||||
store.imapSendUpdate(update)
|
||||
return update
|
||||
}
|
||||
|
||||
func (store *Store) imapMailboxCreated(address, mailboxName string) {
|
||||
func (store *Store) imapMailboxCreated(address, mailboxName string) *imapBackend.MailboxInfoUpdate {
|
||||
store.log.WithFields(logrus.Fields{
|
||||
"address": address,
|
||||
"mailbox": mailboxName,
|
||||
@ -97,9 +100,10 @@ func (store *Store) imapMailboxCreated(address, mailboxName string) {
|
||||
Name: mailboxName,
|
||||
}
|
||||
store.imapSendUpdate(update)
|
||||
return update
|
||||
}
|
||||
|
||||
func (store *Store) imapMailboxStatus(address, mailboxName string, total, unread, unreadSeqNum uint) {
|
||||
func (store *Store) imapMailboxStatus(address, mailboxName string, total, unread, unreadSeqNum uint) *imapBackend.MailboxUpdate {
|
||||
store.log.WithFields(logrus.Fields{
|
||||
"address": address,
|
||||
"mailbox": mailboxName,
|
||||
@ -114,6 +118,7 @@ func (store *Store) imapMailboxStatus(address, mailboxName string, total, unread
|
||||
update.MailboxStatus.Unseen = uint32(unread)
|
||||
update.MailboxStatus.UnseenSeqNum = uint32(unreadSeqNum)
|
||||
store.imapSendUpdate(update)
|
||||
return update
|
||||
}
|
||||
|
||||
func (store *Store) imapSendUpdate(update imapBackend.Update) {
|
||||
|
||||
@ -21,6 +21,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/sirupsen/logrus"
|
||||
@ -38,6 +39,8 @@ type Mailbox struct {
|
||||
color string
|
||||
|
||||
log *logrus.Entry
|
||||
|
||||
isDeleting atomic.Value
|
||||
}
|
||||
|
||||
func newMailbox(storeAddress *Address, labelID, labelPrefix, labelName, color string) (mb *Mailbox, err error) {
|
||||
@ -59,6 +62,7 @@ func txNewMailbox(tx *bolt.Tx, storeAddress *Address, labelID, labelPrefix, labe
|
||||
color: color,
|
||||
log: l,
|
||||
}
|
||||
mb.isDeleting.Store(false)
|
||||
|
||||
err := initMailboxBucket(tx, mb.getBucketName())
|
||||
if err != nil {
|
||||
@ -215,6 +219,7 @@ func (storeMailbox *Mailbox) Rename(newName string) error {
|
||||
// Deletion has to be propagated to all the same mailboxes in all addresses.
|
||||
// The propagation is processed by the event loop.
|
||||
func (storeMailbox *Mailbox) Delete() error {
|
||||
storeMailbox.isDeleting.Store(true)
|
||||
return storeMailbox.storeAddress.deleteMailbox(storeMailbox.labelID)
|
||||
}
|
||||
|
||||
@ -226,6 +231,14 @@ func (storeMailbox *Mailbox) GetDelimiter() string {
|
||||
// deleteMailboxEvent deletes the mailbox bucket.
|
||||
// This is called from the event loop.
|
||||
func (storeMailbox *Mailbox) deleteMailboxEvent() error {
|
||||
if !storeMailbox.isDeleting.Load().(bool) {
|
||||
// Deleting label removes bucket. Any ongoing connection selected
|
||||
// in such mailbox then might panic because of non-existing bucket.
|
||||
// Closing connetions prevents that panic but if the connection
|
||||
// asked for deletion, it should not be closed so it can receive
|
||||
// successful response.
|
||||
storeMailbox.store.user.CloseAllConnections()
|
||||
}
|
||||
return storeMailbox.db().Update(func(tx *bolt.Tx) error {
|
||||
return tx.Bucket(mailboxesBucket).DeleteBucket(storeMailbox.getBucketName())
|
||||
})
|
||||
|
||||
@ -18,6 +18,8 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
@ -501,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))
|
||||
storeMailbox.store.imapUpdateMessage(
|
||||
update := storeMailbox.store.imapUpdateMessage(
|
||||
storeMailbox.storeAddress.address,
|
||||
storeMailbox.labelName,
|
||||
uid,
|
||||
@ -509,6 +511,14 @@ 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
|
||||
|
||||
@ -5,9 +5,10 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
|
||||
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockPanicHandler is a mock of PanicHandler interface
|
||||
@ -105,6 +106,18 @@ func (m *MockBridgeUser) EXPECT() *MockBridgeUserMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// CloseAllConnections mocks base method
|
||||
func (m *MockBridgeUser) CloseAllConnections() {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "CloseAllConnections")
|
||||
}
|
||||
|
||||
// CloseAllConnections indicates an expected call of CloseAllConnections
|
||||
func (mr *MockBridgeUserMockRecorder) CloseAllConnections() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CloseAllConnections", reflect.TypeOf((*MockBridgeUser)(nil).CloseAllConnections))
|
||||
}
|
||||
|
||||
// CloseConnection mocks base method
|
||||
func (m *MockBridgeUser) CloseConnection(arg0 string) {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
@ -5,9 +5,10 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
reflect "reflect"
|
||||
time "time"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
// MockListener is a mock of Listener interface
|
||||
|
||||
@ -36,6 +36,7 @@ type BridgeUser interface {
|
||||
GetPrimaryAddress() string
|
||||
GetStoreAddresses() []string
|
||||
UpdateUser() error
|
||||
CloseAllConnections()
|
||||
CloseConnection(string)
|
||||
Logout() error
|
||||
}
|
||||
|
||||
@ -58,6 +58,7 @@ type MessageStatus struct {
|
||||
targetID string // Message ID at the target (if any).
|
||||
bodyHash string // Hash of the message body.
|
||||
|
||||
skipped bool
|
||||
exported bool
|
||||
imported bool
|
||||
exportErr error
|
||||
@ -96,7 +97,7 @@ func (status *MessageStatus) setDetailsFromHeader(header mail.Header) {
|
||||
}
|
||||
|
||||
func (status *MessageStatus) hasError(includeMissing bool) bool {
|
||||
return status.exportErr != nil || status.importErr != nil || (includeMissing && !status.imported)
|
||||
return status.exportErr != nil || status.importErr != nil || (includeMissing && !status.skipped && !status.imported)
|
||||
}
|
||||
|
||||
// GetErrorMessage returns error message.
|
||||
@ -105,6 +106,9 @@ func (status *MessageStatus) GetErrorMessage() string {
|
||||
}
|
||||
|
||||
func (status *MessageStatus) getErrorMessage(includeMissing bool) string {
|
||||
if status.skipped {
|
||||
return ""
|
||||
}
|
||||
if status.exportErr != nil {
|
||||
return fmt.Sprintf("failed to export: %s", status.exportErr)
|
||||
}
|
||||
|
||||
@ -1,13 +1,16 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/ProtonMail/proton-bridge/internal/transfer (interfaces: PanicHandler,ClientManager)
|
||||
// Source: github.com/ProtonMail/proton-bridge/internal/transfer (interfaces: PanicHandler,ClientManager,IMAPClientProvider)
|
||||
|
||||
// Package mocks is a generated GoMock package.
|
||||
package mocks
|
||||
|
||||
import (
|
||||
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
reflect "reflect"
|
||||
|
||||
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
imap "github.com/emersion/go-imap"
|
||||
sasl "github.com/emersion/go-sasl"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
// MockPanicHandler is a mock of PanicHandler interface
|
||||
@ -95,3 +98,170 @@ func (mr *MockClientManagerMockRecorder) GetClient(arg0 interface{}) *gomock.Cal
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClient", reflect.TypeOf((*MockClientManager)(nil).GetClient), arg0)
|
||||
}
|
||||
|
||||
// MockIMAPClientProvider is a mock of IMAPClientProvider interface
|
||||
type MockIMAPClientProvider struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockIMAPClientProviderMockRecorder
|
||||
}
|
||||
|
||||
// MockIMAPClientProviderMockRecorder is the mock recorder for MockIMAPClientProvider
|
||||
type MockIMAPClientProviderMockRecorder struct {
|
||||
mock *MockIMAPClientProvider
|
||||
}
|
||||
|
||||
// NewMockIMAPClientProvider creates a new mock instance
|
||||
func NewMockIMAPClientProvider(ctrl *gomock.Controller) *MockIMAPClientProvider {
|
||||
mock := &MockIMAPClientProvider{ctrl: ctrl}
|
||||
mock.recorder = &MockIMAPClientProviderMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockIMAPClientProvider) EXPECT() *MockIMAPClientProviderMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Authenticate mocks base method
|
||||
func (m *MockIMAPClientProvider) Authenticate(arg0 sasl.Client) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Authenticate", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Authenticate indicates an expected call of Authenticate
|
||||
func (mr *MockIMAPClientProviderMockRecorder) Authenticate(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Authenticate", reflect.TypeOf((*MockIMAPClientProvider)(nil).Authenticate), arg0)
|
||||
}
|
||||
|
||||
// Capability mocks base method
|
||||
func (m *MockIMAPClientProvider) Capability() (map[string]bool, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Capability")
|
||||
ret0, _ := ret[0].(map[string]bool)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Capability indicates an expected call of Capability
|
||||
func (mr *MockIMAPClientProviderMockRecorder) Capability() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Capability", reflect.TypeOf((*MockIMAPClientProvider)(nil).Capability))
|
||||
}
|
||||
|
||||
// Fetch mocks base method
|
||||
func (m *MockIMAPClientProvider) Fetch(arg0 *imap.SeqSet, arg1 []imap.FetchItem, arg2 chan *imap.Message) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Fetch", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Fetch indicates an expected call of Fetch
|
||||
func (mr *MockIMAPClientProviderMockRecorder) Fetch(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fetch", reflect.TypeOf((*MockIMAPClientProvider)(nil).Fetch), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// List mocks base method
|
||||
func (m *MockIMAPClientProvider) List(arg0, arg1 string, arg2 chan *imap.MailboxInfo) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "List", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// List indicates an expected call of List
|
||||
func (mr *MockIMAPClientProviderMockRecorder) List(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockIMAPClientProvider)(nil).List), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// Login mocks base method
|
||||
func (m *MockIMAPClientProvider) Login(arg0, arg1 string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Login", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Login indicates an expected call of Login
|
||||
func (mr *MockIMAPClientProviderMockRecorder) Login(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Login", reflect.TypeOf((*MockIMAPClientProvider)(nil).Login), arg0, arg1)
|
||||
}
|
||||
|
||||
// Select mocks base method
|
||||
func (m *MockIMAPClientProvider) Select(arg0 string, arg1 bool) (*imap.MailboxStatus, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Select", arg0, arg1)
|
||||
ret0, _ := ret[0].(*imap.MailboxStatus)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Select indicates an expected call of Select
|
||||
func (mr *MockIMAPClientProviderMockRecorder) Select(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Select", reflect.TypeOf((*MockIMAPClientProvider)(nil).Select), arg0, arg1)
|
||||
}
|
||||
|
||||
// State mocks base method
|
||||
func (m *MockIMAPClientProvider) State() imap.ConnState {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "State")
|
||||
ret0, _ := ret[0].(imap.ConnState)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// State indicates an expected call of State
|
||||
func (mr *MockIMAPClientProviderMockRecorder) State() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "State", reflect.TypeOf((*MockIMAPClientProvider)(nil).State))
|
||||
}
|
||||
|
||||
// Support mocks base method
|
||||
func (m *MockIMAPClientProvider) Support(arg0 string) (bool, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Support", arg0)
|
||||
ret0, _ := ret[0].(bool)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Support indicates an expected call of Support
|
||||
func (mr *MockIMAPClientProviderMockRecorder) Support(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Support", reflect.TypeOf((*MockIMAPClientProvider)(nil).Support), arg0)
|
||||
}
|
||||
|
||||
// SupportAuth mocks base method
|
||||
func (m *MockIMAPClientProvider) SupportAuth(arg0 string) (bool, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SupportAuth", arg0)
|
||||
ret0, _ := ret[0].(bool)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// SupportAuth indicates an expected call of SupportAuth
|
||||
func (mr *MockIMAPClientProviderMockRecorder) SupportAuth(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SupportAuth", reflect.TypeOf((*MockIMAPClientProvider)(nil).SupportAuth), arg0)
|
||||
}
|
||||
|
||||
// UidFetch mocks base method
|
||||
func (m *MockIMAPClientProvider) UidFetch(arg0 *imap.SeqSet, arg1 []imap.FetchItem, arg2 chan *imap.Message) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UidFetch", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// UidFetch indicates an expected call of UidFetch
|
||||
func (mr *MockIMAPClientProviderMockRecorder) UidFetch(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UidFetch", reflect.TypeOf((*MockIMAPClientProvider)(nil).UidFetch), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
@ -140,6 +140,19 @@ func (p *Progress) addMessage(messageID string, sourceNames, targetNames []strin
|
||||
}
|
||||
}
|
||||
|
||||
// messageSkipped should be called once the message is skipped due to some
|
||||
// filter such as time or folder and so on.
|
||||
func (p *Progress) messageSkipped(messageID string) {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
defer p.update()
|
||||
|
||||
p.log.WithField("id", messageID).Debug("Message skipped")
|
||||
|
||||
p.messageStatuses[messageID].skipped = true
|
||||
p.logMessage(messageID)
|
||||
}
|
||||
|
||||
// messageExported should be called right before message is exported.
|
||||
func (p *Progress) messageExported(messageID string, body []byte, err error) {
|
||||
p.lock.Lock()
|
||||
@ -330,35 +343,40 @@ func (p *Progress) GetFailedMessages() []*MessageStatus {
|
||||
}
|
||||
|
||||
// GetCounts returns counts of exported and imported messages.
|
||||
func (p *Progress) GetCounts() (failed, imported, exported, added, total uint) {
|
||||
func (p *Progress) GetCounts() ProgressCounts {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
|
||||
counts := ProgressCounts{}
|
||||
|
||||
// Return counts only once total is estimated or the process already
|
||||
// ended (for a case when it ended quickly to report it correctly).
|
||||
if p.updateCh != nil && !p.messageCounted {
|
||||
return
|
||||
return counts
|
||||
}
|
||||
|
||||
// Include lost messages in the process only when transfer is done.
|
||||
includeMissing := p.updateCh == nil
|
||||
|
||||
for _, mailboxCount := range p.messageCounts {
|
||||
total += mailboxCount
|
||||
counts.Total += mailboxCount
|
||||
}
|
||||
for _, status := range p.messageStatuses {
|
||||
added++
|
||||
counts.Added++
|
||||
if status.skipped {
|
||||
counts.Skipped++
|
||||
}
|
||||
if status.exported {
|
||||
exported++
|
||||
counts.Exported++
|
||||
}
|
||||
if status.imported {
|
||||
imported++
|
||||
counts.Imported++
|
||||
}
|
||||
if status.hasError(includeMissing) {
|
||||
failed++
|
||||
counts.Failed++
|
||||
}
|
||||
}
|
||||
return
|
||||
return counts
|
||||
}
|
||||
|
||||
// GenerateBugReport generates similar file to import log except private information.
|
||||
|
||||
35
internal/transfer/progress_counts.go
Normal file
35
internal/transfer/progress_counts.go
Normal file
@ -0,0 +1,35 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
// ProtonMail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ProtonMail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package transfer
|
||||
|
||||
// ProgressCounts holds counts counted by Progress.
|
||||
type ProgressCounts struct {
|
||||
Failed,
|
||||
Skipped,
|
||||
Imported,
|
||||
Exported,
|
||||
Added,
|
||||
Total uint
|
||||
}
|
||||
|
||||
// Progress returns ratio between processed messages (fully imported, skipped
|
||||
// and failed ones) and total number of messages as percentage (0 - 1).
|
||||
func (c *ProgressCounts) Progress() float32 {
|
||||
progressed := c.Imported + c.Skipped + c.Failed
|
||||
return float32(progressed) / float32(c.Total)
|
||||
}
|
||||
@ -39,8 +39,8 @@ func TestProgressUpdateCount(t *testing.T) {
|
||||
|
||||
progress.finish()
|
||||
|
||||
_, _, _, _, total := progress.GetCounts() //nolint[dogsled]
|
||||
r.Equal(t, uint(42), total)
|
||||
counts := progress.GetCounts()
|
||||
r.Equal(t, uint(42), counts.Total)
|
||||
}
|
||||
|
||||
func TestProgressAddingMessages(t *testing.T) {
|
||||
@ -66,13 +66,18 @@ func TestProgressAddingMessages(t *testing.T) {
|
||||
progress.messageExported("msg4", []byte(""), errors.New("failed export"))
|
||||
progress.messageImported("msg4", "", nil)
|
||||
|
||||
// msg5 is skipped.
|
||||
progress.addMessage("msg5", []string{}, []string{})
|
||||
progress.messageSkipped("msg5")
|
||||
|
||||
progress.finish()
|
||||
|
||||
failed, imported, exported, added, _ := progress.GetCounts()
|
||||
a.Equal(t, uint(4), added)
|
||||
a.Equal(t, uint(2), exported)
|
||||
a.Equal(t, uint(2), imported)
|
||||
a.Equal(t, uint(3), failed)
|
||||
counts := progress.GetCounts()
|
||||
a.Equal(t, uint(5), counts.Added)
|
||||
a.Equal(t, uint(2), counts.Exported)
|
||||
a.Equal(t, uint(2), counts.Imported)
|
||||
a.Equal(t, uint(1), counts.Skipped)
|
||||
a.Equal(t, uint(3), counts.Failed)
|
||||
|
||||
errorsMap := map[string]string{}
|
||||
for _, status := range progress.GetFailedMessages() {
|
||||
|
||||
@ -82,8 +82,6 @@ func (p *EMLProvider) getFilePathsPerFolder(rules transferRules) (map[string][]s
|
||||
}
|
||||
|
||||
func (p *EMLProvider) exportMessages(rule *Rule, filePaths []string, progress *Progress, ch chan<- Message) {
|
||||
count := uint(len(filePaths))
|
||||
|
||||
for _, filePath := range filePaths {
|
||||
if progress.shouldStop() {
|
||||
break
|
||||
@ -91,6 +89,8 @@ func (p *EMLProvider) exportMessages(rule *Rule, filePaths []string, progress *P
|
||||
|
||||
msg, err := p.exportMessage(rule, filePath)
|
||||
|
||||
progress.addMessage(filePath, msg.sourceNames(), msg.targetNames())
|
||||
|
||||
// Read and check time in body only if the rule specifies it
|
||||
// to not waste energy.
|
||||
if err == nil && rule.HasTimeLimit() {
|
||||
@ -99,17 +99,11 @@ func (p *EMLProvider) exportMessages(rule *Rule, filePaths []string, progress *P
|
||||
err = msgTimeErr
|
||||
} else if !rule.isTimeInRange(msgTime) {
|
||||
log.WithField("msg", filePath).Debug("Message skipped due to time")
|
||||
|
||||
count--
|
||||
progress.updateCount(rule.SourceMailbox.Name, count)
|
||||
progress.messageSkipped(filePath)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// addMessage is called after time check to not report message
|
||||
// which should not be exported but any error from reading body
|
||||
// or parsing time is reported as an error.
|
||||
progress.addMessage(filePath, msg.sourceNames(), msg.targetNames())
|
||||
progress.messageExported(filePath, msg.Body, err)
|
||||
if err == nil {
|
||||
ch <- msg
|
||||
|
||||
@ -21,28 +21,49 @@ import (
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
imapClient "github.com/emersion/go-imap/client"
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-sasl"
|
||||
)
|
||||
|
||||
type IMAPClientProvider interface {
|
||||
Capability() (map[string]bool, error)
|
||||
Support(cap string) (bool, error)
|
||||
State() imap.ConnState
|
||||
SupportAuth(mech string) (bool, error)
|
||||
Authenticate(auth sasl.Client) error
|
||||
Login(username, password string) error
|
||||
List(ref, name string, ch chan *imap.MailboxInfo) error
|
||||
Select(name string, readOnly bool) (*imap.MailboxStatus, error)
|
||||
Fetch(seqset *imap.SeqSet, items []imap.FetchItem, ch chan *imap.Message) error
|
||||
UidFetch(seqset *imap.SeqSet, items []imap.FetchItem, ch chan *imap.Message) error
|
||||
}
|
||||
|
||||
// IMAPProvider implements export from IMAP server.
|
||||
type IMAPProvider struct {
|
||||
username string
|
||||
password string
|
||||
addr string
|
||||
|
||||
client *imapClient.Client
|
||||
clientDialer func(addr string) (IMAPClientProvider, error)
|
||||
client IMAPClientProvider
|
||||
|
||||
timeIt *timeIt
|
||||
}
|
||||
|
||||
// NewIMAPProvider returns new IMAPProvider.
|
||||
func NewIMAPProvider(username, password, host, port string) (*IMAPProvider, error) {
|
||||
return newIMAPProvider(imapClientDial, username, password, host, port)
|
||||
}
|
||||
|
||||
func newIMAPProvider(clientDialer func(string) (IMAPClientProvider, error), username, password, host, port string) (*IMAPProvider, error) {
|
||||
p := &IMAPProvider{
|
||||
username: username,
|
||||
password: password,
|
||||
addr: net.JoinHostPort(host, port),
|
||||
|
||||
timeIt: newTimeIt("imap"),
|
||||
|
||||
clientDialer: clientDialer,
|
||||
}
|
||||
|
||||
if err := p.auth(); err != nil {
|
||||
|
||||
@ -84,12 +84,37 @@ func (p *IMAPProvider) loadMessagesInfo(rule *Rule, progress *Progress, uidValid
|
||||
p.timeIt.start("load", rule.SourceMailbox.Name)
|
||||
defer p.timeIt.stop("load", rule.SourceMailbox.Name)
|
||||
|
||||
log := log.WithField("mailbox", rule.SourceMailbox.Name)
|
||||
messagesInfo := map[string]imapMessageInfo{}
|
||||
|
||||
fetchItems := []imap.FetchItem{imap.FetchUid, imap.FetchRFC822Size}
|
||||
if rule.HasTimeLimit() {
|
||||
fetchItems = append(fetchItems, imap.FetchEnvelope)
|
||||
}
|
||||
|
||||
processMessageCallback := func(imapMessage *imap.Message) {
|
||||
if rule.HasTimeLimit() {
|
||||
t := imapMessage.Envelope.Date.Unix()
|
||||
if t != 0 && !rule.isTimeInRange(t) {
|
||||
log.WithField("uid", imapMessage.Uid).Debug("Message skipped due to time")
|
||||
return
|
||||
}
|
||||
}
|
||||
id := getUniqueMessageID(rule.SourceMailbox.Name, uidValidity, imapMessage.Uid)
|
||||
// We use ID as key to ensure we have every unique message only once.
|
||||
// Some IMAP servers responded twice the same message...
|
||||
messagesInfo[id] = imapMessageInfo{
|
||||
id: id,
|
||||
uid: imapMessage.Uid,
|
||||
size: imapMessage.Size,
|
||||
}
|
||||
progress.addMessage(id, []string{rule.SourceMailbox.Name}, rule.TargetMailboxNames())
|
||||
}
|
||||
|
||||
pageStart := uint32(1)
|
||||
pageEnd := imapPageSize
|
||||
for {
|
||||
if progress.shouldStop() {
|
||||
if progress.shouldStop() || pageStart > count {
|
||||
break
|
||||
}
|
||||
|
||||
@ -100,45 +125,21 @@ func (p *IMAPProvider) loadMessagesInfo(rule *Rule, progress *Progress, uidValid
|
||||
|
||||
seqSet := &imap.SeqSet{}
|
||||
seqSet.AddRange(pageStart, pageEnd)
|
||||
|
||||
items := []imap.FetchItem{imap.FetchUid, imap.FetchRFC822Size}
|
||||
if rule.HasTimeLimit() {
|
||||
items = append(items, imap.FetchEnvelope)
|
||||
}
|
||||
|
||||
pageMsgCount := uint32(0)
|
||||
processMessageCallback := func(imapMessage *imap.Message) {
|
||||
pageMsgCount++
|
||||
if rule.HasTimeLimit() {
|
||||
t := imapMessage.Envelope.Date.Unix()
|
||||
if t != 0 && !rule.isTimeInRange(t) {
|
||||
log.WithField("uid", imapMessage.Uid).Debug("Message skipped due to time")
|
||||
return
|
||||
err := p.fetch(rule.SourceMailbox.Name, seqSet, fetchItems, processMessageCallback)
|
||||
if err != nil {
|
||||
log.WithError(err).WithField("idx", seqSet).Warning("Load batch fetch failed, trying one by one")
|
||||
for ; pageStart <= pageEnd; pageStart++ {
|
||||
seqSet := &imap.SeqSet{}
|
||||
seqSet.AddNum(pageStart)
|
||||
if err := p.fetch(rule.SourceMailbox.Name, seqSet, fetchItems, processMessageCallback); err != nil {
|
||||
log.WithError(err).WithField("idx", seqSet).Warning("Load fetch failed, skipping the message")
|
||||
}
|
||||
}
|
||||
id := getUniqueMessageID(rule.SourceMailbox.Name, uidValidity, imapMessage.Uid)
|
||||
// We use ID as key to ensure we have every unique message only once.
|
||||
// Some IMAP servers responded twice the same message...
|
||||
messagesInfo[id] = imapMessageInfo{
|
||||
id: id,
|
||||
uid: imapMessage.Uid,
|
||||
size: imapMessage.Size,
|
||||
}
|
||||
progress.addMessage(id, []string{rule.SourceMailbox.Name}, rule.TargetMailboxNames())
|
||||
}
|
||||
|
||||
progress.callWrap(func() error {
|
||||
return p.fetch(rule.SourceMailbox.Name, seqSet, items, processMessageCallback)
|
||||
})
|
||||
|
||||
if pageMsgCount < imapPageSize {
|
||||
break
|
||||
}
|
||||
|
||||
pageStart = pageEnd
|
||||
pageStart = pageEnd + 1
|
||||
pageEnd += imapPageSize
|
||||
}
|
||||
|
||||
return messagesInfo
|
||||
}
|
||||
|
||||
|
||||
100
internal/transfer/provider_imap_test.go
Normal file
100
internal/transfer/provider_imap_test.go
Normal file
@ -0,0 +1,100 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
// ProtonMail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ProtonMail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package transfer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
"github.com/pkg/errors"
|
||||
r "github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func newTestIMAPProvider(t *testing.T, m mocks) *IMAPProvider {
|
||||
m.imapClientProvider.EXPECT().State().Return(imap.ConnectedState).AnyTimes()
|
||||
m.imapClientProvider.EXPECT().Capability().Return(map[string]bool{
|
||||
"AUTH": true,
|
||||
}, nil).AnyTimes()
|
||||
|
||||
dialer := func(string) (IMAPClientProvider, error) {
|
||||
return m.imapClientProvider, nil
|
||||
}
|
||||
provider, err := newIMAPProvider(dialer, "user", "pass", "host", "42")
|
||||
r.NoError(t, err)
|
||||
return provider
|
||||
}
|
||||
|
||||
func TestProviderIMAPLoadMessagesInfo(t *testing.T) {
|
||||
m := initMocks(t)
|
||||
defer m.ctrl.Finish()
|
||||
|
||||
provider := newTestIMAPProvider(t, m)
|
||||
|
||||
progress := newProgress(log, nil)
|
||||
drainProgressUpdateChannel(&progress)
|
||||
|
||||
rule := &Rule{SourceMailbox: Mailbox{Name: "Mailbox"}}
|
||||
uidValidity := 1
|
||||
count := 2200
|
||||
failingIndex := 2100
|
||||
|
||||
m.imapClientProvider.EXPECT().Select(rule.SourceMailbox.Name, gomock.Any()).Return(&imap.MailboxStatus{}, nil).AnyTimes()
|
||||
m.imapClientProvider.EXPECT().
|
||||
Fetch(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
DoAndReturn(func(seqSet *imap.SeqSet, items []imap.FetchItem, ch chan *imap.Message) error {
|
||||
defer close(ch)
|
||||
for _, seq := range seqSet.Set {
|
||||
for i := seq.Start; i <= seq.Stop; i++ {
|
||||
if int(i) == failingIndex {
|
||||
return errors.New("internal server error")
|
||||
}
|
||||
ch <- &imap.Message{
|
||||
SeqNum: i,
|
||||
Uid: i * 10,
|
||||
Size: i * 100,
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}).
|
||||
// 2200 messages is split into two batches (2000 and 200),
|
||||
// the second one fails and makes 200 calls (one-by-one).
|
||||
// Plus two failed requests are repeated `imapRetries` times.
|
||||
Times(2 + 200 + (2 * (imapRetries - 1)))
|
||||
|
||||
messageInfo := provider.loadMessagesInfo(rule, &progress, uint32(uidValidity), uint32(count))
|
||||
|
||||
r.Equal(t, count-1, len(messageInfo)) // One message produces internal server error.
|
||||
for index := 1; index <= count; index++ {
|
||||
uid := index * 10
|
||||
key := fmt.Sprintf("%s_%d:%d", rule.SourceMailbox.Name, uidValidity, uid)
|
||||
|
||||
if index == failingIndex {
|
||||
r.Empty(t, messageInfo[key])
|
||||
continue
|
||||
}
|
||||
|
||||
r.Equal(t, imapMessageInfo{
|
||||
id: key,
|
||||
uid: uint32(uid),
|
||||
size: uint32(index * 100),
|
||||
}, messageInfo[key])
|
||||
}
|
||||
}
|
||||
@ -24,10 +24,11 @@ import (
|
||||
"time"
|
||||
|
||||
imapID "github.com/ProtonMail/go-imap-id"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/constants"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/emersion/go-imap"
|
||||
imapClient "github.com/emersion/go-imap/client"
|
||||
sasl "github.com/emersion/go-sasl"
|
||||
"github.com/emersion/go-sasl"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
@ -51,6 +52,43 @@ func (l *imapErrorLogger) Println(v ...interface{}) {
|
||||
l.log.Errorln(v...)
|
||||
}
|
||||
|
||||
func imapClientDial(addr string) (IMAPClientProvider, error) {
|
||||
if _, err := net.DialTimeout("tcp", addr, imapDialTimeout); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to dial server")
|
||||
}
|
||||
|
||||
client, err := imapClientDialHelper(addr)
|
||||
if err == nil {
|
||||
client.ErrorLog = &imapErrorLogger{logrus.WithField("pkg", "imap-client")}
|
||||
// Logrus `WriterLevel` fails for big messages because of bufio.MaxScanTokenSize limit.
|
||||
// Also, this spams a lot, uncomment once needed during development.
|
||||
//client.SetDebug(imap.NewDebugWriter(
|
||||
// logrus.WithField("pkg", "imap/client").WriterLevel(logrus.TraceLevel),
|
||||
// logrus.WithField("pkg", "imap/server").WriterLevel(logrus.TraceLevel),
|
||||
//))
|
||||
}
|
||||
return client, err
|
||||
}
|
||||
|
||||
func imapClientDialHelper(addr string) (*imapClient.Client, error) {
|
||||
host, _, _ := net.SplitHostPort(addr)
|
||||
if host == "127.0.0.1" {
|
||||
return imapClient.Dial(addr)
|
||||
}
|
||||
|
||||
// IMAP mail.yahoo.com has problem with golang TLS 1.3 implementation
|
||||
// with weird behaviour, i.e., Yahoo does not return error during dial
|
||||
// or handshake but server does logs out right after successful login
|
||||
// leaving no time to perform any action.
|
||||
// Limiting TLS to version 1.2 is working just fine.
|
||||
var tlsConf *tls.Config
|
||||
if strings.Contains(strings.ToLower(host), "yahoo") {
|
||||
log.Warning("Yahoo server detected: limiting maximal TLS version to 1.2.")
|
||||
tlsConf = &tls.Config{MaxVersion: tls.VersionTLS12}
|
||||
}
|
||||
return imapClient.DialTLS(addr, tlsConf)
|
||||
}
|
||||
|
||||
func (p *IMAPProvider) ensureConnection(callback func() error) error {
|
||||
return p.ensureConnectionAndSelection(callback, "")
|
||||
}
|
||||
@ -138,41 +176,10 @@ func (p *IMAPProvider) auth() error { //nolint[funlen]
|
||||
|
||||
log.Info("Connecting to server")
|
||||
|
||||
if _, err := net.DialTimeout("tcp", p.addr, imapDialTimeout); err != nil {
|
||||
return ErrIMAPConnection{imapError{Err: err, Message: "failed to dial server"}}
|
||||
}
|
||||
|
||||
var client *imapClient.Client
|
||||
var err error
|
||||
host, _, _ := net.SplitHostPort(p.addr)
|
||||
if host == "127.0.0.1" {
|
||||
client, err = imapClient.Dial(p.addr)
|
||||
} else {
|
||||
// IMAP.mail.yahoo.com have problem with golang TLS1.3
|
||||
// implementation with weird behaviour i.e. Yahoo
|
||||
// no error during dial or handshake but server logs out right
|
||||
// after successful login leaving no time to perform any
|
||||
// action. It was discovered that limiting to maximum TLS
|
||||
// version 1.2 for yahoo servers is working solution.
|
||||
|
||||
var tlsConf *tls.Config
|
||||
if strings.Contains(strings.ToLower(host), "yahoo") {
|
||||
log.Warning("Yahoo server detected: limiting maximal TLS version to 1.2.")
|
||||
tlsConf = &tls.Config{MaxVersion: tls.VersionTLS12}
|
||||
}
|
||||
client, err = imapClient.DialTLS(p.addr, tlsConf)
|
||||
}
|
||||
client, err := p.clientDialer(p.addr)
|
||||
if err != nil {
|
||||
return ErrIMAPConnection{imapError{Err: err, Message: "failed to connect to server"}}
|
||||
}
|
||||
|
||||
client.ErrorLog = &imapErrorLogger{logrus.WithField("pkg", "imap-client")}
|
||||
// Logrus `WriterLevel` fails for big messages because of bufio.MaxScanTokenSize limit.
|
||||
// Also, this spams a lot, uncomment once needed during development.
|
||||
//client.SetDebug(imap.NewDebugWriter(
|
||||
// logrus.WithField("pkg", "imap/client").WriterLevel(logrus.TraceLevel),
|
||||
// logrus.WithField("pkg", "imap/server").WriterLevel(logrus.TraceLevel),
|
||||
//))
|
||||
p.client = client
|
||||
|
||||
log.Info("Connected")
|
||||
@ -210,13 +217,15 @@ func (p *IMAPProvider) auth() error { //nolint[funlen]
|
||||
|
||||
log.Info("Logged in")
|
||||
|
||||
idClient := imapID.NewClient(p.client)
|
||||
if ok, err := idClient.SupportID(); err == nil && ok {
|
||||
serverID, err := idClient.ID(imapID.ID{
|
||||
imapID.FieldName: "ImportExport",
|
||||
imapID.FieldVersion: "beta",
|
||||
})
|
||||
log.WithField("ID", serverID).WithError(err).Debug("Server info")
|
||||
if c, ok := p.client.(*imapClient.Client); ok {
|
||||
idClient := imapID.NewClient(c)
|
||||
if ok, err := idClient.SupportID(); err == nil && ok {
|
||||
serverID, err := idClient.ID(imapID.ID{
|
||||
imapID.FieldName: "ImportExport",
|
||||
imapID.FieldVersion: constants.Version,
|
||||
})
|
||||
log.WithField("ID", serverID).WithError(err).Debug("Server info")
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
|
||||
@ -114,7 +114,6 @@ func (p *MBOXProvider) transferTo(rules transferRules, progress *Progress, ch ch
|
||||
}
|
||||
|
||||
index := 0
|
||||
count := 0
|
||||
for {
|
||||
if progress.shouldStop() {
|
||||
break
|
||||
@ -133,24 +132,18 @@ func (p *MBOXProvider) transferTo(rules transferRules, progress *Progress, ch ch
|
||||
|
||||
msg, err := p.exportMessage(rules, folderName, id, msgReader)
|
||||
|
||||
progress.addMessage(id, msg.sourceNames(), msg.targetNames())
|
||||
|
||||
if err == nil && len(msg.Targets) == 0 {
|
||||
// Here should be called progress.messageSkipped(id) once we have
|
||||
// this feature, and following progress.updateCount can be removed.
|
||||
progress.messageSkipped(id)
|
||||
continue
|
||||
}
|
||||
|
||||
count++
|
||||
|
||||
// addMessage is called after time check to not report message
|
||||
// which should not be exported but any error from reading body
|
||||
// or parsing time is reported as an error.
|
||||
progress.addMessage(id, msg.sourceNames(), msg.targetNames())
|
||||
progress.messageExported(id, msg.Body, err)
|
||||
if err == nil {
|
||||
ch <- msg
|
||||
}
|
||||
}
|
||||
progress.updateCount(filePath, uint(count))
|
||||
}
|
||||
|
||||
func (p *MBOXProvider) exportMessage(rules transferRules, folderName, id string, msgReader io.Reader) (Message, error) {
|
||||
@ -177,7 +170,7 @@ func (p *MBOXProvider) getMessageRules(rules transferRules, folderName, id strin
|
||||
folderRule, err := rules.getRuleBySourceMailboxName(folderName)
|
||||
if err != nil {
|
||||
log.WithField("msg", id).WithField("source", folderName).Debug("Message source doesn't have a rule")
|
||||
} else {
|
||||
} else if folderRule.Active {
|
||||
msgRules = append(msgRules, folderRule)
|
||||
}
|
||||
|
||||
@ -191,7 +184,9 @@ func (p *MBOXProvider) getMessageRules(rules transferRules, folderName, id strin
|
||||
log.WithField("msg", id).WithField("source", label).Debug("Message source doesn't have a rule")
|
||||
continue
|
||||
}
|
||||
msgRules = append(msgRules, rule)
|
||||
if rule.Active {
|
||||
msgRules = append(msgRules, rule)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -155,6 +155,29 @@ func TestMBOXProviderTransferFromTo(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestMBOXProviderGetMessageRules(t *testing.T) {
|
||||
provider := newTestMBOXProvider("")
|
||||
|
||||
body := []byte(`Subject: Test
|
||||
X-Gmail-Labels: foo,bar
|
||||
|
||||
`)
|
||||
rules := transferRules{
|
||||
rules: map[string]*Rule{
|
||||
"1": {Active: true, SourceMailbox: Mailbox{Name: "folder"}},
|
||||
"2": {Active: false, SourceMailbox: Mailbox{Name: "foo"}},
|
||||
"3": {Active: true, SourceMailbox: Mailbox{Name: "bar"}},
|
||||
"4": {Active: false, SourceMailbox: Mailbox{Name: "baz"}},
|
||||
"5": {Active: true, SourceMailbox: Mailbox{Name: "other"}},
|
||||
},
|
||||
}
|
||||
|
||||
gotRules := provider.getMessageRules(rules, "folder", "id", body)
|
||||
r.Equal(t, 2, len(gotRules))
|
||||
r.Equal(t, "folder", gotRules[0].SourceMailbox.Name)
|
||||
r.Equal(t, "bar", gotRules[1].SourceMailbox.Name)
|
||||
}
|
||||
|
||||
func TestMBOXProviderGetMessageTargetsReturnsOnlyOneFolder(t *testing.T) {
|
||||
provider := newTestMBOXProvider("")
|
||||
|
||||
|
||||
@ -31,11 +31,12 @@ import (
|
||||
type mocks struct {
|
||||
t *testing.T
|
||||
|
||||
ctrl *gomock.Controller
|
||||
panicHandler *transfermocks.MockPanicHandler
|
||||
clientManager *transfermocks.MockClientManager
|
||||
pmapiClient *pmapimocks.MockClient
|
||||
pmapiConfig *pmapi.ClientConfig
|
||||
ctrl *gomock.Controller
|
||||
panicHandler *transfermocks.MockPanicHandler
|
||||
clientManager *transfermocks.MockClientManager
|
||||
imapClientProvider *transfermocks.MockIMAPClientProvider
|
||||
pmapiClient *pmapimocks.MockClient
|
||||
pmapiConfig *pmapi.ClientConfig
|
||||
|
||||
keyring *crypto.KeyRing
|
||||
}
|
||||
@ -46,12 +47,13 @@ func initMocks(t *testing.T) mocks {
|
||||
m := mocks{
|
||||
t: t,
|
||||
|
||||
ctrl: mockCtrl,
|
||||
panicHandler: transfermocks.NewMockPanicHandler(mockCtrl),
|
||||
clientManager: transfermocks.NewMockClientManager(mockCtrl),
|
||||
pmapiClient: pmapimocks.NewMockClient(mockCtrl),
|
||||
pmapiConfig: &pmapi.ClientConfig{},
|
||||
keyring: newTestKeyring(),
|
||||
ctrl: mockCtrl,
|
||||
panicHandler: transfermocks.NewMockPanicHandler(mockCtrl),
|
||||
clientManager: transfermocks.NewMockClientManager(mockCtrl),
|
||||
imapClientProvider: transfermocks.NewMockIMAPClientProvider(mockCtrl),
|
||||
pmapiClient: pmapimocks.NewMockClient(mockCtrl),
|
||||
pmapiConfig: &pmapi.ClientConfig{},
|
||||
keyring: newTestKeyring(),
|
||||
}
|
||||
|
||||
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).AnyTimes()
|
||||
|
||||
@ -28,6 +28,7 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/go-rfc5322"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@ -136,14 +137,21 @@ func getFilePathsWithSuffixInner(prefix, root, suffix string, includeDir bool) (
|
||||
|
||||
// getMessageTime returns time of the message specified in the message header.
|
||||
func getMessageTime(body []byte) (int64, error) {
|
||||
mailHeader, err := getMessageHeader(body)
|
||||
hdr, err := getMessageHeader(body)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if t, err := mailHeader.Date(); err == nil && !t.IsZero() {
|
||||
return t.Unix(), nil
|
||||
|
||||
t, err := rfc5322.ParseDateTime(hdr.Get("Date"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return 0, nil
|
||||
|
||||
if t.IsZero() {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return t.Unix(), nil
|
||||
}
|
||||
|
||||
// getMessageHeader returns headers of the message body.
|
||||
|
||||
@ -5,11 +5,12 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
|
||||
store "github.com/ProtonMail/proton-bridge/internal/store"
|
||||
credentials "github.com/ProtonMail/proton-bridge/internal/users/credentials"
|
||||
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockConfiger is a mock of Configer interface
|
||||
|
||||
@ -437,7 +437,7 @@ func (u *User) SwitchAddressMode() (err error) {
|
||||
|
||||
u.lock.Lock()
|
||||
defer u.lock.Unlock()
|
||||
u.closeAllConnections()
|
||||
u.CloseAllConnections()
|
||||
|
||||
if u.store == nil {
|
||||
err = errors.New("store is not initialised")
|
||||
@ -509,7 +509,7 @@ func (u *User) Logout() (err error) {
|
||||
// Do not close whole store, just event loop. Some information might be needed offline (e.g. addressID)
|
||||
u.closeEventLoop()
|
||||
|
||||
u.closeAllConnections()
|
||||
u.CloseAllConnections()
|
||||
|
||||
runtime.GC()
|
||||
|
||||
@ -532,8 +532,8 @@ func (u *User) closeEventLoop() {
|
||||
u.store.CloseEventLoop()
|
||||
}
|
||||
|
||||
// closeAllConnections calls CloseConnection for all users addresses.
|
||||
func (u *User) closeAllConnections() {
|
||||
// CloseAllConnections calls CloseConnection for all users addresses.
|
||||
func (u *User) CloseAllConnections() {
|
||||
for _, address := range u.creds.EmailList() {
|
||||
u.CloseConnection(address)
|
||||
}
|
||||
|
||||
@ -186,7 +186,7 @@ func (u *Users) watchAPIAuths() {
|
||||
|
||||
func (u *Users) closeAllConnections() {
|
||||
for _, user := range u.users {
|
||||
user.closeAllConnections()
|
||||
user.CloseAllConnections()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -21,6 +21,7 @@ import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/ProtonMail/go-appdir"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
@ -247,3 +248,17 @@ func (c *Config) GetDefaultIMAPPort() int {
|
||||
func (c *Config) GetDefaultSMTPPort() int {
|
||||
return 1025
|
||||
}
|
||||
|
||||
// getAPIOS returns actual operating system.
|
||||
func (c *Config) getAPIOS() string {
|
||||
switch os := runtime.GOOS; os {
|
||||
case "darwin": // nolint: goconst
|
||||
return "macOS"
|
||||
case "linux":
|
||||
return "Linux"
|
||||
case "windows":
|
||||
return "Windows"
|
||||
}
|
||||
|
||||
return "Linux"
|
||||
}
|
||||
|
||||
@ -29,7 +29,7 @@ import (
|
||||
|
||||
func (c *Config) GetAPIConfig() *pmapi.ClientConfig {
|
||||
return &pmapi.ClientConfig{
|
||||
AppVersion: strings.Title(c.appName) + "_" + c.version,
|
||||
AppVersion: c.getAPIOS() + strings.Title(c.appName) + "_" + c.version,
|
||||
ClientID: c.appName,
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,7 +31,7 @@ import (
|
||||
|
||||
func (c *Config) GetAPIConfig() *pmapi.ClientConfig {
|
||||
return &pmapi.ClientConfig{
|
||||
AppVersion: strings.Title(c.appName) + "_" + c.version,
|
||||
AppVersion: c.getAPIOS() + strings.Title(c.appName) + "_" + c.version,
|
||||
ClientID: c.appName,
|
||||
Timeout: 25 * time.Minute, // Overall request timeout (~25MB / 25 mins => ~16kB/s, should be reasonable).
|
||||
FirstReadTimeout: 30 * time.Second, // 30s to match 30s response header timeout.
|
||||
|
||||
@ -19,9 +19,7 @@ package message
|
||||
|
||||
import (
|
||||
"mime"
|
||||
"net/mail"
|
||||
"net/textproto"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -86,10 +84,6 @@ func GetHeader(msg *pmapi.Message) textproto.MIMEHeader { //nolint[funlen]
|
||||
}
|
||||
if msg.ConversationID != "" {
|
||||
h.Set("X-Pm-ConversationID-Id", msg.ConversationID)
|
||||
if references := h.Get("References"); !strings.Contains(references, msg.ConversationID) {
|
||||
references += " <" + msg.ConversationID + "@" + pmapi.ConversationIDDomain + ">"
|
||||
h.Set("References", references)
|
||||
}
|
||||
}
|
||||
|
||||
return h
|
||||
@ -141,46 +135,3 @@ func GetAttachmentHeader(att *pmapi.Attachment) textproto.MIMEHeader {
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
var reEmailComment = regexp.MustCompile("[(][^)]*[)]") // nolint[gochecknoglobals]
|
||||
|
||||
// parseAddressComment removes the comments completely even though they should be allowed
|
||||
// http://tools.wordtothewise.com/rfc/822
|
||||
// NOTE: This should be supported in go>1.10 but it seems it's not ¯\_(ツ)_/¯
|
||||
func parseAddressComment(raw string) string {
|
||||
return reEmailComment.ReplaceAllString(raw, "")
|
||||
}
|
||||
|
||||
func parseAddressList(val string) (addrs []*mail.Address, err error) {
|
||||
if val == "" || val == "<>" {
|
||||
return
|
||||
}
|
||||
|
||||
addrs, err = mail.ParseAddressList(parseAddressComment(val))
|
||||
if err == nil {
|
||||
if addrs == nil {
|
||||
addrs = []*mail.Address{}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Probably missing encoding error -- try to at least parse addresses in brackets.
|
||||
first := strings.Index(val, "<")
|
||||
last := strings.LastIndex(val, ">")
|
||||
if first < 0 || last < 0 || first >= last {
|
||||
return
|
||||
}
|
||||
var addrList []string
|
||||
open := first
|
||||
for open < last && 0 <= open {
|
||||
val = val[open:]
|
||||
close := strings.Index(val, ">")
|
||||
addrList = append(addrList, val[:close+1])
|
||||
val = val[close:]
|
||||
open = strings.Index(val, "<")
|
||||
last = strings.LastIndex(val, ">")
|
||||
}
|
||||
val = strings.Join(addrList, ", ")
|
||||
|
||||
return mail.ParseAddressList(val)
|
||||
}
|
||||
|
||||
27
pkg/message/init.go
Normal file
27
pkg/message/init.go
Normal file
@ -0,0 +1,27 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
// ProtonMail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ProtonMail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package message
|
||||
|
||||
import (
|
||||
"github.com/ProtonMail/go-rfc5322"
|
||||
pmmime "github.com/ProtonMail/proton-bridge/pkg/mime"
|
||||
)
|
||||
|
||||
func init() { // nolint[noinit]
|
||||
rfc5322.CharsetReader = pmmime.CharsetReader
|
||||
}
|
||||
@ -26,6 +26,7 @@ import (
|
||||
"net/textproto"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/go-rfc5322"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/message/parser"
|
||||
pmmime "github.com/ProtonMail/proton-bridge/pkg/mime"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
@ -365,7 +366,6 @@ func attachPublicKey(p *parser.Part, key, keyName string) {
|
||||
})
|
||||
}
|
||||
|
||||
// NOTE: We should use our own ParseAddressList here.
|
||||
func parseMessageHeader(m *pmapi.Message, h message.Header) error { // nolint[funlen]
|
||||
mimeHeader, err := toMailHeader(h)
|
||||
if err != nil {
|
||||
@ -373,59 +373,64 @@ func parseMessageHeader(m *pmapi.Message, h message.Header) error { // nolint[fu
|
||||
}
|
||||
m.Header = mimeHeader
|
||||
|
||||
if err := forEachDecodedHeaderField(h, func(key, val string) error {
|
||||
switch strings.ToLower(key) {
|
||||
fields := h.Fields()
|
||||
|
||||
for fields.Next() {
|
||||
switch strings.ToLower(fields.Key()) {
|
||||
case "subject":
|
||||
m.Subject = val
|
||||
s, err := fields.Text()
|
||||
if err != nil {
|
||||
if s, err = pmmime.DecodeHeader(fields.Value()); err != nil {
|
||||
return errors.Wrap(err, "failed to parse subject")
|
||||
}
|
||||
}
|
||||
|
||||
m.Subject = s
|
||||
|
||||
case "from":
|
||||
sender, err := parseAddressList(val)
|
||||
sender, err := rfc5322.ParseAddressList(fields.Value())
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(err, "failed to parse from")
|
||||
}
|
||||
if len(sender) > 0 {
|
||||
m.Sender = sender[0]
|
||||
}
|
||||
|
||||
case "to":
|
||||
toList, err := parseAddressList(val)
|
||||
toList, err := rfc5322.ParseAddressList(fields.Value())
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(err, "failed to parse to")
|
||||
}
|
||||
m.ToList = toList
|
||||
|
||||
case "reply-to":
|
||||
replyTos, err := parseAddressList(val)
|
||||
replyTos, err := rfc5322.ParseAddressList(fields.Value())
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(err, "failed to parse reply-to")
|
||||
}
|
||||
m.ReplyTos = replyTos
|
||||
|
||||
case "cc":
|
||||
ccList, err := parseAddressList(val)
|
||||
ccList, err := rfc5322.ParseAddressList(fields.Value())
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(err, "failed to parse cc")
|
||||
}
|
||||
m.CCList = ccList
|
||||
|
||||
case "bcc":
|
||||
bccList, err := parseAddressList(val)
|
||||
bccList, err := rfc5322.ParseAddressList(fields.Value())
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(err, "failed to parse bcc")
|
||||
}
|
||||
m.BCCList = bccList
|
||||
|
||||
case "date":
|
||||
date, err := mail.ParseDate(val)
|
||||
date, err := rfc5322.ParseDateTime(fields.Value())
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(err, "failed to parse date")
|
||||
}
|
||||
m.Time = date.Unix()
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -469,29 +474,6 @@ func parseAttachment(h message.Header) (*pmapi.Attachment, error) {
|
||||
return att, nil
|
||||
}
|
||||
|
||||
func forEachDecodedHeaderField(h message.Header, fn func(string, string) error) error {
|
||||
fields := h.Fields()
|
||||
|
||||
for fields.Next() {
|
||||
text, err := fields.Text()
|
||||
if err != nil {
|
||||
if !message.IsUnknownCharset(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
if text, err = pmmime.DecodeHeader(fields.Value()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := fn(fields.Key(), text); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func toMailHeader(h message.Header) (mail.Header, error) {
|
||||
mimeHeader := make(mail.Header)
|
||||
|
||||
@ -517,3 +499,26 @@ func toMIMEHeader(h message.Header) (textproto.MIMEHeader, error) {
|
||||
|
||||
return mimeHeader, nil
|
||||
}
|
||||
|
||||
func forEachDecodedHeaderField(h message.Header, fn func(string, string) error) error {
|
||||
fields := h.Fields()
|
||||
|
||||
for fields.Next() {
|
||||
text, err := fields.Text()
|
||||
if err != nil {
|
||||
if !message.IsUnknownCharset(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
if text, err = pmmime.DecodeHeader(fields.Value()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := fn(fields.Key(), text); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -498,80 +498,3 @@ func readerToString(r io.Reader) string {
|
||||
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func TestRFC822AddressFormat(t *testing.T) { //nolint[funlen]
|
||||
tests := []struct {
|
||||
address string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
" normal name <username@server.com>",
|
||||
[]string{
|
||||
"\"normal name\" <username@server.com>",
|
||||
},
|
||||
},
|
||||
{
|
||||
" \"comma, name\" <username@server.com>",
|
||||
[]string{
|
||||
"\"comma, name\" <username@server.com>",
|
||||
},
|
||||
},
|
||||
{
|
||||
" name <username@server.com> (ignore comment)",
|
||||
[]string{
|
||||
"\"name\" <username@server.com>",
|
||||
},
|
||||
},
|
||||
{
|
||||
" name (ignore comment) <username@server.com>, (Comment as name) username2@server.com",
|
||||
[]string{
|
||||
"\"name\" <username@server.com>",
|
||||
"<username2@server.com>",
|
||||
},
|
||||
},
|
||||
{
|
||||
" normal name <username@server.com>, (comment)All.(around)address@(the)server.com",
|
||||
[]string{
|
||||
"\"normal name\" <username@server.com>",
|
||||
"<All.address@server.com>",
|
||||
},
|
||||
},
|
||||
{
|
||||
" normal name <username@server.com>, All.(\"comma, in comment\")address@(the)server.com",
|
||||
[]string{
|
||||
"\"normal name\" <username@server.com>",
|
||||
"<All.address@server.com>",
|
||||
},
|
||||
},
|
||||
{
|
||||
" \"normal name\" <username@server.com>, \"comma, name\" <address@server.com>",
|
||||
[]string{
|
||||
"\"normal name\" <username@server.com>",
|
||||
"\"comma, name\" <address@server.com>",
|
||||
},
|
||||
},
|
||||
{
|
||||
" \"comma, one\" <username@server.com>, \"comma, two\" <address@server.com>",
|
||||
[]string{
|
||||
"\"comma, one\" <username@server.com>",
|
||||
"\"comma, two\" <address@server.com>",
|
||||
},
|
||||
},
|
||||
{
|
||||
" \"comma, name\" <username@server.com>, another, name <address@server.com>",
|
||||
[]string{
|
||||
"\"comma, name\" <username@server.com>",
|
||||
"\"another, name\" <address@server.com>",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, data := range tests {
|
||||
result, err := parseAddressList(data.address)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, result, len(data.expected))
|
||||
for i, result := range result {
|
||||
assert.Equal(t, data.expected[i], result.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,17 +32,19 @@ import (
|
||||
"golang.org/x/text/encoding/htmlindex"
|
||||
)
|
||||
|
||||
var wordDec = &mime.WordDecoder{
|
||||
CharsetReader: func(charset string, input io.Reader) (io.Reader, error) {
|
||||
dec, err := SelectDecoder(charset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if dec == nil { // utf-8
|
||||
return input, nil
|
||||
}
|
||||
return dec.Reader(input), nil
|
||||
},
|
||||
func CharsetReader(charset string, input io.Reader) (io.Reader, error) {
|
||||
dec, err := SelectDecoder(charset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if dec == nil { // utf-8
|
||||
return input, nil
|
||||
}
|
||||
return dec.Reader(input), nil
|
||||
}
|
||||
|
||||
var WordDec = &mime.WordDecoder{
|
||||
CharsetReader: CharsetReader,
|
||||
}
|
||||
|
||||
// Expects trimmed lowercase.
|
||||
@ -180,7 +182,7 @@ func SelectDecoder(charset string) (decoder *encoding.Decoder, err error) {
|
||||
|
||||
// DecodeHeader if needed. Returns error if raw contains non-utf8 characters.
|
||||
func DecodeHeader(raw string) (decoded string, err error) {
|
||||
if decoded, err = wordDec.DecodeHeader(raw); err != nil {
|
||||
if decoded, err = WordDec.DecodeHeader(raw); err != nil {
|
||||
decoded = raw
|
||||
}
|
||||
if !utf8.ValidString(decoded) {
|
||||
|
||||
94
pkg/pmapi/check_connection.go
Normal file
94
pkg/pmapi/check_connection.go
Normal file
@ -0,0 +1,94 @@
|
||||
// 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 pmapi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const protonStatusURL = "http://protonstatus.com/vpn_status"
|
||||
|
||||
// ErrNoInternetConnection indicates that both protonstatus and the API are unreachable.
|
||||
var ErrNoInternetConnection = errors.New("no internet connection")
|
||||
|
||||
// CheckConnection returns an error if there is no internet connection.
|
||||
// This should be moved to the ConnectionManager when it is implemented.
|
||||
func (cm *ClientManager) CheckConnection() error {
|
||||
// We use a normal dialer here which doesn't check tls fingerprints.
|
||||
client := &http.Client{Timeout: time.Second * 10}
|
||||
|
||||
// Do not cumulate timeouts, use goroutines.
|
||||
retStatus := make(chan error)
|
||||
retAPI := make(chan error)
|
||||
|
||||
// vpn_status endpoint is fast and returns only OK. We check the connection only.
|
||||
go checkConnection(client, protonStatusURL, retStatus)
|
||||
|
||||
// Check of API reachability also uses a fast endpoint.
|
||||
go checkConnection(client, cm.GetRootURL()+"/tests/ping", retAPI)
|
||||
|
||||
errStatus := <-retStatus
|
||||
errAPI := <-retAPI
|
||||
|
||||
switch {
|
||||
case errStatus == nil && errAPI == nil:
|
||||
return nil
|
||||
|
||||
case errStatus == nil && errAPI != nil:
|
||||
cm.log.Error("ProtonStatus is reachable but API is not")
|
||||
return ErrAPINotReachable
|
||||
|
||||
case errStatus != nil && errAPI == nil:
|
||||
cm.log.Warn("API is reachable but protonstatus is not")
|
||||
return nil
|
||||
|
||||
case errStatus != nil && errAPI != nil:
|
||||
cm.log.Error("Both ProtonStatus and API are unreachable")
|
||||
return ErrNoInternetConnection
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckConnection returns an error if there is no internet connection.
|
||||
func CheckConnection() error {
|
||||
client := &http.Client{Timeout: time.Second * 10}
|
||||
retStatus := make(chan error)
|
||||
go checkConnection(client, protonStatusURL, retStatus)
|
||||
return <-retStatus
|
||||
}
|
||||
|
||||
func checkConnection(client *http.Client, url string, errorChannel chan error) {
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
errorChannel <- err
|
||||
return
|
||||
}
|
||||
|
||||
_ = resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
errorChannel <- fmt.Errorf("HTTP status code %d", resp.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
errorChannel <- nil
|
||||
}
|
||||
@ -254,7 +254,7 @@ func (c *client) doBuffered(req *http.Request, bodyBuffer []byte, retryUnauthori
|
||||
head += "\n"
|
||||
}
|
||||
c.log.Tracef("REQHEAD \n%s", head)
|
||||
c.log.Tracef("REQBODY '%s'", string(bodyBuffer))
|
||||
c.log.Tracef("REQBODY '%s'", printBytes(bodyBuffer))
|
||||
}
|
||||
|
||||
hasBody := len(bodyBuffer) > 0
|
||||
|
||||
@ -305,72 +305,6 @@ func (cm *ClientManager) GetAuthUpdateChannel() chan ClientAuth {
|
||||
return cm.authUpdates
|
||||
}
|
||||
|
||||
// ErrNoInternetConnection indicates that both protonstatus and the API are unreachable.
|
||||
var ErrNoInternetConnection = errors.New("no internet connection")
|
||||
|
||||
// CheckConnection returns an error if there is no internet connection.
|
||||
// This should be moved to the ConnectionManager when it is implemented.
|
||||
func (cm *ClientManager) CheckConnection() error {
|
||||
client := getHTTPClient(cm.config, cm.roundTripper, cm.cookieJar)
|
||||
|
||||
// Do not cumulate timeouts, use goroutines.
|
||||
retStatus := make(chan error)
|
||||
retAPI := make(chan error)
|
||||
|
||||
// vpn_status endpoint is fast and returns only OK. We check the connection only.
|
||||
go checkConnection(client, "https://protonstatus.com/vpn_status", retStatus)
|
||||
|
||||
// Check of API reachability also uses a fast endpoint.
|
||||
go checkConnection(client, cm.GetRootURL()+"/tests/ping", retAPI)
|
||||
|
||||
errStatus := <-retStatus
|
||||
errAPI := <-retAPI
|
||||
|
||||
switch {
|
||||
case errStatus == nil && errAPI == nil:
|
||||
return nil
|
||||
|
||||
case errStatus == nil && errAPI != nil:
|
||||
cm.log.Error("ProtonStatus is reachable but API is not")
|
||||
return ErrAPINotReachable
|
||||
|
||||
case errStatus != nil && errAPI == nil:
|
||||
cm.log.Warn("API is reachable but protonstatus is not")
|
||||
return nil
|
||||
|
||||
case errStatus != nil && errAPI != nil:
|
||||
cm.log.Error("Both ProtonStatus and API are unreachable")
|
||||
return ErrNoInternetConnection
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckConnection returns an error if there is no internet connection.
|
||||
func CheckConnection() error {
|
||||
client := &http.Client{Timeout: time.Second * 10}
|
||||
retStatus := make(chan error)
|
||||
go checkConnection(client, "https://protonstatus.com/vpn_status", retStatus)
|
||||
return <-retStatus
|
||||
}
|
||||
|
||||
func checkConnection(client *http.Client, url string, errorChannel chan error) {
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
errorChannel <- err
|
||||
return
|
||||
}
|
||||
|
||||
_ = resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
errorChannel <- fmt.Errorf("HTTP status code %d", resp.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
errorChannel <- nil
|
||||
}
|
||||
|
||||
// setTokenIfUnset sets the token for the given userID if it wasn't already set.
|
||||
// The set token does not expire.
|
||||
func (cm *ClientManager) setTokenIfUnset(userID, token string) {
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// +build pmapi_env
|
||||
// +build pmapi_qa
|
||||
|
||||
package pmapi
|
||||
|
||||
|
||||
43
pkg/pmapi/debug.go
Normal file
43
pkg/pmapi/debug.go
Normal file
@ -0,0 +1,43 @@
|
||||
// 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 pmapi
|
||||
|
||||
import "unicode/utf8"
|
||||
|
||||
func printBytes(body []byte) string {
|
||||
if utf8.Valid(body) {
|
||||
return string(body)
|
||||
}
|
||||
enc := []rune{}
|
||||
for _, b := range body {
|
||||
switch {
|
||||
case b == 9:
|
||||
enc = append(enc, rune('⟼'))
|
||||
case b == 13:
|
||||
enc = append(enc, rune('↵'))
|
||||
case b < 32, b == 127:
|
||||
enc = append(enc, '◡')
|
||||
case b > 31 && b < 127, b == 10:
|
||||
enc = append(enc, rune(b))
|
||||
default:
|
||||
enc = append(enc, 9728+rune(b))
|
||||
}
|
||||
}
|
||||
|
||||
return string(enc)
|
||||
}
|
||||
@ -19,6 +19,7 @@ package pmapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
@ -289,3 +290,57 @@ func signAttachment(encrypter *crypto.KeyRing, data io.Reader) (signature io.Rea
|
||||
}
|
||||
return bytes.NewReader(sig.GetBinary()), nil
|
||||
}
|
||||
|
||||
func encryptAndEncodeSessionKeys(
|
||||
pubkey *crypto.KeyRing,
|
||||
bodyKey *crypto.SessionKey,
|
||||
attkeys map[string]*crypto.SessionKey,
|
||||
) (bodyPacket string, attachmentPackets map[string]string, err error) {
|
||||
// Encrypt message body keys.
|
||||
packetBytes, err := pubkey.EncryptSessionKey(bodyKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
bodyPacket = base64.StdEncoding.EncodeToString(packetBytes)
|
||||
|
||||
// Encrypt attachment keys.
|
||||
attachmentPackets = make(map[string]string)
|
||||
for id, attkey := range attkeys {
|
||||
var packets []byte
|
||||
if packets, err = pubkey.EncryptSessionKey(attkey); err != nil {
|
||||
return
|
||||
}
|
||||
attachmentPackets[id] = base64.StdEncoding.EncodeToString(packets)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func encryptSymmDecryptKey(
|
||||
kr *crypto.KeyRing,
|
||||
textToEncrypt string,
|
||||
) (decryptedKey *crypto.SessionKey, symEncryptedData []byte, err error) {
|
||||
// We use only primary key to encrypt the message. Our keyring contains all keys (primary, old and deacivated ones).
|
||||
firstKey, err := kr.FirstKey()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
pgpMessage, err := firstKey.Encrypt(crypto.NewPlainMessageFromString(textToEncrypt), kr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
pgpSplitMessage, err := pgpMessage.SeparateKeyAndData(len(textToEncrypt), 0)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
decryptedKey, err = kr.DecryptSessionKey(pgpSplitMessage.GetBinaryKeyPacket())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
symEncryptedData = pgpSplitMessage.GetBinaryDataPacket()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
367
pkg/pmapi/message_send.go
Normal file
367
pkg/pmapi/message_send.go
Normal file
@ -0,0 +1,367 @@
|
||||
// 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 pmapi
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
)
|
||||
|
||||
// Draft actions
|
||||
const (
|
||||
DraftActionReply = 0
|
||||
DraftActionReplyAll = 1
|
||||
DraftActionForward = 2
|
||||
)
|
||||
|
||||
// PackageFlag for send message package types
|
||||
type PackageFlag int
|
||||
|
||||
func (p *PackageFlag) Has(flag PackageFlag) bool { return iHasFlag(int(*p), int(flag)) }
|
||||
func (p *PackageFlag) HasAtLeastOne(flag PackageFlag) bool {
|
||||
return iHasAtLeastOneFlag(int(*p), int(flag))
|
||||
}
|
||||
func (p *PackageFlag) Is(flag PackageFlag) bool { return iIsFlag(int(*p), int(flag)) }
|
||||
func (p *PackageFlag) HasNo(flag PackageFlag) bool { return iHasNoneOfFlag(int(*p), int(flag)) }
|
||||
|
||||
// Send message package types.
|
||||
const (
|
||||
InternalPackage = PackageFlag(1)
|
||||
EncryptedOutsidePackage = PackageFlag(2)
|
||||
ClearPackage = PackageFlag(4)
|
||||
PGPInlinePackage = PackageFlag(8)
|
||||
PGPMIMEPackage = PackageFlag(16)
|
||||
ClearMIMEPackage = PackageFlag(32)
|
||||
)
|
||||
|
||||
// SignatureFlag for send signature types.
|
||||
type SignatureFlag int
|
||||
|
||||
func (p *SignatureFlag) Is(flag SignatureFlag) bool { return iIsFlag(int(*p), int(flag)) }
|
||||
func (p *SignatureFlag) Has(flag SignatureFlag) bool { return iHasFlag(int(*p), int(flag)) }
|
||||
func (p *SignatureFlag) HasNo(flag SignatureFlag) bool { return iHasNoneOfFlag(int(*p), int(flag)) }
|
||||
|
||||
// Send signature types.
|
||||
const (
|
||||
SignatureNone = SignatureFlag(0)
|
||||
SignatureDetached = SignatureFlag(1)
|
||||
SignatureAttachedArmored = SignatureFlag(2)
|
||||
)
|
||||
|
||||
// DraftReq defines paylod for creating drafts
|
||||
type DraftReq struct {
|
||||
Message *Message
|
||||
ParentID string `json:",omitempty"`
|
||||
Action int
|
||||
AttachmentKeyPackets []string
|
||||
}
|
||||
|
||||
func (c *client) CreateDraft(m *Message, parent string, action int) (created *Message, err error) {
|
||||
createReq := &DraftReq{Message: m, ParentID: parent, Action: action, AttachmentKeyPackets: []string{}}
|
||||
|
||||
req, err := c.NewJSONRequest("POST", "/mail/v4/messages", createReq)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var res MessageRes
|
||||
if err = c.DoJSON(req, &res); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
created, err = res.Message, res.Err()
|
||||
return
|
||||
}
|
||||
|
||||
type AlgoKey struct {
|
||||
Key string
|
||||
Algorithm string
|
||||
}
|
||||
|
||||
type MessageAddress struct {
|
||||
Type PackageFlag
|
||||
EncryptedBodyKeyPacket string `json:"BodyKeyPacket"` // base64-encoded key packet.
|
||||
Signature SignatureFlag
|
||||
EncryptedAttachmentKeyPackets map[string]string `json:"AttachmentKeyPackets"`
|
||||
}
|
||||
|
||||
type MessagePackage struct {
|
||||
Addresses map[string]*MessageAddress
|
||||
Type PackageFlag
|
||||
MIMEType string
|
||||
EncryptedBody string `json:"Body"` // base64-encoded encrypted data packet.
|
||||
DecryptedBodyKey AlgoKey `json:"BodyKey"` // base64-encoded session key (only if cleartext recipients).
|
||||
DecryptedAttachmentKeys map[string]AlgoKey `json:"AttachmentKeys"` // Only include if cleartext & attachments.
|
||||
}
|
||||
|
||||
func newMessagePackage(
|
||||
send sendData,
|
||||
attKeys map[string]AlgoKey,
|
||||
) (pkg *MessagePackage) {
|
||||
pkg = &MessagePackage{
|
||||
EncryptedBody: base64.StdEncoding.EncodeToString(send.ciphertext),
|
||||
Addresses: send.addressMap,
|
||||
MIMEType: send.contentType,
|
||||
Type: send.sharedScheme,
|
||||
}
|
||||
|
||||
if send.sharedScheme.HasAtLeastOne(ClearPackage | ClearMIMEPackage) {
|
||||
pkg.DecryptedBodyKey.Key = send.decryptedBodyKey.GetBase64Key()
|
||||
pkg.DecryptedBodyKey.Algorithm = send.decryptedBodyKey.Algo
|
||||
}
|
||||
|
||||
if len(attKeys) != 0 && send.sharedScheme.Has(ClearPackage) {
|
||||
pkg.DecryptedAttachmentKeys = attKeys
|
||||
}
|
||||
|
||||
return pkg
|
||||
}
|
||||
|
||||
type sendData struct {
|
||||
decryptedBodyKey *crypto.SessionKey //body session key
|
||||
addressMap map[string]*MessageAddress
|
||||
sharedScheme PackageFlag
|
||||
ciphertext []byte
|
||||
cleartext string
|
||||
contentType string
|
||||
}
|
||||
|
||||
type SendMessageReq struct {
|
||||
ExpirationTime int64 `json:",omitempty"`
|
||||
// AutoSaveContacts int `json:",omitempty"`
|
||||
|
||||
// Data for encrypted recipients.
|
||||
Packages []*MessagePackage
|
||||
|
||||
mime, plain, rich sendData
|
||||
attKeys map[string]*crypto.SessionKey
|
||||
kr *crypto.KeyRing
|
||||
}
|
||||
|
||||
func NewSendMessageReq(
|
||||
kr *crypto.KeyRing,
|
||||
mimeBody, plainBody, richBody string,
|
||||
attKeys map[string]*crypto.SessionKey,
|
||||
) *SendMessageReq {
|
||||
req := &SendMessageReq{}
|
||||
|
||||
req.mime.addressMap = make(map[string]*MessageAddress)
|
||||
req.plain.addressMap = make(map[string]*MessageAddress)
|
||||
req.rich.addressMap = make(map[string]*MessageAddress)
|
||||
|
||||
req.mime.cleartext = mimeBody
|
||||
req.plain.cleartext = plainBody
|
||||
req.rich.cleartext = richBody
|
||||
|
||||
req.attKeys = attKeys
|
||||
req.kr = kr
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
var (
|
||||
errUnknownContentType = errors.New("unknown content type")
|
||||
errMultipartInNonMIME = errors.New("multipart mixed not allowed in this scheme")
|
||||
errAttSignNotSupported = errors.New("attached signature not supported")
|
||||
errEncryptMustSign = errors.New("encrypted package must be signed")
|
||||
errEncryptedOutsideNotSupported = errors.New("encrypted outside is not supported")
|
||||
errWrongSendScheme = errors.New("wrong send scheme")
|
||||
errInternalMustEncrypt = errors.New("internal package must be encrypted")
|
||||
errInlineMustBePlain = errors.New("PGP Inline package must be plain text")
|
||||
errMissingPubkey = errors.New("cannot encrypt body key packet: missing pubkey")
|
||||
errClearSignMustNotBeHTML = errors.New("clear signed packet must be multipart or plain")
|
||||
errMIMEMustBeMultipart = errors.New("MIME packet must be multipart")
|
||||
errClearMIMEMustSign = errors.New("clear MIME must be signed")
|
||||
errClearSignMustNotBePGPInline = errors.New("clear sign must not be PGP inline")
|
||||
)
|
||||
|
||||
func (req *SendMessageReq) AddRecipient(
|
||||
email string, sendScheme PackageFlag,
|
||||
pubkey *crypto.KeyRing, signature SignatureFlag,
|
||||
contentType string, doEncrypt bool,
|
||||
) (err error) {
|
||||
if signature.Has(SignatureAttachedArmored) {
|
||||
return errAttSignNotSupported
|
||||
}
|
||||
|
||||
if doEncrypt && signature.HasNo(SignatureDetached) {
|
||||
return errEncryptMustSign
|
||||
}
|
||||
|
||||
switch sendScheme {
|
||||
case PGPMIMEPackage, ClearMIMEPackage:
|
||||
if contentType != ContentTypeMultipartMixed {
|
||||
return errMIMEMustBeMultipart
|
||||
}
|
||||
return req.addMIMERecipient(email, sendScheme, pubkey, signature)
|
||||
case InternalPackage, ClearPackage, PGPInlinePackage:
|
||||
if contentType == ContentTypeMultipartMixed {
|
||||
return errMultipartInNonMIME
|
||||
}
|
||||
return req.addNonMIMERecipient(email, sendScheme, pubkey, signature, contentType, doEncrypt)
|
||||
case EncryptedOutsidePackage:
|
||||
return errEncryptedOutsideNotSupported
|
||||
default:
|
||||
return errWrongSendScheme
|
||||
}
|
||||
}
|
||||
|
||||
func (req *SendMessageReq) addNonMIMERecipient(
|
||||
email string, sendScheme PackageFlag,
|
||||
pubkey *crypto.KeyRing, signature SignatureFlag,
|
||||
contentType string, doEncrypt bool,
|
||||
) (err error) {
|
||||
if signature.Is(SignatureDetached) && !doEncrypt {
|
||||
if sendScheme.Is(PGPInlinePackage) {
|
||||
return errClearSignMustNotBePGPInline
|
||||
}
|
||||
if sendScheme.Is(ClearPackage) && contentType == ContentTypeHTML {
|
||||
return errClearSignMustNotBeHTML
|
||||
}
|
||||
}
|
||||
|
||||
var send *sendData
|
||||
|
||||
switch contentType {
|
||||
case ContentTypePlainText:
|
||||
send = &req.plain
|
||||
send.contentType = ContentTypePlainText
|
||||
case ContentTypeHTML, "":
|
||||
send = &req.rich
|
||||
send.contentType = ContentTypeHTML
|
||||
case ContentTypeMultipartMixed:
|
||||
return errMultipartInNonMIME
|
||||
default:
|
||||
return errUnknownContentType
|
||||
}
|
||||
|
||||
if send.decryptedBodyKey == nil {
|
||||
if send.decryptedBodyKey, send.ciphertext, err = encryptSymmDecryptKey(req.kr, send.cleartext); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
newAddress := &MessageAddress{Type: sendScheme, Signature: signature}
|
||||
|
||||
if sendScheme.Is(PGPInlinePackage) && contentType == ContentTypeHTML {
|
||||
return errInlineMustBePlain
|
||||
}
|
||||
if sendScheme.Is(InternalPackage) && !doEncrypt {
|
||||
return errInternalMustEncrypt
|
||||
}
|
||||
if doEncrypt && pubkey == nil {
|
||||
return errMissingPubkey
|
||||
}
|
||||
|
||||
if doEncrypt {
|
||||
newAddress.EncryptedBodyKeyPacket, newAddress.EncryptedAttachmentKeyPackets, err = encryptAndEncodeSessionKeys(pubkey, send.decryptedBodyKey, req.attKeys)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
send.addressMap[email] = newAddress
|
||||
send.sharedScheme |= sendScheme
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (req *SendMessageReq) addMIMERecipient(
|
||||
email string, sendScheme PackageFlag,
|
||||
pubkey *crypto.KeyRing, signature SignatureFlag,
|
||||
) (err error) {
|
||||
if sendScheme.Is(ClearMIMEPackage) && signature.HasNo(SignatureDetached) {
|
||||
return errClearMIMEMustSign
|
||||
}
|
||||
|
||||
req.mime.contentType = ContentTypeMultipartMixed
|
||||
if req.mime.decryptedBodyKey == nil {
|
||||
if req.mime.decryptedBodyKey, req.mime.ciphertext, err = encryptSymmDecryptKey(req.kr, req.mime.cleartext); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if sendScheme.Is(PGPMIMEPackage) {
|
||||
if pubkey == nil {
|
||||
return errMissingPubkey
|
||||
}
|
||||
// Attachment keys are not needed because attachments are part
|
||||
// of MIME body and therefore attachments are encrypted with
|
||||
// body session key.
|
||||
mimeBodyPacket, _, err := encryptAndEncodeSessionKeys(pubkey, req.mime.decryptedBodyKey, map[string]*crypto.SessionKey{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.mime.addressMap[email] = &MessageAddress{Type: sendScheme, EncryptedBodyKeyPacket: mimeBodyPacket, Signature: signature}
|
||||
} else {
|
||||
req.mime.addressMap[email] = &MessageAddress{Type: sendScheme, Signature: signature}
|
||||
}
|
||||
req.mime.sharedScheme |= sendScheme
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (req *SendMessageReq) PreparePackages() {
|
||||
attkeysEncoded := make(map[string]AlgoKey)
|
||||
for attID, attkey := range req.attKeys {
|
||||
attkeysEncoded[attID] = AlgoKey{
|
||||
Key: attkey.GetBase64Key(),
|
||||
Algorithm: attkey.Algo,
|
||||
}
|
||||
}
|
||||
|
||||
for _, send := range []sendData{req.mime, req.plain, req.rich} {
|
||||
if len(send.addressMap) == 0 {
|
||||
continue
|
||||
}
|
||||
req.Packages = append(req.Packages, newMessagePackage(send, attkeysEncoded))
|
||||
}
|
||||
}
|
||||
|
||||
type SendMessageRes struct {
|
||||
Res
|
||||
|
||||
Sent *Message
|
||||
|
||||
// Parent is only present if the sent message has a parent (reply/reply all/forward).
|
||||
Parent *Message
|
||||
}
|
||||
|
||||
func (c *client) SendMessage(id string, sendReq *SendMessageReq) (sent, parent *Message, err error) {
|
||||
if id == "" {
|
||||
err = errors.New("pmapi: cannot send message with an empty id")
|
||||
return
|
||||
}
|
||||
|
||||
if sendReq.Packages == nil {
|
||||
sendReq.Packages = []*MessagePackage{}
|
||||
}
|
||||
|
||||
req, err := c.NewJSONRequest("POST", "/mail/v4/messages/"+id, sendReq)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var res SendMessageRes
|
||||
if err = c.DoJSON(req, &res); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
sent, parent, err = res.Sent, res.Parent, res.Err()
|
||||
return
|
||||
}
|
||||
617
pkg/pmapi/message_send_test.go
Normal file
617
pkg/pmapi/message_send_test.go
Normal file
@ -0,0 +1,617 @@
|
||||
// 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 pmapi
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type recipient struct {
|
||||
email string
|
||||
sendScheme PackageFlag
|
||||
pubkey *crypto.KeyRing
|
||||
signature SignatureFlag
|
||||
contentType string
|
||||
doEncrypt bool
|
||||
wantError error
|
||||
}
|
||||
|
||||
type testData struct {
|
||||
emails []string
|
||||
recipients []recipient
|
||||
wantPackages []*MessagePackage
|
||||
|
||||
allRecipients map[string]recipient
|
||||
allAddresses map[string]*MessageAddress
|
||||
|
||||
attKeys map[string]*crypto.SessionKey
|
||||
mimeBody, plainBody, richBody string
|
||||
}
|
||||
|
||||
func (td *testData) addRecipients(t testing.TB) {
|
||||
for _, email := range td.emails {
|
||||
rcp, ok := td.allRecipients[email]
|
||||
require.True(t, ok, "missing recipient %s", email)
|
||||
rcp.email = email
|
||||
td.recipients = append(td.recipients, rcp)
|
||||
}
|
||||
}
|
||||
|
||||
func (td *testData) addAddresses(t testing.TB) {
|
||||
for i, wantPackage := range td.wantPackages {
|
||||
for email := range wantPackage.Addresses {
|
||||
address, ok := td.allAddresses[email]
|
||||
require.True(t, ok, "missing address %s", email)
|
||||
td.wantPackages[i].Addresses[email] = address
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (td *testData) prepareAndCheck(t *testing.T) {
|
||||
r := require.New(t)
|
||||
|
||||
matchPresence := func(want string) require.ValueAssertionFunc {
|
||||
if len(want) == 0 {
|
||||
return require.Empty
|
||||
}
|
||||
return require.NotEmpty
|
||||
}
|
||||
|
||||
have := NewSendMessageReq(testPrivateKeyRing, td.mimeBody, td.plainBody, td.richBody, td.attKeys)
|
||||
for _, rec := range td.recipients {
|
||||
err := have.AddRecipient(rec.email, rec.sendScheme, rec.pubkey, rec.signature, rec.contentType, rec.doEncrypt)
|
||||
|
||||
if rec.wantError == nil {
|
||||
r.NoError(err, "email %s", rec.email)
|
||||
} else {
|
||||
r.EqualError(err, rec.wantError.Error(), "email %s", rec.email)
|
||||
}
|
||||
}
|
||||
have.PreparePackages()
|
||||
|
||||
r.Equal(len(td.wantPackages), len(have.Packages))
|
||||
|
||||
for i, wantPackage := range td.wantPackages {
|
||||
havePackage := have.Packages[i]
|
||||
|
||||
r.Equal(wantPackage.MIMEType, havePackage.MIMEType, "pkg %d", i)
|
||||
r.Equal(wantPackage.Type, havePackage.Type, "pkg %d", i)
|
||||
|
||||
r.Equal(len(wantPackage.Addresses), len(havePackage.Addresses), "pkg %d", i)
|
||||
for email, wantAddress := range wantPackage.Addresses {
|
||||
haveAddress, ok := havePackage.Addresses[email]
|
||||
r.True(ok, "pkg %d email %s", i, email)
|
||||
|
||||
r.Equal(wantAddress.Type, haveAddress.Type, "pkg %d email %s", i, email)
|
||||
matchPresence(wantAddress.EncryptedBodyKeyPacket)(t, haveAddress.EncryptedBodyKeyPacket, "pkg %d email %s", i, email)
|
||||
r.Equal(wantAddress.Signature, haveAddress.Signature, "pkg %d email %s", i, email)
|
||||
|
||||
if len(td.attKeys) == 0 {
|
||||
r.Len(haveAddress.EncryptedAttachmentKeyPackets, 0)
|
||||
} else {
|
||||
r.Equal(
|
||||
len(wantAddress.EncryptedAttachmentKeyPackets),
|
||||
len(haveAddress.EncryptedAttachmentKeyPackets),
|
||||
"pkg %d email %s", i, email,
|
||||
)
|
||||
for attID, wantAttKey := range wantAddress.EncryptedAttachmentKeyPackets {
|
||||
haveAttKey, ok := haveAddress.EncryptedAttachmentKeyPackets[attID]
|
||||
r.True(ok, "pkg %d email %s att %s", i, email, attID)
|
||||
matchPresence(wantAttKey)(t, haveAttKey, "pkg %d email %s att %s", i, email, attID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
matchPresence(wantPackage.EncryptedBody)(t, havePackage.EncryptedBody, "pkg %d", i)
|
||||
|
||||
wantBodyKey := wantPackage.DecryptedBodyKey
|
||||
haveBodyKey := havePackage.DecryptedBodyKey
|
||||
|
||||
matchPresence(wantBodyKey.Algorithm)(t, haveBodyKey.Algorithm, "pkg %d", i)
|
||||
matchPresence(wantBodyKey.Key)(t, haveBodyKey.Key, "pkg %d", i)
|
||||
|
||||
if len(td.attKeys) == 0 {
|
||||
r.Len(havePackage.DecryptedAttachmentKeys, 0)
|
||||
} else {
|
||||
r.Equal(
|
||||
len(wantPackage.DecryptedAttachmentKeys),
|
||||
len(havePackage.DecryptedAttachmentKeys),
|
||||
"pkg %d", i,
|
||||
)
|
||||
for attID, wantAttKey := range wantPackage.DecryptedAttachmentKeys {
|
||||
haveAttKey, ok := havePackage.DecryptedAttachmentKeys[attID]
|
||||
r.True(ok, "pkg %d att %s", i, attID)
|
||||
matchPresence(wantAttKey.Key)(t, haveAttKey.Key, "pkg %d att %s", i, attID)
|
||||
matchPresence(wantAttKey.Algorithm)(t, haveAttKey.Algorithm, "pkg %d att %s", i, attID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendReq(t *testing.T) {
|
||||
attKeyB64 := "EvjO/2RIJNn6HdoU6ACqFdZglzJhpjQ/PpjsvL3mB5Q="
|
||||
token, err := base64.StdEncoding.DecodeString(attKeyB64)
|
||||
require.NoError(t, err)
|
||||
|
||||
attKey := crypto.NewSessionKeyFromToken(token, "aes256")
|
||||
attKeyPackets := map[string]string{"attID": "not-empty"}
|
||||
attAlgoKeys := map[string]AlgoKey{"attID": {"not-empty", "not-empty"}}
|
||||
|
||||
allRecipients := map[string]recipient{
|
||||
// Internal OK
|
||||
"none@pm.me": {"", InternalPackage, testPublicKeyRing, SignatureDetached, "", true, nil},
|
||||
"html@pm.me": {"", InternalPackage, testPublicKeyRing, SignatureDetached, ContentTypeHTML, true, nil},
|
||||
"plain@pm.me": {"", InternalPackage, testPublicKeyRing, SignatureDetached, ContentTypePlainText, true, nil},
|
||||
// Internal bad
|
||||
"wrongtype@pm.me": {"", InternalPackage, testPublicKeyRing, SignatureDetached, "application/rfc822", true, errUnknownContentType},
|
||||
"multipart@pm.me": {"", InternalPackage, testPublicKeyRing, SignatureDetached, ContentTypeMultipartMixed, true, errMultipartInNonMIME},
|
||||
"noencrypt@pm.me": {"", InternalPackage, testPublicKeyRing, SignatureDetached, ContentTypeHTML, false, errInternalMustEncrypt},
|
||||
"no-pubkey@pm.me": {"", InternalPackage, nil, SignatureDetached, ContentTypeHTML, true, errMissingPubkey},
|
||||
"nosigning@pm.me": {"", InternalPackage, testPublicKeyRing, SignatureNone, ContentTypeHTML, true, errEncryptMustSign},
|
||||
// testing combination
|
||||
"internal1@pm.me": {"", InternalPackage, testPublicKeyRing, SignatureDetached, ContentTypePlainText, true, nil},
|
||||
// Clear OK
|
||||
"html@email.com": {"", ClearPackage, nil, SignatureNone, ContentTypeHTML, false, nil},
|
||||
"none@email.com": {"", ClearPackage, nil, SignatureNone, "", false, nil},
|
||||
"plain@email.com": {"", ClearPackage, nil, SignatureNone, ContentTypePlainText, false, nil},
|
||||
"plain-sign@email.com": {"", ClearPackage, nil, SignatureDetached, ContentTypePlainText, false, nil},
|
||||
"mime-sign@email.com": {"", ClearMIMEPackage, nil, SignatureDetached, ContentTypeMultipartMixed, false, nil},
|
||||
// Clear bad
|
||||
"mime@email.com": {"", ClearMIMEPackage, nil, SignatureNone, ContentTypeMultipartMixed, false, errClearMIMEMustSign},
|
||||
"clear-plain-sign@email.com": {"", PGPInlinePackage, nil, SignatureDetached, ContentTypePlainText, false, errClearSignMustNotBePGPInline},
|
||||
"html-sign@email.com": {"", ClearPackage, nil, SignatureDetached, ContentTypeHTML, false, errClearSignMustNotBeHTML},
|
||||
"mime-plain@email.com": {"", ClearMIMEPackage, nil, SignatureDetached, ContentTypePlainText, false, errMIMEMustBeMultipart},
|
||||
"mime-html@email.com": {"", ClearMIMEPackage, nil, SignatureDetached, ContentTypeHTML, false, errMIMEMustBeMultipart},
|
||||
// External Encryption OK
|
||||
"mime@gpg.com": {"", PGPMIMEPackage, testPublicKeyRing, SignatureDetached, ContentTypeMultipartMixed, true, nil},
|
||||
"plain@gpg.com": {"", PGPInlinePackage, testPublicKeyRing, SignatureDetached, ContentTypePlainText, true, nil},
|
||||
// External Encryption bad
|
||||
"eo@gpg.com": {"", EncryptedOutsidePackage, testPublicKeyRing, SignatureDetached, ContentTypeHTML, true, errEncryptedOutsideNotSupported},
|
||||
"inline-html@gpg.com": {"", PGPInlinePackage, testPublicKeyRing, SignatureDetached, ContentTypeHTML, true, errInlineMustBePlain},
|
||||
"inline-mixed@gpg.com": {"", PGPInlinePackage, testPublicKeyRing, SignatureDetached, ContentTypeMultipartMixed, true, errMultipartInNonMIME},
|
||||
"mime-plain@gpg.com": {"", PGPMIMEPackage, nil, SignatureDetached, ContentTypePlainText, true, errMIMEMustBeMultipart},
|
||||
"mime-html@sgpg.com": {"", PGPMIMEPackage, nil, SignatureDetached, ContentTypeHTML, true, errMIMEMustBeMultipart},
|
||||
"no-pubkey@gpg.com": {"", PGPMIMEPackage, nil, SignatureDetached, ContentTypeMultipartMixed, true, errMissingPubkey},
|
||||
"not-signed@gpg.com": {"", PGPMIMEPackage, testPublicKeyRing, SignatureNone, ContentTypeMultipartMixed, true, errEncryptMustSign},
|
||||
}
|
||||
|
||||
allAddresses := map[string]*MessageAddress{
|
||||
"none@pm.me": {
|
||||
Type: InternalPackage,
|
||||
Signature: SignatureDetached,
|
||||
EncryptedBodyKeyPacket: "not-empty",
|
||||
EncryptedAttachmentKeyPackets: attKeyPackets,
|
||||
},
|
||||
"plain@pm.me": {
|
||||
Type: InternalPackage,
|
||||
Signature: SignatureDetached,
|
||||
EncryptedBodyKeyPacket: "not-empty",
|
||||
EncryptedAttachmentKeyPackets: attKeyPackets,
|
||||
},
|
||||
"html@pm.me": {
|
||||
Type: InternalPackage,
|
||||
Signature: SignatureDetached,
|
||||
EncryptedBodyKeyPacket: "not-empty",
|
||||
EncryptedAttachmentKeyPackets: attKeyPackets,
|
||||
},
|
||||
"internal1@pm.me": {
|
||||
Type: InternalPackage,
|
||||
Signature: SignatureDetached,
|
||||
EncryptedBodyKeyPacket: "not-empty",
|
||||
EncryptedAttachmentKeyPackets: attKeyPackets,
|
||||
},
|
||||
|
||||
"html@email.com": {
|
||||
Type: ClearPackage,
|
||||
Signature: SignatureNone,
|
||||
},
|
||||
"none@email.com": {
|
||||
Type: ClearPackage,
|
||||
Signature: SignatureNone,
|
||||
},
|
||||
"plain@email.com": {
|
||||
Type: ClearPackage,
|
||||
Signature: SignatureNone,
|
||||
},
|
||||
"plain-sign@email.com": {
|
||||
Type: ClearPackage,
|
||||
Signature: SignatureDetached,
|
||||
},
|
||||
"mime-sign@email.com": {
|
||||
Type: ClearMIMEPackage,
|
||||
Signature: SignatureDetached,
|
||||
},
|
||||
|
||||
"mime@gpg.com": {
|
||||
Type: PGPMIMEPackage,
|
||||
Signature: SignatureDetached,
|
||||
EncryptedBodyKeyPacket: "non-empty",
|
||||
},
|
||||
"plain@gpg.com": {
|
||||
Type: PGPInlinePackage,
|
||||
Signature: SignatureDetached,
|
||||
EncryptedBodyKeyPacket: "non-empty",
|
||||
EncryptedAttachmentKeyPackets: attKeyPackets,
|
||||
},
|
||||
}
|
||||
|
||||
// NOTE naming
|
||||
// Single: there should be one package
|
||||
// Multiple: there should be more than one package
|
||||
// Internal: there should be internal package
|
||||
// Clear: there should be non-encrypted package
|
||||
// Encrypted: there should be encrypted package
|
||||
// NotAllowed: combination of inputs which are not allowed
|
||||
newTests := map[string]testData{
|
||||
"Nothing": { // expect no crash
|
||||
emails: []string{},
|
||||
wantPackages: []*MessagePackage{},
|
||||
},
|
||||
"Fails": {
|
||||
emails: []string{
|
||||
"wrongtype@pm.me",
|
||||
"multipart@pm.me",
|
||||
"noencrypt@pm.me",
|
||||
"no-pubkey@pm.me",
|
||||
"nosigning@pm.me",
|
||||
|
||||
"html-sign@email.com",
|
||||
"mime-plain@email.com",
|
||||
"mime-html@email.com",
|
||||
"mime@email.com",
|
||||
"clear-plain-sign@email.com",
|
||||
|
||||
"eo@gpg.com",
|
||||
"inline-html@gpg.com",
|
||||
"inline-mixed@gpg.com",
|
||||
"mime-plain@gpg.com",
|
||||
"mime-html@sgpg.com",
|
||||
"no-pubkey@gpg.com",
|
||||
"not-signed@gpg.com",
|
||||
},
|
||||
},
|
||||
|
||||
// one scheme in one package
|
||||
"SingleInternalHTML": {
|
||||
emails: []string{"none@pm.me", "html@pm.me"},
|
||||
wantPackages: []*MessagePackage{
|
||||
{
|
||||
Addresses: map[string]*MessageAddress{
|
||||
"none@pm.me": nil,
|
||||
"html@pm.me": nil,
|
||||
},
|
||||
Type: InternalPackage,
|
||||
MIMEType: ContentTypeHTML,
|
||||
EncryptedBody: "non-empty",
|
||||
},
|
||||
},
|
||||
},
|
||||
"SingleInternalPlain": {
|
||||
emails: []string{"plain@pm.me"},
|
||||
wantPackages: []*MessagePackage{
|
||||
{
|
||||
Addresses: map[string]*MessageAddress{
|
||||
"plain@pm.me": nil,
|
||||
},
|
||||
Type: InternalPackage,
|
||||
MIMEType: ContentTypePlainText,
|
||||
EncryptedBody: "non-empty",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
"SingleClearHTML": {
|
||||
emails: []string{"none@email.com", "html@email.com"},
|
||||
wantPackages: []*MessagePackage{
|
||||
{
|
||||
Addresses: map[string]*MessageAddress{
|
||||
"html@email.com": nil,
|
||||
"none@email.com": nil,
|
||||
},
|
||||
Type: ClearPackage,
|
||||
MIMEType: ContentTypeHTML,
|
||||
EncryptedBody: "non-empty",
|
||||
DecryptedBodyKey: AlgoKey{"non-empty", "non-empty"},
|
||||
DecryptedAttachmentKeys: attAlgoKeys,
|
||||
},
|
||||
},
|
||||
},
|
||||
"SingleClearPlain": {
|
||||
emails: []string{"plain@email.com", "plain-sign@email.com"},
|
||||
wantPackages: []*MessagePackage{
|
||||
{
|
||||
Addresses: map[string]*MessageAddress{
|
||||
"plain@email.com": nil,
|
||||
"plain-sign@email.com": nil,
|
||||
},
|
||||
Type: ClearPackage,
|
||||
MIMEType: ContentTypePlainText,
|
||||
EncryptedBody: "non-empty",
|
||||
DecryptedBodyKey: AlgoKey{"non-empty", "non-empty"},
|
||||
DecryptedAttachmentKeys: attAlgoKeys,
|
||||
},
|
||||
},
|
||||
},
|
||||
"SingleClearMIME": {
|
||||
emails: []string{"mime-sign@email.com"},
|
||||
wantPackages: []*MessagePackage{
|
||||
{
|
||||
Addresses: map[string]*MessageAddress{
|
||||
"mime-sign@email.com": nil,
|
||||
},
|
||||
Type: ClearMIMEPackage,
|
||||
MIMEType: ContentTypeMultipartMixed,
|
||||
EncryptedBody: "non-empty",
|
||||
DecryptedBodyKey: AlgoKey{"non-empty", "non-empty"},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
"SingleEncyptedPlain": {
|
||||
emails: []string{"plain@gpg.com"},
|
||||
wantPackages: []*MessagePackage{
|
||||
{
|
||||
Addresses: map[string]*MessageAddress{
|
||||
"plain@gpg.com": nil,
|
||||
},
|
||||
Type: PGPInlinePackage,
|
||||
MIMEType: ContentTypePlainText,
|
||||
EncryptedBody: "non-empty",
|
||||
},
|
||||
},
|
||||
},
|
||||
"SingleEncyptedMIME": {
|
||||
emails: []string{"mime@gpg.com"},
|
||||
wantPackages: []*MessagePackage{
|
||||
{
|
||||
Addresses: map[string]*MessageAddress{
|
||||
"mime@gpg.com": nil,
|
||||
},
|
||||
Type: PGPMIMEPackage,
|
||||
MIMEType: ContentTypeMultipartMixed,
|
||||
EncryptedBody: "non-empty",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// two schemes combined to one package
|
||||
"SingleClearInternalPlain": {
|
||||
emails: []string{"plain@email.com", "plain-sign@email.com", "plain@pm.me"},
|
||||
wantPackages: []*MessagePackage{
|
||||
{
|
||||
Addresses: map[string]*MessageAddress{
|
||||
"plain@pm.me": nil,
|
||||
"plain@email.com": nil,
|
||||
"plain-sign@email.com": nil,
|
||||
},
|
||||
Type: InternalPackage | ClearPackage,
|
||||
MIMEType: ContentTypePlainText,
|
||||
EncryptedBody: "non-empty",
|
||||
DecryptedBodyKey: AlgoKey{"non-empty", "non-empty"},
|
||||
DecryptedAttachmentKeys: attAlgoKeys,
|
||||
},
|
||||
},
|
||||
},
|
||||
"SingleClearInternalHTML": {
|
||||
emails: []string{"none@email.com", "html@email.com", "html@pm.me", "none@pm.me"},
|
||||
wantPackages: []*MessagePackage{
|
||||
{
|
||||
Addresses: map[string]*MessageAddress{
|
||||
"none@pm.me": nil,
|
||||
"html@pm.me": nil,
|
||||
"html@email.com": nil,
|
||||
"none@email.com": nil,
|
||||
},
|
||||
Type: InternalPackage | ClearPackage,
|
||||
MIMEType: ContentTypeHTML,
|
||||
EncryptedBody: "non-empty",
|
||||
DecryptedBodyKey: AlgoKey{"non-empty", "non-empty"},
|
||||
DecryptedAttachmentKeys: attAlgoKeys,
|
||||
},
|
||||
},
|
||||
},
|
||||
"SingleEncryptedInternalPlain": {
|
||||
emails: []string{"plain@gpg.com", "plain@pm.me"},
|
||||
wantPackages: []*MessagePackage{
|
||||
{
|
||||
Addresses: map[string]*MessageAddress{
|
||||
"plain@pm.me": nil,
|
||||
"plain@gpg.com": nil,
|
||||
},
|
||||
Type: InternalPackage | PGPInlinePackage,
|
||||
MIMEType: ContentTypePlainText,
|
||||
EncryptedBody: "non-empty",
|
||||
},
|
||||
},
|
||||
},
|
||||
"SingleEncryptedClearMIME": {
|
||||
emails: []string{"mime@gpg.com", "mime-sign@email.com"},
|
||||
wantPackages: []*MessagePackage{
|
||||
{
|
||||
Addresses: map[string]*MessageAddress{
|
||||
"mime@gpg.com": nil,
|
||||
"mime-sign@email.com": nil,
|
||||
},
|
||||
Type: ClearMIMEPackage | PGPMIMEPackage,
|
||||
MIMEType: ContentTypeMultipartMixed,
|
||||
EncryptedBody: "non-empty",
|
||||
DecryptedBodyKey: AlgoKey{"non-empty", "non-empty"},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// one scheme separated to multiple packages
|
||||
"MultipleInternal": {
|
||||
emails: []string{"none@pm.me", "html@pm.me", "plain@pm.me"},
|
||||
wantPackages: []*MessagePackage{
|
||||
{
|
||||
Addresses: map[string]*MessageAddress{
|
||||
"plain@pm.me": nil,
|
||||
},
|
||||
Type: InternalPackage,
|
||||
MIMEType: ContentTypePlainText,
|
||||
EncryptedBody: "non-empty",
|
||||
},
|
||||
{
|
||||
Addresses: map[string]*MessageAddress{
|
||||
"none@pm.me": nil,
|
||||
"html@pm.me": nil,
|
||||
},
|
||||
Type: InternalPackage,
|
||||
MIMEType: ContentTypeHTML,
|
||||
EncryptedBody: "non-empty",
|
||||
},
|
||||
},
|
||||
},
|
||||
"MultipleClear": {
|
||||
emails: []string{
|
||||
"none@email.com", "html@email.com",
|
||||
"plain@email.com", "plain-sign@email.com",
|
||||
"mime-sign@email.com",
|
||||
},
|
||||
wantPackages: []*MessagePackage{
|
||||
{
|
||||
Addresses: map[string]*MessageAddress{
|
||||
"mime-sign@email.com": nil,
|
||||
},
|
||||
Type: ClearMIMEPackage,
|
||||
MIMEType: ContentTypeMultipartMixed,
|
||||
EncryptedBody: "non-empty",
|
||||
DecryptedBodyKey: AlgoKey{"non-empty", "non-empty"},
|
||||
},
|
||||
{
|
||||
Addresses: map[string]*MessageAddress{
|
||||
"plain@email.com": nil,
|
||||
"plain-sign@email.com": nil,
|
||||
},
|
||||
Type: ClearPackage,
|
||||
MIMEType: ContentTypePlainText,
|
||||
EncryptedBody: "non-empty",
|
||||
DecryptedBodyKey: AlgoKey{"non-empty", "non-empty"},
|
||||
DecryptedAttachmentKeys: attAlgoKeys,
|
||||
},
|
||||
{
|
||||
Addresses: map[string]*MessageAddress{
|
||||
"html@email.com": nil,
|
||||
"none@email.com": nil,
|
||||
},
|
||||
Type: ClearPackage,
|
||||
MIMEType: ContentTypeHTML,
|
||||
EncryptedBody: "non-empty",
|
||||
DecryptedBodyKey: AlgoKey{"non-empty", "non-empty"},
|
||||
DecryptedAttachmentKeys: attAlgoKeys,
|
||||
},
|
||||
},
|
||||
},
|
||||
"MultipleEncrypted": {
|
||||
emails: []string{"plain@gpg.com", "mime@gpg.com"},
|
||||
wantPackages: []*MessagePackage{
|
||||
{
|
||||
Addresses: map[string]*MessageAddress{
|
||||
"mime@gpg.com": nil,
|
||||
},
|
||||
Type: PGPMIMEPackage,
|
||||
MIMEType: ContentTypeMultipartMixed,
|
||||
EncryptedBody: "non-empty",
|
||||
},
|
||||
{
|
||||
Addresses: map[string]*MessageAddress{
|
||||
"plain@gpg.com": nil,
|
||||
},
|
||||
Type: PGPInlinePackage,
|
||||
MIMEType: ContentTypePlainText,
|
||||
EncryptedBody: "non-empty",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
"MultipleComboAll": {
|
||||
emails: []string{
|
||||
"none@pm.me",
|
||||
"plain@pm.me",
|
||||
"html@pm.me",
|
||||
|
||||
"none@email.com",
|
||||
"html@email.com",
|
||||
"plain@email.com",
|
||||
"plain-sign@email.com",
|
||||
"mime-sign@email.com",
|
||||
|
||||
"mime@gpg.com",
|
||||
"plain@gpg.com",
|
||||
},
|
||||
wantPackages: []*MessagePackage{
|
||||
{
|
||||
Addresses: map[string]*MessageAddress{
|
||||
"mime@gpg.com": nil,
|
||||
"mime-sign@email.com": nil,
|
||||
},
|
||||
Type: ClearMIMEPackage | PGPMIMEPackage,
|
||||
MIMEType: ContentTypeMultipartMixed,
|
||||
EncryptedBody: "non-empty",
|
||||
DecryptedBodyKey: AlgoKey{"non-empty", "non-empty"},
|
||||
},
|
||||
{
|
||||
Addresses: map[string]*MessageAddress{
|
||||
"plain@gpg.com": nil,
|
||||
"plain@email.com": nil,
|
||||
"plain-sign@email.com": nil,
|
||||
"plain@pm.me": nil,
|
||||
},
|
||||
Type: InternalPackage | ClearPackage | PGPInlinePackage,
|
||||
MIMEType: ContentTypePlainText,
|
||||
EncryptedBody: "non-empty",
|
||||
DecryptedBodyKey: AlgoKey{"non-empty", "non-empty"},
|
||||
DecryptedAttachmentKeys: attAlgoKeys,
|
||||
},
|
||||
{
|
||||
Addresses: map[string]*MessageAddress{
|
||||
"none@pm.me": nil,
|
||||
"html@pm.me": nil,
|
||||
"none@email.com": nil,
|
||||
"html@email.com": nil,
|
||||
},
|
||||
Type: InternalPackage | ClearPackage,
|
||||
MIMEType: ContentTypeHTML,
|
||||
EncryptedBody: "non-empty",
|
||||
DecryptedBodyKey: AlgoKey{"non-empty", "non-empty"},
|
||||
DecryptedAttachmentKeys: attAlgoKeys,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range newTests {
|
||||
test.mimeBody = "Mime body"
|
||||
test.plainBody = "Plain body"
|
||||
test.richBody = "HTML body"
|
||||
test.allRecipients = allRecipients
|
||||
test.allAddresses = allAddresses
|
||||
|
||||
test.addRecipients(t)
|
||||
test.addAddresses(t)
|
||||
|
||||
t.Run("NoAtt"+name, test.prepareAndCheck)
|
||||
test.attKeys = map[string]*crypto.SessionKey{"attID": attKey}
|
||||
t.Run("Att"+name, test.prepareAndCheck)
|
||||
}
|
||||
}
|
||||
@ -569,114 +569,6 @@ func (c *client) GetMessage(id string) (msg *Message, err error) {
|
||||
return res.Message, res.Err()
|
||||
}
|
||||
|
||||
type SendMessageReq struct {
|
||||
ExpirationTime int64 `json:",omitempty"`
|
||||
// AutoSaveContacts int `json:",omitempty"`
|
||||
|
||||
// Data for encrypted recipients.
|
||||
Packages []*MessagePackage
|
||||
}
|
||||
|
||||
// Message package types.
|
||||
const (
|
||||
InternalPackage = 1
|
||||
EncryptedOutsidePackage = 2
|
||||
ClearPackage = 4
|
||||
PGPInlinePackage = 8
|
||||
PGPMIMEPackage = 16
|
||||
ClearMIMEPackage = 32
|
||||
)
|
||||
|
||||
// Signature types.
|
||||
const (
|
||||
NoSignature = 0
|
||||
YesSignature = 1
|
||||
)
|
||||
|
||||
type MessagePackage struct {
|
||||
Addresses map[string]*MessageAddress
|
||||
Type int
|
||||
MIMEType string
|
||||
Body string // base64-encoded encrypted data packet.
|
||||
BodyKey AlgoKey // base64-encoded session key (only if cleartext recipients).
|
||||
AttachmentKeys map[string]AlgoKey // Only include if cleartext & attachments.
|
||||
}
|
||||
|
||||
type MessageAddress struct {
|
||||
Type int
|
||||
BodyKeyPacket string // base64-encoded key packet.
|
||||
Signature int // 0 = None, 1 = Detached, 2 = Attached/Armored
|
||||
AttachmentKeyPackets map[string]string
|
||||
}
|
||||
|
||||
type AlgoKey struct {
|
||||
Key string
|
||||
Algorithm string
|
||||
}
|
||||
|
||||
type SendMessageRes struct {
|
||||
Res
|
||||
|
||||
Sent *Message
|
||||
|
||||
// Parent is only present if the sent message has a parent (reply/reply all/forward).
|
||||
Parent *Message
|
||||
}
|
||||
|
||||
func (c *client) SendMessage(id string, sendReq *SendMessageReq) (sent, parent *Message, err error) {
|
||||
if id == "" {
|
||||
err = errors.New("pmapi: cannot send message with an empty id")
|
||||
return
|
||||
}
|
||||
|
||||
if sendReq.Packages == nil {
|
||||
sendReq.Packages = []*MessagePackage{}
|
||||
}
|
||||
|
||||
req, err := c.NewJSONRequest("POST", "/mail/v4/messages/"+id, sendReq)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var res SendMessageRes
|
||||
if err = c.DoJSON(req, &res); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
sent, parent, err = res.Sent, res.Parent, res.Err()
|
||||
return
|
||||
}
|
||||
|
||||
const (
|
||||
DraftActionReply = 0
|
||||
DraftActionReplyAll = 1
|
||||
DraftActionForward = 2
|
||||
)
|
||||
|
||||
type DraftReq struct {
|
||||
Message *Message
|
||||
ParentID string `json:",omitempty"`
|
||||
Action int
|
||||
AttachmentKeyPackets []string
|
||||
}
|
||||
|
||||
func (c *client) CreateDraft(m *Message, parent string, action int) (created *Message, err error) {
|
||||
createReq := &DraftReq{Message: m, ParentID: parent, Action: action, AttachmentKeyPackets: []string{}}
|
||||
|
||||
req, err := c.NewJSONRequest("POST", "/mail/v4/messages", createReq)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var res MessageRes
|
||||
if err = c.DoJSON(req, &res); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
created, err = res.Message, res.Err()
|
||||
return
|
||||
}
|
||||
|
||||
type MessagesActionReq struct {
|
||||
IDs []string
|
||||
}
|
||||
|
||||
@ -5,12 +5,11 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
io "io"
|
||||
reflect "reflect"
|
||||
|
||||
crypto "github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
io "io"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockClient is a mock of Client interface
|
||||
|
||||
@ -382,7 +382,7 @@ func TestProxyProvider_UseProxy_RevertIfProxyStopsWorkingAndOriginalAPIIsReachab
|
||||
// The error should be ErrAPINotReachable because the connection dropped intermittently but
|
||||
// the original API is now reachable (see Alternative-Routing-v2 spec for details).
|
||||
url, err = cm.switchToReachableServer()
|
||||
require.EqualError(t, err, ErrAPINotReachable.Error())
|
||||
require.Error(t, err)
|
||||
require.Equal(t, rootURL, url)
|
||||
require.Equal(t, rootURL, cm.getHost())
|
||||
}
|
||||
|
||||
@ -85,7 +85,7 @@ type MailSettings struct {
|
||||
RightToLeft int
|
||||
AttachPublicKey int
|
||||
Sign int
|
||||
PGPScheme int
|
||||
PGPScheme PackageFlag
|
||||
PromptPin int
|
||||
Autocrypt int
|
||||
NumMessagePerPage int
|
||||
|
||||
@ -35,13 +35,21 @@ var ErrTLSMismatch = errors.New("no TLS fingerprint match found")
|
||||
// TrustedAPIPins contains trusted public keys of the protonmail API and proxies.
|
||||
// NOTE: the proxy pins are the same for all proxy servers, guaranteed by infra team ;)
|
||||
var TrustedAPIPins = []string{ // nolint[gochecknoglobals]
|
||||
// api.protonmail.ch
|
||||
`pin-sha256="drtmcR2kFkM8qJClsuWgUzxgBkePfRCkRpqUesyDmeE="`, // current
|
||||
`pin-sha256="YRGlaY0jyJ4Jw2/4M8FIftwbDIQfh8Sdro96CeEel54="`, // hot
|
||||
`pin-sha256="AfMENBVvOS8MnISprtvyPsjKlPooqh8nMB/pvCrpJpw="`, // cold
|
||||
`pin-sha256="EU6TS9MO0L/GsDHvVc9D5fChYLNy5JdGYpJw0ccgetM="`, // proxy main
|
||||
`pin-sha256="iKPIHPnDNqdkvOnTClQ8zQAIKG0XavaPkcEo0LBAABA="`, // proxy backup 1
|
||||
`pin-sha256="MSlVrBCdL0hKyczvgYVSRNm88RicyY04Q2y5qrBt0xA="`, // proxy backup 2
|
||||
`pin-sha256="C2UxW0T1Ckl9s+8cXfjXxlEqwAfPM4HiW2y3UdtBeCw="`, // proxy backup 3
|
||||
`pin-sha256="YRGlaY0jyJ4Jw2/4M8FIftwbDIQfh8Sdro96CeEel54="`, // hot backup
|
||||
`pin-sha256="AfMENBVvOS8MnISprtvyPsjKlPooqh8nMB/pvCrpJpw="`, // cold backup
|
||||
|
||||
// protonmail.com
|
||||
`pin-sha256="8joiNBdqaYiQpKskgtkJsqRxF7zN0C0aqfi8DacknnI="`, // current
|
||||
`pin-sha256="JMI8yrbc6jB1FYGyyWRLFTmDNgIszrNEMGlgy972e7w="`, // hot backup
|
||||
`pin-sha256="Iu44zU84EOCZ9vx/vz67/MRVrxF1IO4i4NIa8ETwiIY="`, // cold backup
|
||||
|
||||
// proxies
|
||||
`pin-sha256="EU6TS9MO0L/GsDHvVc9D5fChYLNy5JdGYpJw0ccgetM="`, // main
|
||||
`pin-sha256="iKPIHPnDNqdkvOnTClQ8zQAIKG0XavaPkcEo0LBAABA="`, // backup 1
|
||||
`pin-sha256="MSlVrBCdL0hKyczvgYVSRNm88RicyY04Q2y5qrBt0xA="`, // backup 2
|
||||
`pin-sha256="C2UxW0T1Ckl9s+8cXfjXxlEqwAfPM4HiW2y3UdtBeCw="`, // backup 3
|
||||
}
|
||||
|
||||
// TLSReportURI is the address where TLS reports should be sent.
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
package pmapi
|
||||
|
||||
import (
|
||||
"github.com/getsentry/raven-go"
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@ -119,7 +119,11 @@ func (c *client) UpdateUser() (user *User, err error) {
|
||||
}
|
||||
|
||||
c.user = user
|
||||
raven.SetUserContext(&raven.User{ID: user.ID})
|
||||
sentry.ConfigureScope(func(scope *sentry.Scope) {
|
||||
scope.SetUser(sentry.User{
|
||||
ID: user.ID,
|
||||
})
|
||||
})
|
||||
|
||||
var tmpList AddressList
|
||||
if tmpList, err = c.GetAddresses(); err == nil {
|
||||
|
||||
23
pkg/pmapi/utils.go
Normal file
23
pkg/pmapi/utils.go
Normal file
@ -0,0 +1,23 @@
|
||||
// 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 pmapi
|
||||
|
||||
func iHasFlag(i, flag int) bool { return i&flag == flag }
|
||||
func iHasAtLeastOneFlag(i, flag int) bool { return i&flag > 0 }
|
||||
func iIsFlag(i, flag int) bool { return i == flag }
|
||||
func iHasNoneOfFlag(i, flag int) bool { return !iHasAtLeastOneFlag(i, flag) }
|
||||
@ -18,135 +18,15 @@
|
||||
package sentry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"errors"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/getsentry/raven-go"
|
||||
"github.com/getsentry/sentry-go"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const fileParseError = "[file parse error]"
|
||||
|
||||
var isGoroutine = regexp.MustCompile("^goroutine [[:digit:]]+.*") //nolint[gochecknoglobals]
|
||||
|
||||
// Threads implements standard sentry thread report.
|
||||
type Threads struct {
|
||||
Values []Thread `json:"values"`
|
||||
}
|
||||
|
||||
// Class specifier.
|
||||
func (s *Threads) Class() string { return "threads" }
|
||||
|
||||
// Thread wraps a single stacktrace.
|
||||
type Thread struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Crashed bool `json:"crashed"`
|
||||
Stacktrace *raven.Stacktrace `json:"stacktrace"`
|
||||
}
|
||||
|
||||
// TraceAllRoutines traces all goroutines and saves them to the current object.
|
||||
func (s *Threads) TraceAllRoutines() {
|
||||
s.Values = []Thread{}
|
||||
goroutines := &strings.Builder{}
|
||||
_ = pprof.Lookup("goroutine").WriteTo(goroutines, 2)
|
||||
|
||||
thread := Thread{ID: -1}
|
||||
var frame *raven.StacktraceFrame
|
||||
for _, v := range strings.Split(goroutines.String(), "\n") {
|
||||
// Ignore empty lines.
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// New routine.
|
||||
if isGoroutine.MatchString(v) {
|
||||
if thread.ID >= 0 {
|
||||
s.Values = append(s.Values, thread)
|
||||
}
|
||||
thread = Thread{ID: thread.ID + 1, Name: v, Crashed: thread.ID == -1, Stacktrace: &raven.Stacktrace{Frames: []*raven.StacktraceFrame{}}}
|
||||
continue
|
||||
}
|
||||
|
||||
// New function.
|
||||
if frame == nil {
|
||||
frame = &raven.StacktraceFrame{Function: v}
|
||||
continue
|
||||
}
|
||||
|
||||
// Set filename and add frame.
|
||||
if frame.Filename == "" {
|
||||
fld := strings.Fields(v)
|
||||
if len(fld) != 2 {
|
||||
frame.Filename = fileParseError
|
||||
frame.AbsolutePath = v
|
||||
} else {
|
||||
frame.Filename = fld[0]
|
||||
sp := strings.Split(fld[0], ":")
|
||||
if len(sp) > 1 {
|
||||
i, err := strconv.Atoi(sp[len(sp)-1])
|
||||
if err == nil {
|
||||
frame.Filename = strings.Join(sp[:len(sp)-1], ":")
|
||||
frame.Lineno = i
|
||||
}
|
||||
}
|
||||
}
|
||||
if frame.AbsolutePath == "" && frame.Filename != fileParseError {
|
||||
frame.AbsolutePath = frame.Filename
|
||||
if sp := strings.Split(frame.Filename, "/"); len(sp) > 1 {
|
||||
frame.Filename = sp[len(sp)-1]
|
||||
}
|
||||
}
|
||||
thread.Stacktrace.Frames = append([]*raven.StacktraceFrame{frame}, thread.Stacktrace.Frames...)
|
||||
frame = nil
|
||||
continue
|
||||
}
|
||||
}
|
||||
// Add last thread.
|
||||
s.Values = append(s.Values, thread)
|
||||
}
|
||||
|
||||
func findPanicSender(s *Threads, err error) string {
|
||||
out := "error nil"
|
||||
if err != nil {
|
||||
out = err.Error()
|
||||
}
|
||||
for _, thread := range s.Values {
|
||||
if !thread.Crashed {
|
||||
continue
|
||||
}
|
||||
for i, fr := range thread.Stacktrace.Frames {
|
||||
if strings.HasSuffix(fr.Filename, "panic.go") && strings.HasPrefix(fr.Function, "panic") {
|
||||
// Next frame if any.
|
||||
j := 0
|
||||
if i > j {
|
||||
j = i - 1
|
||||
}
|
||||
|
||||
// Directory and filename.
|
||||
fname := thread.Stacktrace.Frames[j].AbsolutePath
|
||||
if sp := strings.Split(fname, "/"); len(sp) > 2 {
|
||||
fname = strings.Join(sp[len(sp)-2:], "/")
|
||||
}
|
||||
|
||||
// Line number.
|
||||
if ln := thread.Stacktrace.Frames[j].Lineno; ln > 0 {
|
||||
fname = fmt.Sprintf("%s:%d", fname, ln)
|
||||
}
|
||||
|
||||
out = fmt.Sprintf("%s: %s", fname, out)
|
||||
break // Just first panic.
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ReportSentryCrash reports a sentry crash with stacktrace from all goroutines.
|
||||
// ReportSentryCrash reports a sentry crash.
|
||||
func ReportSentryCrash(clientID, appVersion, userAgent string, reportErr error) (err error) {
|
||||
if reportErr == nil {
|
||||
return
|
||||
@ -160,18 +40,16 @@ func ReportSentryCrash(clientID, appVersion, userAgent string, reportErr error)
|
||||
"UserID": "",
|
||||
}
|
||||
|
||||
threads := &Threads{}
|
||||
threads.TraceAllRoutines()
|
||||
errorWithFile := findPanicSender(threads, reportErr)
|
||||
packet := raven.NewPacket(errorWithFile, threads)
|
||||
sentry.WithScope(func(scope *sentry.Scope) {
|
||||
scope.SetTags(tags)
|
||||
sentry.CaptureException(reportErr)
|
||||
})
|
||||
|
||||
eventID, ch := raven.Capture(packet, tags)
|
||||
|
||||
if err = <-ch; err == nil {
|
||||
log.WithField("errorID", eventID).Warn("Reported sentry error")
|
||||
} else {
|
||||
log.WithField("error", reportErr).WithError(err).Error("Failed to report sentry error")
|
||||
if !sentry.Flush(time.Second * 10) {
|
||||
log.WithField("error", reportErr).Error("failed to report sentry error")
|
||||
return errors.New("failed to report sentry error")
|
||||
}
|
||||
|
||||
return err
|
||||
log.WithField("error", reportErr).Warn("reported sentry error")
|
||||
return
|
||||
}
|
||||
|
||||
@ -1,69 +0,0 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
// ProtonMail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ProtonMail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package sentry
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/getsentry/raven-go"
|
||||
)
|
||||
|
||||
func TestSentryCrashReport(t *testing.T) {
|
||||
if err := ReportSentryCrash(
|
||||
"clientID",
|
||||
"appVersion",
|
||||
"useragent",
|
||||
errors.New("Testing crash report - api proxy; goroutines with threads, find origin"),
|
||||
); err != nil {
|
||||
t.Fatal("Expected no error while report, but have", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Threads) TraceAllRoutinesTest() {
|
||||
s.Values = []Thread{
|
||||
{
|
||||
ID: 0,
|
||||
Name: "goroutine 20 [running]",
|
||||
Crashed: true,
|
||||
Stacktrace: &raven.Stacktrace{
|
||||
Frames: []*raven.StacktraceFrame{
|
||||
{
|
||||
Filename: "/home/dev/build/go-1.10.2/go/src/runtime/pprof/pprof.go",
|
||||
Function: "runtime/pprof.writeGoroutineStacks(0x9b7de0, 0xc4203e2900, 0xd0, 0xd0)",
|
||||
Lineno: 650,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: 1,
|
||||
Name: "goroutine 20 [chan receive]",
|
||||
Crashed: false,
|
||||
Stacktrace: &raven.Stacktrace{
|
||||
Frames: []*raven.StacktraceFrame{
|
||||
{
|
||||
Filename: "/home/dev/build/go-1.10.2/go/src/testing/testing.go",
|
||||
Function: "testing.(*T).Run(0xc4203e42d0, 0x90f445, 0x15, 0x97d358, 0x47a501)",
|
||||
Lineno: 825,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
• Fixed rare mail loss when moving from Spam folder
|
||||
• Limited log size
|
||||
• Fixed Linux font issues (mouse hover).
|
||||
• 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)
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
• Linux font issues - Fedora specific
|
||||
• App response to the user pausing and canceling import or export
|
||||
• Handling errors during update
|
||||
|
||||
@ -1,8 +1,4 @@
|
||||
• Bulletproofing against any potential data loss and/or duplication
|
||||
• Performance improvements for handling attachments and non-standard formatting
|
||||
• Better stability of the message parser
|
||||
• Additional foreign encoding support for outgoing messages
|
||||
• Complete refactor of the way messages are parsed to simplify code maintenance
|
||||
• Improved User-Agent detection
|
||||
• Added MacOS Big Sur compatibility
|
||||
• Added persistent anonymous API cookies
|
||||
• 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
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
• 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
|
||||
• Various enhancements of the import process related to parsing
|
||||
• Cosmetic GUI changes
|
||||
• Better error handling
|
||||
• Further improvements to address and date parsing
|
||||
• Better handling and displaying of skipped messages
|
||||
• Improved error reporting
|
||||
|
||||
@ -28,7 +28,7 @@ import (
|
||||
|
||||
func APIChecksFeatureContext(s *godog.Suite) {
|
||||
s.Step(`^API endpoint "([^"]*)" is called with:$`, apiIsCalledWith)
|
||||
s.Step(`^message is sent with API call:$`, messageIsSentWithAPICall)
|
||||
s.Step(`^message is sent with API call$`, messageIsSentWithAPICall)
|
||||
s.Step(`^API mailbox "([^"]*)" for "([^"]*)" has messages$`, apiMailboxForUserHasMessages)
|
||||
s.Step(`^API mailbox "([^"]*)" for address "([^"]*)" of "([^"]*)" has messages$`, apiMailboxForAddressOfUserHasMessages)
|
||||
}
|
||||
|
||||
@ -4,14 +4,14 @@ Feature: IMAP update messages in Spam folder
|
||||
# Messages are inserted in opposite way to keep increasing ID.
|
||||
# Sequence numbers are then opposite than listed above.
|
||||
And there are messages in mailbox "Spam" for "user"
|
||||
| from | to | subject | body | read | starred | deleted |
|
||||
| john.doe@mail.com | user@pm.me | foo | hello | false | false | false |
|
||||
| jane.doe@mail.com | name@pm.me | bar | world | true | true | false |
|
||||
| id | from | to | subject | body | read | starred | deleted |
|
||||
| 1 | john.doe@mail.com | user@pm.me | foo | hello | false | false | false |
|
||||
| 2 | jane.doe@mail.com | name@pm.me | bar | world | true | true | false |
|
||||
And there is IMAP client logged in as "user"
|
||||
And there is IMAP client selected in "Spam"
|
||||
|
||||
Scenario: Mark message as read only
|
||||
When IMAP client marks message "2" with "\Seen"
|
||||
When IMAP client marks message seq "1" with "\Seen"
|
||||
Then IMAP response is "OK"
|
||||
And message "1" in "Spam" for "user" is marked as read
|
||||
And message "1" in "Spam" for "user" is marked as unstarred
|
||||
|
||||
@ -17,7 +17,7 @@ Feature: SMTP with bcc
|
||||
And mailbox "Sent" for "user" has messages
|
||||
| from | to | subject |
|
||||
| [userAddress] | bridgetest@protonmail.com | hello |
|
||||
And message is sent with API call:
|
||||
And message is sent with API call
|
||||
"""
|
||||
{
|
||||
"Message": {
|
||||
@ -52,7 +52,7 @@ Feature: SMTP with bcc
|
||||
And mailbox "Sent" for "user" has messages
|
||||
| from | to | subject |
|
||||
| [userAddress] | | hello |
|
||||
And message is sent with API call:
|
||||
And message is sent with API call
|
||||
"""
|
||||
{
|
||||
"Message": {
|
||||
|
||||
@ -21,7 +21,7 @@ Feature: SMTP sending of HTML messages
|
||||
And mailbox "Sent" for "user" has messages
|
||||
| from | to | subject |
|
||||
| [userAddress] | pm.bridge.qa@gmail.com | HTML text external |
|
||||
And message is sent with API call:
|
||||
And message is sent with API call
|
||||
"""
|
||||
{
|
||||
"Message": {
|
||||
@ -96,7 +96,7 @@ Feature: SMTP sending of HTML messages
|
||||
And mailbox "Sent" for "user" has messages
|
||||
| from | to | subject |
|
||||
| [userAddress] | pm.bridge.qa@gmail.com | Html Inline External |
|
||||
And message is sent with API call:
|
||||
And message is sent with API call
|
||||
"""
|
||||
{
|
||||
"Message": {
|
||||
@ -185,7 +185,7 @@ Feature: SMTP sending of HTML messages
|
||||
And mailbox "Sent" for "user" has messages
|
||||
| from | to | subject |
|
||||
| [userAddress] | bridgetest@protonmail.com | Html Inline Alternative Internal |
|
||||
And message is sent with API call:
|
||||
And message is sent with API call
|
||||
"""
|
||||
{
|
||||
"Message": {
|
||||
@ -274,7 +274,7 @@ Feature: SMTP sending of HTML messages
|
||||
And mailbox "Sent" for "user" has messages
|
||||
| from | to | subject |
|
||||
| [userAddress] | pm.bridge.qa@gmail.com | Html Inline Alternative External |
|
||||
And message is sent with API call:
|
||||
And message is sent with API call
|
||||
"""
|
||||
{
|
||||
"Message": {
|
||||
|
||||
@ -41,7 +41,7 @@ Feature: SMTP sending of HTML messages with attachments
|
||||
And mailbox "Sent" for "user" has messages
|
||||
| from | to | subject |
|
||||
| [userAddress] | bridgetest@protonmail.com | HTML with attachment internal |
|
||||
And message is sent with API call:
|
||||
And message is sent with API call
|
||||
"""
|
||||
{
|
||||
"Message": {
|
||||
@ -100,7 +100,7 @@ Feature: SMTP sending of HTML messages with attachments
|
||||
And mailbox "Sent" for "user" has messages
|
||||
| from | to | subject |
|
||||
| [userAddress] | pm.bridge.qa@gmail.com | HTML with attachment external PGP |
|
||||
And message is sent with API call:
|
||||
And message is sent with API call
|
||||
"""
|
||||
{
|
||||
"Message": {
|
||||
|
||||
@ -16,7 +16,7 @@ Feature: SMTP sending of plain messages
|
||||
And mailbox "Sent" for "user" has messages
|
||||
| from | to | subject |
|
||||
| [userAddress] | bridgetest@protonmail.com | |
|
||||
And message is sent with API call:
|
||||
And message is sent with API call
|
||||
"""
|
||||
{
|
||||
"Message": {
|
||||
@ -50,7 +50,7 @@ Feature: SMTP sending of plain messages
|
||||
And mailbox "Sent" for "user" has messages
|
||||
| from | to | subject |
|
||||
| [userAddress] | pm.bridge.qa@gmail.com | |
|
||||
And message is sent with API call:
|
||||
And message is sent with API call
|
||||
"""
|
||||
{
|
||||
"Message": {
|
||||
@ -87,7 +87,7 @@ Feature: SMTP sending of plain messages
|
||||
And mailbox "Sent" for "user" has messages
|
||||
| from | to | subject |
|
||||
| [userAddress] | bridgetest@protonmail.com | Plain text internal |
|
||||
And message is sent with API call:
|
||||
And message is sent with API call
|
||||
"""
|
||||
{
|
||||
"Message": {
|
||||
@ -124,7 +124,7 @@ Feature: SMTP sending of plain messages
|
||||
And mailbox "Sent" for "user" has messages
|
||||
| from | to | subject |
|
||||
| [userAddress] | pm.bridge.qa@gmail.com | Plain text external |
|
||||
And message is sent with API call:
|
||||
And message is sent with API call
|
||||
"""
|
||||
{
|
||||
"Message": {
|
||||
@ -161,7 +161,7 @@ Feature: SMTP sending of plain messages
|
||||
And mailbox "Sent" for "user" has messages
|
||||
| from | to | subject |
|
||||
| [userAddress] | pm.bridge.qa@gmail.com | Plain text no charset external |
|
||||
And message is sent with API call:
|
||||
And message is sent with API call
|
||||
"""
|
||||
{
|
||||
"Message": {
|
||||
@ -201,7 +201,7 @@ Feature: SMTP sending of plain messages
|
||||
And mailbox "Sent" for "user" has messages
|
||||
| from | to | subject |
|
||||
| [userAddress] | pm.bridge.qa@gmail.com | Plain text no charset external |
|
||||
And message is sent with API call:
|
||||
And message is sent with API call
|
||||
"""
|
||||
{
|
||||
"Message": {
|
||||
@ -236,7 +236,7 @@ Feature: SMTP sending of plain messages
|
||||
And mailbox "Sent" for "user" has messages
|
||||
| from | to | subject |
|
||||
| [userAddress] | pm.bridge.qa@gmail.com | Plain, no charset, no content, external |
|
||||
And message is sent with API call:
|
||||
And message is sent with API call
|
||||
"""
|
||||
{
|
||||
"Message": {
|
||||
|
||||
@ -41,7 +41,7 @@ Feature: SMTP sending of plain messages with attachments
|
||||
And mailbox "Sent" for "user" has messages
|
||||
| from | to | subject |
|
||||
| [userAddress] | bridgetest@protonmail.com | Plain with attachment |
|
||||
And message is sent with API call:
|
||||
And message is sent with API call
|
||||
"""
|
||||
{
|
||||
"Message": {
|
||||
@ -100,7 +100,7 @@ Feature: SMTP sending of plain messages with attachments
|
||||
And mailbox "Sent" for "user" has messages
|
||||
| from | to | subject |
|
||||
| [userAddress] | pm.bridge.qa@gmail.com | Plain with attachment external |
|
||||
And message is sent with API call:
|
||||
And message is sent with API call
|
||||
"""
|
||||
{
|
||||
"Message": {
|
||||
@ -160,7 +160,7 @@ Feature: SMTP sending of plain messages with attachments
|
||||
And mailbox "Sent" for "user" has messages
|
||||
| from | to | cc | subject |
|
||||
| [userAddress] | pm.bridge.qa@gmail.com | bridgeqa@seznam.cz | Plain with attachment external PGP and external CC |
|
||||
And message is sent with API call:
|
||||
And message is sent with API call
|
||||
"""
|
||||
{
|
||||
"Message": {
|
||||
|
||||
50
test/features/bridge/smtp/send/send_append.feature
Normal file
50
test/features/bridge/smtp/send/send_append.feature
Normal file
@ -0,0 +1,50 @@
|
||||
Feature: SMTP sending with APPENDing to Sent
|
||||
Background:
|
||||
Given there is connected user "user"
|
||||
And there is IMAP client logged in as "user"
|
||||
And there is IMAP client selected in "Sent"
|
||||
And there is SMTP client logged in as "user"
|
||||
|
||||
Scenario: Send message and append to Sent
|
||||
# First do sending.
|
||||
When SMTP client sends message
|
||||
"""
|
||||
To: Internal Bridge <bridgetest@protonmail.com>
|
||||
Subject: Manual send and append
|
||||
Message-ID: bridgemessage42
|
||||
|
||||
hello
|
||||
|
||||
"""
|
||||
Then SMTP response is "OK"
|
||||
And mailbox "Sent" for "user" has 1 messages
|
||||
And mailbox "Sent" for "user" has messages
|
||||
| externalid | from | to | subject |
|
||||
| bridgemessage42 | [userAddress] | bridgetest@protonmail.com | Manual send and append |
|
||||
And message is sent with API call
|
||||
"""
|
||||
{
|
||||
"Message": {
|
||||
"Subject": "Manual send and append",
|
||||
"ExternalID": "bridgemessage42"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
# Then simulate manual append to Sent mailbox - message should be detected as a duplicate.
|
||||
When IMAP client imports message to "Sent"
|
||||
"""
|
||||
To: Internal Bridge <bridgetest@protonmail.com>
|
||||
Subject: Manual send and append
|
||||
Message-ID: bridgemessage42
|
||||
|
||||
hello
|
||||
|
||||
"""
|
||||
Then IMAP response is "OK"
|
||||
And mailbox "Sent" for "user" has 1 messages
|
||||
|
||||
# Check that the external ID was not lost in the process.
|
||||
When IMAP client sends command "FETCH 1 body.peek[header]"
|
||||
Then IMAP response is "OK"
|
||||
And IMAP response contains "bridgemessage42"
|
||||
@ -155,7 +155,7 @@ func (ir *IMAPResponse) AssertSectionsInOrder(wantRegexps ...string) *IMAPRespon
|
||||
func (ir *IMAPResponse) AssertSections(wantRegexps ...string) *IMAPResponse {
|
||||
ir.wait()
|
||||
for _, wantRegexp := range wantRegexps {
|
||||
a.NoError(ir.t, ir.hasSectionRegexp(wantRegexp), "regexp %v not found", wantRegexp)
|
||||
a.NoError(ir.t, ir.hasSectionRegexp(wantRegexp), "regexp %v not found\nSections: %v", wantRegexp, ir.sections)
|
||||
}
|
||||
return ir
|
||||
}
|
||||
|
||||
@ -23,12 +23,12 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/mail"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/go-rfc5322"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@ -148,12 +148,12 @@ func (c *SMTPClient) SendMail(r io.Reader, bcc string) *SMTPResponse {
|
||||
|
||||
from = c.address
|
||||
if from == "" && strings.HasPrefix(line, "From: ") {
|
||||
if addr, err := mail.ParseAddress(line[6:]); err == nil {
|
||||
from = addr.Address
|
||||
if addrs, err := rfc5322.ParseAddressList(line[6:]); err == nil {
|
||||
from = addrs[0].Address
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(line, "To: ") || strings.HasPrefix(line, "CC: ") {
|
||||
if addrs, err := mail.ParseAddressList(line[4:]); err == nil {
|
||||
if addrs, err := rfc5322.ParseAddressList(line[4:]); err == nil {
|
||||
for _, addr := range addrs {
|
||||
tos = append(tos, addr.Address)
|
||||
}
|
||||
|
||||
@ -19,10 +19,10 @@ package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/mail"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/go-rfc5322"
|
||||
"github.com/ProtonMail/proton-bridge/internal/store"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/ProtonMail/proton-bridge/test/accounts"
|
||||
@ -211,6 +211,10 @@ func messagesContainsMessageRow(account *accounts.TestAccount, allMessages []int
|
||||
if message.ID != id {
|
||||
matches = false
|
||||
}
|
||||
case "externalid":
|
||||
if message.ExternalID != cell.Value {
|
||||
matches = false
|
||||
}
|
||||
case "from": //nolint[goconst]
|
||||
address := ctx.EnsureAddress(account.Username(), cell.Value)
|
||||
if !areAddressesSame(message.Sender.Address, address) {
|
||||
@ -276,15 +280,15 @@ func messagesContainsMessageRow(account *accounts.TestAccount, allMessages []int
|
||||
}
|
||||
|
||||
func areAddressesSame(first, second string) bool {
|
||||
firstAddress, err := mail.ParseAddress(first)
|
||||
firstAddress, err := rfc5322.ParseAddressList(first)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
secondAddress, err := mail.ParseAddress(second)
|
||||
secondAddress, err := rfc5322.ParseAddressList(second)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return firstAddress.Address == secondAddress.Address
|
||||
return firstAddress[0].Address == secondAddress[0].Address
|
||||
}
|
||||
|
||||
func messagesInMailboxForUserIsMarkedAsRead(bddMessageIDs, mailboxName, bddUserID string) error {
|
||||
|
||||
@ -21,13 +21,13 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/mail"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/go-rfc5322"
|
||||
"github.com/cucumber/godog"
|
||||
"github.com/cucumber/godog/gherkin"
|
||||
"github.com/emersion/go-mbox"
|
||||
@ -65,15 +65,15 @@ func progressFinishedWith(wantResponse string) error {
|
||||
|
||||
func transferExportedNumberOfMessages(wantCount int) error {
|
||||
progress := ctx.GetTransferProgress()
|
||||
_, _, exported, _, _ := progress.GetCounts() //nolint[dogsled]
|
||||
a.Equal(ctx.GetTestingT(), uint(wantCount), exported)
|
||||
counts := progress.GetCounts()
|
||||
a.Equal(ctx.GetTestingT(), uint(wantCount), counts.Exported)
|
||||
return ctx.GetTestingError()
|
||||
}
|
||||
|
||||
func transferImportedNumberOfMessages(wantCount int) error {
|
||||
progress := ctx.GetTransferProgress()
|
||||
_, imported, _, _, _ := progress.GetCounts() //nolint[dogsled]
|
||||
a.Equal(ctx.GetTestingT(), uint(wantCount), imported)
|
||||
counts := progress.GetCounts()
|
||||
a.Equal(ctx.GetTestingT(), uint(wantCount), counts.Imported)
|
||||
return ctx.GetTestingError()
|
||||
}
|
||||
|
||||
@ -243,7 +243,7 @@ func parseTime(input string) (time.Time, error) {
|
||||
}
|
||||
|
||||
func parseAddresses(input string) ([]string, error) {
|
||||
addresses, err := mail.ParseAddressList(input)
|
||||
addresses, err := rfc5322.ParseAddressList(input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -255,11 +255,11 @@ func parseAddresses(input string) ([]string, error) {
|
||||
}
|
||||
|
||||
func parseAddress(input string) (string, error) {
|
||||
address, err := mail.ParseAddress(input)
|
||||
address, err := rfc5322.ParseAddressList(input)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return address.Address, nil
|
||||
return address[0].Address, nil
|
||||
}
|
||||
|
||||
// BySubject implements sort.Interface based on the subject field.
|
||||
|
||||
11
unreleased.md
Normal file
11
unreleased.md
Normal file
@ -0,0 +1,11 @@
|
||||
# ProtonMail Bridge and Import-Export app Changelog
|
||||
|
||||
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Added
|
||||
|
||||
### Changed
|
||||
|
||||
### Removed
|
||||
Reference in New Issue
Block a user