Compare commits

...

19 Commits

Author SHA1 Message Date
072ce54fe1 Bridge 1.5.2 Golden Gate
Release Notes
* Improved package creation logic
* Refactor of sending functions to simplify code maintenance
* Added tests for package creation

Fixed
* 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)
2020-11-24 10:38:36 +01:00
1f31df3a94 Bridge 1.5.1 Golden Gate
Release Notes
* Improved package creation logic
* Refactor of sending functions to simplify code maintenance
* Added tests for package creation

Fixed
* 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)
2020-11-23 07:43:43 +01:00
9ee30e4923 Add sentry fingerprint 2020-11-20 14:44:42 +00:00
7b44f12ab1 Update sentry client 2020-11-20 14:44:42 +00:00
874882b554 Logic change to follow old code. 2020-11-20 13:39:13 +00:00
945bdf4c60 Custom types for flags and encrypted outside test 2020-11-20 13:39:13 +00:00
6e1e5a2afe re-organise test definitions 2020-11-20 13:39:13 +00:00
b709b51790 Simplify test cases 2020-11-20 13:39:13 +00:00
d380485bb6 Fixing lint and integration tests, changelog, GODT-880, and typos 2020-11-20 13:39:13 +00:00
87c8228cd0 rename 2020-11-20 13:39:13 +00:00
152046bf97 refactor smtp sending
* [x] move package creation logic to `pmapi.SendMessageReq`
* [ ] write test of package creation logic
    * [x] internal
    * [x] plain
    * [x] external encrypted
    * [ ] signature ???
    * [x] attachments
2020-11-20 13:39:13 +00:00
a0fbed5859 use unreleased for changes 2020-11-20 14:35:24 +01:00
89e9e17d26 Fix typos in InlineLabelSelect.qml 2020-11-20 07:43:43 +00:00
b595247392 chore: add version info to github issue template 2020-11-19 16:57:21 +01:00
9d50a8cef2 Add OS to app version 2020-11-18 09:46:01 +00:00
f888176485 Build creates proper binary names 2020-11-18 08:56:38 +00:00
2f9876ad74 Remove unnecessary semicolon 2020-11-13 13:18:16 +00:00
53404122cc Integration test of sending and manual appending to Sent mailbox 2020-11-13 13:18:16 +00:00
ba65494fce Try load messages one-by-one 2020-11-13 09:43:04 +00:00
46 changed files with 1870 additions and 646 deletions

2
.gitattributes vendored
View File

@ -1 +1 @@
Changelog.md merge=union
unreleased.md merge=union

View File

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

View File

@ -2,6 +2,30 @@
Changelog [format](http://keepachangelog.com/en/1.0.0/)
## [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
@ -16,6 +40,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
## [Bridge 1.5.0] Golden Gate
### Changed
* Updated go-mbox dependency back to upstream.

View File

@ -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.5.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,17 +42,22 @@ 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"
@ -62,7 +69,7 @@ build-ie:
TARGET_CMD=Import-Export $(MAKE) build
build-nogui:
go build ${BUILD_FLAGS_NOGUI} -o ${TARGET_CMD} cmd/${TARGET_CMD}/main.go
go build ${BUILD_FLAGS_NOGUI} -o ${EXE_NAME} cmd/${TARGET_CMD}/main.go
build-ie-nogui:
TARGET_CMD=Import-Export $(MAKE) build-nogui
@ -77,12 +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
@ -100,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}
@ -196,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

3
go.mod
View File

@ -25,7 +25,6 @@ require (
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
@ -43,7 +42,7 @@ require (
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

149
go.sum
View File

@ -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=
@ -31,22 +35,30 @@ github.com/ProtonMail/gopenpgp/v2 v2.0.1 h1:x0uvDhry5WzoHeJO4J3dgMLhG4Z9PeBJ2O+s
github.com/ProtonMail/gopenpgp/v2 v2.0.1/go.mod h1:wQQCJo7DURO6S9VwH+kSDEYs/B63yZnAEfGlOg8YNBY=
github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE=
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0=
github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw=
github.com/abiosoft/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzgHf/aS6+zVXRj14cVk9qg=
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8=
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db/go.mod h1:rB3B4rKii8V21ydCbIzH5hZiCQE7f5E9SzUb/ZZx530=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc h1:mZca0/HZ/XWXP9txkfdl2GH6mUzBqAlyJz3u5Lg8fuA=
github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc/go.mod h1:qqsTQiwdyqxU05iDCsi0oN3P4nrVxAmn8xCtODDSf/U=
github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/antlr/antlr4 v0.0.0-20201029161626-9a95f0cc3d7c h1:j/C2kxPfyE0d87/ggAjIsCV5Cdkqmjb+O0W8W+1J+IY=
github.com/antlr/antlr4 v0.0.0-20201029161626-9a95f0cc3d7c/go.mod h1:T7PbCXFs94rrTttyxjbyT5+/1V8T2TYDejxUfHJjw1Y=
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 h1:JLaf/iINcLyjwbtTsCJjc6rtlASgHeIJPrB6QmwURnA=
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
github.com/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=
@ -57,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=
@ -81,69 +98,145 @@ 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/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=
@ -152,6 +245,14 @@ 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=
@ -168,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=
@ -201,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=
@ -210,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=

View File

@ -15,8 +15,8 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Code generated by ./credits.sh at Wed Nov 4 13:57:47 CET 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/PuerkitoBio/goquery;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/antlr/antlr4;github.com/certifi/gocertifi;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-specialuse;github.com/emersion/go-imap-unselect;github.com/emersion/go-mbox;github.com/emersion/go-message;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/raven-go;github.com/go-resty/resty/v2;github.com/golang/mock;github.com/google/go-cmp;github.com/google/uuid;github.com/gopherjs/gopherjs;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/mattn/go-runewidth;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/olekukonko/tablewriter;github.com/pkg/errors;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/ssor/bom;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"
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;"

View File

@ -15,17 +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 'Wed Nov 4 12:24:35 PM CET 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 = `Ensured better message flow by refactoring both address and date parsing
Improved secure connectivity checks
Better deb packaging
More robust error handling
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 = `Ensured that conversations are properly threaded
Fixed Linux font issues (Fedora)
Better handling of Mime encrypted messages
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)
`

View File

@ -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()

View File

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

View File

@ -15,8 +15,8 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Code generated by ./credits.sh at Wed Nov 4 13:57:47 CET 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/PuerkitoBio/goquery;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/antlr/antlr4;github.com/certifi/gocertifi;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-specialuse;github.com/emersion/go-imap-unselect;github.com/emersion/go-mbox;github.com/emersion/go-message;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/raven-go;github.com/go-resty/resty/v2;github.com/golang/mock;github.com/google/go-cmp;github.com/google/uuid;github.com/gopherjs/gopherjs;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/mattn/go-runewidth;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/olekukonko/tablewriter;github.com/pkg/errors;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/ssor/bom;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"
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;"

View File

@ -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:
case b.shouldSign() && !b.shouldEncrypt():
if b.getScheme() == pgpInline {
p.Scheme = pmapi.ClearPackage
} else {
p.Scheme = pmapi.ClearMIMEPackage
}
default:
p.Scheme = pmapi.ClearPackage

View File

@ -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",

View File

@ -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
}
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
signature = pmapi.SignatureNone
}
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)
}

