diff --git a/.gitattributes b/.gitattributes index d6c5853d..c03a430f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1 @@ -Changelog.md merge=union +unreleased.md merge=union diff --git a/.github/ISSUE_TEMPLATE/general-issue-template.md b/.github/ISSUE_TEMPLATE/general-issue-template.md index 7217b269..6eaaefe3 100644 --- a/.github/ISSUE_TEMPLATE/general-issue-template.md +++ b/.github/ISSUE_TEMPLATE/general-issue-template.md @@ -27,6 +27,9 @@ Issue tracker is ONLY used for reporting bugs with technical details. "It doesn' 3. 4. +## Version Information + + ## Context (Environment) diff --git a/Changelog.md b/Changelog.md index 89fe96e1..15c110e5 100644 --- a/Changelog.md +++ b/Changelog.md @@ -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, 1.2.2] 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. diff --git a/Makefile b/Makefile index 0cb6ab86..537bbaba 100644 --- a/Makefile +++ b/Makefile @@ -10,19 +10,21 @@ TARGET_OS?=${GOOS} .PHONY: build build-ie build-nogui build-ie-nogui check-has-go # Keep version hardcoded so app build works also without Git repository. -BRIDGE_APP_VERSION?=1.5.0-git +BRIDGE_APP_VERSION?=1.5.2-git IE_APP_VERSION?=1.2.2-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 diff --git a/go.mod b/go.mod index e1ac9ae2..06851867 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 9db92e54..bd9323b0 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,10 @@ github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1 h1:j9HaafapDbPbGRDku6e/HRs6KBMcKHiWcm1/9Sbxnl4= github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1/go.mod h1:NtXa9WwQsukMHZpjNakTTz0LArxvGYdPA9CjIcUSZ6s= +github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= +github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMdUywE7VMo= +github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Masterminds/semver/v3 v3.1.0 h1:Y2lUDsFKVRSYGojLJ1yLxSXdMmMYTYls0rCvoqmMUQk= github.com/Masterminds/semver/v3 v3.1.0/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998 h1:YT2uVwQiRQZxCaaahwfcgTq2j3j66w00n/27gb/zubs= @@ -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= diff --git a/internal/bridge/credits.go b/internal/bridge/credits.go index f6fb4c16..9977b19f 100644 --- a/internal/bridge/credits.go +++ b/internal/bridge/credits.go @@ -15,8 +15,8 @@ // You should have received a copy of the GNU General Public License // along with ProtonMail Bridge. If not, see . -// 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;" diff --git a/internal/bridge/release_notes.go b/internal/bridge/release_notes.go index d2a782a0..ccc4bc4f 100644 --- a/internal/bridge/release_notes.go +++ b/internal/bridge/release_notes.go @@ -15,17 +15,18 @@ // You should have received a copy of the GNU General Public License // along with ProtonMail Bridge. If not, see . -// 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) ` diff --git a/internal/cmd/main.go b/internal/cmd/main.go index e83d806b..fa15636d 100644 --- a/internal/cmd/main.go +++ b/internal/cmd/main.go @@ -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() diff --git a/internal/frontend/qml/ImportExportUI/InlineLabelSelect.qml b/internal/frontend/qml/ImportExportUI/InlineLabelSelect.qml index e7ee4680..f63f7a83 100644 --- a/internal/frontend/qml/ImportExportUI/InlineLabelSelect.qml +++ b/internal/frontend/qml/ImportExportUI/InlineLabelSelect.qml @@ -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 } diff --git a/internal/importexport/credits.go b/internal/importexport/credits.go index a7520e34..2f5febee 100644 --- a/internal/importexport/credits.go +++ b/internal/importexport/credits.go @@ -15,8 +15,8 @@ // You should have received a copy of the GNU General Public License // along with ProtonMail Bridge. If not, see . -// 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;" diff --git a/internal/smtp/preferences.go b/internal/smtp/preferences.go index 15b78583..2def010e 100644 --- a/internal/smtp/preferences.go +++ b/internal/smtp/preferences.go @@ -45,7 +45,7 @@ type SendPreferences struct { // internal emails (including the so-called encrypted-to-outside emails, // which even though meant for external users, they don't really get out of // our platform). If the email is sent unencrypted, no PGP scheme is needed. - Scheme int + Scheme pmapi.PackageFlag // MIMEType is the MIME type to use for formatting the body of the email // (before encryption/after decryption). The standard possibilities are the @@ -191,8 +191,12 @@ func (b *sendPreferencesBuilder) build() (p SendPreferences) { p.Scheme = pmapi.PGPMIMEPackage } - case b.shouldSign() && !b.shouldEncrypt() && b.getScheme() == pgpMIME: - p.Scheme = pmapi.ClearMIMEPackage + case b.shouldSign() && !b.shouldEncrypt(): + if b.getScheme() == pgpInline { + p.Scheme = pmapi.ClearPackage + } else { + p.Scheme = pmapi.ClearMIMEPackage + } default: p.Scheme = pmapi.ClearPackage diff --git a/internal/smtp/preferences_test.go b/internal/smtp/preferences_test.go index c0e3446e..e79f366a 100644 --- a/internal/smtp/preferences_test.go +++ b/internal/smtp/preferences_test.go @@ -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", diff --git a/internal/smtp/user.go b/internal/smtp/user.go index 37957249..b438256c 100644 --- a/internal/smtp/user.go +++ b/internal/smtp/user.go @@ -187,7 +187,7 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err log.WithError(err).Error("Failed to parse message") return } - clearBody := message.Body + richBody := message.Body externalID := message.Header.Get("Message-Id") externalID = strings.Trim(externalID, "<>") @@ -256,7 +256,6 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err atts = append(atts, message.Attachments...) // Decrypt attachment keys, because we will need to re-encrypt them with the recipients' public keys. attkeys := make(map[string]*crypto.SessionKey) - attkeysEncoded := make(map[string]pmapi.AlgoKey) for _, att := range atts { var keyPackets []byte @@ -266,23 +265,9 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err if attkeys[att.ID], err = kr.DecryptSessionKey(keyPackets); err != nil { return errors.Wrap(err, "decrypting attachment session key") } - attkeysEncoded[att.ID] = pmapi.AlgoKey{ - Key: attkeys[att.ID].GetBase64Key(), - Algorithm: attkeys[att.ID].Algo, - } } - plainSharedScheme := 0 - htmlSharedScheme := 0 - mimeSharedType := 0 - - plainAddressMap := make(map[string]*pmapi.MessageAddress) - htmlAddressMap := make(map[string]*pmapi.MessageAddress) - mimeAddressMap := make(map[string]*pmapi.MessageAddress) - - var plainKey, htmlKey, mimeKey *crypto.SessionKey - var plainData, htmlData, mimeData []byte - + req := pmapi.NewSendMessageReq(kr, mimeBody, plainBody, richBody, attkeys) containsUnencryptedRecipients := false for _, email := range to { @@ -298,61 +283,15 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err return err } - var signature int + var signature pmapi.SignatureFlag if sendPreferences.Sign { - signature = pmapi.YesSignature + signature = pmapi.SignatureDetached } else { - signature = pmapi.NoSignature + signature = pmapi.SignatureNone } - if sendPreferences.Scheme == pmapi.PGPMIMEPackage || sendPreferences.Scheme == pmapi.ClearMIMEPackage { - if mimeKey == nil { - if mimeKey, mimeData, err = encryptSymmetric(kr, mimeBody, true); err != nil { - return err - } - } - if sendPreferences.Scheme == pmapi.PGPMIMEPackage { - mimeBodyPacket, _, err := createPackets(sendPreferences.PublicKey, mimeKey, map[string]*crypto.SessionKey{}) - if err != nil { - return err - } - mimeAddressMap[email] = &pmapi.MessageAddress{Type: sendPreferences.Scheme, BodyKeyPacket: mimeBodyPacket, Signature: signature} - } else { - mimeAddressMap[email] = &pmapi.MessageAddress{Type: sendPreferences.Scheme, Signature: signature} - } - mimeSharedType |= sendPreferences.Scheme - } else { - switch sendPreferences.MIMEType { - case pmapi.ContentTypePlainText: - if plainKey == nil { - if plainKey, plainData, err = encryptSymmetric(kr, plainBody, true); err != nil { - return err - } - } - newAddress := &pmapi.MessageAddress{Type: sendPreferences.Scheme, Signature: signature} - if sendPreferences.Encrypt && sendPreferences.PublicKey != nil { - newAddress.BodyKeyPacket, newAddress.AttachmentKeyPackets, err = createPackets(sendPreferences.PublicKey, plainKey, attkeys) - if err != nil { - return err - } - } - plainAddressMap[email] = newAddress - plainSharedScheme |= sendPreferences.Scheme - case pmapi.ContentTypeHTML: - if htmlKey == nil { - if htmlKey, htmlData, err = encryptSymmetric(kr, clearBody, true); err != nil { - return err - } - } - newAddress := &pmapi.MessageAddress{Type: sendPreferences.Scheme, Signature: signature} - if sendPreferences.Encrypt && sendPreferences.PublicKey != nil { - newAddress.BodyKeyPacket, newAddress.AttachmentKeyPackets, err = createPackets(sendPreferences.PublicKey, htmlKey, attkeys) - if err != nil { - return err - } - } - htmlAddressMap[email] = newAddress - htmlSharedScheme |= sendPreferences.Scheme - } + + if err := req.AddRecipient(email, sendPreferences.Scheme, sendPreferences.PublicKey, signature, sendPreferences.MIMEType, sendPreferences.Encrypt); err != nil { + return errors.Wrap(err, "failed to add recipient") } } @@ -370,31 +309,7 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err } } - req := &pmapi.SendMessageReq{} - - plainPkg := buildPackage(plainAddressMap, plainSharedScheme, pmapi.ContentTypePlainText, plainData, plainKey, attkeysEncoded) - if plainPkg != nil { - req.Packages = append(req.Packages, plainPkg) - } - - htmlPkg := buildPackage(htmlAddressMap, htmlSharedScheme, pmapi.ContentTypeHTML, htmlData, htmlKey, attkeysEncoded) - if htmlPkg != nil { - req.Packages = append(req.Packages, htmlPkg) - } - - if len(mimeAddressMap) > 0 { - pkg := &pmapi.MessagePackage{ - Body: base64.StdEncoding.EncodeToString(mimeData), - Addresses: mimeAddressMap, - MIMEType: pmapi.ContentTypeMultipartMixed, - Type: mimeSharedType, - BodyKey: pmapi.AlgoKey{ - Key: mimeKey.GetBase64Key(), - Algorithm: mimeKey.Algo, - }, - } - req.Packages = append(req.Packages, pkg) - } + req.PreparePackages() return su.storeUser.SendMessage(message.ID, req) } diff --git a/internal/smtp/utils.go b/internal/smtp/utils.go index 36b98171..745ff2dc 100644 --- a/internal/smtp/utils.go +++ b/internal/smtp/utils.go @@ -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 -} diff --git a/internal/transfer/mocks/mocks.go b/internal/transfer/mocks/mocks.go index 560303bf..8c78a87a 100644 --- a/internal/transfer/mocks/mocks.go +++ b/internal/transfer/mocks/mocks.go @@ -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) +} diff --git a/internal/transfer/provider_imap.go b/internal/transfer/provider_imap.go index c2669c49..19cc5214 100644 --- a/internal/transfer/provider_imap.go +++ b/internal/transfer/provider_imap.go @@ -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 { diff --git a/internal/transfer/provider_imap_source.go b/internal/transfer/provider_imap_source.go index 9f669a0c..04b68179 100644 --- a/internal/transfer/provider_imap_source.go +++ b/internal/transfer/provider_imap_source.go @@ -84,12 +84,37 @@ func (p *IMAPProvider) loadMessagesInfo(rule *Rule, progress *Progress, uidValid p.timeIt.start("load", rule.SourceMailbox.Name) defer p.timeIt.stop("load", rule.SourceMailbox.Name) + log := log.WithField("mailbox", rule.SourceMailbox.Name) messagesInfo := map[string]imapMessageInfo{} + fetchItems := []imap.FetchItem{imap.FetchUid, imap.FetchRFC822Size} + if rule.HasTimeLimit() { + fetchItems = append(fetchItems, imap.FetchEnvelope) + } + + processMessageCallback := func(imapMessage *imap.Message) { + if rule.HasTimeLimit() { + t := imapMessage.Envelope.Date.Unix() + if t != 0 && !rule.isTimeInRange(t) { + log.WithField("uid", imapMessage.Uid).Debug("Message skipped due to time") + return + } + } + id := getUniqueMessageID(rule.SourceMailbox.Name, uidValidity, imapMessage.Uid) + // We use ID as key to ensure we have every unique message only once. + // Some IMAP servers responded twice the same message... + messagesInfo[id] = imapMessageInfo{ + id: id, + uid: imapMessage.Uid, + size: imapMessage.Size, + } + progress.addMessage(id, []string{rule.SourceMailbox.Name}, rule.TargetMailboxNames()) + } + pageStart := uint32(1) pageEnd := imapPageSize for { - if progress.shouldStop() { + if progress.shouldStop() || pageStart > count { break } @@ -100,45 +125,21 @@ func (p *IMAPProvider) loadMessagesInfo(rule *Rule, progress *Progress, uidValid seqSet := &imap.SeqSet{} seqSet.AddRange(pageStart, pageEnd) - - items := []imap.FetchItem{imap.FetchUid, imap.FetchRFC822Size} - if rule.HasTimeLimit() { - items = append(items, imap.FetchEnvelope) - } - - pageMsgCount := uint32(0) - processMessageCallback := func(imapMessage *imap.Message) { - pageMsgCount++ - if rule.HasTimeLimit() { - t := imapMessage.Envelope.Date.Unix() - if t != 0 && !rule.isTimeInRange(t) { - log.WithField("uid", imapMessage.Uid).Debug("Message skipped due to time") - return + err := p.fetch(rule.SourceMailbox.Name, seqSet, fetchItems, processMessageCallback) + if err != nil { + log.WithError(err).WithField("idx", seqSet).Warning("Load batch fetch failed, trying one by one") + for ; pageStart <= pageEnd; pageStart++ { + seqSet := &imap.SeqSet{} + seqSet.AddNum(pageStart) + if err := p.fetch(rule.SourceMailbox.Name, seqSet, fetchItems, processMessageCallback); err != nil { + log.WithError(err).WithField("idx", seqSet).Warning("Load fetch failed, skipping the message") } } - id := getUniqueMessageID(rule.SourceMailbox.Name, uidValidity, imapMessage.Uid) - // We use ID as key to ensure we have every unique message only once. - // Some IMAP servers responded twice the same message... - messagesInfo[id] = imapMessageInfo{ - id: id, - uid: imapMessage.Uid, - size: imapMessage.Size, - } - progress.addMessage(id, []string{rule.SourceMailbox.Name}, rule.TargetMailboxNames()) } - progress.callWrap(func() error { - return p.fetch(rule.SourceMailbox.Name, seqSet, items, processMessageCallback) - }) - - if pageMsgCount < imapPageSize { - break - } - - pageStart = pageEnd + pageStart = pageEnd + 1 pageEnd += imapPageSize } - return messagesInfo } diff --git a/internal/transfer/provider_imap_test.go b/internal/transfer/provider_imap_test.go new file mode 100644 index 00000000..55443d4c --- /dev/null +++ b/internal/transfer/provider_imap_test.go @@ -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 . + +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]) + } +} diff --git a/internal/transfer/provider_imap_utils.go b/internal/transfer/provider_imap_utils.go index 16f2166e..4dbda047 100644 --- a/internal/transfer/provider_imap_utils.go +++ b/internal/transfer/provider_imap_utils.go @@ -24,10 +24,11 @@ import ( "time" imapID "github.com/ProtonMail/go-imap-id" + "github.com/ProtonMail/proton-bridge/pkg/constants" "github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/emersion/go-imap" imapClient "github.com/emersion/go-imap/client" - sasl "github.com/emersion/go-sasl" + "github.com/emersion/go-sasl" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -51,6 +52,43 @@ func (l *imapErrorLogger) Println(v ...interface{}) { l.log.Errorln(v...) } +func imapClientDial(addr string) (IMAPClientProvider, error) { + if _, err := net.DialTimeout("tcp", addr, imapDialTimeout); err != nil { + return nil, errors.Wrap(err, "failed to dial server") + } + + client, err := imapClientDialHelper(addr) + if err == nil { + client.ErrorLog = &imapErrorLogger{logrus.WithField("pkg", "imap-client")} + // Logrus `WriterLevel` fails for big messages because of bufio.MaxScanTokenSize limit. + // Also, this spams a lot, uncomment once needed during development. + //client.SetDebug(imap.NewDebugWriter( + // logrus.WithField("pkg", "imap/client").WriterLevel(logrus.TraceLevel), + // logrus.WithField("pkg", "imap/server").WriterLevel(logrus.TraceLevel), + //)) + } + return client, err +} + +func imapClientDialHelper(addr string) (*imapClient.Client, error) { + host, _, _ := net.SplitHostPort(addr) + if host == "127.0.0.1" { + return imapClient.Dial(addr) + } + + // IMAP mail.yahoo.com has problem with golang TLS 1.3 implementation + // with weird behaviour, i.e., Yahoo does not return error during dial + // or handshake but server does logs out right after successful login + // leaving no time to perform any action. + // Limiting TLS to version 1.2 is working just fine. + var tlsConf *tls.Config + if strings.Contains(strings.ToLower(host), "yahoo") { + log.Warning("Yahoo server detected: limiting maximal TLS version to 1.2.") + tlsConf = &tls.Config{MaxVersion: tls.VersionTLS12} + } + return imapClient.DialTLS(addr, tlsConf) +} + func (p *IMAPProvider) ensureConnection(callback func() error) error { return p.ensureConnectionAndSelection(callback, "") } @@ -138,41 +176,10 @@ func (p *IMAPProvider) auth() error { //nolint[funlen] log.Info("Connecting to server") - if _, err := net.DialTimeout("tcp", p.addr, imapDialTimeout); err != nil { - return ErrIMAPConnection{imapError{Err: err, Message: "failed to dial server"}} - } - - var client *imapClient.Client - var err error - host, _, _ := net.SplitHostPort(p.addr) - if host == "127.0.0.1" { - client, err = imapClient.Dial(p.addr) - } else { - // IMAP.mail.yahoo.com have problem with golang TLS1.3 - // implementation with weird behaviour i.e. Yahoo - // no error during dial or handshake but server logs out right - // after successful login leaving no time to perform any - // action. It was discovered that limiting to maximum TLS - // version 1.2 for yahoo servers is working solution. - - var tlsConf *tls.Config - if strings.Contains(strings.ToLower(host), "yahoo") { - log.Warning("Yahoo server detected: limiting maximal TLS version to 1.2.") - tlsConf = &tls.Config{MaxVersion: tls.VersionTLS12} - } - client, err = imapClient.DialTLS(p.addr, tlsConf) - } + client, err := p.clientDialer(p.addr) if err != nil { return ErrIMAPConnection{imapError{Err: err, Message: "failed to connect to server"}} } - - client.ErrorLog = &imapErrorLogger{logrus.WithField("pkg", "imap-client")} - // Logrus `WriterLevel` fails for big messages because of bufio.MaxScanTokenSize limit. - // Also, this spams a lot, uncomment once needed during development. - //client.SetDebug(imap.NewDebugWriter( - // logrus.WithField("pkg", "imap/client").WriterLevel(logrus.TraceLevel), - // logrus.WithField("pkg", "imap/server").WriterLevel(logrus.TraceLevel), - //)) p.client = client log.Info("Connected") @@ -210,13 +217,15 @@ func (p *IMAPProvider) auth() error { //nolint[funlen] log.Info("Logged in") - idClient := imapID.NewClient(p.client) - if ok, err := idClient.SupportID(); err == nil && ok { - serverID, err := idClient.ID(imapID.ID{ - imapID.FieldName: "ImportExport", - imapID.FieldVersion: "beta", - }) - log.WithField("ID", serverID).WithError(err).Debug("Server info") + if c, ok := p.client.(*imapClient.Client); ok { + idClient := imapID.NewClient(c) + if ok, err := idClient.SupportID(); err == nil && ok { + serverID, err := idClient.ID(imapID.ID{ + imapID.FieldName: "ImportExport", + imapID.FieldVersion: constants.Version, + }) + log.WithField("ID", serverID).WithError(err).Debug("Server info") + } } return err diff --git a/internal/transfer/transfer_test.go b/internal/transfer/transfer_test.go index 653407f9..f71e6d9c 100644 --- a/internal/transfer/transfer_test.go +++ b/internal/transfer/transfer_test.go @@ -31,11 +31,12 @@ import ( type mocks struct { t *testing.T - ctrl *gomock.Controller - panicHandler *transfermocks.MockPanicHandler - clientManager *transfermocks.MockClientManager - pmapiClient *pmapimocks.MockClient - pmapiConfig *pmapi.ClientConfig + ctrl *gomock.Controller + panicHandler *transfermocks.MockPanicHandler + clientManager *transfermocks.MockClientManager + imapClientProvider *transfermocks.MockIMAPClientProvider + pmapiClient *pmapimocks.MockClient + pmapiConfig *pmapi.ClientConfig keyring *crypto.KeyRing } @@ -46,12 +47,13 @@ func initMocks(t *testing.T) mocks { m := mocks{ t: t, - ctrl: mockCtrl, - panicHandler: transfermocks.NewMockPanicHandler(mockCtrl), - clientManager: transfermocks.NewMockClientManager(mockCtrl), - pmapiClient: pmapimocks.NewMockClient(mockCtrl), - pmapiConfig: &pmapi.ClientConfig{}, - keyring: newTestKeyring(), + ctrl: mockCtrl, + panicHandler: transfermocks.NewMockPanicHandler(mockCtrl), + clientManager: transfermocks.NewMockClientManager(mockCtrl), + imapClientProvider: transfermocks.NewMockIMAPClientProvider(mockCtrl), + pmapiClient: pmapimocks.NewMockClient(mockCtrl), + pmapiConfig: &pmapi.ClientConfig{}, + keyring: newTestKeyring(), } m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).AnyTimes() diff --git a/pkg/config/config.go b/pkg/config/config.go index 5a6a0cf9..e1544e62 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -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" +} diff --git a/pkg/config/pmapi_noprod.go b/pkg/config/pmapi_noprod.go index 653d1afb..ec1c093d 100644 --- a/pkg/config/pmapi_noprod.go +++ b/pkg/config/pmapi_noprod.go @@ -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, } } diff --git a/pkg/config/pmapi_prod.go b/pkg/config/pmapi_prod.go index 38cd22a6..fde1274c 100644 --- a/pkg/config/pmapi_prod.go +++ b/pkg/config/pmapi_prod.go @@ -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. diff --git a/pkg/pmapi/client.go b/pkg/pmapi/client.go index 3e1c9cea..3f513b08 100644 --- a/pkg/pmapi/client.go +++ b/pkg/pmapi/client.go @@ -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 diff --git a/pkg/pmapi/debug.go b/pkg/pmapi/debug.go new file mode 100644 index 00000000..6f74bc59 --- /dev/null +++ b/pkg/pmapi/debug.go @@ -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 . + +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) +} diff --git a/pkg/pmapi/keyring.go b/pkg/pmapi/keyring.go index 978ac6e7..46d9dc1c 100644 --- a/pkg/pmapi/keyring.go +++ b/pkg/pmapi/keyring.go @@ -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 +} diff --git a/pkg/pmapi/message_send.go b/pkg/pmapi/message_send.go new file mode 100644 index 00000000..ec4140eb --- /dev/null +++ b/pkg/pmapi/message_send.go @@ -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 . + +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 +} diff --git a/pkg/pmapi/message_send_test.go b/pkg/pmapi/message_send_test.go new file mode 100644 index 00000000..8739234f --- /dev/null +++ b/pkg/pmapi/message_send_test.go @@ -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 . + +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) + } +} diff --git a/pkg/pmapi/messages.go b/pkg/pmapi/messages.go index 623bd07b..32ee60d2 100644 --- a/pkg/pmapi/messages.go +++ b/pkg/pmapi/messages.go @@ -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 } diff --git a/pkg/pmapi/settings.go b/pkg/pmapi/settings.go index dca1097d..0ab9c30f 100644 --- a/pkg/pmapi/settings.go +++ b/pkg/pmapi/settings.go @@ -85,7 +85,7 @@ type MailSettings struct { RightToLeft int AttachPublicKey int Sign int - PGPScheme int + PGPScheme PackageFlag PromptPin int Autocrypt int NumMessagePerPage int diff --git a/pkg/pmapi/users.go b/pkg/pmapi/users.go index b5c86e09..4d723ddb 100644 --- a/pkg/pmapi/users.go +++ b/pkg/pmapi/users.go @@ -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 { diff --git a/pkg/pmapi/utils.go b/pkg/pmapi/utils.go new file mode 100644 index 00000000..dfcc0e00 --- /dev/null +++ b/pkg/pmapi/utils.go @@ -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 . + +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) } diff --git a/pkg/sentry/report.go b/pkg/sentry/report.go index a5bc478e..cd967584 100644 --- a/pkg/sentry/report.go +++ b/pkg/sentry/report.go @@ -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 } diff --git a/pkg/sentry/report_test.go b/pkg/sentry/report_test.go deleted file mode 100644 index 4c902ee4..00000000 --- a/pkg/sentry/report_test.go +++ /dev/null @@ -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 . - -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, - }, - }, - }, - }, - } -} diff --git a/release-notes/bugs-bridge.txt b/release-notes/bugs-bridge.txt index 35c99caa..53ccd62c 100644 --- a/release-notes/bugs-bridge.txt +++ b/release-notes/bugs-bridge.txt @@ -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) diff --git a/release-notes/notes-bridge.txt b/release-notes/notes-bridge.txt index 1491c60c..9d5c3f98 100644 --- a/release-notes/notes-bridge.txt +++ b/release-notes/notes-bridge.txt @@ -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 diff --git a/test/api_checks_test.go b/test/api_checks_test.go index d052932f..231da38c 100644 --- a/test/api_checks_test.go +++ b/test/api_checks_test.go @@ -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) } diff --git a/test/features/bridge/smtp/send/bcc.feature b/test/features/bridge/smtp/send/bcc.feature index ce7348ec..683804f8 100644 --- a/test/features/bridge/smtp/send/bcc.feature +++ b/test/features/bridge/smtp/send/bcc.feature @@ -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": { diff --git a/test/features/bridge/smtp/send/html.feature b/test/features/bridge/smtp/send/html.feature index a14a341f..4524a742 100644 --- a/test/features/bridge/smtp/send/html.feature +++ b/test/features/bridge/smtp/send/html.feature @@ -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": { diff --git a/test/features/bridge/smtp/send/html_att.feature b/test/features/bridge/smtp/send/html_att.feature index 7111154b..005d0302 100644 --- a/test/features/bridge/smtp/send/html_att.feature +++ b/test/features/bridge/smtp/send/html_att.feature @@ -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": { diff --git a/test/features/bridge/smtp/send/plain.feature b/test/features/bridge/smtp/send/plain.feature index 5f8bfcf6..f34e4d8f 100644 --- a/test/features/bridge/smtp/send/plain.feature +++ b/test/features/bridge/smtp/send/plain.feature @@ -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": { diff --git a/test/features/bridge/smtp/send/plain_att.feature b/test/features/bridge/smtp/send/plain_att.feature index e0c21fda..56462a2d 100644 --- a/test/features/bridge/smtp/send/plain_att.feature +++ b/test/features/bridge/smtp/send/plain_att.feature @@ -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": { diff --git a/test/features/bridge/smtp/send/send_append.feature b/test/features/bridge/smtp/send/send_append.feature new file mode 100644 index 00000000..ebf79b7f --- /dev/null +++ b/test/features/bridge/smtp/send/send_append.feature @@ -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 + 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 + 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" diff --git a/test/store_checks_test.go b/test/store_checks_test.go index 435d6f34..3a5dd9b4 100644 --- a/test/store_checks_test.go +++ b/test/store_checks_test.go @@ -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) { diff --git a/unreleased.md b/unreleased.md index f0d45e4d..0fb9ce82 100644 --- a/unreleased.md +++ b/unreleased.md @@ -3,3 +3,9 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/) ## Unreleased + +### Added + +### Changed + +### Removed