View File

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

View File

@ -1,5 +1,5 @@
// 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
@ -8,6 +8,8 @@ import (
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"
)
@ -96,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)
}

View File

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

View File

@ -84,31 +84,15 @@ 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{}
pageStart := uint32(1)
pageEnd := imapPageSize
for {
if progress.shouldStop() {
break
}
// Some servers do not accept message sequence number higher than the total count.
if pageEnd > count {
pageEnd = count
}
seqSet := &imap.SeqSet{}
seqSet.AddRange(pageStart, pageEnd)
items := []imap.FetchItem{imap.FetchUid, imap.FetchRFC822Size}
fetchItems := []imap.FetchItem{imap.FetchUid, imap.FetchRFC822Size}
if rule.HasTimeLimit() {
items = append(items, imap.FetchEnvelope)
fetchItems = append(fetchItems, 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) {
@ -127,18 +111,35 @@ func (p *IMAPProvider) loadMessagesInfo(rule *Rule, progress *Progress, uidValid
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 {
pageStart := uint32(1)
pageEnd := imapPageSize
for {
if progress.shouldStop() || pageStart > count {
break
}
pageStart = pageEnd
pageEnd += imapPageSize
// Some servers do not accept message sequence number higher than the total count.
if pageEnd > count {
pageEnd = count
}
seqSet := &imap.SeqSet{}
seqSet.AddRange(pageStart, pageEnd)
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")
}
}
}
pageStart = pageEnd + 1
pageEnd += imapPageSize
}
return messagesInfo
}

View 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])
}
}

View File

@ -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,14 +217,16 @@ func (p *IMAPProvider) auth() error { //nolint[funlen]
log.Info("Logged in")
idClient := imapID.NewClient(p.client)
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: "beta",
imapID.FieldVersion: constants.Version,
})
log.WithField("ID", serverID).WithError(err).Debug("Server info")
}
}
return err
}

View File

@ -34,6 +34,7 @@ type mocks struct {
ctrl *gomock.Controller
panicHandler *transfermocks.MockPanicHandler
clientManager *transfermocks.MockClientManager
imapClientProvider *transfermocks.MockIMAPClientProvider
pmapiClient *pmapimocks.MockClient
pmapiConfig *pmapi.ClientConfig
@ -49,6 +50,7 @@ func initMocks(t *testing.T) mocks {
ctrl: mockCtrl,
panicHandler: transfermocks.NewMockPanicHandler(mockCtrl),
clientManager: transfermocks.NewMockClientManager(mockCtrl),
imapClientProvider: transfermocks.NewMockIMAPClientProvider(mockCtrl),
pmapiClient: pmapimocks.NewMockClient(mockCtrl),
pmapiConfig: &pmapi.ClientConfig{},
keyring: newTestKeyring(),

View File

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

View File

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

View File

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

View File

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

43
pkg/pmapi/debug.go Normal file
View 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)
}

View File

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

View 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)
}
}

View File

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

View File

@ -85,7 +85,7 @@ type MailSettings struct {
RightToLeft int
AttachPublicKey int
Sign int
PGPScheme int
PGPScheme PackageFlag
PromptPin int
Autocrypt int
NumMessagePerPage int

View File

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

View File

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

View File

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

View File

@ -1,3 +1,4 @@
Ensured that conversations are properly threaded
Fixed Linux font issues (Fedora)
Better handling of Mime encrypted messages
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)

View File

@ -1,4 +1,4 @@
Ensured better message flow by refactoring both address and date parsing
Improved secure connectivity checks
Better deb packaging
More robust error handling
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

View File

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

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View 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"

View File

@ -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) {

View File

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