forked from Silverfish/proton-bridge
Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 40f2d8b30f | |||
| 95a1acec0d | |||
| 5ff074cc49 | |||
| 4f0660bb8c | |||
| 708184439e | |||
| b8a33b9618 | |||
| 1c385d5c9b | |||
| 96773f3225 | |||
| 0f320dbd80 | |||
| 6cb233473a | |||
| 1ac4e70115 | |||
| 07f93d276b | |||
| d29571fb01 | |||
| d6000d025e | |||
| 09ef3b20db | |||
| 405331d59b | |||
| eff7df2136 | |||
| 5823e3a99f | |||
| 26d866bbbd | |||
| d3f7be059d | |||
| b52706a3ca | |||
| aebe7baed0 | |||
| ef31e2917c | |||
| 9eea26459a | |||
| 5747b85543 | |||
| ff78a23084 | |||
| 2a95e1ab41 | |||
| ab76cab533 | |||
| dda2a5d01a | |||
| c2afb42fd4 | |||
| 1d53044803 | |||
| d3f8297eb4 | |||
| b02203e3d3 | |||
| 5c7e4e04f9 | |||
| d7dadd7578 | |||
| ab9a758d63 | |||
| cb0935be96 | |||
| 441b388f62 | |||
| cdbcd30d15 | |||
| acc7ca8d4a | |||
| 42e1dd4c41 | |||
| 4cbd3ca832 | |||
| de0b6c0737 | |||
| 1c344211d1 | |||
| c11a87c16a | |||
| 3bf4282037 |
@ -18,6 +18,10 @@
|
|||||||
---
|
---
|
||||||
image: gitlab.protontech.ch:4567/go/bridge-internal:test-go1.20
|
image: gitlab.protontech.ch:4567/go/bridge-internal:test-go1.20
|
||||||
|
|
||||||
|
default:
|
||||||
|
tags:
|
||||||
|
- shared-small
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
GOPRIVATE: gitlab.protontech.ch
|
GOPRIVATE: gitlab.protontech.ch
|
||||||
GOMAXPROCS: $(( ${CI_TAG_CPU} / 2 ))
|
GOMAXPROCS: $(( ${CI_TAG_CPU} / 2 ))
|
||||||
@ -118,7 +122,7 @@ stages:
|
|||||||
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
|
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
|
||||||
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
|
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
|
||||||
tags:
|
tags:
|
||||||
- large
|
- shared-large
|
||||||
|
|
||||||
# Stage: TEST
|
# Stage: TEST
|
||||||
|
|
||||||
@ -129,7 +133,7 @@ lint:
|
|||||||
script:
|
script:
|
||||||
- make lint
|
- make lint
|
||||||
tags:
|
tags:
|
||||||
- medium
|
- shared-medium
|
||||||
|
|
||||||
bug-report-preview:
|
bug-report-preview:
|
||||||
stage: test
|
stage: test
|
||||||
@ -138,7 +142,7 @@ bug-report-preview:
|
|||||||
script:
|
script:
|
||||||
- make lint-bug-report-preview
|
- make lint-bug-report-preview
|
||||||
tags:
|
tags:
|
||||||
- medium
|
- shared-medium
|
||||||
|
|
||||||
.script-test:
|
.script-test:
|
||||||
stage: test
|
stage: test
|
||||||
@ -154,7 +158,7 @@ test-linux:
|
|||||||
extends:
|
extends:
|
||||||
- .script-test
|
- .script-test
|
||||||
tags:
|
tags:
|
||||||
- large
|
- shared-large
|
||||||
|
|
||||||
fuzz-linux:
|
fuzz-linux:
|
||||||
stage: test
|
stage: test
|
||||||
@ -163,7 +167,7 @@ fuzz-linux:
|
|||||||
script:
|
script:
|
||||||
- make fuzz
|
- make fuzz
|
||||||
tags:
|
tags:
|
||||||
- large
|
- shared-large
|
||||||
|
|
||||||
test-linux-race:
|
test-linux-race:
|
||||||
extends:
|
extends:
|
||||||
@ -218,7 +222,7 @@ test-coverage:
|
|||||||
- test-integration
|
- test-integration
|
||||||
- test-integration-nightly
|
- test-integration-nightly
|
||||||
tags:
|
tags:
|
||||||
- small
|
- shared-small
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- coverage*
|
- coverage*
|
||||||
@ -282,7 +286,7 @@ build-windows-qa:
|
|||||||
variables:
|
variables:
|
||||||
BUILD_TAGS: "build_qa"
|
BUILD_TAGS: "build_qa"
|
||||||
|
|
||||||
trigeer-qa-installer:
|
trigger-qa-installer:
|
||||||
stage: build
|
stage: build
|
||||||
needs: ["lint"]
|
needs: ["lint"]
|
||||||
extends:
|
extends:
|
||||||
|
|||||||
@ -133,5 +133,6 @@ Proton Mail Bridge includes the following 3rd party software:
|
|||||||
* [yaml](https://gopkg.in/yaml.v3) available under [license](https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE)
|
* [yaml](https://gopkg.in/yaml.v3) available under [license](https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE)
|
||||||
* [docker-credential-helpers](https://github.com/ProtonMail/docker-credential-helpers) available under [license](https://github.com/ProtonMail/docker-credential-helpers/blob/master/LICENSE)
|
* [docker-credential-helpers](https://github.com/ProtonMail/docker-credential-helpers) available under [license](https://github.com/ProtonMail/docker-credential-helpers/blob/master/LICENSE)
|
||||||
* [go-message](https://github.com/ProtonMail/go-message) available under [license](https://github.com/ProtonMail/go-message/blob/master/LICENSE)
|
* [go-message](https://github.com/ProtonMail/go-message) available under [license](https://github.com/ProtonMail/go-message/blob/master/LICENSE)
|
||||||
|
* [resty](https://github.com/LBeernaertProton/resty/v2) available under [license](https://github.com/LBeernaertProton/resty/v2/blob/master/LICENSE)
|
||||||
* [go-keychain](https://github.com/cuthix/go-keychain) available under [license](https://github.com/cuthix/go-keychain/blob/master/LICENSE)
|
* [go-keychain](https://github.com/cuthix/go-keychain) available under [license](https://github.com/cuthix/go-keychain/blob/master/LICENSE)
|
||||||
<!-- END AUTOGEN -->
|
<!-- END AUTOGEN -->
|
||||||
|
|||||||
51
Changelog.md
51
Changelog.md
@ -3,12 +3,63 @@
|
|||||||
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||||
|
|
||||||
|
|
||||||
|
## Wakato Bridge 3.7.0
|
||||||
|
|
||||||
|
### Added
|
||||||
|
* Test(GODT-1224): Add testing around package creation.
|
||||||
|
* Add debug_assemble binary.
|
||||||
|
* Test(GODT-2723): Add importing a message with remote content.
|
||||||
|
* Test(GODT-2737): Sending HTML messages to internal.
|
||||||
|
* Test(GODT-3036): Keep inline attachment order on GPA Fake Server.
|
||||||
|
* GODT-3015: Add simple algorithm to deal with multiple attachment for bug report.
|
||||||
|
* Test: make message structure check more verbose.
|
||||||
|
* Test: Add test around account settings.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* GODT-3097: Warn about PGPInline encryption scheme which will be deprecated.
|
||||||
|
* Test: Support multiple users when waiting for sync event.
|
||||||
|
* Test: Update fake server with defautl draft content-type and test it.
|
||||||
|
* Test: be less aggressive while checking for message structure.
|
||||||
|
* GODT-2996: Set password fields to hidden when resetting the login form.
|
||||||
|
* GODT-2990: Change runner tags.
|
||||||
|
* GODT-2835: Bump GPA adding support for AsyncAttachments for BugReport +...
|
||||||
|
* GODT-2940: Allow 3 attempts for mailbox password.
|
||||||
|
* GODT-3095: Update GOpenPGP.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* GODT-3106: Broken import route.
|
||||||
|
* GODT-3041: Fix Invalid Or Missing message signature during send.
|
||||||
|
* GODT-3087: Exclude attachment content-disposition part when determining...
|
||||||
|
* GODT-2887: Inline images with Apple Mail.
|
||||||
|
* GODT-3100: Fix issue where a fatal error that bubble up to cli.Run() is not written in the log file.
|
||||||
|
* GODT-3094: Clean up old update files on bridge startup.
|
||||||
|
* GODT-3012: Fix multipart request retries.
|
||||||
|
* GODT-2935: Do not allow parentID into drafts.
|
||||||
|
* GODT-2935: Correct error message when draft fails to create.
|
||||||
|
* GODT-2970: Correctly handle rename of Inbox.
|
||||||
|
* GODT-2969: Prevent duration corruption for config status event.
|
||||||
|
* Fixed type in QA installer CI job name.
|
||||||
|
* GODT-3019: Fix title of main window when no account is connected.
|
||||||
|
* GODT-3013: IMAP service getting "stuck".
|
||||||
|
* GODT-2966: Allow permissive parsing of MediaType parameters for import.
|
||||||
|
* GODT-2966: Add more test regarding quoted/unquoted filename in attachment.
|
||||||
|
* GODT-2490: Fix sync progress not being reset when toggling split mode.
|
||||||
|
* GODT-2515: Customized notification of unavailable keychain on macOS.
|
||||||
|
|
||||||
|
|
||||||
## Vasco da Gama Bridge 3.6.1
|
## Vasco da Gama Bridge 3.6.1
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
* GODT-3033: Unable to receive new mail.
|
* GODT-3033: Unable to receive new mail.
|
||||||
|
|
||||||
|
|
||||||
|
## Umshiang Bridge 3.5.4
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* GODT-3033: Unable to receive new mail.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Vasco da Gama Bridge 3.6.0
|
## Vasco da Gama Bridge 3.6.0
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
2
Makefile
2
Makefile
@ -11,7 +11,7 @@ ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
|
|||||||
.PHONY: build build-gui build-nogui build-launcher versioner hasher
|
.PHONY: build build-gui build-nogui build-launcher versioner hasher
|
||||||
|
|
||||||
# Keep version hardcoded so app build works also without Git repository.
|
# Keep version hardcoded so app build works also without Git repository.
|
||||||
BRIDGE_APP_VERSION?=3.6.1+git
|
BRIDGE_APP_VERSION?=3.7.0+git
|
||||||
APP_VERSION:=${BRIDGE_APP_VERSION}
|
APP_VERSION:=${BRIDGE_APP_VERSION}
|
||||||
APP_FULL_NAME:=Proton Mail Bridge
|
APP_FULL_NAME:=Proton Mail Bridge
|
||||||
APP_VENDOR:=Proton AG
|
APP_VENDOR:=Proton AG
|
||||||
|
|||||||
@ -23,7 +23,6 @@ import (
|
|||||||
|
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/app"
|
"github.com/ProtonMail/proton-bridge/v3/internal/app"
|
||||||
"github.com/bradenaw/juniper/xslices"
|
"github.com/bradenaw/juniper/xslices"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -44,7 +43,5 @@ import (
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if err := app.New().Run(xslices.Filter(os.Args, func(arg string) bool { return !strings.Contains(arg, "-psn_") })); err != nil {
|
_ = app.New().Run(xslices.Filter(os.Args, func(arg string) bool { return !strings.Contains(arg, "-psn_") }))
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
21
go.mod
21
go.mod
@ -5,10 +5,10 @@ go 1.20
|
|||||||
require (
|
require (
|
||||||
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
|
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
|
||||||
github.com/Masterminds/semver/v3 v3.2.0
|
github.com/Masterminds/semver/v3 v3.2.0
|
||||||
github.com/ProtonMail/gluon v0.17.1-0.20231009084701-3af0474b0b3c
|
github.com/ProtonMail/gluon v0.17.1-0.20231025125916-5c7941465df8
|
||||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
|
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
|
||||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20231011132529-24b5b817ee1f
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20231106093533-5f248dfc820d
|
||||||
github.com/ProtonMail/gopenpgp/v2 v2.7.3-proton
|
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton
|
||||||
github.com/PuerkitoBio/goquery v1.8.1
|
github.com/PuerkitoBio/goquery v1.8.1
|
||||||
github.com/abiosoft/ishell v2.0.0+incompatible
|
github.com/abiosoft/ishell v2.0.0+incompatible
|
||||||
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37
|
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37
|
||||||
@ -43,10 +43,10 @@ require (
|
|||||||
github.com/vmihailenco/msgpack/v5 v5.3.5
|
github.com/vmihailenco/msgpack/v5 v5.3.5
|
||||||
go.uber.org/goleak v1.2.1
|
go.uber.org/goleak v1.2.1
|
||||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
|
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
|
||||||
golang.org/x/net v0.10.0
|
golang.org/x/net v0.17.0
|
||||||
golang.org/x/sys v0.8.0
|
golang.org/x/sys v0.13.0
|
||||||
golang.org/x/text v0.9.0
|
golang.org/x/text v0.13.0
|
||||||
google.golang.org/grpc v1.53.0
|
google.golang.org/grpc v1.56.3
|
||||||
google.golang.org/protobuf v1.30.0
|
google.golang.org/protobuf v1.30.0
|
||||||
howett.net/plist v1.0.0
|
howett.net/plist v1.0.0
|
||||||
)
|
)
|
||||||
@ -79,7 +79,7 @@ require (
|
|||||||
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.2 // indirect
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
github.com/gofrs/uuid v4.3.0+incompatible // indirect
|
github.com/gofrs/uuid v4.3.0+incompatible // indirect
|
||||||
github.com/golang/protobuf v1.5.2 // indirect
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect
|
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||||
@ -110,16 +110,17 @@ require (
|
|||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||||
gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a // indirect
|
gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a // indirect
|
||||||
golang.org/x/arch v0.3.0 // indirect
|
golang.org/x/arch v0.3.0 // indirect
|
||||||
golang.org/x/crypto v0.9.0 // indirect
|
golang.org/x/crypto v0.14.0 // indirect
|
||||||
golang.org/x/mod v0.8.0 // indirect
|
golang.org/x/mod v0.8.0 // indirect
|
||||||
golang.org/x/sync v0.2.0 // indirect
|
golang.org/x/sync v0.2.0 // indirect
|
||||||
golang.org/x/tools v0.6.0 // indirect
|
golang.org/x/tools v0.6.0 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20230221151758-ace64dc21148 // indirect
|
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
replace (
|
replace (
|
||||||
github.com/docker/docker-credential-helpers => github.com/ProtonMail/docker-credential-helpers v1.1.0
|
github.com/docker/docker-credential-helpers => github.com/ProtonMail/docker-credential-helpers v1.1.0
|
||||||
github.com/emersion/go-message => github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7
|
github.com/emersion/go-message => github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7
|
||||||
|
github.com/go-resty/resty/v2 => github.com/LBeernaertProton/resty/v2 v2.0.0-20231030122409-92db8bee3605
|
||||||
github.com/keybase/go-keychain => github.com/cuthix/go-keychain v0.0.0-20230517073537-fc1740a83768
|
github.com/keybase/go-keychain => github.com/cuthix/go-keychain v0.0.0-20230517073537-fc1740a83768
|
||||||
)
|
)
|
||||||
|
|||||||
46
go.sum
46
go.sum
@ -15,6 +15,8 @@ github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557 h1:l6surSnJ3RP4qA
|
|||||||
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557/go.mod h1:sTrmvD/TxuypdOERsDOS7SndZg0rzzcCi1b6wQMXUYM=
|
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557/go.mod h1:sTrmvD/TxuypdOERsDOS7SndZg0rzzcCi1b6wQMXUYM=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||||
|
github.com/LBeernaertProton/resty/v2 v2.0.0-20231030122409-92db8bee3605 h1:54Fh3JS6s2Tjy6ZIRLtt1amZOqfYDcjErdye45z8fkQ=
|
||||||
|
github.com/LBeernaertProton/resty/v2 v2.0.0-20231030122409-92db8bee3605/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A=
|
||||||
github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
|
github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
|
||||||
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||||
@ -23,8 +25,8 @@ github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs
|
|||||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
|
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
|
||||||
github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
|
github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
|
||||||
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
|
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
|
||||||
github.com/ProtonMail/gluon v0.17.1-0.20231009084701-3af0474b0b3c h1:gUDu4pOswgbou0QczfreNiXQFrmvVlpSh8Q+vft/JvI=
|
github.com/ProtonMail/gluon v0.17.1-0.20231025125916-5c7941465df8 h1:sG0o5pEoS2z2jNR9zK7Juq5Tr3X+GfHmQ8L99RPowaE=
|
||||||
github.com/ProtonMail/gluon v0.17.1-0.20231009084701-3af0474b0b3c/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo=
|
github.com/ProtonMail/gluon v0.17.1-0.20231025125916-5c7941465df8/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo=
|
||||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
|
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
|
||||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
|
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
|
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
|
||||||
@ -34,12 +36,12 @@ github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7 h1:+j+Kd/
|
|||||||
github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
|
github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
|
||||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
|
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
|
||||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
|
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
|
||||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20231011132529-24b5b817ee1f h1:n0oBMAz2dJhn5+1WA6NrjkWqkZN+22FQMkPlRwNGhpU=
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20231106093533-5f248dfc820d h1:LI2kvxBisX19f7lyMh0H6NcAHHg/Y7/x/xZWtxVrXOc=
|
||||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20231011132529-24b5b817ee1f/go.mod h1:ZmvQMA8hanLiD1tFsvu9+qGBcuxbIRfch/4z/nqBhXA=
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20231106093533-5f248dfc820d/go.mod h1:WEXJqj5DSc2YI77SgXdpMY0nk33Qy92Vu2r4tOEazA8=
|
||||||
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
|
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
|
||||||
github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
|
github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
|
||||||
github.com/ProtonMail/gopenpgp/v2 v2.7.3-proton h1:wuAxBUU9qF2wyDVJprn/2xPDx000eol5gwlKbOUYY88=
|
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton h1:8tqHYM6IGsdEc6Vxf1TWiwpHNj8yIEQNACPhxsDagrk=
|
||||||
github.com/ProtonMail/gopenpgp/v2 v2.7.3-proton/go.mod h1:omVkSsfPAhmptzPF/piMXb16wKIWUvVhZbVW7sJKh0A=
|
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton/go.mod h1:omVkSsfPAhmptzPF/piMXb16wKIWUvVhZbVW7sJKh0A=
|
||||||
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
|
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
|
||||||
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
|
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
|
||||||
github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw=
|
github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw=
|
||||||
@ -155,8 +157,6 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
|||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
||||||
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||||
github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
|
|
||||||
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
|
|
||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
@ -178,8 +178,8 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
|
|||||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
@ -421,8 +421,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
|||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||||
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
|
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||||
@ -465,15 +465,15 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
|
|||||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||||
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
|
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||||
|
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
@ -521,14 +521,17 @@ golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||||
|
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||||
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
|
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
@ -539,10 +542,13 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|||||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||||
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||||
|
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
@ -588,13 +594,13 @@ google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98
|
|||||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||||
google.golang.org/genproto v0.0.0-20230221151758-ace64dc21148 h1:muK+gVBJBfFb4SejshDBlN2/UgxCCOKH9Y34ljqEGOc=
|
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
|
||||||
google.golang.org/genproto v0.0.0-20230221151758-ace64dc21148/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw=
|
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
|
||||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||||
google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc=
|
google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc=
|
||||||
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
|
google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||||
|
|||||||
@ -204,7 +204,7 @@ func run(c *cli.Context) error {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
// Restart the app if requested.
|
// Restart the app if requested.
|
||||||
return withRestarter(exe, func(restarter *restarter.Restarter) error {
|
err = withRestarter(exe, func(restarter *restarter.Restarter) error {
|
||||||
// Handle crashes with various actions.
|
// Handle crashes with various actions.
|
||||||
return withCrashHandler(restarter, reporter, func(crashHandler *crash.Handler, quitCh <-chan struct{}) error {
|
return withCrashHandler(restarter, reporter, func(crashHandler *crash.Handler, quitCh <-chan struct{}) error {
|
||||||
migrationErr := migrateOldVersions()
|
migrationErr := migrateOldVersions()
|
||||||
@ -276,6 +276,9 @@ func run(c *cli.Context) error {
|
|||||||
b.PushError(bridge.ErrVaultCorrupt)
|
b.PushError(bridge.ErrVaultCorrupt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove old updates files
|
||||||
|
b.RemoveOldUpdates()
|
||||||
|
|
||||||
// Start telemetry heartbeat process
|
// Start telemetry heartbeat process
|
||||||
b.StartHeartbeat(b)
|
b.StartHeartbeat(b)
|
||||||
|
|
||||||
@ -290,6 +293,13 @@ func run(c *cli.Context) error {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// if an error occurs, it must be logged now because we're about to close the log file.
|
||||||
|
if err != nil {
|
||||||
|
logrus.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there's another instance already running, try to raise it and exit.
|
// If there's another instance already running, try to raise it and exit.
|
||||||
|
|||||||
@ -155,7 +155,7 @@ func newUpdater(locations *locations.Locations) (*updater.Updater, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return updater.NewUpdater(
|
return updater.NewUpdater(
|
||||||
updater.NewInstaller(versioner.New(updatesDir)),
|
versioner.New(updatesDir),
|
||||||
verifier,
|
verifier,
|
||||||
constants.UpdateName,
|
constants.UpdateName,
|
||||||
runtime.GOOS,
|
runtime.GOOS,
|
||||||
|
|||||||
@ -487,27 +487,15 @@ func (bridge *Bridge) remWatcher(watcher *watcher.Watcher[events.Event]) {
|
|||||||
watcher.Close()
|
watcher.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) onStatusUp(ctx context.Context) {
|
func (bridge *Bridge) onStatusUp(_ context.Context) {
|
||||||
logrus.Info("Handling API status up")
|
logrus.Info("Handling API status up")
|
||||||
|
|
||||||
safe.RLock(func() {
|
|
||||||
for _, user := range bridge.users {
|
|
||||||
user.OnStatusUp(ctx)
|
|
||||||
}
|
|
||||||
}, bridge.usersLock)
|
|
||||||
|
|
||||||
bridge.goLoad()
|
bridge.goLoad()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) onStatusDown(ctx context.Context) {
|
func (bridge *Bridge) onStatusDown(ctx context.Context) {
|
||||||
logrus.Info("Handling API status down")
|
logrus.Info("Handling API status down")
|
||||||
|
|
||||||
safe.RLock(func() {
|
|
||||||
for _, user := range bridge.users {
|
|
||||||
user.OnStatusDown(ctx)
|
|
||||||
}
|
|
||||||
}, bridge.usersLock)
|
|
||||||
|
|
||||||
for backoff := time.Second; ; backoff = min(backoff*2, 30*time.Second) {
|
for backoff := time.Second; ; backoff = min(backoff*2, 30*time.Second) {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
|||||||
@ -19,6 +19,7 @@ package bridge
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
@ -33,63 +34,133 @@ const (
|
|||||||
DefaultMaxSessionCountForBugReport = 10
|
DefaultMaxSessionCountForBugReport = 10
|
||||||
)
|
)
|
||||||
|
|
||||||
func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, title, description, username, email, client string, attachLogs bool) error {
|
type ReportBugReq struct {
|
||||||
var account = username
|
OSType string
|
||||||
|
OSVersion string
|
||||||
|
Title string
|
||||||
|
Description string
|
||||||
|
Username string
|
||||||
|
Email string
|
||||||
|
EmailClient string
|
||||||
|
IncludeLogs bool
|
||||||
|
}
|
||||||
|
|
||||||
if info, err := bridge.QueryUserInfo(username); err == nil {
|
func (bridge *Bridge) ReportBug(ctx context.Context, report *ReportBugReq) error {
|
||||||
account = info.Username
|
if info, err := bridge.QueryUserInfo(report.Username); err == nil {
|
||||||
|
report.Username = info.Username
|
||||||
} else if userIDs := bridge.GetUserIDs(); len(userIDs) > 0 {
|
} else if userIDs := bridge.GetUserIDs(); len(userIDs) > 0 {
|
||||||
if err := bridge.vault.GetUser(userIDs[0], func(user *vault.User) {
|
if err := bridge.vault.GetUser(userIDs[0], func(user *vault.User) {
|
||||||
account = user.Username()
|
report.Username = user.Username()
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var attachment []proton.ReportBugAttachment
|
var attachments []proton.ReportBugAttachment
|
||||||
|
if report.IncludeLogs {
|
||||||
if attachLogs {
|
logs, err := bridge.CollectLogs()
|
||||||
logsPath, err := bridge.locator.ProvideLogsPath()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
attachments = append(attachments, logs)
|
||||||
buffer, err := logging.ZipLogsForBugReport(logsPath, DefaultMaxSessionCountForBugReport, DefaultMaxBugReportZipSize)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := io.ReadAll(buffer)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
attachment = append(attachment, proton.ReportBugAttachment{
|
|
||||||
Name: "logs.zip",
|
|
||||||
Filename: "logs.zip",
|
|
||||||
MIMEType: "application/zip",
|
|
||||||
Body: body,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
safe.Lock(func() {
|
var firstAtt proton.ReportBugAttachment
|
||||||
|
if len(attachments) > 0 && report.IncludeLogs {
|
||||||
|
firstAtt = attachments[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
attachmentType := proton.AttachmentTypeSync
|
||||||
|
if len(attachments) > 1 {
|
||||||
|
attachmentType = proton.AttachmentTypeAsync
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := bridge.createTicket(ctx, report, attachmentType, firstAtt)
|
||||||
|
if err != nil || token == "" {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
safe.RLock(func() {
|
||||||
for _, user := range bridge.users {
|
for _, user := range bridge.users {
|
||||||
user.ReportBugSent()
|
user.ReportBugSent()
|
||||||
}
|
}
|
||||||
}, bridge.usersLock)
|
}, bridge.usersLock)
|
||||||
|
|
||||||
return bridge.api.ReportBug(ctx, proton.ReportBugReq{
|
// if we have a token we can append more attachment to the bugReport
|
||||||
OS: osType,
|
for i, att := range attachments {
|
||||||
OSVersion: osVersion,
|
if i == 0 && report.IncludeLogs {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err := bridge.appendComment(ctx, token, att)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
Title: "[Bridge] Bug - " + title,
|
func (bridge *Bridge) CollectLogs() (proton.ReportBugAttachment, error) {
|
||||||
Description: description,
|
logsPath, err := bridge.locator.ProvideLogsPath()
|
||||||
|
if err != nil {
|
||||||
|
return proton.ReportBugAttachment{}, err
|
||||||
|
}
|
||||||
|
|
||||||
Client: client,
|
buffer, err := logging.ZipLogsForBugReport(logsPath, DefaultMaxSessionCountForBugReport, DefaultMaxBugReportZipSize)
|
||||||
|
if err != nil {
|
||||||
|
return proton.ReportBugAttachment{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(buffer)
|
||||||
|
if err != nil {
|
||||||
|
return proton.ReportBugAttachment{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return proton.ReportBugAttachment{
|
||||||
|
Name: "logs.zip",
|
||||||
|
Filename: "logs.zip",
|
||||||
|
MIMEType: "application/zip",
|
||||||
|
Body: body,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) createTicket(ctx context.Context, report *ReportBugReq,
|
||||||
|
asyncAttach proton.AttachmentType, att proton.ReportBugAttachment) (string, error) {
|
||||||
|
var attachments []proton.ReportBugAttachment
|
||||||
|
attachments = append(attachments, att)
|
||||||
|
res, err := bridge.api.ReportBug(ctx, proton.ReportBugReq{
|
||||||
|
OS: report.OSType,
|
||||||
|
OSVersion: report.OSVersion,
|
||||||
|
|
||||||
|
Title: "[Bridge] Bug - " + report.Title,
|
||||||
|
Description: report.Description,
|
||||||
|
|
||||||
|
Client: report.EmailClient,
|
||||||
ClientType: proton.ClientTypeEmail,
|
ClientType: proton.ClientTypeEmail,
|
||||||
ClientVersion: constants.AppVersion(bridge.curVersion.Original()),
|
ClientVersion: constants.AppVersion(bridge.curVersion.Original()),
|
||||||
|
|
||||||
Username: account,
|
Username: report.Username,
|
||||||
Email: email,
|
Email: report.Email,
|
||||||
}, attachment...)
|
|
||||||
|
AsyncAttachments: asyncAttach,
|
||||||
|
}, attachments...)
|
||||||
|
|
||||||
|
if err != nil || asyncAttach != proton.AttachmentTypeAsync {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if asyncAttach == proton.AttachmentTypeAsync && res.Token == nil {
|
||||||
|
return "", errors.New("no token returns for AsyncAttachments")
|
||||||
|
}
|
||||||
|
|
||||||
|
return *res.Token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) appendComment(ctx context.Context, token string, att proton.ReportBugAttachment) error {
|
||||||
|
var attachments []proton.ReportBugAttachment
|
||||||
|
attachments = append(attachments, att)
|
||||||
|
return bridge.api.ReportBugAttachement(ctx, proton.ReportBugAttachmentReq{
|
||||||
|
Product: proton.ClientTypeEmail,
|
||||||
|
Body: "Comment adding attachment: " + att.Filename,
|
||||||
|
Token: token,
|
||||||
|
}, attachments...)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,7 +22,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (bridge *Bridge) ReportBugClicked() {
|
func (bridge *Bridge) ReportBugClicked() {
|
||||||
safe.Lock(func() {
|
safe.RLock(func() {
|
||||||
for _, user := range bridge.users {
|
for _, user := range bridge.users {
|
||||||
user.ReportBugClicked()
|
user.ReportBugClicked()
|
||||||
}
|
}
|
||||||
@ -30,7 +30,7 @@ func (bridge *Bridge) ReportBugClicked() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) AutoconfigUsed(client string) {
|
func (bridge *Bridge) AutoconfigUsed(client string) {
|
||||||
safe.Lock(func() {
|
safe.RLock(func() {
|
||||||
for _, user := range bridge.users {
|
for _, user := range bridge.users {
|
||||||
user.AutoconfigUsed(client)
|
user.AutoconfigUsed(client)
|
||||||
}
|
}
|
||||||
@ -38,7 +38,7 @@ func (bridge *Bridge) AutoconfigUsed(client string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) KBArticleOpened(article string) {
|
func (bridge *Bridge) KBArticleOpened(article string) {
|
||||||
safe.Lock(func() {
|
safe.RLock(func() {
|
||||||
for _, user := range bridge.users {
|
for _, user := range bridge.users {
|
||||||
user.KBArticleOpened(article)
|
user.KBArticleOpened(article)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -154,3 +154,7 @@ func (testUpdater *TestUpdater) GetVersionInfo(_ context.Context, _ updater.Down
|
|||||||
func (testUpdater *TestUpdater) InstallUpdate(_ context.Context, _ updater.Downloader, _ updater.VersionInfo) error {
|
func (testUpdater *TestUpdater) InstallUpdate(_ context.Context, _ updater.Downloader, _ updater.VersionInfo) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (testUpdater *TestUpdater) RemoveOldUpdates() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@ -336,6 +336,9 @@ func TestBridge_SendInvite(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestBridge_SendAddTextBodyPartIfNotExists(t *testing.T) {
|
func TestBridge_SendAddTextBodyPartIfNotExists(t *testing.T) {
|
||||||
|
// NOTE: Prior to GODT-2887, these tests had inline images, however after the implementation to support
|
||||||
|
// inline images new parts are injected to reference inline images without content-id set. The images
|
||||||
|
// in this test have been changed to regular attachments to keep the original checks in place.
|
||||||
const messageMultipartWithoutText = `Content-Type: multipart/mixed;
|
const messageMultipartWithoutText = `Content-Type: multipart/mixed;
|
||||||
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
|
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
|
||||||
Subject: A new message
|
Subject: A new message
|
||||||
@ -343,7 +346,7 @@ Date: Mon, 13 Mar 2023 16:06:16 +0100
|
|||||||
|
|
||||||
|
|
||||||
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
||||||
Content-Disposition: inline;
|
Content-Disposition: attachment;
|
||||||
filename=Cat_August_2010-4.jpeg
|
filename=Cat_August_2010-4.jpeg
|
||||||
Content-Type: image/jpeg;
|
Content-Type: image/jpeg;
|
||||||
name="Cat_August_2010-4.jpeg"
|
name="Cat_August_2010-4.jpeg"
|
||||||
@ -360,7 +363,7 @@ Subject: A new message Part2
|
|||||||
Date: Mon, 13 Mar 2023 16:06:16 +0100
|
Date: Mon, 13 Mar 2023 16:06:16 +0100
|
||||||
|
|
||||||
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
||||||
Content-Disposition: inline;
|
Content-Disposition: attachment;
|
||||||
filename=Cat_August_2010-4.jpeg
|
filename=Cat_August_2010-4.jpeg
|
||||||
Content-Type: image/jpeg;
|
Content-Type: image/jpeg;
|
||||||
name="Cat_August_2010-4.jpeg"
|
name="Cat_August_2010-4.jpeg"
|
||||||
@ -520,3 +523,181 @@ SGVsbG8gd29ybGQK
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBridge_SendInlineImage(t *testing.T) {
|
||||||
|
const messageInlineImageOnly = `Content-Type: multipart/mixed;
|
||||||
|
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
|
||||||
|
Subject: A new message
|
||||||
|
Date: Mon, 13 Mar 2023 16:06:16 +0100
|
||||||
|
|
||||||
|
|
||||||
|
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
||||||
|
Content-Disposition: inline;
|
||||||
|
filename=Cat_August_2010-4.jpeg
|
||||||
|
Content-Type: image/jpeg;
|
||||||
|
name="Cat_August_2010-4.jpeg"
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
|
||||||
|
SGVsbG8gd29ybGQ=
|
||||||
|
|
||||||
|
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84--
|
||||||
|
`
|
||||||
|
|
||||||
|
const messageInlineImageWithHTML = `Content-Type: multipart/mixed;
|
||||||
|
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
|
||||||
|
Subject: A new message Part2
|
||||||
|
Date: Mon, 13 Mar 2023 16:06:16 +0100
|
||||||
|
|
||||||
|
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
||||||
|
Content-Type: text/html;charset=utf8
|
||||||
|
Content-Transfer-Encoding: quoted-printable
|
||||||
|
|
||||||
|
Hello world
|
||||||
|
|
||||||
|
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
||||||
|
Content-Disposition: inline;
|
||||||
|
filename=Cat_August_2010-4.jpeg
|
||||||
|
Content-Type: image/jpeg;
|
||||||
|
name="Cat_August_2010-4.jpeg"
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
|
||||||
|
SGVsbG8gd29ybGQ=
|
||||||
|
|
||||||
|
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84--
|
||||||
|
`
|
||||||
|
|
||||||
|
const messageInlineImageWithText = `Content-Type: multipart/mixed;
|
||||||
|
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
|
||||||
|
Subject: A new message Part3
|
||||||
|
Date: Mon, 13 Mar 2023 16:06:16 +0100
|
||||||
|
|
||||||
|
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
||||||
|
Content-Type: text/plain;charset=utf8
|
||||||
|
Content-Transfer-Encoding: quoted-printable
|
||||||
|
|
||||||
|
Hello world
|
||||||
|
|
||||||
|
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
||||||
|
Content-Disposition: inline;
|
||||||
|
filename=Cat_August_2010-4.jpeg
|
||||||
|
Content-Type: image/jpeg;
|
||||||
|
name="Cat_August_2010-4.jpeg"
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
|
||||||
|
SGVsbG8gd29ybGQ=
|
||||||
|
|
||||||
|
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84--
|
||||||
|
`
|
||||||
|
|
||||||
|
const messageInlineImageFollowedByText = `Content-Type: multipart/mixed;
|
||||||
|
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
|
||||||
|
Subject: A new message Part4
|
||||||
|
Date: Mon, 13 Mar 2023 16:06:16 +0100
|
||||||
|
|
||||||
|
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
||||||
|
Content-Disposition: inline;
|
||||||
|
filename=Cat_August_2010-4.jpeg
|
||||||
|
Content-Type: image/jpeg;
|
||||||
|
name="Cat_August_2010-4.jpeg"
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
|
||||||
|
SGVsbG8gd29ybGQ=
|
||||||
|
|
||||||
|
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
||||||
|
Content-Type: text/plain;charset=utf8
|
||||||
|
Content-Transfer-Encoding: quoted-printable
|
||||||
|
|
||||||
|
Hello world
|
||||||
|
|
||||||
|
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84--
|
||||||
|
`
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
|
_, _, err := s.CreateUser("recipient", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
smtpWaiter := waitForSMTPServerReady(bridge)
|
||||||
|
defer smtpWaiter.Done()
|
||||||
|
|
||||||
|
senderUserID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
recipientUserID, err := bridge.LoginFull(ctx, "recipient", password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
senderInfo, err := bridge.GetUserInfo(senderUserID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
recipientInfo, err := bridge.GetUserInfo(recipientUserID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
messages := []string{
|
||||||
|
messageInlineImageOnly,
|
||||||
|
messageInlineImageWithHTML,
|
||||||
|
messageInlineImageWithText,
|
||||||
|
messageInlineImageFollowedByText,
|
||||||
|
}
|
||||||
|
|
||||||
|
smtpWaiter.Wait()
|
||||||
|
|
||||||
|
for _, m := range messages {
|
||||||
|
// Dial the server.
|
||||||
|
client, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer client.Close() //nolint:errcheck
|
||||||
|
|
||||||
|
// Upgrade to TLS.
|
||||||
|
require.NoError(t, client.StartTLS(&tls.Config{InsecureSkipVerify: true}))
|
||||||
|
|
||||||
|
// Authorize with SASL LOGIN.
|
||||||
|
require.NoError(t, client.Auth(sasl.NewLoginClient(
|
||||||
|
senderInfo.Addresses[0],
|
||||||
|
string(senderInfo.BridgePass)),
|
||||||
|
))
|
||||||
|
|
||||||
|
// Send the message.
|
||||||
|
require.NoError(t, client.SendMail(
|
||||||
|
senderInfo.Addresses[0],
|
||||||
|
[]string{recipientInfo.Addresses[0]},
|
||||||
|
strings.NewReader(m),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect the sender IMAP client.
|
||||||
|
senderIMAPClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, senderIMAPClient.Login(senderInfo.Addresses[0], string(senderInfo.BridgePass)))
|
||||||
|
defer senderIMAPClient.Logout() //nolint:errcheck
|
||||||
|
|
||||||
|
// Connect the recipient IMAP client.
|
||||||
|
recipientIMAPClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, recipientIMAPClient.Login(recipientInfo.Addresses[0], string(recipientInfo.BridgePass)))
|
||||||
|
defer recipientIMAPClient.Logout() //nolint:errcheck
|
||||||
|
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
messages, err := clientFetch(senderIMAPClient, `Sent`, imap.FetchBodyStructure)
|
||||||
|
require.NoError(t, err)
|
||||||
|
if len(messages) != 4 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// messages may not be in order
|
||||||
|
for _, message := range messages {
|
||||||
|
require.Equal(t, 1, len(message.BodyStructure.Parts))
|
||||||
|
require.Equal(t, "multipart", message.BodyStructure.MIMEType)
|
||||||
|
require.Equal(t, "mixed", message.BodyStructure.MIMESubType)
|
||||||
|
require.Equal(t, "multipart", message.BodyStructure.Parts[0].MIMEType)
|
||||||
|
require.Equal(t, "related", message.BodyStructure.Parts[0].MIMESubType)
|
||||||
|
require.Len(t, message.BodyStructure.Parts[0].Parts, 2)
|
||||||
|
require.Equal(t, "text", message.BodyStructure.Parts[0].Parts[0].MIMEType)
|
||||||
|
require.Equal(t, "html", message.BodyStructure.Parts[0].Parts[0].MIMESubType)
|
||||||
|
require.Equal(t, "image", message.BodyStructure.Parts[0].Parts[1].MIMEType)
|
||||||
|
require.Equal(t, "jpeg", message.BodyStructure.Parts[0].Parts[1].MIMESubType)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}, 10*time.Second, 100*time.Millisecond)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@ -53,4 +53,5 @@ type Autostarter interface {
|
|||||||
type Updater interface {
|
type Updater interface {
|
||||||
GetVersionInfo(context.Context, updater.Downloader, updater.Channel) (updater.VersionInfo, error)
|
GetVersionInfo(context.Context, updater.Downloader, updater.Channel) (updater.VersionInfo, error)
|
||||||
InstallUpdate(context.Context, updater.Downloader, updater.VersionInfo) error
|
InstallUpdate(context.Context, updater.Downloader, updater.VersionInfo) error
|
||||||
|
RemoveOldUpdates() error
|
||||||
}
|
}
|
||||||
|
|||||||
@ -139,3 +139,9 @@ func (bridge *Bridge) installUpdate(ctx context.Context, job installJob) {
|
|||||||
}
|
}
|
||||||
}, bridge.newVersionLock)
|
}, bridge.newVersionLock)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) RemoveOldUpdates() {
|
||||||
|
if err := bridge.updater.RemoveOldUpdates(); err != nil {
|
||||||
|
logrus.WithError(err).Error("Remove old updates fails")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -46,6 +46,8 @@ const (
|
|||||||
Connected
|
Connected
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var ErrFailedToUnlock = errors.New("failed to unlock user keys")
|
||||||
|
|
||||||
type UserInfo struct {
|
type UserInfo struct {
|
||||||
// UserID is the user's API ID.
|
// UserID is the user's API ID.
|
||||||
UserID string
|
UserID string
|
||||||
@ -66,10 +68,10 @@ type UserInfo struct {
|
|||||||
BridgePass []byte
|
BridgePass []byte
|
||||||
|
|
||||||
// UsedSpace is the amount of space used by the user.
|
// UsedSpace is the amount of space used by the user.
|
||||||
UsedSpace int
|
UsedSpace uint64
|
||||||
|
|
||||||
// MaxSpace is the total amount of space available to the user.
|
// MaxSpace is the total amount of space available to the user.
|
||||||
MaxSpace int
|
MaxSpace uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserIDs returns the IDs of all known users (authorized or not).
|
// GetUserIDs returns the IDs of all known users (authorized or not).
|
||||||
@ -157,11 +159,15 @@ func (bridge *Bridge) LoginUser(
|
|||||||
func() (string, error) {
|
func() (string, error) {
|
||||||
return bridge.loginUser(ctx, client, auth.UID, auth.RefreshToken, keyPass)
|
return bridge.loginUser(ctx, client, auth.UID, auth.RefreshToken, keyPass)
|
||||||
},
|
},
|
||||||
func() error {
|
|
||||||
return client.AuthDelete(ctx)
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// Failure to unlock will allow retries, so we do not delete auth.
|
||||||
|
if !errors.Is(err, ErrFailedToUnlock) {
|
||||||
|
if deleteErr := client.AuthDelete(ctx); deleteErr != nil {
|
||||||
|
logrus.WithError(deleteErr).Error("Failed to delete auth")
|
||||||
|
}
|
||||||
|
}
|
||||||
return "", fmt.Errorf("failed to login user: %w", err)
|
return "", fmt.Errorf("failed to login user: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -217,7 +223,16 @@ func (bridge *Bridge) LoginFull(
|
|||||||
keyPass = password
|
keyPass = password
|
||||||
}
|
}
|
||||||
|
|
||||||
return bridge.LoginUser(ctx, client, auth, keyPass)
|
userID, err := bridge.LoginUser(ctx, client, auth, keyPass)
|
||||||
|
if err != nil {
|
||||||
|
if deleteErr := client.AuthDelete(ctx); deleteErr != nil {
|
||||||
|
logrus.WithError(err).Error("Failed to delete auth")
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return userID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LogoutUser logs out the given user.
|
// LogoutUser logs out the given user.
|
||||||
@ -314,7 +329,7 @@ func (bridge *Bridge) SetAddressMode(ctx context.Context, userID string, mode va
|
|||||||
func (bridge *Bridge) SendBadEventUserFeedback(_ context.Context, userID string, doResync bool) error {
|
func (bridge *Bridge) SendBadEventUserFeedback(_ context.Context, userID string, doResync bool) error {
|
||||||
logrus.WithField("userID", userID).WithField("doResync", doResync).Info("Passing bad event feedback to user")
|
logrus.WithField("userID", userID).WithField("doResync", doResync).Info("Passing bad event feedback to user")
|
||||||
|
|
||||||
return safe.LockRet(func() error {
|
return safe.RLockRet(func() error {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
user, ok := bridge.users[userID]
|
user, ok := bridge.users[userID]
|
||||||
@ -374,9 +389,9 @@ func (bridge *Bridge) loginUser(ctx context.Context, client *proton.Client, auth
|
|||||||
}
|
}
|
||||||
|
|
||||||
if userKR, err := apiUser.Keys.Unlock(saltedKeyPass, nil); err != nil {
|
if userKR, err := apiUser.Keys.Unlock(saltedKeyPass, nil); err != nil {
|
||||||
return "", fmt.Errorf("failed to unlock user keys: %w", err)
|
return "", fmt.Errorf("%w: %w", ErrFailedToUnlock, err)
|
||||||
} else if userKR.CountDecryptionEntities() == 0 {
|
} else if userKR.CountDecryptionEntities() == 0 {
|
||||||
return "", fmt.Errorf("failed to unlock user keys")
|
return "", ErrFailedToUnlock
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := bridge.addUser(ctx, client, apiUser, authUID, authRef, saltedKeyPass, true); err != nil {
|
if err := bridge.addUser(ctx, client, apiUser, authUID, authRef, saltedKeyPass, true); err != nil {
|
||||||
|
|||||||
@ -49,7 +49,7 @@ func (bridge *Bridge) handleUserDeauth(ctx context.Context, user *user.User) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) handleUserBadEvent(ctx context.Context, user *user.User, event events.UserBadEvent) {
|
func (bridge *Bridge) handleUserBadEvent(ctx context.Context, user *user.User, event events.UserBadEvent) {
|
||||||
safe.Lock(func() {
|
safe.RLock(func() {
|
||||||
if rerr := bridge.reporter.ReportMessageWithContext("Failed to handle event", reporter.Context{
|
if rerr := bridge.reporter.ReportMessageWithContext("Failed to handle event", reporter.Context{
|
||||||
"user_id": user.ID(),
|
"user_id": user.ID(),
|
||||||
"old_event_id": event.OldEventID,
|
"old_event_id": event.OldEventID,
|
||||||
|
|||||||
@ -95,6 +95,13 @@ func (status *ConfigurationStatus) IsPending() bool {
|
|||||||
return !status.Data.DataV1.PendingSince.IsZero()
|
return !status.Data.DataV1.PendingSince.IsZero()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (status *ConfigurationStatus) isPendingSinceMin() int {
|
||||||
|
if min := int(time.Since(status.Data.DataV1.PendingSince).Minutes()); min > 0 {
|
||||||
|
return min
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
func (status *ConfigurationStatus) IsFromFailure() bool {
|
func (status *ConfigurationStatus) IsFromFailure() bool {
|
||||||
status.DataLock.RLock()
|
status.DataLock.RLock()
|
||||||
defer status.DataLock.RUnlock()
|
defer status.DataLock.RUnlock()
|
||||||
|
|||||||
@ -19,7 +19,6 @@ package configstatus
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ConfigAbortValues struct {
|
type ConfigAbortValues struct {
|
||||||
@ -41,17 +40,20 @@ type ConfigAbortData struct {
|
|||||||
|
|
||||||
type ConfigAbortBuilder struct{}
|
type ConfigAbortBuilder struct{}
|
||||||
|
|
||||||
func (*ConfigAbortBuilder) New(data *ConfigurationStatusData) ConfigAbortData {
|
func (*ConfigAbortBuilder) New(config *ConfigurationStatus) ConfigAbortData {
|
||||||
|
config.DataLock.RLock()
|
||||||
|
defer config.DataLock.RUnlock()
|
||||||
|
|
||||||
return ConfigAbortData{
|
return ConfigAbortData{
|
||||||
MeasurementGroup: "bridge.any.configuration",
|
MeasurementGroup: "bridge.any.configuration",
|
||||||
Event: "bridge_config_abort",
|
Event: "bridge_config_abort",
|
||||||
Values: ConfigSuccessValues{
|
Values: ConfigSuccessValues{
|
||||||
Duration: int(time.Since(data.DataV1.PendingSince).Minutes()),
|
Duration: config.isPendingSinceMin(),
|
||||||
},
|
},
|
||||||
Dimensions: ConfigSuccessDimensions{
|
Dimensions: ConfigSuccessDimensions{
|
||||||
ReportClick: strconv.FormatBool(data.DataV1.ReportClick),
|
ReportClick: strconv.FormatBool(config.Data.DataV1.ReportClick),
|
||||||
ReportSent: strconv.FormatBool(data.DataV1.ReportSent),
|
ReportSent: strconv.FormatBool(config.Data.DataV1.ReportSent),
|
||||||
ClickedLink: data.clickedLinkToString(),
|
ClickedLink: config.Data.clickedLinkToString(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,7 +33,7 @@ func TestConfigurationAbort_default(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var builder = configstatus.ConfigAbortBuilder{}
|
var builder = configstatus.ConfigAbortBuilder{}
|
||||||
req := builder.New(config.Data)
|
req := builder.New(config)
|
||||||
|
|
||||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||||
require.Equal(t, "bridge_config_abort", req.Event)
|
require.Equal(t, "bridge_config_abort", req.Event)
|
||||||
@ -64,7 +64,7 @@ func TestConfigurationAbort_fed(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var builder = configstatus.ConfigAbortBuilder{}
|
var builder = configstatus.ConfigAbortBuilder{}
|
||||||
req := builder.New(config.Data)
|
req := builder.New(config)
|
||||||
|
|
||||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||||
require.Equal(t, "bridge_config_abort", req.Event)
|
require.Equal(t, "bridge_config_abort", req.Event)
|
||||||
|
|||||||
@ -33,13 +33,16 @@ type ConfigProgressData struct {
|
|||||||
|
|
||||||
type ConfigProgressBuilder struct{}
|
type ConfigProgressBuilder struct{}
|
||||||
|
|
||||||
func (*ConfigProgressBuilder) New(data *ConfigurationStatusData) ConfigProgressData {
|
func (*ConfigProgressBuilder) New(config *ConfigurationStatus) ConfigProgressData {
|
||||||
|
config.DataLock.RLock()
|
||||||
|
defer config.DataLock.RUnlock()
|
||||||
|
|
||||||
return ConfigProgressData{
|
return ConfigProgressData{
|
||||||
MeasurementGroup: "bridge.any.configuration",
|
MeasurementGroup: "bridge.any.configuration",
|
||||||
Event: "bridge_config_progress",
|
Event: "bridge_config_progress",
|
||||||
Values: ConfigProgressValues{
|
Values: ConfigProgressValues{
|
||||||
NbDay: numberOfDay(time.Now(), data.DataV1.PendingSince),
|
NbDay: numberOfDay(time.Now(), config.Data.DataV1.PendingSince),
|
||||||
NbDaySinceLast: numberOfDay(time.Now(), data.DataV1.LastProgress),
|
NbDaySinceLast: numberOfDay(time.Now(), config.Data.DataV1.LastProgress),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,7 +33,7 @@ func TestConfigurationProgress_default(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var builder = configstatus.ConfigProgressBuilder{}
|
var builder = configstatus.ConfigProgressBuilder{}
|
||||||
req := builder.New(config.Data)
|
req := builder.New(config)
|
||||||
|
|
||||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||||
require.Equal(t, "bridge_config_progress", req.Event)
|
require.Equal(t, "bridge_config_progress", req.Event)
|
||||||
@ -62,7 +62,7 @@ func TestConfigurationProgress_fed(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var builder = configstatus.ConfigProgressBuilder{}
|
var builder = configstatus.ConfigProgressBuilder{}
|
||||||
req := builder.New(config.Data)
|
req := builder.New(config)
|
||||||
|
|
||||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||||
require.Equal(t, "bridge_config_progress", req.Event)
|
require.Equal(t, "bridge_config_progress", req.Event)
|
||||||
|
|||||||
@ -19,7 +19,6 @@ package configstatus
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ConfigRecoveryValues struct {
|
type ConfigRecoveryValues struct {
|
||||||
@ -43,19 +42,22 @@ type ConfigRecoveryData struct {
|
|||||||
|
|
||||||
type ConfigRecoveryBuilder struct{}
|
type ConfigRecoveryBuilder struct{}
|
||||||
|
|
||||||
func (*ConfigRecoveryBuilder) New(data *ConfigurationStatusData) ConfigRecoveryData {
|
func (*ConfigRecoveryBuilder) New(config *ConfigurationStatus) ConfigRecoveryData {
|
||||||
|
config.DataLock.RLock()
|
||||||
|
defer config.DataLock.RUnlock()
|
||||||
|
|
||||||
return ConfigRecoveryData{
|
return ConfigRecoveryData{
|
||||||
MeasurementGroup: "bridge.any.configuration",
|
MeasurementGroup: "bridge.any.configuration",
|
||||||
Event: "bridge_config_recovery",
|
Event: "bridge_config_recovery",
|
||||||
Values: ConfigRecoveryValues{
|
Values: ConfigRecoveryValues{
|
||||||
Duration: int(time.Since(data.DataV1.PendingSince).Minutes()),
|
Duration: config.isPendingSinceMin(),
|
||||||
},
|
},
|
||||||
Dimensions: ConfigRecoveryDimensions{
|
Dimensions: ConfigRecoveryDimensions{
|
||||||
Autoconf: data.DataV1.Autoconf,
|
Autoconf: config.Data.DataV1.Autoconf,
|
||||||
ReportClick: strconv.FormatBool(data.DataV1.ReportClick),
|
ReportClick: strconv.FormatBool(config.Data.DataV1.ReportClick),
|
||||||
ReportSent: strconv.FormatBool(data.DataV1.ReportSent),
|
ReportSent: strconv.FormatBool(config.Data.DataV1.ReportSent),
|
||||||
ClickedLink: data.clickedLinkToString(),
|
ClickedLink: config.Data.clickedLinkToString(),
|
||||||
FailureDetails: data.DataV1.FailureDetails,
|
FailureDetails: config.Data.DataV1.FailureDetails,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,7 +33,7 @@ func TestConfigurationRecovery_default(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var builder = configstatus.ConfigRecoveryBuilder{}
|
var builder = configstatus.ConfigRecoveryBuilder{}
|
||||||
req := builder.New(config.Data)
|
req := builder.New(config)
|
||||||
|
|
||||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||||
require.Equal(t, "bridge_config_recovery", req.Event)
|
require.Equal(t, "bridge_config_recovery", req.Event)
|
||||||
@ -66,7 +66,7 @@ func TestConfigurationRecovery_fed(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var builder = configstatus.ConfigRecoveryBuilder{}
|
var builder = configstatus.ConfigRecoveryBuilder{}
|
||||||
req := builder.New(config.Data)
|
req := builder.New(config)
|
||||||
|
|
||||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||||
require.Equal(t, "bridge_config_recovery", req.Event)
|
require.Equal(t, "bridge_config_recovery", req.Event)
|
||||||
|
|||||||
@ -19,7 +19,6 @@ package configstatus
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ConfigSuccessValues struct {
|
type ConfigSuccessValues struct {
|
||||||
@ -42,18 +41,21 @@ type ConfigSuccessData struct {
|
|||||||
|
|
||||||
type ConfigSuccessBuilder struct{}
|
type ConfigSuccessBuilder struct{}
|
||||||
|
|
||||||
func (*ConfigSuccessBuilder) New(data *ConfigurationStatusData) ConfigSuccessData {
|
func (*ConfigSuccessBuilder) New(config *ConfigurationStatus) ConfigSuccessData {
|
||||||
|
config.DataLock.RLock()
|
||||||
|
defer config.DataLock.RUnlock()
|
||||||
|
|
||||||
return ConfigSuccessData{
|
return ConfigSuccessData{
|
||||||
MeasurementGroup: "bridge.any.configuration",
|
MeasurementGroup: "bridge.any.configuration",
|
||||||
Event: "bridge_config_success",
|
Event: "bridge_config_success",
|
||||||
Values: ConfigSuccessValues{
|
Values: ConfigSuccessValues{
|
||||||
Duration: int(time.Since(data.DataV1.PendingSince).Minutes()),
|
Duration: config.isPendingSinceMin(),
|
||||||
},
|
},
|
||||||
Dimensions: ConfigSuccessDimensions{
|
Dimensions: ConfigSuccessDimensions{
|
||||||
Autoconf: data.DataV1.Autoconf,
|
Autoconf: config.Data.DataV1.Autoconf,
|
||||||
ReportClick: strconv.FormatBool(data.DataV1.ReportClick),
|
ReportClick: strconv.FormatBool(config.Data.DataV1.ReportClick),
|
||||||
ReportSent: strconv.FormatBool(data.DataV1.ReportSent),
|
ReportSent: strconv.FormatBool(config.Data.DataV1.ReportSent),
|
||||||
ClickedLink: data.clickedLinkToString(),
|
ClickedLink: config.Data.clickedLinkToString(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,7 +33,7 @@ func TestConfigurationSuccess_default(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var builder = configstatus.ConfigSuccessBuilder{}
|
var builder = configstatus.ConfigSuccessBuilder{}
|
||||||
req := builder.New(config.Data)
|
req := builder.New(config)
|
||||||
|
|
||||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||||
require.Equal(t, "bridge_config_success", req.Event)
|
require.Equal(t, "bridge_config_success", req.Event)
|
||||||
@ -65,7 +65,7 @@ func TestConfigurationSuccess_fed(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var builder = configstatus.ConfigSuccessBuilder{}
|
var builder = configstatus.ConfigSuccessBuilder{}
|
||||||
req := builder.New(config.Data)
|
req := builder.New(config)
|
||||||
|
|
||||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||||
require.Equal(t, "bridge_config_success", req.Event)
|
require.Equal(t, "bridge_config_success", req.Event)
|
||||||
|
|||||||
@ -175,7 +175,7 @@ type UsedSpaceChanged struct {
|
|||||||
|
|
||||||
UserID string
|
UserID string
|
||||||
|
|
||||||
UsedSpace int
|
UsedSpace uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (event UsedSpaceChanged) String() string {
|
func (event UsedSpaceChanged) String() string {
|
||||||
|
|||||||
@ -368,7 +368,7 @@ Item {
|
|||||||
currentIndex: hasAccount() ? 1 : 0
|
currentIndex: hasAccount() ? 1 : 0
|
||||||
NoAccountView {
|
NoAccountView {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
onLinkClicked: function() {
|
onStartSetup: {
|
||||||
root.showLogin("")
|
root.showLogin("")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,7 +23,7 @@ Rectangle {
|
|||||||
|
|
||||||
color: root.colorScheme.background_norm
|
color: root.colorScheme.background_norm
|
||||||
|
|
||||||
signal linkClicked()
|
signal startSetup()
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@ -38,8 +38,10 @@ Rectangle {
|
|||||||
wizard: setupWizard
|
wizard: setupWizard
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
showOnboarding();
|
showNoAccount();
|
||||||
link1.setCallback(root.linkClicked, "Start setup", false)
|
}
|
||||||
|
onStartSetup: {
|
||||||
|
root.startSetup();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Image {
|
Image {
|
||||||
|
|||||||
@ -728,10 +728,12 @@ QtObject {
|
|||||||
}
|
}
|
||||||
property Notification noKeychain: Notification {
|
property Notification noKeychain: Notification {
|
||||||
brief: title
|
brief: title
|
||||||
description: qsTr("Bridge is not able to detect a supported password manager (pass or secret-service). Please install and setup supported password manager and restart the application.")
|
description: Backend.goos === "darwin" ?
|
||||||
|
qsTr("Bridge is not able to access your keychain. Please make sure your keychain is not locked and restart the application.") :
|
||||||
|
qsTr("Bridge is not able to detect a supported password manager (pass or secret-service). Please install and setup supported password manager and restart the application.")
|
||||||
group: Notifications.Group.Dialogs | Notifications.Group.Configuration
|
group: Notifications.Group.Dialogs | Notifications.Group.Configuration
|
||||||
icon: "./icons/ic-exclamation-circle-filled.svg"
|
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||||
title: qsTr("No keychain available")
|
title: Backend.goos === "darwin" ? qsTr("Cannot access keychain") : qsTr("No keychain available")
|
||||||
type: Notification.NotificationType.Danger
|
type: Notification.NotificationType.Danger
|
||||||
|
|
||||||
action: [
|
action: [
|
||||||
|
|||||||
@ -114,6 +114,9 @@ FocusScope {
|
|||||||
function getText(start, end) {
|
function getText(start, end) {
|
||||||
control.getText(start, end);
|
control.getText(start, end);
|
||||||
}
|
}
|
||||||
|
function hidePassword() {
|
||||||
|
eyeButton.checked = false;
|
||||||
|
}
|
||||||
function insert(position, text) {
|
function insert(position, text) {
|
||||||
control.insert(position, text);
|
control.insert(position, text);
|
||||||
}
|
}
|
||||||
@ -147,6 +150,9 @@ FocusScope {
|
|||||||
function selectWord() {
|
function selectWord() {
|
||||||
control.selectWord();
|
control.selectWord();
|
||||||
}
|
}
|
||||||
|
function showPassword() {
|
||||||
|
eyeButton.checked = true;
|
||||||
|
}
|
||||||
function undo() {
|
function undo() {
|
||||||
control.undo();
|
control.undo();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,14 +18,21 @@ import QtQuick.Controls
|
|||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
readonly property string addAccountTitle: qsTr("Add a Proton Mail account")
|
||||||
|
readonly property string welcomeDescription: qsTr("Bridge is the gateway between your Proton account and your email client. It runs in the background and encrypts and decrypts your messages seamlessly. ");
|
||||||
|
readonly property string welcomeTitle: qsTr("Welcome to\nProton Mail Bridge")
|
||||||
|
readonly property string welcomeImage: "/qml/icons/img-welcome.svg"
|
||||||
|
readonly property int welcomeImageHeight: 148;
|
||||||
|
readonly property int welcomeImageWidth: 265;
|
||||||
|
|
||||||
property int iconHeight
|
property int iconHeight
|
||||||
property string iconSource
|
property string iconSource
|
||||||
property int iconWidth
|
property int iconWidth
|
||||||
property var wizard
|
property var wizard
|
||||||
property ColorScheme colorScheme
|
property ColorScheme colorScheme
|
||||||
property var _colorScheme: wizard ? wizard.colorScheme : colorScheme
|
property var _colorScheme: wizard ? wizard.colorScheme : colorScheme
|
||||||
property var link1: linkLabel1
|
|
||||||
property var link2: linkLabel2
|
signal startSetup()
|
||||||
|
|
||||||
function showAppleMailAutoconfigCertificateInstall() {
|
function showAppleMailAutoconfigCertificateInstall() {
|
||||||
showAppleMailAutoconfigCommon();
|
showAppleMailAutoconfigCommon();
|
||||||
@ -65,14 +72,25 @@ Item {
|
|||||||
function showLoginMailboxPassword() {
|
function showLoginMailboxPassword() {
|
||||||
showOnboarding();
|
showOnboarding();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showNoAccount() {
|
||||||
|
titleLabel.text = welcomeTitle;
|
||||||
|
descriptionLabel.text = welcomeDescription;
|
||||||
|
linkLabel1.setCallback(startSetup, "Start setup", false);
|
||||||
|
linkLabel2.clear();
|
||||||
|
root.iconSource = welcomeImage;
|
||||||
|
root.iconHeight = welcomeImageHeight;
|
||||||
|
root.iconWidth = welcomeImageWidth;
|
||||||
|
}
|
||||||
|
|
||||||
function showOnboarding() {
|
function showOnboarding() {
|
||||||
titleLabel.text = (Backend.users.count === 0) ? qsTr("Welcome to\nProton Mail Bridge") : qsTr("Add a Proton Mail account");
|
titleLabel.text = (Backend.users.count === 0) ? welcomeTitle : addAccountTitle;
|
||||||
descriptionLabel.text = qsTr("Bridge is the gateway between your Proton account and your email client. It runs in the background and encrypts and decrypts your messages seamlessly. ");
|
descriptionLabel.text = welcomeDescription
|
||||||
linkLabel1.setCallback(function() { Backend.openKBArticle("https://proton.me/support/why-you-need-bridge"); }, qsTr("Why do I need Bridge?"), true);
|
linkLabel1.setCallback(function() { Backend.openKBArticle("https://proton.me/support/why-you-need-bridge"); }, qsTr("Why do I need Bridge?"), true);
|
||||||
linkLabel2.clear();
|
linkLabel2.clear();
|
||||||
root.iconSource = "/qml/icons/img-welcome.svg";
|
root.iconSource = welcomeImage;
|
||||||
root.iconHeight = 148;
|
root.iconHeight = welcomeImageHeight;
|
||||||
root.iconWidth = 265;
|
root.iconWidth = welcomeImageWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
|
|||||||
@ -44,6 +44,8 @@ FocusScope {
|
|||||||
} else {
|
} else {
|
||||||
passwordTextField.forceActiveFocus();
|
passwordTextField.forceActiveFocus();
|
||||||
}
|
}
|
||||||
|
passwordTextField.hidePassword();
|
||||||
|
secondPasswordTextField.hidePassword();
|
||||||
}
|
}
|
||||||
|
|
||||||
StackLayout {
|
StackLayout {
|
||||||
|
|||||||
@ -309,6 +309,8 @@ void User::setIsSyncing(bool syncing) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isSyncing_ = syncing;
|
isSyncing_ = syncing;
|
||||||
|
syncProgress_ = 0;
|
||||||
|
|
||||||
emit isSyncingChanged(syncing);
|
emit isSyncingChanged(syncing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -193,7 +193,7 @@ func NewUserBadEvent(userID string, errorMessage string) *StreamEvent {
|
|||||||
return userEvent(&UserEvent{Event: &UserEvent_UserBadEvent{UserBadEvent: &UserBadEvent{UserID: userID, ErrorMessage: errorMessage}}})
|
return userEvent(&UserEvent{Event: &UserEvent_UserBadEvent{UserBadEvent: &UserBadEvent{UserID: userID, ErrorMessage: errorMessage}}})
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUsedBytesChangedEvent(userID string, usedBytes int) *StreamEvent {
|
func NewUsedBytesChangedEvent(userID string, usedBytes uint64) *StreamEvent {
|
||||||
return userEvent(&UserEvent{Event: &UserEvent_UsedBytesChangedEvent{UsedBytesChangedEvent: &UsedBytesChangedEvent{UserID: userID, UsedBytes: int64(usedBytes)}}})
|
return userEvent(&UserEvent{Event: &UserEvent_UsedBytesChangedEvent{UsedBytesChangedEvent: &UsedBytesChangedEvent{UserID: userID, UsedBytes: int64(usedBytes)}}})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -54,8 +54,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
serverConfigFileName = "grpcServerConfig.json"
|
serverConfigFileName = "grpcServerConfig.json"
|
||||||
serverTokenMetadataKey = "server-token"
|
serverTokenMetadataKey = "server-token"
|
||||||
|
twoPasswordsMaxAttemptCount = 3 // The number of attempts allowed for the mailbox password.
|
||||||
)
|
)
|
||||||
|
|
||||||
// Service is the RPC service struct.
|
// Service is the RPC service struct.
|
||||||
@ -82,9 +83,10 @@ type Service struct { // nolint:structcheck
|
|||||||
target updater.VersionInfo
|
target updater.VersionInfo
|
||||||
targetLock safe.RWMutex
|
targetLock safe.RWMutex
|
||||||
|
|
||||||
authClient *proton.Client
|
authClient *proton.Client
|
||||||
auth proton.Auth
|
auth proton.Auth
|
||||||
password []byte
|
password []byte
|
||||||
|
twoPasswordAttemptCount int
|
||||||
|
|
||||||
log *logrus.Entry
|
log *logrus.Entry
|
||||||
initializing sync.WaitGroup
|
initializing sync.WaitGroup
|
||||||
@ -338,6 +340,11 @@ func (s *Service) watchEvents() {
|
|||||||
case events.SyncFinished:
|
case events.SyncFinished:
|
||||||
_ = s.SendEvent(NewSyncFinishedEvent(event.UserID))
|
_ = s.SendEvent(NewSyncFinishedEvent(event.UserID))
|
||||||
|
|
||||||
|
case events.SyncFailed:
|
||||||
|
if errors.Is(event.Error, context.Canceled) {
|
||||||
|
_ = s.SendEvent(NewSyncFinishedEvent(event.UserID))
|
||||||
|
}
|
||||||
|
|
||||||
case events.SyncProgress:
|
case events.SyncProgress:
|
||||||
_ = s.SendEvent(NewSyncProgressEvent(event.UserID, event.Progress, event.Elapsed.Milliseconds(), event.Remaining.Milliseconds()))
|
_ = s.SendEvent(NewSyncProgressEvent(event.UserID, event.Progress, event.Elapsed.Milliseconds(), event.Remaining.Milliseconds()))
|
||||||
|
|
||||||
@ -408,7 +415,12 @@ func (s *Service) loginClean() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) finishLogin() {
|
func (s *Service) finishLogin() {
|
||||||
defer s.loginClean()
|
performCleanup := true
|
||||||
|
defer func() {
|
||||||
|
if performCleanup {
|
||||||
|
s.loginClean()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
wasSignedOut := s.bridge.HasUser(s.auth.UserID)
|
wasSignedOut := s.bridge.HasUser(s.auth.UserID)
|
||||||
|
|
||||||
@ -426,10 +438,24 @@ func (s *Service) finishLogin() {
|
|||||||
eventCh, done := s.bridge.GetEvents(events.UserLoggedIn{})
|
eventCh, done := s.bridge.GetEvents(events.UserLoggedIn{})
|
||||||
defer done()
|
defer done()
|
||||||
|
|
||||||
userID, err := s.bridge.LoginUser(context.Background(), s.authClient, s.auth, s.password)
|
ctx := context.Background()
|
||||||
|
userID, err := s.bridge.LoginUser(ctx, s.authClient, s.auth, s.password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.log.WithError(err).Errorf("Finish login failed")
|
s.log.WithError(err).Errorf("Finish login failed")
|
||||||
_ = s.SendEvent(NewLoginError(LoginErrorType_TWO_PASSWORDS_ABORT, err.Error()))
|
s.twoPasswordAttemptCount++
|
||||||
|
errType := LoginErrorType_TWO_PASSWORDS_ABORT
|
||||||
|
if errors.Is(err, bridge.ErrFailedToUnlock) {
|
||||||
|
if s.twoPasswordAttemptCount < twoPasswordsMaxAttemptCount {
|
||||||
|
performCleanup = false
|
||||||
|
errType = LoginErrorType_TWO_PASSWORDS_ERROR
|
||||||
|
} else {
|
||||||
|
if deleteErr := s.authClient.AuthDelete(ctx); deleteErr != nil {
|
||||||
|
s.log.WithError(deleteErr).Error("Failed to delete auth")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = s.SendEvent(NewLoginError(errType, err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -339,18 +339,17 @@ func (s *Service) ReportBug(_ context.Context, report *ReportBugRequest) (*empty
|
|||||||
defer async.HandlePanic(s.panicHandler)
|
defer async.HandlePanic(s.panicHandler)
|
||||||
|
|
||||||
defer func() { _ = s.SendEvent(NewReportBugFinishedEvent()) }()
|
defer func() { _ = s.SendEvent(NewReportBugFinishedEvent()) }()
|
||||||
|
reportReq := bridge.ReportBugReq{
|
||||||
if err := s.bridge.ReportBug(
|
OSType: report.OsType,
|
||||||
context.Background(),
|
OSVersion: report.OsVersion,
|
||||||
report.OsType,
|
Title: report.Title,
|
||||||
report.OsVersion,
|
Description: report.Description,
|
||||||
report.Title,
|
Username: report.Address,
|
||||||
report.Description,
|
Email: report.Address,
|
||||||
report.Address,
|
EmailClient: report.EmailClient,
|
||||||
report.Address,
|
IncludeLogs: report.IncludeLogs,
|
||||||
report.EmailClient,
|
}
|
||||||
report.IncludeLogs,
|
if err := s.bridge.ReportBug(context.Background(), &reportReq); err != nil {
|
||||||
); err != nil {
|
|
||||||
s.log.WithError(err).Error("Failed to report bug")
|
s.log.WithError(err).Error("Failed to report bug")
|
||||||
_ = s.SendEvent(NewReportBugErrorEvent())
|
_ = s.SendEvent(NewReportBugErrorEvent())
|
||||||
return
|
return
|
||||||
@ -384,6 +383,7 @@ func (s *Service) Login(_ context.Context, login *LoginRequest) (*emptypb.Empty,
|
|||||||
go func() {
|
go func() {
|
||||||
defer async.HandlePanic(s.panicHandler)
|
defer async.HandlePanic(s.panicHandler)
|
||||||
|
|
||||||
|
s.twoPasswordAttemptCount = 0
|
||||||
password, err := base64Decode(login.Password)
|
password, err := base64Decode(login.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.log.WithError(err).Error("Cannot decode password")
|
s.log.WithError(err).Error("Cannot decode password")
|
||||||
|
|||||||
@ -152,7 +152,7 @@ func NewService(
|
|||||||
connectors: make(map[string]*Connector),
|
connectors: make(map[string]*Connector),
|
||||||
maxSyncMemory: maxSyncMemory,
|
maxSyncMemory: maxSyncMemory,
|
||||||
|
|
||||||
eventWatcher: subscription.Add(events.IMAPServerCreated{}),
|
eventWatcher: subscription.Add(events.IMAPServerCreated{}, events.ConnStatusUp{}, events.ConnStatusDown{}),
|
||||||
eventSubscription: subscription,
|
eventSubscription: subscription,
|
||||||
showAllMail: showAllMail,
|
showAllMail: showAllMail,
|
||||||
|
|
||||||
@ -218,18 +218,6 @@ func (s *Service) Resync(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) CancelSync(ctx context.Context) error {
|
|
||||||
_, err := s.cpc.Send(ctx, &cancelSyncReq{})
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) ResumeSync(ctx context.Context) error {
|
|
||||||
_, err := s.cpc.Send(ctx, &resumeSyncReq{})
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) OnBadEvent(ctx context.Context) error {
|
func (s *Service) OnBadEvent(ctx context.Context) error {
|
||||||
_, err := s.cpc.Send(ctx, &onBadEventReq{})
|
_, err := s.cpc.Send(ctx, &onBadEventReq{})
|
||||||
|
|
||||||
@ -342,6 +330,7 @@ func (s *Service) run(ctx context.Context) { //nolint gocyclo
|
|||||||
}
|
}
|
||||||
switch r := req.Value().(type) {
|
switch r := req.Value().(type) {
|
||||||
case *setAddressModeReq:
|
case *setAddressModeReq:
|
||||||
|
s.log.Debug("Set Address Mode Request")
|
||||||
err := s.setAddressMode(ctx, r.mode)
|
err := s.setAddressMode(ctx, r.mode)
|
||||||
req.Reply(ctx, nil, err)
|
req.Reply(ctx, nil, err)
|
||||||
|
|
||||||
@ -351,38 +340,33 @@ func (s *Service) run(ctx context.Context) { //nolint gocyclo
|
|||||||
req.Reply(ctx, nil, err)
|
req.Reply(ctx, nil, err)
|
||||||
s.log.Info("Resync reply sent, handling as refresh event")
|
s.log.Info("Resync reply sent, handling as refresh event")
|
||||||
|
|
||||||
case *cancelSyncReq:
|
|
||||||
s.log.Info("Cancelling sync")
|
|
||||||
s.syncHandler.Cancel()
|
|
||||||
req.Reply(ctx, nil, nil)
|
|
||||||
|
|
||||||
case *resumeSyncReq:
|
|
||||||
s.log.Info("Resuming sync")
|
|
||||||
// Cancel previous run, if any, just in case.
|
|
||||||
s.cancelSync()
|
|
||||||
s.startSyncing()
|
|
||||||
req.Reply(ctx, nil, nil)
|
|
||||||
case *getLabelsReq:
|
case *getLabelsReq:
|
||||||
|
s.log.Debug("Get labels Request")
|
||||||
labels := s.labels.GetLabelMap()
|
labels := s.labels.GetLabelMap()
|
||||||
req.Reply(ctx, labels, nil)
|
req.Reply(ctx, labels, nil)
|
||||||
|
|
||||||
case *onBadEventReq:
|
case *onBadEventReq:
|
||||||
|
s.log.Debug("Bad Event Request")
|
||||||
err := s.removeConnectorsFromServer(ctx, s.connectors, false)
|
err := s.removeConnectorsFromServer(ctx, s.connectors, false)
|
||||||
req.Reply(ctx, nil, err)
|
req.Reply(ctx, nil, err)
|
||||||
|
|
||||||
case *onBadEventResyncReq:
|
case *onBadEventResyncReq:
|
||||||
|
s.log.Debug("Bad Event Resync Request")
|
||||||
err := s.addConnectorsToServer(ctx, s.connectors)
|
err := s.addConnectorsToServer(ctx, s.connectors)
|
||||||
req.Reply(ctx, nil, err)
|
req.Reply(ctx, nil, err)
|
||||||
|
|
||||||
case *onLogoutReq:
|
case *onLogoutReq:
|
||||||
|
s.log.Debug("Logout Request")
|
||||||
err := s.removeConnectorsFromServer(ctx, s.connectors, false)
|
err := s.removeConnectorsFromServer(ctx, s.connectors, false)
|
||||||
req.Reply(ctx, nil, err)
|
req.Reply(ctx, nil, err)
|
||||||
|
|
||||||
case *showAllMailReq:
|
case *showAllMailReq:
|
||||||
|
s.log.Debug("Show all mail request")
|
||||||
req.Reply(ctx, nil, nil)
|
req.Reply(ctx, nil, nil)
|
||||||
s.setShowAllMail(r.v)
|
s.setShowAllMail(r.v)
|
||||||
|
|
||||||
case *getSyncFailedMessagesReq:
|
case *getSyncFailedMessagesReq:
|
||||||
|
s.log.Debug("Get sync failed messages Request")
|
||||||
status, err := s.syncStateProvider.GetSyncStatus(ctx)
|
status, err := s.syncStateProvider.GetSyncStatus(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
req.Reply(ctx, nil, fmt.Errorf("failed to get sync status: %w", err))
|
req.Reply(ctx, nil, fmt.Errorf("failed to get sync status: %w", err))
|
||||||
@ -476,10 +460,21 @@ func (s *Service) run(ctx context.Context) { //nolint gocyclo
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := e.(events.IMAPServerCreated); ok {
|
switch e.(type) {
|
||||||
|
case events.IMAPServerCreated:
|
||||||
|
s.log.Debug("On IMAPServerCreated")
|
||||||
if err := s.addConnectorsToServer(ctx, s.connectors); err != nil {
|
if err := s.addConnectorsToServer(ctx, s.connectors); err != nil {
|
||||||
s.log.WithError(err).Error("Failed to add connector to server after created")
|
s.log.WithError(err).Error("Failed to add connector to server after created")
|
||||||
}
|
}
|
||||||
|
case events.ConnStatusUp:
|
||||||
|
s.log.Info("Connection Restored Resuming Sync (if any)")
|
||||||
|
// Cancel previous run, if any, just in case.
|
||||||
|
s.cancelSync()
|
||||||
|
s.startSyncing()
|
||||||
|
|
||||||
|
case events.ConnStatusDown:
|
||||||
|
s.log.Info("Connection Lost cancelling sync")
|
||||||
|
s.cancelSync()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -632,10 +627,6 @@ func (s *Service) cancelSync() {
|
|||||||
|
|
||||||
type resyncReq struct{}
|
type resyncReq struct{}
|
||||||
|
|
||||||
type cancelSyncReq struct{}
|
|
||||||
|
|
||||||
type resumeSyncReq struct{}
|
|
||||||
|
|
||||||
type getLabelsReq struct{}
|
type getLabelsReq struct{}
|
||||||
|
|
||||||
type onBadEventReq struct{}
|
type onBadEventReq struct{}
|
||||||
|
|||||||
@ -95,6 +95,11 @@ func (s *Service) smtpSendMail(ctx context.Context, authID string, from string,
|
|||||||
// If the message contains a sender, use it instead of the one from the return path.
|
// If the message contains a sender, use it instead of the one from the return path.
|
||||||
if sender, ok := getMessageSender(parser); ok {
|
if sender, ok := getMessageSender(parser); ok {
|
||||||
from = sender
|
from = sender
|
||||||
|
fromAddr, err = s.identityState.GetAddr(from)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Errorf("Failed to get identity from sender address %v", sender)
|
||||||
|
return ErrInvalidReturnPath
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the user's mail settings.
|
// Load the user's mail settings.
|
||||||
@ -220,7 +225,7 @@ func (s *Service) sendWithKey(
|
|||||||
ExternalID: message.ExternalID,
|
ExternalID: message.ExternalID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return proton.Message{}, fmt.Errorf("failed to create attachments: %w", err)
|
return proton.Message{}, fmt.Errorf("failed to create draft: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
attKeys, err := s.createAttachments(ctx, s.client, addrKR, draft.ID, message.Attachments)
|
attKeys, err := s.createAttachments(ctx, s.client, addrKR, draft.ID, message.Attachments)
|
||||||
@ -315,7 +320,11 @@ func getParentID(
|
|||||||
switch len(metadata) {
|
switch len(metadata) {
|
||||||
case 1:
|
case 1:
|
||||||
// found exactly one parent
|
// found exactly one parent
|
||||||
parentID = metadata[0].ID
|
// We can only reference messages that have been sent or received. If this message is a draft
|
||||||
|
// it needs to be ignored.
|
||||||
|
if metadata[0].Flags.Has(proton.MessageFlagSent) || metadata[0].Flags.Has(proton.MessageFlagReceived) {
|
||||||
|
parentID = metadata[0].ID
|
||||||
|
}
|
||||||
case 0:
|
case 0:
|
||||||
// found no parents
|
// found no parents
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -18,11 +18,14 @@
|
|||||||
package smtp
|
package smtp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/ProtonMail/gluon/rfc822"
|
"github.com/ProtonMail/gluon/rfc822"
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/pkg/message"
|
"github.com/ProtonMail/proton-bridge/v3/pkg/message"
|
||||||
"github.com/bradenaw/juniper/xslices"
|
"github.com/bradenaw/juniper/xslices"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
"golang.org/x/exp/maps"
|
"golang.org/x/exp/maps"
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
@ -43,6 +46,9 @@ func createSendReq(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if recs := recipients.scheme(proton.InternalScheme, proton.ClearScheme, proton.PGPInlineScheme); len(recs) > 0 {
|
if recs := recipients.scheme(proton.InternalScheme, proton.ClearScheme, proton.PGPInlineScheme); len(recs) > 0 {
|
||||||
|
if recs := recipients.scheme(proton.PGPInlineScheme); len(recs) > 0 {
|
||||||
|
logrus.WithFields(logrus.Fields{"service": "smtp", "settings": "recipient"}).Warn("PGPInline scheme used. Planed to be deprecated.")
|
||||||
|
}
|
||||||
if recs := recs.content(rfc822.TextHTML); len(recs) > 0 {
|
if recs := recs.content(rfc822.TextHTML); len(recs) > 0 {
|
||||||
if err := req.AddTextPackage(kr, string(richBody), rfc822.TextHTML, recs, attKeys); err != nil {
|
if err := req.AddTextPackage(kr, string(richBody), rfc822.TextHTML, recs, attKeys); err != nil {
|
||||||
return proton.SendDraftReq{}, err
|
return proton.SendDraftReq{}, err
|
||||||
@ -54,6 +60,10 @@ func createSendReq(
|
|||||||
return proton.SendDraftReq{}, err
|
return proton.SendDraftReq{}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if recs := recs.content(rfc822.MultipartMixed); len(recs) > 0 {
|
||||||
|
return proton.SendDraftReq{}, fmt.Errorf("invalid MIME type for MIME package: %s", rfc822.MultipartMixed)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return req, nil
|
return req, nil
|
||||||
|
|||||||
1271
internal/services/smtp/smtp_packages_test.go
Normal file
1271
internal/services/smtp/smtp_packages_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@ -24,6 +24,7 @@ import (
|
|||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -547,6 +548,7 @@ func (b *sendPrefsBuilder) setEncryptionPreferences(mailSettings proton.MailSett
|
|||||||
// Otherwise keep the defined value.
|
// Otherwise keep the defined value.
|
||||||
switch mailSettings.PGPScheme {
|
switch mailSettings.PGPScheme {
|
||||||
case proton.PGPInlineScheme:
|
case proton.PGPInlineScheme:
|
||||||
|
logrus.WithFields(logrus.Fields{"service": "smtp", "settings": "account"}).Warn("PGPInline scheme used. Planed to be deprecated.")
|
||||||
b.withSchemeDefault(pgpInline)
|
b.withSchemeDefault(pgpInline)
|
||||||
case proton.PGPMIMEScheme:
|
case proton.PGPMIMEScheme:
|
||||||
b.withSchemeDefault(pgpMIME)
|
b.withSchemeDefault(pgpMIME)
|
||||||
|
|||||||
@ -296,7 +296,7 @@ func (m *MockUserUsedSpaceEventHandler) EXPECT() *MockUserUsedSpaceEventHandlerM
|
|||||||
}
|
}
|
||||||
|
|
||||||
// HandleUsedSpaceEvent mocks base method.
|
// HandleUsedSpaceEvent mocks base method.
|
||||||
func (m *MockUserUsedSpaceEventHandler) HandleUsedSpaceEvent(arg0 context.Context, arg1 int) error {
|
func (m *MockUserUsedSpaceEventHandler) HandleUsedSpaceEvent(arg0 context.Context, arg1 int64) error {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "HandleUsedSpaceEvent", arg0, arg1)
|
ret := m.ctrl.Call(m, "HandleUsedSpaceEvent", arg0, arg1)
|
||||||
ret0, _ := ret[0].(error)
|
ret0, _ := ret[0].(error)
|
||||||
|
|||||||
@ -29,6 +29,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ProtonMail/gluon/async"
|
"github.com/ProtonMail/gluon/async"
|
||||||
|
"github.com/ProtonMail/gluon/watcher"
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal"
|
"github.com/ProtonMail/proton-bridge/v3/internal"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
@ -67,6 +68,8 @@ type Service struct {
|
|||||||
|
|
||||||
eventPollWaiters []*EventPollWaiter
|
eventPollWaiters []*EventPollWaiter
|
||||||
eventPollWaitersLock sync.Mutex
|
eventPollWaitersLock sync.Mutex
|
||||||
|
eventSubscription events.Subscription
|
||||||
|
eventWatcher *watcher.Watcher[events.Event]
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(
|
func NewService(
|
||||||
@ -78,6 +81,7 @@ func NewService(
|
|||||||
jitter time.Duration,
|
jitter time.Duration,
|
||||||
eventTimeout time.Duration,
|
eventTimeout time.Duration,
|
||||||
panicHandler async.PanicHandler,
|
panicHandler async.PanicHandler,
|
||||||
|
eventSubscription events.Subscription,
|
||||||
) *Service {
|
) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
cpc: cpc.NewCPC(),
|
cpc: cpc.NewCPC(),
|
||||||
@ -88,11 +92,13 @@ func NewService(
|
|||||||
"service": "user-events",
|
"service": "user-events",
|
||||||
"user": userID,
|
"user": userID,
|
||||||
}),
|
}),
|
||||||
eventPublisher: eventPublisher,
|
eventPublisher: eventPublisher,
|
||||||
timer: proton.NewTicker(pollPeriod, jitter, panicHandler),
|
timer: proton.NewTicker(pollPeriod, jitter, panicHandler),
|
||||||
paused: 1,
|
paused: 1,
|
||||||
eventTimeout: eventTimeout,
|
eventTimeout: eventTimeout,
|
||||||
panicHandler: panicHandler,
|
panicHandler: panicHandler,
|
||||||
|
eventSubscription: eventSubscription,
|
||||||
|
eventWatcher: eventSubscription.Add(events.ConnStatusDown{}, events.ConnStatusUp{}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -224,6 +230,19 @@ func (s *Service) run(ctx context.Context, lastEventID string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
continue
|
continue
|
||||||
|
case e, ok := <-s.eventWatcher.GetChannel():
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch e.(type) {
|
||||||
|
case events.ConnStatusDown:
|
||||||
|
s.log.Info("Connection Lost, pausing")
|
||||||
|
s.Pause()
|
||||||
|
case events.ConnStatusUp:
|
||||||
|
s.log.Info("Connection Restored, resuming")
|
||||||
|
s.Resume()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply any pending subscription changes.
|
// Apply any pending subscription changes.
|
||||||
@ -295,6 +314,11 @@ func (s *Service) run(ctx context.Context, lastEventID string) {
|
|||||||
|
|
||||||
// Close should be called after the service has been cancelled to clean up any remaining pending operations.
|
// Close should be called after the service has been cancelled to clean up any remaining pending operations.
|
||||||
func (s *Service) Close() {
|
func (s *Service) Close() {
|
||||||
|
if s.eventSubscription != nil {
|
||||||
|
s.eventSubscription.Remove(s.eventWatcher)
|
||||||
|
s.eventSubscription = nil
|
||||||
|
}
|
||||||
|
|
||||||
s.pendingSubscriptionsLock.Lock()
|
s.pendingSubscriptionsLock.Lock()
|
||||||
defer s.pendingSubscriptionsLock.Unlock()
|
defer s.pendingSubscriptionsLock.Unlock()
|
||||||
|
|
||||||
|
|||||||
@ -48,6 +48,7 @@ func TestServiceHandleEventError_SubscriberEventUnwrapping(t *testing.T) {
|
|||||||
time.Millisecond,
|
time.Millisecond,
|
||||||
time.Second,
|
time.Second,
|
||||||
async.NoopPanicHandler{},
|
async.NoopPanicHandler{},
|
||||||
|
events.NewNullSubscription(),
|
||||||
)
|
)
|
||||||
|
|
||||||
lastEventID := "PrevEvent"
|
lastEventID := "PrevEvent"
|
||||||
@ -85,6 +86,7 @@ func TestServiceHandleEventError_BadEventPutsServiceOnPause(t *testing.T) {
|
|||||||
time.Millisecond,
|
time.Millisecond,
|
||||||
time.Second,
|
time.Second,
|
||||||
async.NoopPanicHandler{},
|
async.NoopPanicHandler{},
|
||||||
|
events.NewNullSubscription(),
|
||||||
)
|
)
|
||||||
service.Resume()
|
service.Resume()
|
||||||
lastEventID := "PrevEvent"
|
lastEventID := "PrevEvent"
|
||||||
@ -118,6 +120,7 @@ func TestServiceHandleEventError_BadEventFromPublishTimeout(t *testing.T) {
|
|||||||
time.Millisecond,
|
time.Millisecond,
|
||||||
time.Second,
|
time.Second,
|
||||||
async.NoopPanicHandler{},
|
async.NoopPanicHandler{},
|
||||||
|
events.NewNullSubscription(),
|
||||||
)
|
)
|
||||||
lastEventID := "PrevEvent"
|
lastEventID := "PrevEvent"
|
||||||
event := proton.Event{EventID: "MyEvent"}
|
event := proton.Event{EventID: "MyEvent"}
|
||||||
@ -148,6 +151,7 @@ func TestServiceHandleEventError_NoBadEventCheck(t *testing.T) {
|
|||||||
time.Millisecond,
|
time.Millisecond,
|
||||||
time.Second,
|
time.Second,
|
||||||
async.NoopPanicHandler{},
|
async.NoopPanicHandler{},
|
||||||
|
events.NewNullSubscription(),
|
||||||
)
|
)
|
||||||
lastEventID := "PrevEvent"
|
lastEventID := "PrevEvent"
|
||||||
event := proton.Event{EventID: "MyEvent"}
|
event := proton.Event{EventID: "MyEvent"}
|
||||||
@ -173,6 +177,7 @@ func TestServiceHandleEventError_JsonUnmarshalEventProducesUncategorizedErrorEve
|
|||||||
time.Millisecond,
|
time.Millisecond,
|
||||||
time.Second,
|
time.Second,
|
||||||
async.NoopPanicHandler{},
|
async.NoopPanicHandler{},
|
||||||
|
events.NewNullSubscription(),
|
||||||
)
|
)
|
||||||
lastEventID := "PrevEvent"
|
lastEventID := "PrevEvent"
|
||||||
event := proton.Event{EventID: "MyEvent"}
|
event := proton.Event{EventID: "MyEvent"}
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import (
|
|||||||
|
|
||||||
"github.com/ProtonMail/gluon/async"
|
"github.com/ProtonMail/gluon/async"
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events/mocks"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events/mocks"
|
||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@ -67,6 +68,7 @@ func TestServiceHandleEvent_CheckEventCategoriesHandledInOrder(t *testing.T) {
|
|||||||
time.Millisecond,
|
time.Millisecond,
|
||||||
10*time.Second,
|
10*time.Second,
|
||||||
async.NoopPanicHandler{},
|
async.NoopPanicHandler{},
|
||||||
|
events.NewNullSubscription(),
|
||||||
)
|
)
|
||||||
|
|
||||||
subscription := NewCallbackSubscriber("test", EventHandler{
|
subscription := NewCallbackSubscriber("test", EventHandler{
|
||||||
@ -84,7 +86,7 @@ func TestServiceHandleEvent_CheckEventCategoriesHandledInOrder(t *testing.T) {
|
|||||||
require.NoError(t, service.handleEvent(context.Background(), "", proton.Event{Refresh: proton.RefreshMail}))
|
require.NoError(t, service.handleEvent(context.Background(), "", proton.Event{Refresh: proton.RefreshMail}))
|
||||||
|
|
||||||
// Simulate Regular event.
|
// Simulate Regular event.
|
||||||
usedSpace := 20
|
usedSpace := int64(20)
|
||||||
require.NoError(t, service.handleEvent(context.Background(), "", proton.Event{
|
require.NoError(t, service.handleEvent(context.Background(), "", proton.Event{
|
||||||
User: new(proton.User),
|
User: new(proton.User),
|
||||||
Addresses: []proton.AddressEvent{
|
Addresses: []proton.AddressEvent{
|
||||||
@ -127,6 +129,7 @@ func TestServiceHandleEvent_CheckEventFailureCausesError(t *testing.T) {
|
|||||||
time.Millisecond,
|
time.Millisecond,
|
||||||
time.Second,
|
time.Second,
|
||||||
async.NoopPanicHandler{},
|
async.NoopPanicHandler{},
|
||||||
|
events.NewNullSubscription(),
|
||||||
)
|
)
|
||||||
|
|
||||||
subscription := NewCallbackSubscriber("test", EventHandler{
|
subscription := NewCallbackSubscriber("test", EventHandler{
|
||||||
@ -164,6 +167,7 @@ func TestServiceHandleEvent_CheckEventFailureCausesErrorParallel(t *testing.T) {
|
|||||||
time.Millisecond,
|
time.Millisecond,
|
||||||
time.Second,
|
time.Second,
|
||||||
async.NoopPanicHandler{},
|
async.NoopPanicHandler{},
|
||||||
|
events.NewNullSubscription(),
|
||||||
)
|
)
|
||||||
|
|
||||||
subscription := NewCallbackSubscriber("test", EventHandler{
|
subscription := NewCallbackSubscriber("test", EventHandler{
|
||||||
|
|||||||
@ -75,6 +75,7 @@ func TestService_EventIDLoadStore(t *testing.T) {
|
|||||||
time.Millisecond,
|
time.Millisecond,
|
||||||
time.Second,
|
time.Second,
|
||||||
async.NoopPanicHandler{},
|
async.NoopPanicHandler{},
|
||||||
|
events.NewNullSubscription(),
|
||||||
)
|
)
|
||||||
|
|
||||||
_, err := service.Start(context.Background(), group)
|
_, err := service.Start(context.Background(), group)
|
||||||
@ -130,6 +131,7 @@ func TestService_RetryEventOnNonCatastrophicFailure(t *testing.T) {
|
|||||||
time.Millisecond,
|
time.Millisecond,
|
||||||
time.Second,
|
time.Second,
|
||||||
async.NoopPanicHandler{},
|
async.NoopPanicHandler{},
|
||||||
|
events.NewNullSubscription(),
|
||||||
)
|
)
|
||||||
service.Subscribe(NewCallbackSubscriber("foo", EventHandler{MessageHandler: subscriber}))
|
service.Subscribe(NewCallbackSubscriber("foo", EventHandler{MessageHandler: subscriber}))
|
||||||
|
|
||||||
@ -179,6 +181,7 @@ func TestService_OnBadEventServiceIsPaused(t *testing.T) {
|
|||||||
time.Millisecond,
|
time.Millisecond,
|
||||||
time.Second,
|
time.Second,
|
||||||
async.NoopPanicHandler{},
|
async.NoopPanicHandler{},
|
||||||
|
events.NewNullSubscription(),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Event publisher expectations.
|
// Event publisher expectations.
|
||||||
@ -245,6 +248,7 @@ func TestService_UnsubscribeDuringEventHandlingDoesNotCauseDeadlock(t *testing.T
|
|||||||
time.Millisecond,
|
time.Millisecond,
|
||||||
time.Second,
|
time.Second,
|
||||||
async.NoopPanicHandler{},
|
async.NoopPanicHandler{},
|
||||||
|
events.NewNullSubscription(),
|
||||||
)
|
)
|
||||||
|
|
||||||
subscription := NewCallbackSubscriber("foo", EventHandler{MessageHandler: subscriber})
|
subscription := NewCallbackSubscriber("foo", EventHandler{MessageHandler: subscriber})
|
||||||
@ -304,6 +308,7 @@ func TestService_UnsubscribeBeforeHandlingEventIsNotConsideredError(t *testing.T
|
|||||||
time.Millisecond,
|
time.Millisecond,
|
||||||
time.Second,
|
time.Second,
|
||||||
async.NoopPanicHandler{},
|
async.NoopPanicHandler{},
|
||||||
|
events.NewNullSubscription(),
|
||||||
)
|
)
|
||||||
|
|
||||||
subscription := NewEventSubscriber("Foo")
|
subscription := NewEventSubscriber("Foo")
|
||||||
@ -363,6 +368,7 @@ func TestService_WaitOnEventPublishAfterPause(t *testing.T) {
|
|||||||
time.Millisecond,
|
time.Millisecond,
|
||||||
time.Second,
|
time.Second,
|
||||||
async.NoopPanicHandler{},
|
async.NoopPanicHandler{},
|
||||||
|
events.NewNullSubscription(),
|
||||||
)
|
)
|
||||||
|
|
||||||
subscriber.EXPECT().HandleMessageEvents(gomock.Any(), gomock.Eq(messageEvents)).Times(1).DoAndReturn(func(_ context.Context, _ []proton.MessageEvent) error {
|
subscriber.EXPECT().HandleMessageEvents(gomock.Any(), gomock.Eq(messageEvents)).Times(1).DoAndReturn(func(_ context.Context, _ []proton.MessageEvent) error {
|
||||||
@ -435,6 +441,7 @@ func TestService_EventRewind(t *testing.T) {
|
|||||||
time.Millisecond,
|
time.Millisecond,
|
||||||
time.Second,
|
time.Second,
|
||||||
async.NoopPanicHandler{},
|
async.NoopPanicHandler{},
|
||||||
|
events.NewNullSubscription(),
|
||||||
)
|
)
|
||||||
|
|
||||||
_, err := service.Start(context.Background(), group)
|
_, err := service.Start(context.Background(), group)
|
||||||
|
|||||||
@ -98,7 +98,7 @@ type UserEventHandler interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UserUsedSpaceEventHandler interface {
|
type UserUsedSpaceEventHandler interface {
|
||||||
HandleUsedSpaceEvent(ctx context.Context, newSpace int) error
|
HandleUsedSpaceEvent(ctx context.Context, newSpace int64) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserSettingsHandler interface {
|
type UserSettingsHandler interface {
|
||||||
|
|||||||
@ -102,13 +102,13 @@ func (s *Service) CheckAuth(ctx context.Context, email string, password []byte)
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) HandleUsedSpaceEvent(ctx context.Context, newSpace int) error {
|
func (s *Service) HandleUsedSpaceEvent(ctx context.Context, newSpace int64) error {
|
||||||
s.log.Info("Handling User Space Changed event")
|
s.log.Info("Handling User Space Changed event")
|
||||||
|
|
||||||
if s.identity.OnUserSpaceChanged(newSpace) {
|
if s.identity.OnUserSpaceChanged(uint64(newSpace)) {
|
||||||
s.eventPublisher.PublishEvent(ctx, events.UsedSpaceChanged{
|
s.eventPublisher.PublishEvent(ctx, events.UsedSpaceChanged{
|
||||||
UserID: s.identity.User.ID,
|
UserID: s.identity.User.ID,
|
||||||
UsedSpace: newSpace,
|
UsedSpace: uint64(newSpace),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -54,7 +54,7 @@ func TestService_OnUserSpaceChanged(t *testing.T) {
|
|||||||
|
|
||||||
// New value, event should be published.
|
// New value, event should be published.
|
||||||
require.NoError(t, service.HandleUsedSpaceEvent(context.Background(), 1024))
|
require.NoError(t, service.HandleUsedSpaceEvent(context.Background(), 1024))
|
||||||
require.Equal(t, 1024, service.identity.User.UsedSpace)
|
require.Equal(t, uint64(1024), service.identity.User.UsedSpace)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestService_OnRefreshEvent(t *testing.T) {
|
func TestService_OnRefreshEvent(t *testing.T) {
|
||||||
|
|||||||
@ -119,7 +119,7 @@ func (s *State) OnRefreshEvent(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *State) OnUserSpaceChanged(value int) bool {
|
func (s *State) OnUserSpaceChanged(value uint64) bool {
|
||||||
if s.User.UsedSpace == value {
|
if s.User.UsedSpace == value {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import (
|
|||||||
|
|
||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/versioner"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
@ -46,15 +47,17 @@ type Installer interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Updater struct {
|
type Updater struct {
|
||||||
|
versioner *versioner.Versioner
|
||||||
installer Installer
|
installer Installer
|
||||||
verifier *crypto.KeyRing
|
verifier *crypto.KeyRing
|
||||||
product string
|
product string
|
||||||
platform string
|
platform string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUpdater(installer Installer, verifier *crypto.KeyRing, product, platform string) *Updater {
|
func NewUpdater(ver *versioner.Versioner, verifier *crypto.KeyRing, product, platform string) *Updater {
|
||||||
return &Updater{
|
return &Updater{
|
||||||
installer: installer,
|
versioner: ver,
|
||||||
|
installer: NewInstaller(ver),
|
||||||
verifier: verifier,
|
verifier: verifier,
|
||||||
product: product,
|
product: product,
|
||||||
platform: platform,
|
platform: platform,
|
||||||
@ -109,6 +112,10 @@ func (u *Updater) InstallUpdate(ctx context.Context, downloader Downloader, upda
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *Updater) RemoveOldUpdates() error {
|
||||||
|
return u.versioner.RemoveOldVersions()
|
||||||
|
}
|
||||||
|
|
||||||
// getVersionFileURL returns the URL of the version file.
|
// getVersionFileURL returns the URL of the version file.
|
||||||
// For example:
|
// For example:
|
||||||
// - https://protonmail.com/download/bridge/version_linux.json
|
// - https://protonmail.com/download/bridge/version_linux.json
|
||||||
|
|||||||
@ -38,7 +38,7 @@ func (user *User) SendConfigStatusSuccess(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var builder configstatus.ConfigSuccessBuilder
|
var builder configstatus.ConfigSuccessBuilder
|
||||||
success := builder.New(user.configStatus.Data)
|
success := builder.New(user.configStatus)
|
||||||
data, err := json.Marshal(success)
|
data, err := json.Marshal(success)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err := user.reporter.ReportMessageWithContext("Cannot parse config_success data.", reporter.Context{
|
if err := user.reporter.ReportMessageWithContext("Cannot parse config_success data.", reporter.Context{
|
||||||
@ -69,7 +69,7 @@ func (user *User) SendConfigStatusAbort(ctx context.Context, withTelemetry bool)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
var builder configstatus.ConfigAbortBuilder
|
var builder configstatus.ConfigAbortBuilder
|
||||||
abort := builder.New(user.configStatus.Data)
|
abort := builder.New(user.configStatus)
|
||||||
data, err := json.Marshal(abort)
|
data, err := json.Marshal(abort)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err := user.reporter.ReportMessageWithContext("Cannot parse config_abort data.", reporter.Context{
|
if err := user.reporter.ReportMessageWithContext("Cannot parse config_abort data.", reporter.Context{
|
||||||
@ -98,7 +98,7 @@ func (user *User) SendConfigStatusRecovery(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var builder configstatus.ConfigRecoveryBuilder
|
var builder configstatus.ConfigRecoveryBuilder
|
||||||
success := builder.New(user.configStatus.Data)
|
success := builder.New(user.configStatus)
|
||||||
data, err := json.Marshal(success)
|
data, err := json.Marshal(success)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err := user.reporter.ReportMessageWithContext("Cannot parse config_recovery data.", reporter.Context{
|
if err := user.reporter.ReportMessageWithContext("Cannot parse config_recovery data.", reporter.Context{
|
||||||
@ -125,7 +125,7 @@ func (user *User) SendConfigStatusProgress(ctx context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
var builder configstatus.ConfigProgressBuilder
|
var builder configstatus.ConfigProgressBuilder
|
||||||
progress := builder.New(user.configStatus.Data)
|
progress := builder.New(user.configStatus)
|
||||||
if progress.Values.NbDay == 0 {
|
if progress.Values.NbDay == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"mime"
|
"mime"
|
||||||
@ -37,6 +38,7 @@ import (
|
|||||||
imapservice "github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice"
|
imapservice "github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/usertypes"
|
"github.com/ProtonMail/proton-bridge/v3/internal/usertypes"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||||
|
bmessage "github.com/ProtonMail/proton-bridge/v3/pkg/message"
|
||||||
"github.com/bradenaw/juniper/xmaps"
|
"github.com/bradenaw/juniper/xmaps"
|
||||||
"github.com/bradenaw/juniper/xslices"
|
"github.com/bradenaw/juniper/xslices"
|
||||||
"github.com/emersion/go-message"
|
"github.com/emersion/go-message"
|
||||||
@ -224,6 +226,55 @@ func (user *User) DebugDownloadMessages(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TryBuildDebugMessage(path string) error {
|
||||||
|
meta, err := loadDebugMetadata(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load metadata: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, bodyDecrypted, err := loadDebugBody(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var da []bmessage.DecryptedAttachment
|
||||||
|
if len(meta.Attachments) != 0 {
|
||||||
|
d, err := loadAttachments(path, &meta)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
da = d
|
||||||
|
}
|
||||||
|
|
||||||
|
decryptedMessage := bmessage.DecryptedMessage{
|
||||||
|
Msg: proton.Message{
|
||||||
|
MessageMetadata: meta.MessageMetadata,
|
||||||
|
Header: meta.Header,
|
||||||
|
ParsedHeaders: meta.ParsedHeaders,
|
||||||
|
Body: "",
|
||||||
|
MIMEType: meta.MIMEType,
|
||||||
|
Attachments: nil,
|
||||||
|
},
|
||||||
|
Body: bytes.Buffer{},
|
||||||
|
BodyErr: nil,
|
||||||
|
Attachments: da,
|
||||||
|
}
|
||||||
|
|
||||||
|
if bodyDecrypted {
|
||||||
|
decryptedMessage.Body.Write(body)
|
||||||
|
} else {
|
||||||
|
decryptedMessage.Msg.Body = string(body)
|
||||||
|
decryptedMessage.BodyErr = fmt.Errorf("body did not decrypt")
|
||||||
|
}
|
||||||
|
|
||||||
|
var rfc822Message bytes.Buffer
|
||||||
|
if err := bmessage.BuildRFC822Into(nil, &decryptedMessage, defaultMessageJobOpts(), &rfc822Message); err != nil {
|
||||||
|
return fmt.Errorf("failed to build message: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func getBodyName(path string) string {
|
func getBodyName(path string) string {
|
||||||
return filepath.Join(path, "body.txt")
|
return filepath.Join(path, "body.txt")
|
||||||
}
|
}
|
||||||
@ -297,16 +348,16 @@ func decodeSimpleMessage(outPath string, kr *crypto.KeyRing, msg proton.Message)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeMetadata(outPath string, msg proton.Message) error {
|
type DebugMetadata struct {
|
||||||
type CustomMetadata struct {
|
proton.MessageMetadata
|
||||||
proton.MessageMetadata
|
Header string
|
||||||
Header string
|
ParsedHeaders proton.Headers
|
||||||
ParsedHeaders proton.Headers
|
MIMEType rfc822.MIMEType
|
||||||
MIMEType rfc822.MIMEType
|
Attachments []proton.Attachment
|
||||||
Attachments []proton.Attachment
|
}
|
||||||
}
|
|
||||||
|
|
||||||
metadata := CustomMetadata{
|
func writeMetadata(outPath string, msg proton.Message) error {
|
||||||
|
metadata := DebugMetadata{
|
||||||
MessageMetadata: msg.MessageMetadata,
|
MessageMetadata: msg.MessageMetadata,
|
||||||
Header: msg.Header,
|
Header: msg.Header,
|
||||||
ParsedHeaders: msg.ParsedHeaders,
|
ParsedHeaders: msg.ParsedHeaders,
|
||||||
@ -433,3 +484,78 @@ func writeCustomAttachmentPart(
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadDebugMetadata(dir string) (DebugMetadata, error) {
|
||||||
|
metadataPath := getMetadataPath(dir)
|
||||||
|
b, err := os.ReadFile(metadataPath) //nolint:gosec
|
||||||
|
if err != nil {
|
||||||
|
return DebugMetadata{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var m DebugMetadata
|
||||||
|
|
||||||
|
if err := json.Unmarshal(b, &m); err != nil {
|
||||||
|
return DebugMetadata{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadDebugBody(dir string) ([]byte, bool, error) {
|
||||||
|
if b, err := os.ReadFile(getBodyName(dir)); err != nil {
|
||||||
|
if !errors.Is(err, os.ErrNotExist) {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return b, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if b, err := os.ReadFile(getBodyNameFailed(dir)); err != nil {
|
||||||
|
if !errors.Is(err, os.ErrNotExist) {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return b, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, false, fmt.Errorf("body is either pgp message, which we can't handle or is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadAttachments(dir string, meta *DebugMetadata) ([]bmessage.DecryptedAttachment, error) {
|
||||||
|
attDecrypted := make([]bmessage.DecryptedAttachment, 0, len(meta.Attachments))
|
||||||
|
|
||||||
|
for _, a := range meta.Attachments {
|
||||||
|
data, err := os.ReadFile(getAttachmentPathSuccess(dir, a.ID, a.Name))
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
return nil, fmt.Errorf("attachment (%v,%v) must have failed to decrypt, we can't do anything since we need the user's keyring", a.ID, a.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("failed to load attachment (%v,%v): %w", a.ID, a.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
da := bmessage.DecryptedAttachment{
|
||||||
|
Packet: nil,
|
||||||
|
Encrypted: nil,
|
||||||
|
Data: bytes.Buffer{},
|
||||||
|
Err: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
da.Data.Write(data)
|
||||||
|
|
||||||
|
attDecrypted = append(attDecrypted, da)
|
||||||
|
}
|
||||||
|
|
||||||
|
return attDecrypted, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultMessageJobOpts() bmessage.JobOptions {
|
||||||
|
return bmessage.JobOptions{
|
||||||
|
IgnoreDecryptionErrors: true, // Whether to ignore decryption errors and create a "custom message" instead.
|
||||||
|
SanitizeDate: true, // Whether to replace all dates before 1970 with RFC822's birthdate.
|
||||||
|
AddInternalID: true, // Whether to include MessageID as X-Pm-Internal-Id.
|
||||||
|
AddExternalID: true, // Whether to include ExternalID as X-Pm-External-Id.
|
||||||
|
AddMessageDate: true, // Whether to include message time as X-Pm-Date.
|
||||||
|
AddMessageIDReference: true, // Whether to include the MessageID in References.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -223,6 +223,7 @@ func newImpl(
|
|||||||
EventJitter,
|
EventJitter,
|
||||||
5*time.Minute,
|
5*time.Minute,
|
||||||
crashHandler,
|
crashHandler,
|
||||||
|
eventSubscription,
|
||||||
)
|
)
|
||||||
|
|
||||||
addressMode := usertypes.VaultToAddressMode(encVault.AddressMode())
|
addressMode := usertypes.VaultToAddressMode(encVault.AddressMode())
|
||||||
@ -515,7 +516,7 @@ func (user *User) BridgePass() []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UsedSpace returns the total space used by the user on the API.
|
// UsedSpace returns the total space used by the user on the API.
|
||||||
func (user *User) UsedSpace() int {
|
func (user *User) UsedSpace() uint64 {
|
||||||
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Minute))
|
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Minute))
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@ -528,7 +529,7 @@ func (user *User) UsedSpace() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MaxSpace returns the amount of space the user can use on the API.
|
// MaxSpace returns the amount of space the user can use on the API.
|
||||||
func (user *User) MaxSpace() int {
|
func (user *User) MaxSpace() uint64 {
|
||||||
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Minute))
|
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Minute))
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@ -554,27 +555,6 @@ func (user *User) CheckAuth(email string, password []byte) (string, error) {
|
|||||||
return user.identityService.CheckAuth(ctx, email, password)
|
return user.identityService.CheckAuth(ctx, email, password)
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnStatusUp is called when the connection goes up.
|
|
||||||
func (user *User) OnStatusUp(ctx context.Context) {
|
|
||||||
user.log.Info("Connection is up")
|
|
||||||
|
|
||||||
user.eventService.Resume()
|
|
||||||
|
|
||||||
if err := user.imapService.ResumeSync(ctx); err != nil {
|
|
||||||
user.log.WithError(err).Error("Failed to resume sync")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnStatusDown is called when the connection goes down.
|
|
||||||
func (user *User) OnStatusDown(ctx context.Context) {
|
|
||||||
user.log.Info("Connection is down")
|
|
||||||
|
|
||||||
user.eventService.Pause()
|
|
||||||
if err := user.imapService.CancelSync(ctx); err != nil {
|
|
||||||
user.log.WithError(err).Error("Failed to cancel sync")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logout logs the user out from the API.
|
// Logout logs the user out from the API.
|
||||||
func (user *User) Logout(ctx context.Context, withAPI bool) error {
|
func (user *User) Logout(ctx context.Context, withAPI bool) error {
|
||||||
user.log.WithField("withAPI", withAPI).Info("Logging out user")
|
user.log.WithField("withAPI", withAPI).Info("Logging out user")
|
||||||
|
|||||||
@ -32,6 +32,7 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v3/pkg/message/parser"
|
"github.com/ProtonMail/proton-bridge/v3/pkg/message/parser"
|
||||||
pmmime "github.com/ProtonMail/proton-bridge/v3/pkg/mime"
|
pmmime "github.com/ProtonMail/proton-bridge/v3/pkg/mime"
|
||||||
"github.com/emersion/go-message"
|
"github.com/emersion/go-message"
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/jaytaylor/html2text"
|
"github.com/jaytaylor/html2text"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
@ -116,6 +117,10 @@ func parse(p *parser.Parser, allowInvalidAddressLists bool) (Message, error) {
|
|||||||
return Message{}, errors.Wrap(err, "failed to convert foreign encodings")
|
return Message{}, errors.Wrap(err, "failed to convert foreign encodings")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := patchInlineImages(p); err != nil {
|
||||||
|
return Message{}, err
|
||||||
|
}
|
||||||
|
|
||||||
m, err := parseMessageHeader(p.Root().Header, allowInvalidAddressLists)
|
m, err := parseMessageHeader(p.Root().Header, allowInvalidAddressLists)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Message{}, errors.Wrap(err, "failed to parse message header")
|
return Message{}, errors.Wrap(err, "failed to parse message header")
|
||||||
@ -142,7 +147,7 @@ func parse(p *parser.Parser, allowInvalidAddressLists bool) (Message, error) {
|
|||||||
m.PlainBody = Body(plainBody)
|
m.PlainBody = Body(plainBody)
|
||||||
m.MIMEBody = MIMEBody(mimeBody)
|
m.MIMEBody = MIMEBody(mimeBody)
|
||||||
|
|
||||||
mimeType, err := determineMIMEType(p)
|
mimeType, err := determineBodyMIMEType(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Message{}, errors.Wrap(err, "failed to get mime type")
|
return Message{}, errors.Wrap(err, "failed to get mime type")
|
||||||
}
|
}
|
||||||
@ -308,7 +313,7 @@ func collectBodyParts(p *parser.Parser, preferredContentType string) (parser.Par
|
|||||||
return bestChoice(childParts, preferredContentType), nil
|
return bestChoice(childParts, preferredContentType), nil
|
||||||
}).
|
}).
|
||||||
RegisterRule("text/plain", func(p *parser.Part, visit parser.Visit) (interface{}, error) {
|
RegisterRule("text/plain", func(p *parser.Part, visit parser.Visit) (interface{}, error) {
|
||||||
disp, _, err := p.Header.ContentDisposition()
|
disp, _, err := p.ContentDisposition()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
disp = ""
|
disp = ""
|
||||||
}
|
}
|
||||||
@ -320,7 +325,7 @@ func collectBodyParts(p *parser.Parser, preferredContentType string) (parser.Par
|
|||||||
return parser.Parts{p}, nil
|
return parser.Parts{p}, nil
|
||||||
}).
|
}).
|
||||||
RegisterRule("text/html", func(p *parser.Part, visit parser.Visit) (interface{}, error) {
|
RegisterRule("text/html", func(p *parser.Part, visit parser.Visit) (interface{}, error) {
|
||||||
disp, _, err := p.Header.ContentDisposition()
|
disp, _, err := p.ContentDisposition()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
disp = ""
|
disp = ""
|
||||||
}
|
}
|
||||||
@ -400,7 +405,7 @@ func allPartsHaveContentType(parts parser.Parts, contentType string) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func determineMIMEType(p *parser.Parser) (string, error) {
|
func determineBodyMIMEType(p *parser.Parser) (string, error) {
|
||||||
var isHTML bool
|
var isHTML bool
|
||||||
|
|
||||||
w := p.NewWalker().
|
w := p.NewWalker().
|
||||||
@ -409,7 +414,7 @@ func determineMIMEType(p *parser.Parser) (string, error) {
|
|||||||
return
|
return
|
||||||
})
|
})
|
||||||
|
|
||||||
if err := w.Walk(); err != nil {
|
if err := w.WalkSkipAttachment(); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -547,8 +552,8 @@ func parseAttachment(h message.Header, body []byte) (Attachment, error) {
|
|||||||
return Attachment{}, err
|
return Attachment{}, err
|
||||||
}
|
}
|
||||||
att.Header = mimeHeader
|
att.Header = mimeHeader
|
||||||
|
mimeType, mimeTypeParams, err := pmmime.ParseMediaType(h.Get("Content-Type"))
|
||||||
|
|
||||||
mimeType, mimeTypeParams, err := h.ContentType()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Attachment{}, err
|
return Attachment{}, err
|
||||||
}
|
}
|
||||||
@ -558,7 +563,8 @@ func parseAttachment(h message.Header, body []byte) (Attachment, error) {
|
|||||||
// Prefer attachment name from filename param in content disposition.
|
// Prefer attachment name from filename param in content disposition.
|
||||||
// If not available, try to get it from name param in content type.
|
// If not available, try to get it from name param in content type.
|
||||||
// Otherwise fallback to attachment.bin.
|
// Otherwise fallback to attachment.bin.
|
||||||
if disp, dispParams, err := h.ContentDisposition(); err == nil {
|
disp, dispParams, err := pmmime.ParseMediaType(h.Get("Content-Disposition"))
|
||||||
|
if err == nil {
|
||||||
att.Disposition = proton.Disposition(disp)
|
att.Disposition = proton.Disposition(disp)
|
||||||
|
|
||||||
if filename, ok := dispParams["filename"]; ok {
|
if filename, ok := dispParams["filename"]; ok {
|
||||||
@ -585,7 +591,7 @@ func parseAttachment(h message.Header, body []byte) (Attachment, error) {
|
|||||||
// (This is necessary because some clients don't set Content-Disposition at all,
|
// (This is necessary because some clients don't set Content-Disposition at all,
|
||||||
// so we need to rely on other information to deduce if it's inline or attachment.)
|
// so we need to rely on other information to deduce if it's inline or attachment.)
|
||||||
if h.Has("Content-Disposition") {
|
if h.Has("Content-Disposition") {
|
||||||
disp, _, err := h.ContentDisposition()
|
disp, _, err := pmmime.ParseMediaType(h.Get("Content-Disposition"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Attachment{}, err
|
return Attachment{}, err
|
||||||
}
|
}
|
||||||
@ -635,3 +641,168 @@ func forEachDecodedHeaderField(h message.Header, fn func(string, string) error)
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func patchInlineImages(p *parser.Parser) error {
|
||||||
|
// This code will only attempt to patch the root level children. I tested with different email clients and as soon
|
||||||
|
// as you reply/forward a message the entire content gets converted into HTML (Apple Mail/Thunderbird/Evolution).
|
||||||
|
// If you are forcing text formatting (Evolution), the inline images of the original email are stripped.
|
||||||
|
// The only reason we need to apply this modification is that Apple Mail can send out text + inline image parts
|
||||||
|
// if the text does not exceed the 76 char column limit.
|
||||||
|
// Based on this, it's unlikely we will see any other variations.
|
||||||
|
root := p.Root()
|
||||||
|
|
||||||
|
children := root.Children()
|
||||||
|
|
||||||
|
if len(children) < 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]inlinePatchJob, len(children))
|
||||||
|
|
||||||
|
var (
|
||||||
|
transformationNeeded bool
|
||||||
|
prevPart *parser.Part
|
||||||
|
prevContentType string
|
||||||
|
prevContentTypeMap map[string]string
|
||||||
|
)
|
||||||
|
|
||||||
|
for i := 0; i < len(children); i++ {
|
||||||
|
curPart := children[i]
|
||||||
|
|
||||||
|
contentType, contentTypeMap, err := curPart.ContentType()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get content type for for child %v:%w", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rfc822.MIMEType(contentType) == rfc822.TextPlain {
|
||||||
|
result[i] = &inlinePatchBodyOnly{part: curPart, contentTypeMap: contentTypeMap}
|
||||||
|
} else if strings.HasPrefix(contentType, "image/") {
|
||||||
|
disposition, _, err := curPart.ContentDisposition()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failted to get content disposition for child %v:%w", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if disposition == "inline" && !curPart.HasContentID() {
|
||||||
|
if rfc822.MIMEType(prevContentType) == rfc822.TextPlain {
|
||||||
|
result[i-1] = &inlinePatchBodyWithInlineImage{
|
||||||
|
textPart: prevPart,
|
||||||
|
imagePart: curPart,
|
||||||
|
textContentTypeMap: prevContentTypeMap,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result[i] = &inlinePatchInlineImageOnly{part: curPart, partIndex: i, root: root}
|
||||||
|
}
|
||||||
|
transformationNeeded = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prevPart = curPart
|
||||||
|
prevContentType = contentType
|
||||||
|
prevContentTypeMap = contentTypeMap
|
||||||
|
}
|
||||||
|
|
||||||
|
if !transformationNeeded {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, t := range result {
|
||||||
|
if t != nil {
|
||||||
|
t.Patch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type inlinePatchJob interface {
|
||||||
|
Patch()
|
||||||
|
}
|
||||||
|
|
||||||
|
// inlinePatchBodyOnly is meant to be used for standalone text parts that need to be converted to html once we applty
|
||||||
|
// one of the changes.
|
||||||
|
type inlinePatchBodyOnly struct {
|
||||||
|
part *parser.Part
|
||||||
|
contentTypeMap map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *inlinePatchBodyOnly) Patch() {
|
||||||
|
newBody := []byte(`<html><body><p>`)
|
||||||
|
newBody = append(newBody, patchNewLineWithHTMLBreaks(i.part.Body)...)
|
||||||
|
newBody = append(newBody, []byte(`</p></body></html>`)...)
|
||||||
|
|
||||||
|
i.part.Body = newBody
|
||||||
|
i.part.Header.SetContentType("text/html", i.contentTypeMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
// inlinePatchBodyWithInlineImage patches a previous text part so that it refers to that inline image.
|
||||||
|
type inlinePatchBodyWithInlineImage struct {
|
||||||
|
textPart *parser.Part
|
||||||
|
textContentTypeMap map[string]string
|
||||||
|
imagePart *parser.Part
|
||||||
|
}
|
||||||
|
|
||||||
|
// inlinePatchInlineImageOnly handle the case where the inline image is not proceeded by a text part. To avoid
|
||||||
|
// having to parse any possible previous part, we just inject a new part that references this image.
|
||||||
|
type inlinePatchInlineImageOnly struct {
|
||||||
|
part *parser.Part
|
||||||
|
partIndex int
|
||||||
|
root *parser.Part
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i inlinePatchInlineImageOnly) Patch() {
|
||||||
|
contentID := uuid.NewString()
|
||||||
|
// Convert previous part to text/html && inject image.
|
||||||
|
newBody := []byte(fmt.Sprintf(`<html><body><img src="cid:%v"/></body></html>`, contentID))
|
||||||
|
|
||||||
|
i.part.Header.Set("content-id", contentID)
|
||||||
|
|
||||||
|
// create new text part
|
||||||
|
textPart := &parser.Part{
|
||||||
|
Header: message.Header{},
|
||||||
|
Body: newBody,
|
||||||
|
}
|
||||||
|
|
||||||
|
textPart.Header.SetContentType("text/html", map[string]string{"charset": "UTF-8"})
|
||||||
|
|
||||||
|
i.root.InsertChild(i.partIndex, textPart)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *inlinePatchBodyWithInlineImage) Patch() {
|
||||||
|
contentID := uuid.NewString()
|
||||||
|
// Convert previous part to text/html && inject image.
|
||||||
|
newBody := []byte(`<html><body><p>`)
|
||||||
|
newBody = append(newBody, patchNewLineWithHTMLBreaks(i.textPart.Body)...)
|
||||||
|
newBody = append(newBody, []byte(`</p>`)...)
|
||||||
|
newBody = append(newBody, []byte(fmt.Sprintf(`<img src="cid:%v"/>`, contentID))...)
|
||||||
|
newBody = append(newBody, []byte(`</body></html>`)...)
|
||||||
|
|
||||||
|
i.textPart.Body = newBody
|
||||||
|
i.textPart.Header.SetContentType("text/html", i.textContentTypeMap)
|
||||||
|
|
||||||
|
// Add content id to curPart
|
||||||
|
i.imagePart.Header.Set("content-id", contentID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func patchNewLineWithHTMLBreaks(input []byte) []byte {
|
||||||
|
dst := make([]byte, 0, len(input))
|
||||||
|
index := 0
|
||||||
|
for {
|
||||||
|
slice := input[index:]
|
||||||
|
newLineIndex := bytes.IndexByte(slice, '\n')
|
||||||
|
|
||||||
|
if newLineIndex == -1 {
|
||||||
|
dst = append(dst, input[index:]...)
|
||||||
|
return dst
|
||||||
|
}
|
||||||
|
|
||||||
|
injectIndex := newLineIndex
|
||||||
|
if newLineIndex > 0 && slice[newLineIndex-1] == '\r' {
|
||||||
|
injectIndex--
|
||||||
|
}
|
||||||
|
|
||||||
|
dst = append(dst, slice[0:injectIndex]...)
|
||||||
|
dst = append(dst, '<', 'b', 'r', '/', '>')
|
||||||
|
dst = append(dst, slice[injectIndex:newLineIndex+1]...)
|
||||||
|
|
||||||
|
index += newLineIndex + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -32,6 +32,10 @@ func (h *handler) matchPart(p *Part) bool {
|
|||||||
return h.matchType(p) || h.matchDisp(p)
|
return h.matchType(p) || h.matchDisp(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *handler) matchPartSkipAttachment(p *Part) bool {
|
||||||
|
return !p.isAttachment() && h.matchPart(p)
|
||||||
|
}
|
||||||
|
|
||||||
func (h *handler) matchType(p *Part) bool {
|
func (h *handler) matchType(p *Part) bool {
|
||||||
if h.typeRegExp == nil {
|
if h.typeRegExp == nil {
|
||||||
return false
|
return false
|
||||||
@ -50,7 +54,7 @@ func (h *handler) matchDisp(p *Part) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
disp, _, err := p.Header.ContentDisposition()
|
disp, _, err := p.ContentDisposition()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
disp = ""
|
disp = ""
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import (
|
|||||||
"github.com/PuerkitoBio/goquery"
|
"github.com/PuerkitoBio/goquery"
|
||||||
"github.com/emersion/go-message"
|
"github.com/emersion/go-message"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
"golang.org/x/net/html"
|
"golang.org/x/net/html"
|
||||||
"golang.org/x/net/html/charset"
|
"golang.org/x/net/html/charset"
|
||||||
"golang.org/x/text/encoding"
|
"golang.org/x/text/encoding"
|
||||||
@ -52,6 +53,14 @@ func (p *Part) ContentType() (string, map[string]string, error) {
|
|||||||
return t, params, err
|
return t, params, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Part) ContentDisposition() (string, map[string]string, error) {
|
||||||
|
return pmmime.ParseMediaType(p.Header.Get("Content-Disposition"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Part) HasContentID() bool {
|
||||||
|
return len(p.Header.Get("content-id")) != 0
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Part) Child(n int) (part *Part, err error) {
|
func (p *Part) Child(n int) (part *Part, err error) {
|
||||||
if len(p.children) < n {
|
if len(p.children) < n {
|
||||||
return nil, errors.New("no such part")
|
return nil, errors.New("no such part")
|
||||||
@ -81,6 +90,14 @@ func (p *Part) AddChild(child *Part) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Part) InsertChild(index int, child *Part) {
|
||||||
|
if p.isMultipartMixedOrRelated() {
|
||||||
|
p.children = slices.Insert(p.children, index, child)
|
||||||
|
} else {
|
||||||
|
p.AddChild(child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Part) ConvertToUTF8() error {
|
func (p *Part) ConvertToUTF8() error {
|
||||||
logrus.Trace("Converting part to utf-8")
|
logrus.Trace("Converting part to utf-8")
|
||||||
|
|
||||||
@ -183,6 +200,23 @@ func (p *Part) isMultipartMixed() bool {
|
|||||||
return t == "multipart/mixed"
|
return t == "multipart/mixed"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Part) isMultipartMixedOrRelated() bool {
|
||||||
|
t, _, err := p.ContentType()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return t == "multipart/mixed" || t == "multipart/related"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Part) isAttachment() bool {
|
||||||
|
disp, _, err := p.ContentDisposition()
|
||||||
|
if err != nil {
|
||||||
|
disp = ""
|
||||||
|
}
|
||||||
|
return disp == "attachment"
|
||||||
|
}
|
||||||
|
|
||||||
func getContentHeaders(header message.Header) message.Header {
|
func getContentHeaders(header message.Header) message.Header {
|
||||||
var res message.Header
|
var res message.Header
|
||||||
|
|
||||||
|
|||||||
86
pkg/message/parser/testdata/forwarding_html_attachment.eml
vendored
Normal file
86
pkg/message/parser/testdata/forwarding_html_attachment.eml
vendored
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
Content-Type: multipart/mixed; boundary="------------MQ01Z9UM8OaR9z39TvzDfdIq"
|
||||||
|
Subject: Fwd: Reply to this message, it has various attachments.
|
||||||
|
References: <something@protonmail.ch>
|
||||||
|
To: <[user:user2]@[domain]>
|
||||||
|
From: <[user:user]@[domain]>
|
||||||
|
In-Reply-To: <something@protonmail.ch>
|
||||||
|
X-Forwarded-Message-Id: <something@protonmail.ch>
|
||||||
|
|
||||||
|
This is a multi-part message in MIME format.
|
||||||
|
--------------MQ01Z9UM8OaR9z39TvzDfdIq
|
||||||
|
Content-Type: text/plain; charset=UTF-8; format=flowed
|
||||||
|
Content-Transfer-Encoding: 7bit
|
||||||
|
|
||||||
|
Forwarding a message with various attachments in it!
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-------- Forwarded Message --------
|
||||||
|
Subject: Reply to this message, it has various attachments.
|
||||||
|
Date: Thu, 26 Oct 2023 10:41:55 +0000
|
||||||
|
From: Gjorgji Testing <gorgitesting@protonmail.com>
|
||||||
|
Reply-To: Gjorgji Testing <gorgitesting@protonmail.com>
|
||||||
|
To: Gjorgji Test v3 <gorgitesting3@protonmail.com>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
For real!
|
||||||
|
|
||||||
|
*Gjorgji Testing
|
||||||
|
TesASID <https://www.youtube.com/watch?v=MifXUbrjYr8>
|
||||||
|
*
|
||||||
|
--------------MQ01Z9UM8OaR9z39TvzDfdIq
|
||||||
|
Content-Type: text/html; charset=UTF-8; name="index.html"
|
||||||
|
Content-Disposition: attachment; filename="index.html"
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
|
||||||
|
IDwhRE9DVFlQRSBodG1sPg0KPGh0bWw+DQo8aGVhZD4NCjx0aXRsZT5QYWdlIFRpdGxlPC90
|
||||||
|
aXRsZT4NCjwvaGVhZD4NCjxib2R5Pg0KDQo8aDE+TXkgRmlyc3QgSGVhZGluZzwvaDE+DQo8
|
||||||
|
cD5NeSBmaXJzdCBwYXJhZ3JhcGguPC9wPg0KDQo8L2JvZHk+DQo8L2h0bWw+IA==
|
||||||
|
--------------MQ01Z9UM8OaR9z39TvzDfdIq
|
||||||
|
Content-Type: text/xml; charset=UTF-8; name="testxml.xml"
|
||||||
|
Content-Disposition: attachment; filename="testxml.xml"
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
|
||||||
|
PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCFET0NUWVBFIHN1aXRl
|
||||||
|
IFNZU1RFTSAiaHR0cDovL3Rlc3RuZy5vcmcvdGVzdG5nLTEuMC5kdGQiID4KCjxzdWl0ZSBu
|
||||||
|
YW1lPSJBZmZpbGlhdGUgTmV0d29ya3MiPgoKICAgIDx0ZXN0IG5hbWU9IkFmZmlsaWF0ZSBO
|
||||||
|
ZXR3b3JrcyIgZW5hYmxlZD0idHJ1ZSI+CiAgICAgICAgPGNsYXNzZXM+CiAgICAgICAgICAg
|
||||||
|
IDxjbGFzcyBuYW1lPSJjb20uY2xpY2tvdXQuYXBpdGVzdGluZy5hZmZOZXR3b3Jrcy5Bd2lu
|
||||||
|
VUtUZXN0Ii8+CiAgICAgICAgPC9jbGFzc2VzPgogICAgPC90ZXN0PgoKPC9zdWl0ZT4=
|
||||||
|
--------------MQ01Z9UM8OaR9z39TvzDfdIq
|
||||||
|
Content-Type: application/pdf; name="test.pdf"
|
||||||
|
Content-Disposition: attachment; filename="test.pdf"
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
|
||||||
|
JVBERi0xLjUKJeLjz9MKNyAwIG9iago8PAovVHlwZSAvRm9udERlc2NyaXB0b3IKL0ZvbnRO
|
||||||
|
MjM0NAolJUVPRgo=
|
||||||
|
--------------MQ01Z9UM8OaR9z39TvzDfdIq
|
||||||
|
Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;
|
||||||
|
name="test.xlsx"
|
||||||
|
Content-Disposition: attachment; filename="test.xlsx"
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
|
||||||
|
UEsDBBQABgAIAAAAIQBi7p1oXgEAAJAEAAATAAgCW0NvbnRlbnRfVHlwZXNdLnhtbCCiBAIo
|
||||||
|
UQIAABEAAAAAAAAAAAAAAAAARBcAAGRvY1Byb3BzL2NvcmUueG1sUEsBAi0AFAAGAAgAAAAh
|
||||||
|
AGFJCRCJAQAAEQMAABAAAAAAAAAAAAAAAAAAvBkAAGRvY1Byb3BzL2FwcC54bWxQSwUGAAAA
|
||||||
|
AAoACgCAAgAAexwAAAAA
|
||||||
|
--------------MQ01Z9UM8OaR9z39TvzDfdIq
|
||||||
|
Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document;
|
||||||
|
name="test.docx"
|
||||||
|
Content-Disposition: attachment; filename="test.docx"
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
|
||||||
|
UEsDBBQABgAIAAAAIQDfpNJsWgEAACAFAAATAAgCW0NvbnRlbnRfVHlwZXNdLnhtbCCiBAIo
|
||||||
|
cHAueG1sUEsBAi0AFAAGAAgAAAAhABA0tG9uAQAA4QIAABEAAAAAAAAAAAAAAAAA2xsAAGRv
|
||||||
|
Y1Byb3BzL2NvcmUueG1sUEsBAi0AFAAGAAgAAAAhAJ/mlBIqCwAAU3AAAA8AAAAAAAAAAAAA
|
||||||
|
AAAAgB4AAHdvcmQvc3R5bGVzLnhtbFBLBQYAAAAACwALAMECAADXKQAAAAA=
|
||||||
|
--------------MQ01Z9UM8OaR9z39TvzDfdIq
|
||||||
|
Content-Type: text/plain; charset=UTF-8; name="text file.txt"
|
||||||
|
Content-Disposition: attachment; filename="text file.txt"
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
|
||||||
|
dGV4dCBmaWxl
|
||||||
|
|
||||||
|
--------------MQ01Z9UM8OaR9z39TvzDfdIq--
|
||||||
@ -33,9 +33,12 @@ func newWalker(root *Part) *Walker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *Walker) Walk() (err error) {
|
func (w *Walker) Walk() error {
|
||||||
return w.walkOverPart(w.root)
|
return w.walkOverPart(w.root)
|
||||||
}
|
}
|
||||||
|
func (w *Walker) WalkSkipAttachment() error {
|
||||||
|
return w.walkOverPartSkipAttachment(w.root)
|
||||||
|
}
|
||||||
|
|
||||||
func (w *Walker) walkOverPart(p *Part) error {
|
func (w *Walker) walkOverPart(p *Part) error {
|
||||||
if err := w.getHandlerFunc(p)(p); err != nil {
|
if err := w.getHandlerFunc(p)(p); err != nil {
|
||||||
@ -51,6 +54,20 @@ func (w *Walker) walkOverPart(p *Part) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *Walker) walkOverPartSkipAttachment(p *Part) error {
|
||||||
|
if err := w.getHandlerFuncSkipAttachment(p)(p); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, child := range p.children {
|
||||||
|
if err := w.walkOverPartSkipAttachment(child); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// RegisterDefaultHandler registers a handler that will be called on every part
|
// RegisterDefaultHandler registers a handler that will be called on every part
|
||||||
// that doesn't match a registered content type/disposition handler.
|
// that doesn't match a registered content type/disposition handler.
|
||||||
func (w *Walker) RegisterDefaultHandler(fn HandlerFunc) *Walker {
|
func (w *Walker) RegisterDefaultHandler(fn HandlerFunc) *Walker {
|
||||||
@ -91,3 +108,13 @@ func (w *Walker) getHandlerFunc(p *Part) HandlerFunc {
|
|||||||
|
|
||||||
return w.defaultHandler
|
return w.defaultHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *Walker) getHandlerFuncSkipAttachment(p *Part) HandlerFunc {
|
||||||
|
for _, handler := range w.handlers {
|
||||||
|
if handler.matchPartSkipAttachment(p) {
|
||||||
|
return handler.fn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return w.defaultHandler
|
||||||
|
}
|
||||||
|
|||||||
@ -60,6 +60,27 @@ func TestWalkerTypeHandler(t *testing.T) {
|
|||||||
}, html)
|
}, html)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWalkerTypeHandler_excludingAttachment(t *testing.T) {
|
||||||
|
p := newTestParser(t, "forwarding_html_attachment.eml")
|
||||||
|
|
||||||
|
html := [][]byte{}
|
||||||
|
plain := [][]byte{}
|
||||||
|
|
||||||
|
walker := p.NewWalker().
|
||||||
|
RegisterContentTypeHandler("text/html", func(p *Part) (err error) {
|
||||||
|
html = append(html, p.Body)
|
||||||
|
return
|
||||||
|
}).
|
||||||
|
RegisterContentTypeHandler("text/plain", func(p *Part) (err error) {
|
||||||
|
plain = append(plain, p.Body)
|
||||||
|
return
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NoError(t, walker.WalkSkipAttachment())
|
||||||
|
assert.Equal(t, 1, len(plain))
|
||||||
|
assert.Equal(t, 0, len(html))
|
||||||
|
}
|
||||||
|
|
||||||
func TestWalkerDispositionHandler(t *testing.T) {
|
func TestWalkerDispositionHandler(t *testing.T) {
|
||||||
p := newTestParser(t, "text_html_octet_attachment.eml")
|
p := newTestParser(t, "text_html_octet_attachment.eml")
|
||||||
|
|
||||||
|
|||||||
@ -19,6 +19,7 @@ package message
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"fmt"
|
||||||
"image/png"
|
"image/png"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
@ -312,11 +313,13 @@ func TestParseTextPlainWithImageInline(t *testing.T) {
|
|||||||
m, err := Parse(f)
|
m, err := Parse(f)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.NotEmpty(t, m.Attachments[0].ContentID)
|
||||||
|
|
||||||
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
|
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
|
||||||
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
|
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
|
||||||
|
|
||||||
assert.Equal(t, "body", string(m.RichBody))
|
|
||||||
assert.Equal(t, "body", string(m.PlainBody))
|
assert.Equal(t, "body", string(m.PlainBody))
|
||||||
|
assert.Equal(t, fmt.Sprintf(`<html><body><p>body</p><img src="cid:%v"/></body></html>`, m.Attachments[0].ContentID), string(m.RichBody))
|
||||||
|
|
||||||
// The inline image is an 8x8 mic-dropping gopher.
|
// The inline image is an 8x8 mic-dropping gopher.
|
||||||
require.Len(t, m.Attachments, 1)
|
require.Len(t, m.Attachments, 1)
|
||||||
@ -326,6 +329,69 @@ func TestParseTextPlainWithImageInline(t *testing.T) {
|
|||||||
assert.Equal(t, 8, img.Height)
|
assert.Equal(t, 8, img.Height)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseTextPlainWithImageInlineWithMoreTextParts(t *testing.T) {
|
||||||
|
// Inline image test with text - image - text, ensure all parts are convert to html
|
||||||
|
f := getFileReader("text_plain_image_inline2.eml")
|
||||||
|
|
||||||
|
m, err := Parse(f)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.NotEmpty(t, m.Attachments[0].ContentID)
|
||||||
|
assert.Equal(t, "bodybody2", string(m.PlainBody))
|
||||||
|
assert.Equal(t, fmt.Sprintf("<html><body><p>body</p><img src=\"cid:%v\"/></body></html><html><body><p>body2<br/>\n</p></body></html>", m.Attachments[0].ContentID), string(m.RichBody))
|
||||||
|
|
||||||
|
// The inline image is an 8x8 mic-dropping gopher.
|
||||||
|
require.Len(t, m.Attachments, 1)
|
||||||
|
img, err := png.DecodeConfig(bytes.NewReader(m.Attachments[0].Data))
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, 8, img.Width)
|
||||||
|
assert.Equal(t, 8, img.Height)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseTextPlainWithImageInlineAfterOtherAttachment(t *testing.T) {
|
||||||
|
// Inline image test with text - image - text, ensure all parts are convert to html
|
||||||
|
f := getFileReader("text_plain_image_inline2.eml")
|
||||||
|
|
||||||
|
m, err := Parse(f)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.NotEmpty(t, m.Attachments[0].ContentID)
|
||||||
|
assert.Equal(t, "bodybody2", string(m.PlainBody))
|
||||||
|
assert.Equal(t, fmt.Sprintf("<html><body><p>body</p><img src=\"cid:%v\"/></body></html><html><body><p>body2<br/>\n</p></body></html>", m.Attachments[0].ContentID), string(m.RichBody))
|
||||||
|
|
||||||
|
// The inline image is an 8x8 mic-dropping gopher.
|
||||||
|
require.Len(t, m.Attachments, 1)
|
||||||
|
img, err := png.DecodeConfig(bytes.NewReader(m.Attachments[0].Data))
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, 8, img.Width)
|
||||||
|
assert.Equal(t, 8, img.Height)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseTextPlainWithImageBetweenAttachments(t *testing.T) {
|
||||||
|
// Inline image test with text - pdf - image - text. A new part must be created to be injected.
|
||||||
|
f := getFileReader("text_plain_image_inline_between_attachment.eml")
|
||||||
|
|
||||||
|
m, err := Parse(f)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Empty(t, m.Attachments[0].ContentID)
|
||||||
|
require.NotEmpty(t, m.Attachments[1].ContentID)
|
||||||
|
assert.Equal(t, "bodybody2", string(m.PlainBody))
|
||||||
|
assert.Equal(t, fmt.Sprintf("<html><body><p>body</p></body></html><html><body><img src=\"cid:%v\"/></body></html><html><body><p>body2<br/>\n</p></body></html>", m.Attachments[1].ContentID), string(m.RichBody))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseTextPlainWithImageFirst(t *testing.T) {
|
||||||
|
// Inline image test with text - pdf - image - text. A new part must be created to be injected.
|
||||||
|
f := getFileReader("text_plain_image_inline_attachment_first.eml")
|
||||||
|
|
||||||
|
m, err := Parse(f)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.NotEmpty(t, m.Attachments[0].ContentID)
|
||||||
|
assert.Equal(t, "body", string(m.PlainBody))
|
||||||
|
assert.Equal(t, fmt.Sprintf("<html><body><img src=\"cid:%v\"/></body></html><html><body><p>body</p></body></html>", m.Attachments[0].ContentID), string(m.RichBody))
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseTextPlainWithDuplicateCharset(t *testing.T) {
|
func TestParseTextPlainWithDuplicateCharset(t *testing.T) {
|
||||||
f := getFileReader("text_plain_duplicate_charset.eml")
|
f := getFileReader("text_plain_duplicate_charset.eml")
|
||||||
|
|
||||||
@ -428,11 +494,12 @@ func TestParseTextHTMLWithImageInline(t *testing.T) {
|
|||||||
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
|
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
|
||||||
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
|
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
|
||||||
|
|
||||||
assert.Equal(t, "<html><head></head><body>This is body of <b>HTML mail</b> with attachment</body></html>", string(m.RichBody))
|
require.Len(t, m.Attachments, 1)
|
||||||
|
|
||||||
|
assert.Equal(t, fmt.Sprintf(`<html><head></head><body>This is body of <b>HTML mail</b> with attachment</body></html><html><body><img src="cid:%v"/></body></html>`, m.Attachments[0].ContentID), string(m.RichBody))
|
||||||
assert.Equal(t, "This is body of *HTML mail* with attachment", string(m.PlainBody))
|
assert.Equal(t, "This is body of *HTML mail* with attachment", string(m.PlainBody))
|
||||||
|
|
||||||
// The inline image is an 8x8 mic-dropping gopher.
|
// The inline image is an 8x8 mic-dropping gopher.
|
||||||
require.Len(t, m.Attachments, 1)
|
|
||||||
img, err := png.DecodeConfig(bytes.NewReader(m.Attachments[0].Data))
|
img, err := png.DecodeConfig(bytes.NewReader(m.Attachments[0].Data))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 8, img.Width)
|
assert.Equal(t, 8, img.Width)
|
||||||
@ -539,6 +606,18 @@ func TestParseMultipartAlternativeLatin1(t *testing.T) {
|
|||||||
assert.Equal(t, "*aoeuaoeu*\n\n", string(m.PlainBody))
|
assert.Equal(t, "*aoeuaoeu*\n\n", string(m.PlainBody))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseMultipartAttachmentEncodedButUnquoted(t *testing.T) {
|
||||||
|
f := getFileReader("multipart_attachment_encoded_no_quote.eml")
|
||||||
|
|
||||||
|
p, err := parser.New(f)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
m, err := ParseWithParser(p, false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, `"Bridge Test" <bridgetest@pm.test>`, m.Sender.String())
|
||||||
|
assert.Equal(t, `"Internal Bridge" <bridgetest@protonmail.com>`, m.ToList[0].String())
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseWithTrailingEndOfMailIndicator(t *testing.T) {
|
func TestParseWithTrailingEndOfMailIndicator(t *testing.T) {
|
||||||
f := getFileReader("text_html_trailing_end_of_mail.eml")
|
f := getFileReader("text_html_trailing_end_of_mail.eml")
|
||||||
|
|
||||||
@ -707,6 +786,23 @@ func TestParseTextPlainWithDocxAttachmentCyrillic(t *testing.T) {
|
|||||||
assert.Equal(t, "АБВГДЃЕЖЗЅИЈКЛЉМНЊОПРСТЌУФХЧЏЗШ.docx", m.Attachments[0].Name)
|
assert.Equal(t, "АБВГДЃЕЖЗЅИЈКЛЉМНЊОПРСТЌУФХЧЏЗШ.docx", m.Attachments[0].Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPatchNewLineWithHtmlBreaks(t *testing.T) {
|
||||||
|
{
|
||||||
|
input := []byte("\nfoo\nbar\n\n\nzz\nddd")
|
||||||
|
expected := []byte("<br/>\nfoo<br/>\nbar<br/>\n<br/>\n<br/>\nzz<br/>\nddd")
|
||||||
|
|
||||||
|
result := patchNewLineWithHTMLBreaks(input)
|
||||||
|
require.Equal(t, expected, result)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
input := []byte("\r\nfoo\r\nbar\r\n\r\n\r\nzz\r\nddd")
|
||||||
|
expected := []byte("<br/>\r\nfoo<br/>\r\nbar<br/>\r\n<br/>\r\n<br/>\r\nzz<br/>\r\nddd")
|
||||||
|
|
||||||
|
result := patchNewLineWithHTMLBreaks(input)
|
||||||
|
require.Equal(t, expected, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func getFileReader(filename string) io.Reader {
|
func getFileReader(filename string) io.Reader {
|
||||||
f, err := os.Open(filepath.Join("testdata", filename))
|
f, err := os.Open(filepath.Join("testdata", filename))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
27
pkg/message/testdata/multipart_attachment_encoded_no_quote.eml
vendored
Normal file
27
pkg/message/testdata/multipart_attachment_encoded_no_quote.eml
vendored
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
From: Bridge Test <bridgetest@pm.test>
|
||||||
|
Date: 01 Jan 1980 00:00:00 +0000
|
||||||
|
To: Internal Bridge <bridgetest@protonmail.com>
|
||||||
|
Subject: Message with attachment name
|
||||||
|
Content-type: multipart/mixed; boundary="boundary"
|
||||||
|
Received: by 2002:0:0:0:0:0:0:0 with SMTP id 0123456789abcdef; Wed, 30 Dec 2020 01:23:45 0000
|
||||||
|
|
||||||
|
This is a multi-part message in MIME format.
|
||||||
|
|
||||||
|
--boundary
|
||||||
|
Content-Type: text/plain
|
||||||
|
|
||||||
|
Hello
|
||||||
|
|
||||||
|
--boundary
|
||||||
|
Content-Type: text/html; charset=utf-8
|
||||||
|
Content-Transfer-Encoding: 7bit
|
||||||
|
|
||||||
|
<h1> HELLO </h1>
|
||||||
|
|
||||||
|
--boundary
|
||||||
|
Content-Type: application/pdf; name==?US-ASCII?Q?filename?=
|
||||||
|
Content-Disposition: attachment; filename==?US-ASCII?Q?filename?=
|
||||||
|
|
||||||
|
somebytes
|
||||||
|
|
||||||
|
--boundary--
|
||||||
39
pkg/message/testdata/text_plain_image_inline2.eml
vendored
Normal file
39
pkg/message/testdata/text_plain_image_inline2.eml
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
From: Sender <sender@pm.me>
|
||||||
|
To: Receiver <receiver@pm.me>
|
||||||
|
Content-Type: multipart/related; boundary=longrandomstring
|
||||||
|
|
||||||
|
--longrandomstring
|
||||||
|
|
||||||
|
body
|
||||||
|
--longrandomstring
|
||||||
|
Content-Type: image/png
|
||||||
|
Content-Disposition: inline
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
|
||||||
|
iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAABGdBTUEAALGPC/xhBQAAACBjSFJ
|
||||||
|
NAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAhGVYSWZNTQAqAAAACAAFAR
|
||||||
|
IAAwAAAAEAAQAAARoABQAAAAEAAABKARsABQAAAAEAAABSASgAAwAAAAEAAgAAh2kABAAAAAEAA
|
||||||
|
ABaAAAAAAAAASwAAAABAAABLAAAAAEAA6ABAAMAAAABAAEAAKACAAQAAAABAAAACKADAAQAAAAB
|
||||||
|
AAAACAAAAAAAXWZ6AAAACXBIWXMAAC4jAAAuIwF4pT92AAACZmlUWHRYTUw6Y29tLmFkb2JlLnh
|
||||||
|
tcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIE
|
||||||
|
NvcmUgNS40LjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5O
|
||||||
|
TkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91
|
||||||
|
dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4
|
||||||
|
wLyIKICAgICAgICAgICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC
|
||||||
|
8iPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgI
|
||||||
|
CAgICA8dGlmZjpSZXNvbHV0aW9uVW5pdD4yPC90aWZmOlJlc29sdXRpb25Vbml0PgogICAgICAg
|
||||||
|
ICA8ZXhpZjpDb2xvclNwYWNlPjE8L2V4aWY6Q29sb3JTcGFjZT4KICAgICAgICAgPGV4aWY6UGl
|
||||||
|
4ZWxYRGltZW5zaW9uPjE2PC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6UG
|
||||||
|
l4ZWxZRGltZW5zaW9uPjE2PC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgPC9yZGY6RGVzY
|
||||||
|
3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+CgZBD4sAAAEISURBVBgZY2CAAO5F
|
||||||
|
x07Zz96xZ0Pn4lXqIKGGhgYmsFTHvAWdW6/dvnb89Yf/B5+9/r/y9IXzbVPahCH6/jMysfAJygo
|
||||||
|
JC2r++/T619Mb139J8HIb8Gs5hYMUzJ+/gJ1Jmo9H6c+L5wz3bt5iEeLmYOHn42fQ4vyacqGNQS
|
||||||
|
0xMfEHc7Cvl6CYho4rh5jUPyYefqafLKyMbH9+/d28/dFfdWtfDaZvTy7Zvv72nYGZkeEvw98/f
|
||||||
|
5j//2P4yCvxq/nU7zVs//8yM2gzMMitOnnu5cUff/8ff/v5/5Xf///vuHBhJcSRDAws9aEMr38c
|
||||||
|
W7XjNgvzexZ2rn9vbjx/IXl/M9iLM2fOZAUAKCZv7dU+UgAAAAAASUVORK5CYII=
|
||||||
|
|
||||||
|
--longrandomstring
|
||||||
|
|
||||||
|
body2
|
||||||
|
|
||||||
|
--longrandomstring--
|
||||||
35
pkg/message/testdata/text_plain_image_inline_attachment_first.eml
vendored
Normal file
35
pkg/message/testdata/text_plain_image_inline_attachment_first.eml
vendored
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
From: Sender <sender@pm.me>
|
||||||
|
To: Receiver <receiver@pm.me>
|
||||||
|
Content-Type: multipart/related; boundary=longrandomstring
|
||||||
|
|
||||||
|
--longrandomstring
|
||||||
|
Content-Type: image/png
|
||||||
|
Content-Disposition: inline
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
|
||||||
|
iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAABGdBTUEAALGPC/xhBQAAACBjSFJ
|
||||||
|
NAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAhGVYSWZNTQAqAAAACAAFAR
|
||||||
|
IAAwAAAAEAAQAAARoABQAAAAEAAABKARsABQAAAAEAAABSASgAAwAAAAEAAgAAh2kABAAAAAEAA
|
||||||
|
ABaAAAAAAAAASwAAAABAAABLAAAAAEAA6ABAAMAAAABAAEAAKACAAQAAAABAAAACKADAAQAAAAB
|
||||||
|
AAAACAAAAAAAXWZ6AAAACXBIWXMAAC4jAAAuIwF4pT92AAACZmlUWHRYTUw6Y29tLmFkb2JlLnh
|
||||||
|
tcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIE
|
||||||
|
NvcmUgNS40LjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5O
|
||||||
|
TkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91
|
||||||
|
dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4
|
||||||
|
wLyIKICAgICAgICAgICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC
|
||||||
|
8iPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgI
|
||||||
|
CAgICA8dGlmZjpSZXNvbHV0aW9uVW5pdD4yPC90aWZmOlJlc29sdXRpb25Vbml0PgogICAgICAg
|
||||||
|
ICA8ZXhpZjpDb2xvclNwYWNlPjE8L2V4aWY6Q29sb3JTcGFjZT4KICAgICAgICAgPGV4aWY6UGl
|
||||||
|
4ZWxYRGltZW5zaW9uPjE2PC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6UG
|
||||||
|
l4ZWxZRGltZW5zaW9uPjE2PC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgPC9yZGY6RGVzY
|
||||||
|
3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+CgZBD4sAAAEISURBVBgZY2CAAO5F
|
||||||
|
x07Zz96xZ0Pn4lXqIKGGhgYmsFTHvAWdW6/dvnb89Yf/B5+9/r/y9IXzbVPahCH6/jMysfAJygo
|
||||||
|
JC2r++/T619Mb139J8HIb8Gs5hYMUzJ+/gJ1Jmo9H6c+L5wz3bt5iEeLmYOHn42fQ4vyacqGNQS
|
||||||
|
0xMfEHc7Cvl6CYho4rh5jUPyYefqafLKyMbH9+/d28/dFfdWtfDaZvTy7Zvv72nYGZkeEvw98/f
|
||||||
|
5j//2P4yCvxq/nU7zVs//8yM2gzMMitOnnu5cUff/8ff/v5/5Xf///vuHBhJcSRDAws9aEMr38c
|
||||||
|
W7XjNgvzexZ2rn9vbjx/IXl/M9iLM2fOZAUAKCZv7dU+UgAAAAAASUVORK5CYII=
|
||||||
|
|
||||||
|
--longrandomstring
|
||||||
|
|
||||||
|
body
|
||||||
|
--longrandomstring--
|
||||||
46
pkg/message/testdata/text_plain_image_inline_between_attachment.eml
vendored
Normal file
46
pkg/message/testdata/text_plain_image_inline_between_attachment.eml
vendored
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
From: Sender <sender@pm.me>
|
||||||
|
To: Receiver <receiver@pm.me>
|
||||||
|
Content-Type: multipart/related; boundary=longrandomstring
|
||||||
|
|
||||||
|
--longrandomstring
|
||||||
|
|
||||||
|
body
|
||||||
|
--longrandomstring
|
||||||
|
Content-Type: application/pdf
|
||||||
|
Content-Disposition: inline
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
|
||||||
|
aGVsbG8gd29ybGQgcGRm
|
||||||
|
|
||||||
|
--longrandomstring
|
||||||
|
Content-Type: image/png
|
||||||
|
Content-Disposition: inline
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
|
||||||
|
iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAABGdBTUEAALGPC/xhBQAAACBjSFJ
|
||||||
|
NAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAhGVYSWZNTQAqAAAACAAFAR
|
||||||
|
IAAwAAAAEAAQAAARoABQAAAAEAAABKARsABQAAAAEAAABSASgAAwAAAAEAAgAAh2kABAAAAAEAA
|
||||||
|
ABaAAAAAAAAASwAAAABAAABLAAAAAEAA6ABAAMAAAABAAEAAKACAAQAAAABAAAACKADAAQAAAAB
|
||||||
|
AAAACAAAAAAAXWZ6AAAACXBIWXMAAC4jAAAuIwF4pT92AAACZmlUWHRYTUw6Y29tLmFkb2JlLnh
|
||||||
|
tcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIE
|
||||||
|
NvcmUgNS40LjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5O
|
||||||
|
TkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91
|
||||||
|
dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4
|
||||||
|
wLyIKICAgICAgICAgICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC
|
||||||
|
8iPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgI
|
||||||
|
CAgICA8dGlmZjpSZXNvbHV0aW9uVW5pdD4yPC90aWZmOlJlc29sdXRpb25Vbml0PgogICAgICAg
|
||||||
|
ICA8ZXhpZjpDb2xvclNwYWNlPjE8L2V4aWY6Q29sb3JTcGFjZT4KICAgICAgICAgPGV4aWY6UGl
|
||||||
|
4ZWxYRGltZW5zaW9uPjE2PC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6UG
|
||||||
|
l4ZWxZRGltZW5zaW9uPjE2PC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgPC9yZGY6RGVzY
|
||||||
|
3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+CgZBD4sAAAEISURBVBgZY2CAAO5F
|
||||||
|
x07Zz96xZ0Pn4lXqIKGGhgYmsFTHvAWdW6/dvnb89Yf/B5+9/r/y9IXzbVPahCH6/jMysfAJygo
|
||||||
|
JC2r++/T619Mb139J8HIb8Gs5hYMUzJ+/gJ1Jmo9H6c+L5wz3bt5iEeLmYOHn42fQ4vyacqGNQS
|
||||||
|
0xMfEHc7Cvl6CYho4rh5jUPyYefqafLKyMbH9+/d28/dFfdWtfDaZvTy7Zvv72nYGZkeEvw98/f
|
||||||
|
5j//2P4yCvxq/nU7zVs//8yM2gzMMitOnnu5cUff/8ff/v5/5Xf///vuHBhJcSRDAws9aEMr38c
|
||||||
|
W7XjNgvzexZ2rn9vbjx/IXl/M9iLM2fOZAUAKCZv7dU+UgAAAAAASUVORK5CYII=
|
||||||
|
|
||||||
|
--longrandomstring
|
||||||
|
|
||||||
|
body2
|
||||||
|
|
||||||
|
--longrandomstring--
|
||||||
@ -256,6 +256,10 @@ func DecodeCharset(original []byte, contentType string) ([]byte, error) {
|
|||||||
|
|
||||||
// ParseMediaType from MIME doesn't support RFC2231 for non asci / utf8 encodings so we have to pre-parse it.
|
// ParseMediaType from MIME doesn't support RFC2231 for non asci / utf8 encodings so we have to pre-parse it.
|
||||||
func ParseMediaType(v string) (mediatype string, params map[string]string, err error) {
|
func ParseMediaType(v string) (mediatype string, params map[string]string, err error) {
|
||||||
v, _ = changeEncodingAndKeepLastParamDefinition(v)
|
decoded, err := DecodeHeader(v)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
v, _ = changeEncodingAndKeepLastParamDefinition(decoded)
|
||||||
return mime.ParseMediaType(v)
|
return mime.ParseMediaType(v)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -155,34 +155,29 @@ func (s *scenario) theUserSetSMTPModeToSSL() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type testBugReport struct {
|
type testBugReport struct {
|
||||||
OSType string `json:"OS"`
|
request bridge.ReportBugReq
|
||||||
OSVersion string `json:"OSVersion"`
|
bridge *bridge.Bridge
|
||||||
Title string `json:"Title"`
|
|
||||||
Description string `json:"Description"`
|
|
||||||
Username string `json:"Username"`
|
|
||||||
Email string `json:"Email"`
|
|
||||||
Client string `json:"Client"`
|
|
||||||
Attachment bool `json:"Attachment"`
|
|
||||||
|
|
||||||
bridge *bridge.Bridge
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTestBugReport(bridge *bridge.Bridge) *testBugReport {
|
func newTestBugReport(br *bridge.Bridge) *testBugReport {
|
||||||
return &testBugReport{
|
request := bridge.ReportBugReq{
|
||||||
OSType: "osType",
|
OSType: "osType",
|
||||||
OSVersion: "osVersion",
|
OSVersion: "osVersion",
|
||||||
Title: "title",
|
Title: "title",
|
||||||
Description: "description",
|
Description: "description",
|
||||||
Username: "username",
|
Username: "username",
|
||||||
Email: "email",
|
Email: "email",
|
||||||
Client: "client",
|
EmailClient: "client",
|
||||||
Attachment: false,
|
IncludeLogs: false,
|
||||||
bridge: bridge,
|
}
|
||||||
|
return &testBugReport{
|
||||||
|
request: request,
|
||||||
|
bridge: br,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *testBugReport) report() error {
|
func (r *testBugReport) report() error {
|
||||||
return r.bridge.ReportBug(context.Background(), r.OSType, r.OSVersion, r.Title, r.Description, r.Username, r.Email, r.Client, r.Attachment)
|
return r.bridge.ReportBug(context.Background(), &r.request)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *scenario) theUserReportsABug() error {
|
func (s *scenario) theUserReportsABug() error {
|
||||||
@ -194,25 +189,25 @@ func (s *scenario) theUserReportsABugWithSingleHeaderChange(key, value string) e
|
|||||||
|
|
||||||
switch key {
|
switch key {
|
||||||
case "osType":
|
case "osType":
|
||||||
bugReport.OSType = value
|
bugReport.request.OSType = value
|
||||||
case "osVersion":
|
case "osVersion":
|
||||||
bugReport.OSVersion = value
|
bugReport.request.OSVersion = value
|
||||||
case "Title":
|
case "Title":
|
||||||
bugReport.Title = value
|
bugReport.request.Title = value
|
||||||
case "Description":
|
case "Description":
|
||||||
bugReport.Description = value
|
bugReport.request.Description = value
|
||||||
case "Username":
|
case "Username":
|
||||||
bugReport.Username = value
|
bugReport.request.Username = value
|
||||||
case "Email":
|
case "Email":
|
||||||
bugReport.Email = value
|
bugReport.request.Email = value
|
||||||
case "Client":
|
case "Client":
|
||||||
bugReport.Client = value
|
bugReport.request.EmailClient = value
|
||||||
case "Attachment":
|
case "Attachment":
|
||||||
att, err := strconv.ParseBool(value)
|
att, err := strconv.ParseBool(value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to parse bug report attachment preferences: %w", err)
|
return fmt.Errorf("failed to parse bug report attachment preferences: %w", err)
|
||||||
}
|
}
|
||||||
bugReport.Attachment = att
|
bugReport.request.IncludeLogs = att
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("Wrong header (\"%s\") is being checked", key)
|
return fmt.Errorf("Wrong header (\"%s\") is being checked", key)
|
||||||
}
|
}
|
||||||
@ -222,10 +217,9 @@ func (s *scenario) theUserReportsABugWithSingleHeaderChange(key, value string) e
|
|||||||
|
|
||||||
func (s *scenario) theUserReportsABugWithDetails(value *godog.DocString) error {
|
func (s *scenario) theUserReportsABugWithDetails(value *godog.DocString) error {
|
||||||
bugReport := newTestBugReport(s.t.bridge)
|
bugReport := newTestBugReport(s.t.bridge)
|
||||||
if err := json.Unmarshal([]byte(value.Content), &bugReport); err != nil {
|
if err := json.Unmarshal([]byte(value.Content), &bugReport.request); err != nil {
|
||||||
return fmt.Errorf("cannot parse bug report details: %w", err)
|
return fmt.Errorf("cannot parse bug report details: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return bugReport.report()
|
return bugReport.report()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -299,17 +293,16 @@ func (s *scenario) bridgeSendsSyncStartedAndFinishedEventsForUser(username strin
|
|||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
for {
|
||||||
|
finishEvent, ok := awaitType(s.t.events, events.SyncFinished{}, 30*time.Second)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("expected sync finished event, got none")
|
||||||
|
}
|
||||||
|
|
||||||
finishEvent, ok := awaitType(s.t.events, events.SyncFinished{}, 30*time.Second)
|
if wantUserID := s.t.getUserByName(username).getUserID(); finishEvent.UserID == wantUserID {
|
||||||
if !ok {
|
return nil
|
||||||
return errors.New("expected sync finished event, got none")
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if wantUserID := s.t.getUserByName(username).getUserID(); finishEvent.UserID != wantUserID {
|
|
||||||
return fmt.Errorf("expected sync finished event for user %s, got %s", wantUserID, finishEvent.UserID)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *scenario) bridgeSendsAnUpdateNotAvailableEvent() error {
|
func (s *scenario) bridgeSendsAnUpdateNotAvailableEvent() error {
|
||||||
|
|||||||
@ -362,8 +362,8 @@ func createContact(ctx context.Context, c *proton.Client, contact, name string,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if res[0].Response.APIError.Code != proton.SuccessCode {
|
if res[0].Response.Code != proton.SuccessCode {
|
||||||
return errors.New("APIError " + res[0].Response.APIError.Message + " while creating contact")
|
return errors.New("APIError " + res[0].Response.Message + " while creating contact")
|
||||||
}
|
}
|
||||||
|
|
||||||
if settings != nil {
|
if settings != nil {
|
||||||
|
|||||||
@ -67,57 +67,46 @@ Feature: IMAP import messages
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
Scenario: Import message with attachment name encoded by RFC 2047 without quoting
|
Scenario Outline: Import multipart message with attachment <message>
|
||||||
When IMAP client "1" appends the following message to "INBOX":
|
When IMAP client "1" appends <message> to "INBOX"
|
||||||
"""
|
|
||||||
From: Bridge Test <bridgetest@pm.test>
|
|
||||||
Date: 01 Jan 1980 00:00:00 +0000
|
|
||||||
To: Internal Bridge <bridgetest@protonmail.com>
|
|
||||||
Subject: Message with attachment name encoded by RFC 2047 without quoting
|
|
||||||
Content-type: multipart/mixed; boundary="boundary"
|
|
||||||
Received: by 2002:0:0:0:0:0:0:0 with SMTP id 0123456789abcdef; Wed, 30 Dec 2020 01:23:45 0000
|
|
||||||
|
|
||||||
--boundary
|
|
||||||
Content-Type: text/plain
|
|
||||||
|
|
||||||
Hello
|
|
||||||
|
|
||||||
--boundary
|
|
||||||
Content-Type: application/pdf; name==?US-ASCII?Q?filename?=
|
|
||||||
Content-Disposition: attachment; filename==?US-ASCII?Q?filename?=
|
|
||||||
|
|
||||||
somebytes
|
|
||||||
|
|
||||||
--boundary--
|
|
||||||
|
|
||||||
"""
|
|
||||||
Then it succeeds
|
Then it succeeds
|
||||||
# And IMAP client "1" eventually sees the following message in "INBOX" with this structure:
|
And IMAP client "1" eventually sees the following message in "INBOX" with this structure:
|
||||||
# """
|
"""
|
||||||
# {
|
{
|
||||||
# "from": "Bridge Test <bridgetest@pm.test>",
|
"from": "Bridge Test <bridgetest@pm.test>",
|
||||||
# "date": "01 Jan 80 00:00 +0000",
|
"date": "01 Jan 80 00:00 +0000",
|
||||||
# "to": "Internal Bridge <bridgetest@protonmail.com>",
|
"to": "Internal Bridge <bridgetest@protonmail.com>",
|
||||||
# "subject": "Message with attachment name encoded by RFC 2047 without quoting",
|
"subject": "Message with attachment name",
|
||||||
# "body-contains": "Hello",
|
"body-contains": "Hello",
|
||||||
# "content": {
|
"content": {
|
||||||
# "content-type": "multipart/mixed; boundary=\"boundary\"",
|
"content-type": "multipart/mixed",
|
||||||
# "sections":[
|
"sections":[
|
||||||
# {
|
{
|
||||||
# "content-type": "text/plain",
|
"content-type": "text/plain",
|
||||||
# "body-is": "Hello"
|
"body-is": "Hello"
|
||||||
# },
|
},
|
||||||
# {
|
{
|
||||||
# "content-type": "application/pdf",
|
"content-type": "text/html",
|
||||||
# "content-type-name": "=?US-ASCII?Q?filename?=",
|
"content-type-charset": "utf-8",
|
||||||
# "content-disposition": "attachment",
|
"transfer-encoding": "7bit",
|
||||||
# "content-disposition-filename": "=?US-ASCII?Q?filename?=",
|
"body-contains": "HELLO"
|
||||||
# "body-is": "somebytes"
|
},
|
||||||
# }
|
{
|
||||||
# ]
|
"content-type": "application/pdf",
|
||||||
# }
|
"content-type-name": "filename",
|
||||||
# }
|
"content-disposition": "attachment",
|
||||||
# """
|
"content-disposition-filename": "filename",
|
||||||
|
"body-is": "somebytes"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
Examples:
|
||||||
|
| message |
|
||||||
|
| "multipart/mixed_with_attachment_encoded.eml" |
|
||||||
|
| "multipart/mixed_with_attachment_encoded_no_quote.eml" |
|
||||||
|
| "multipart/mixed_with_attachment_no_quote.eml" |
|
||||||
|
|
||||||
|
|
||||||
# The message is imported as UTF-8 and the content type is determined at build time.
|
# The message is imported as UTF-8 and the content type is determined at build time.
|
||||||
@ -322,8 +311,6 @@ Feature: IMAP import messages
|
|||||||
Content-Type: multipart/mixed; boundary="boundary"
|
Content-Type: multipart/mixed; boundary="boundary"
|
||||||
Received: by 2002:0:0:0:0:0:0:0 with SMTP id 0123456789abcdef; Wed, 30 Dec 2020 01:23:45 0000
|
Received: by 2002:0:0:0:0:0:0:0 with SMTP id 0123456789abcdef; Wed, 30 Dec 2020 01:23:45 0000
|
||||||
|
|
||||||
--boundary
|
|
||||||
|
|
||||||
This is a multi-part message in MIME format.
|
This is a multi-part message in MIME format.
|
||||||
|
|
||||||
--boundary
|
--boundary
|
||||||
@ -366,9 +353,6 @@ Feature: IMAP import messages
|
|||||||
"content": {
|
"content": {
|
||||||
"content-type": "multipart/mixed",
|
"content-type": "multipart/mixed",
|
||||||
"sections":[
|
"sections":[
|
||||||
{
|
|
||||||
"body-is": "This is a multi-part message in MIME format."
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"content-type": "text/plain",
|
"content-type": "text/plain",
|
||||||
"content-type-charset": "utf-8",
|
"content-type-charset": "utf-8",
|
||||||
@ -393,3 +377,84 @@ Feature: IMAP import messages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@regression
|
||||||
|
Scenario: Import message with remote content
|
||||||
|
When IMAP client "1" appends the following message to "Inbox":
|
||||||
|
"""
|
||||||
|
Date: 01 Jan 1980 00:00:00 +0000
|
||||||
|
To: Bridge Test <bridge@test.com>
|
||||||
|
From: Bridge Second Test <bridge_second@test.com>
|
||||||
|
Subject: MESSAGE WITH REMOTE CONTENT
|
||||||
|
Content-Type: multipart/alternative;
|
||||||
|
boundary="------------vUMV7TiM65KWBg30p6OgD3Vp"
|
||||||
|
|
||||||
|
This is a multi-part message in MIME format.
|
||||||
|
--------------vUMV7TiM65KWBg30p6OgD3Vp
|
||||||
|
Content-Type: text/plain; charset=utf-8; format=flowed
|
||||||
|
Content-Transfer-Encoding: 7bit
|
||||||
|
|
||||||
|
Remote content
|
||||||
|
|
||||||
|
|
||||||
|
Bridge
|
||||||
|
|
||||||
|
|
||||||
|
Remote content
|
||||||
|
|
||||||
|
|
||||||
|
--------------vUMV7TiM65KWBg30p6OgD3Vp
|
||||||
|
Content-Type: text/html; charset=utf-8
|
||||||
|
Content-Transfer-Encoding: 7bit
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
|
||||||
|
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p><tt>Remote content</tt></p>
|
||||||
|
<p><tt><br>
|
||||||
|
</tt></p>
|
||||||
|
<p><img
|
||||||
|
src="https://bridgeteam.protontech.ch/bridgeteam/tmp/bridge.jpg"
|
||||||
|
alt="Bridge" width="180" height="180"></p>
|
||||||
|
<p><br>
|
||||||
|
</p>
|
||||||
|
<p><tt>Remote content</tt><br>
|
||||||
|
</p>
|
||||||
|
<br>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
--------------vUMV7TiM65KWBg30p6OgD3Vp--
|
||||||
|
|
||||||
|
"""
|
||||||
|
Then it succeeds
|
||||||
|
And IMAP client "1" eventually sees the following message in "Inbox" with this structure:
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"date": "01 Jan 80 00:00 +0000",
|
||||||
|
"to": "Bridge Test <bridge@test.com>",
|
||||||
|
"from": "Bridge Second Test <bridge_second@test.com>",
|
||||||
|
"subject": "MESSAGE WITH REMOTE CONTENT",
|
||||||
|
"content": {
|
||||||
|
"content-type": "multipart/alternative",
|
||||||
|
"sections":[
|
||||||
|
{
|
||||||
|
"content-type": "text/plain",
|
||||||
|
"content-type-charset": "utf-8",
|
||||||
|
"transfer-encoding": "7bit",
|
||||||
|
"body-is": "Remote content\n\n\nBridge\n\n\nRemote content"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content-type": "text/html",
|
||||||
|
"content-type-charset": "utf-8",
|
||||||
|
"transfer-encoding": "7bit",
|
||||||
|
"body-is": "<!DOCTYPE html>\n<html>\n <head>\n\n <meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\">\n </head>\n <body>\n <p><tt>Remote content</tt></p>\n <p><tt><br>\n </tt></p>\n <p><img\n src=\"https://bridgeteam.protontech.ch/bridgeteam/tmp/bridge.jpg\"\n alt=\"Bridge\" width=\"180\" height=\"180\"></p>\n <p><br>\n </p>\n <p><tt>Remote content</tt><br>\n </p>\n <br>\n </body>\n</html>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|||||||
@ -140,4 +140,80 @@ Feature: SMTP sending with attachment
|
|||||||
"Disposition": "attachment"
|
"Disposition": "attachment"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
And IMAP client "1" eventually sees the following message in "Sent" with this structure:
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"subject": "Test with cyrillic attachment",
|
||||||
|
"body-contains": "Shake that body",
|
||||||
|
"content": {
|
||||||
|
"content-type": "multipart/mixed",
|
||||||
|
"sections":[
|
||||||
|
{
|
||||||
|
"content-type": "text/plain",
|
||||||
|
"body-is": "Shake that body"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content-type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
"content-type-name": "АБВГДЃЕЖЗЅИЈКЛЉМНЊОПРСТЌУФХЧЏЗШ.docx",
|
||||||
|
"content-disposition": "attachment",
|
||||||
|
"content-disposition-filename": "АБВГДЃЕЖЗЅИЈКЛЉМНЊОПРСТЌУФХЧЏЗШ.docx"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
Scenario Outline: Send message with attachment <UseCase>
|
||||||
|
When SMTP client "1" sends the following message from "[user:user1]@[domain]" to "[user:user2]@[domain]":
|
||||||
|
"""
|
||||||
|
Subject: Message with attachment name
|
||||||
|
Content-type: multipart/mixed; boundary="boundary"
|
||||||
|
Received: by 2002:0:0:0:0:0:0:0 with SMTP id 0123456789abcdef; Wed, 30 Dec 2020 01:23:45 0000
|
||||||
|
|
||||||
|
This is a multi-part message in MIME format.
|
||||||
|
|
||||||
|
--boundary
|
||||||
|
Content-Type: text/plain
|
||||||
|
|
||||||
|
Hello
|
||||||
|
|
||||||
|
--boundary
|
||||||
|
Content-Type: application/pdf; name=<filename>
|
||||||
|
Content-Disposition: attachment; filename=<filename>
|
||||||
|
|
||||||
|
somebytes
|
||||||
|
|
||||||
|
--boundary--
|
||||||
|
"""
|
||||||
|
Then it succeeds
|
||||||
|
And IMAP client "1" eventually sees the following message in "Sent" with this structure:
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"subject": "Message with attachment name",
|
||||||
|
"body-contains": "Hello",
|
||||||
|
"content": {
|
||||||
|
"content-type": "multipart/mixed",
|
||||||
|
"sections":[
|
||||||
|
{
|
||||||
|
"content-type": "text/plain",
|
||||||
|
"body-is": "Hello"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content-type": "application/pdf",
|
||||||
|
"content-type-name": "filename",
|
||||||
|
"content-disposition": "attachment",
|
||||||
|
"content-disposition-filename": "filename",
|
||||||
|
"transfer-encoding":"base64",
|
||||||
|
"body-is": "c29tZWJ5dGVzDQo="
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
Examples:
|
||||||
|
| UseCase | filename |
|
||||||
|
| encoded quoted | "=?US-ASCII?Q?filename?=" |
|
||||||
|
| encoded unquoted | =?US-ASCII?Q?filename?= |
|
||||||
|
| non quoted | filename |
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
Feature: SMTP sending of plain messages
|
Feature: SMTP sending of plain messages
|
||||||
Background:
|
Background:
|
||||||
Given there exists an account with username "[user:user]" and password "password"
|
Given there exists an account with username "[user:user]" and password "password"
|
||||||
And there exists an account with username "[user:to]" and password "password"
|
And there exists an account with username "[user:user2]" and password "password"
|
||||||
Then it succeeds
|
Then it succeeds
|
||||||
When bridge starts
|
When bridge starts
|
||||||
And the user logs in with username "[user:user]" and password "password"
|
And the user logs in with username "[user:user]" and password "password"
|
||||||
|
And user "[user:user]" finishes syncing
|
||||||
And user "[user:user]" connects and authenticates SMTP client "1"
|
And user "[user:user]" connects and authenticates SMTP client "1"
|
||||||
Then it succeeds
|
Then it succeeds
|
||||||
|
|
||||||
@ -123,12 +124,45 @@ Feature: SMTP sending of plain messages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
And IMAP client "1" eventually sees the following message in "Sent" with this structure:
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"date": "01 Jan 01 00:00 +0000",
|
||||||
|
"to": "External Bridge <pm.bridge.qa@gmail.com>",
|
||||||
|
"from": "Bridge Test <[user:user]@[domain]>",
|
||||||
|
"subject": "Html Inline External",
|
||||||
|
"content": {
|
||||||
|
"content-type": "multipart/mixed",
|
||||||
|
"sections":[
|
||||||
|
{
|
||||||
|
"content-type": "multipart/related",
|
||||||
|
"sections":[
|
||||||
|
{
|
||||||
|
"content-type": "text/html",
|
||||||
|
"content-type-charset": "utf-8",
|
||||||
|
"transfer-encoding": "quoted-printable",
|
||||||
|
"body-is": "<html><head>\r\n<meta http-equiv=3D\"content-type\" content=3D\"text/html; charset=3DUTF-8\"/>\r\n</head>\r\n<body text=3D\"#000000\" bgcolor=3D\"#FFFFFF\">\r\n<p><br/>\r\n</p>\r\n<p>Behold! An inline <img moz-do-not-send=3D\"false\" src=3D\"cid:part1.D96BFA=\r\nE9.E2E1CAE3@protonmail.com\" alt=3D\"\" width=3D\"24\" height=3D\"24\"/><br/>\r\n</p>\r\n\r\n\r\n</body></html>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content-type": "image/gif",
|
||||||
|
"content-type-name": "email-action-left.gif",
|
||||||
|
"content-disposition": "inline",
|
||||||
|
"content-disposition-filename": "email-action-left.gif",
|
||||||
|
"transfer-encoding": "base64",
|
||||||
|
"body-is": "R0lGODlhGAAYANUAACcsKOHs4kppTH6tgYWxiIq0jTVENpG5lDI/M7bRuEaJSkqOTk2RUU+PU16l\r\nYl+lY2iva262cXS6d3rDfYLNhWeeamKTZGSVZkNbRGqhbOPt4////+7u7qioqFZWVlNTUyIiIgAA\r\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\r\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAGAAYAAAG/8CNcLjRJAqVRqNS\r\nSGiI0GFgoKhar4NAdHioMhyRCYUyiTgY1cOWUH1ILgIDAGAQXCSPKgHaXUAyGCCCg4IYGRALCmpC\r\nAVUQFgiEkiAIFhBVWhtUDxmRk5IIGXkDRQoMEoGfHpIYEmhGCg4XnyAdHB+SFw4KRwoRArQdG7eE\r\nAhEKSAoTBoIdzs/Cw7iCBhMKSQoUAIJbQ8QgABQKStnbIN1C3+HjFcrMtdDO6dMg1dcFvsCfwt+C\r\nxsgJYs3a10+QLl4aTKGitYpQq1eaFHDyREtQqFGMHEGqSMkSJi4K/ACiZQiRIihsJL6JM6fOnTwK\r\n9kTpYgqMGDJm0JzsNuWKTw0FWdANMYJECRMnW4IAADs="
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
Scenario: HTML message with alternative inline to internal account
|
Scenario: HTML message with alternative inline to internal account
|
||||||
When SMTP client "1" sends the following message from "[user:user]@[domain]" to "[user:to]@[domain]":
|
When SMTP client "1" sends the following message from "[user:user]@[domain]" to "[user:user2]@[domain]":
|
||||||
"""
|
"""
|
||||||
From: Bridge Test <[user:user]@[domain]>
|
From: Bridge Test <[user:user]@[domain]>
|
||||||
To: Internal Bridge <[user:to]@[domain]>
|
To: Internal Bridge <[user:user2]@[domain]>
|
||||||
Subject: Html Inline Alternative Internal
|
Subject: Html Inline Alternative Internal
|
||||||
Content-Disposition: inline
|
Content-Disposition: inline
|
||||||
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Thunderbird/60.5.0
|
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Thunderbird/60.5.0
|
||||||
@ -192,7 +226,7 @@ Feature: SMTP sending of plain messages
|
|||||||
When user "[user:user]" connects and authenticates IMAP client "1"
|
When user "[user:user]" connects and authenticates IMAP client "1"
|
||||||
Then IMAP client "1" eventually sees the following messages in "Sent":
|
Then IMAP client "1" eventually sees the following messages in "Sent":
|
||||||
| from | to | subject |
|
| from | to | subject |
|
||||||
| [user:user]@[domain] | [user:to]@[domain] | Html Inline Alternative Internal |
|
| [user:user]@[domain] | [user:user2]@[domain] | Html Inline Alternative Internal |
|
||||||
And the body in the "POST" request to "/mail/v4/messages" is:
|
And the body in the "POST" request to "/mail/v4/messages" is:
|
||||||
"""
|
"""
|
||||||
{
|
{
|
||||||
@ -203,7 +237,7 @@ Feature: SMTP sending of plain messages
|
|||||||
},
|
},
|
||||||
"ToList": [
|
"ToList": [
|
||||||
{
|
{
|
||||||
"Address": "[user:to]@[domain]",
|
"Address": "[user:user2]@[domain]",
|
||||||
"Name": "Internal Bridge"
|
"Name": "Internal Bridge"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -344,7 +378,7 @@ Feature: SMTP sending of plain messages
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
Scenario: HTML message with Foreign/Nonascii chars in Subject and Body
|
Scenario: HTML message with Foreign/Nonascii chars in Subject and Body to external
|
||||||
When there exists an account with username "bridgetest" and password "password"
|
When there exists an account with username "bridgetest" and password "password"
|
||||||
And the user logs in with username "bridgetest" and password "password"
|
And the user logs in with username "bridgetest" and password "password"
|
||||||
And user "bridgetest" connects and authenticates SMTP client "1"
|
And user "bridgetest" connects and authenticates SMTP client "1"
|
||||||
@ -374,3 +408,242 @@ Feature: SMTP sending of plain messages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# It is expected for the structure check to look a bit different. More info on GODT-3011
|
||||||
|
@regression
|
||||||
|
Scenario: HTML message with remote content in Body
|
||||||
|
When SMTP client "1" sends the following message from "[user:user]@[domain]" to "[user:to]@[domain]":
|
||||||
|
"""
|
||||||
|
Date: 01 Jan 1980 00:00:00 +0000
|
||||||
|
To: Internal Bridge Test <[user:to]@[domain]>
|
||||||
|
From: Bridge Test <[user:user]@[domain]>
|
||||||
|
Subject: MESSAGE WITH REMOTE CONTENT SENT
|
||||||
|
Content-Type: multipart/alternative;
|
||||||
|
boundary="------------vUMV7TiM65KWBg30p6OgD3Vp"
|
||||||
|
|
||||||
|
This is a multi-part message in MIME format.
|
||||||
|
--------------vUMV7TiM65KWBg30p6OgD3Vp
|
||||||
|
Content-Type: text/plain; charset=utf-8; format=flowed
|
||||||
|
Content-Transfer-Encoding: 7bit
|
||||||
|
|
||||||
|
Remote content
|
||||||
|
|
||||||
|
|
||||||
|
Bridge
|
||||||
|
|
||||||
|
|
||||||
|
Remote content
|
||||||
|
|
||||||
|
|
||||||
|
--------------vUMV7TiM65KWBg30p6OgD3Vp
|
||||||
|
Content-Type: text/html; charset=utf-8
|
||||||
|
Content-Transfer-Encoding: 7bit
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
|
||||||
|
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p><tt>Remote content</tt></p>
|
||||||
|
<p><tt><br>
|
||||||
|
</tt></p>
|
||||||
|
<p><img
|
||||||
|
src="https://bridgeteam.protontech.ch/bridgeteam/tmp/bridge.jpg"
|
||||||
|
alt="Bridge" width="180" height="180"></p>
|
||||||
|
<p><br>
|
||||||
|
</p>
|
||||||
|
<p><tt>Remote content</tt><br>
|
||||||
|
</p>
|
||||||
|
<br>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
--------------vUMV7TiM65KWBg30p6OgD3Vp--
|
||||||
|
|
||||||
|
"""
|
||||||
|
Then it succeeds
|
||||||
|
When user "[user:user]" connects and authenticates IMAP client "1"
|
||||||
|
And IMAP client "1" eventually sees the following message in "Sent" with this structure:
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"date": "01 Jan 01 00:00 +0000",
|
||||||
|
"to": "Internal Bridge Test <[user:to]@[domain]>",
|
||||||
|
"from": "Bridge Test <[user:user]@[domain]>",
|
||||||
|
"subject": "MESSAGE WITH REMOTE CONTENT SENT",
|
||||||
|
"content": {
|
||||||
|
"content-type": "text/html",
|
||||||
|
"content-type-charset": "utf-8",
|
||||||
|
"transfer-encoding": "quoted-printable",
|
||||||
|
"body-is": "<!DOCTYPE html><html><head>\n\n <meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\"/>\n </head>\n <body>\n <p><tt>Remote content</tt></p>\n <p><tt><br/>\n </tt></p>\n <p><img src=\"https://bridgeteam.protontech.ch/bridgeteam/tmp/bridge.jpg\" alt=\"Bridge\" width=\"180\" height=\"180\"/></p>\n <p><br/>\n </p>\n <p><tt>Remote content</tt><br/>\n </p>\n <br/>\n \n\n</body></html>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
Scenario: Forward a message containing various attachments
|
||||||
|
When SMTP client "1" sends the following message from "[user:user]@[domain]" to "[user:user2]@[domain]":
|
||||||
|
"""
|
||||||
|
Content-Type: multipart/mixed; boundary="------------MQ01Z9UM8OaR9z39TvzDfdIq"
|
||||||
|
Subject: Fwd: Reply to this message, it has various attachments.
|
||||||
|
References: <something@protonmail.ch>
|
||||||
|
To: <[user:user2]@[domain]>
|
||||||
|
From: <[user:user]@[domain]>
|
||||||
|
In-Reply-To: <something@protonmail.ch>
|
||||||
|
X-Forwarded-Message-Id: <something@protonmail.ch>
|
||||||
|
|
||||||
|
This is a multi-part message in MIME format.
|
||||||
|
--------------MQ01Z9UM8OaR9z39TvzDfdIq
|
||||||
|
Content-Type: text/plain; charset=UTF-8; format=flowed
|
||||||
|
Content-Transfer-Encoding: 7bit
|
||||||
|
|
||||||
|
Forwarding a message with various attachments in it!
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-------- Forwarded Message --------
|
||||||
|
Subject: Reply to this message, it has various attachments.
|
||||||
|
Date: Thu, 26 Oct 2023 10:41:55 +0000
|
||||||
|
From: Gjorgji Testing <gorgitesting@protonmail.com>
|
||||||
|
Reply-To: Gjorgji Testing <gorgitesting@protonmail.com>
|
||||||
|
To: Gjorgji Test v3 <gorgitesting3@protonmail.com>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
For real!
|
||||||
|
|
||||||
|
*Gjorgji Testing
|
||||||
|
TesASID <https://www.youtube.com/watch?v=MifXUbrjYr8>
|
||||||
|
*
|
||||||
|
--------------MQ01Z9UM8OaR9z39TvzDfdIq
|
||||||
|
Content-Type: text/html; charset=UTF-8; name="index.html"
|
||||||
|
Content-Disposition: attachment; filename="index.html"
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
|
||||||
|
IDwhRE9DVFlQRSBodG1sPg0KPGh0bWw+DQo8aGVhZD4NCjx0aXRsZT5QYWdlIFRpdGxlPC90
|
||||||
|
aXRsZT4NCjwvaGVhZD4NCjxib2R5Pg0KDQo8aDE+TXkgRmlyc3QgSGVhZGluZzwvaDE+DQo8
|
||||||
|
cD5NeSBmaXJzdCBwYXJhZ3JhcGguPC9wPg0KDQo8L2JvZHk+DQo8L2h0bWw+IA==
|
||||||
|
--------------MQ01Z9UM8OaR9z39TvzDfdIq
|
||||||
|
Content-Type: text/xml; charset=UTF-8; name="testxml.xml"
|
||||||
|
Content-Disposition: attachment; filename="testxml.xml"
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
|
||||||
|
PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCFET0NUWVBFIHN1aXRl
|
||||||
|
IFNZU1RFTSAiaHR0cDovL3Rlc3RuZy5vcmcvdGVzdG5nLTEuMC5kdGQiID4KCjxzdWl0ZSBu
|
||||||
|
YW1lPSJBZmZpbGlhdGUgTmV0d29ya3MiPgoKICAgIDx0ZXN0IG5hbWU9IkFmZmlsaWF0ZSBO
|
||||||
|
ZXR3b3JrcyIgZW5hYmxlZD0idHJ1ZSI+CiAgICAgICAgPGNsYXNzZXM+CiAgICAgICAgICAg
|
||||||
|
IDxjbGFzcyBuYW1lPSJjb20uY2xpY2tvdXQuYXBpdGVzdGluZy5hZmZOZXR3b3Jrcy5Bd2lu
|
||||||
|
VUtUZXN0Ii8+CiAgICAgICAgPC9jbGFzc2VzPgogICAgPC90ZXN0PgoKPC9zdWl0ZT4=
|
||||||
|
--------------MQ01Z9UM8OaR9z39TvzDfdIq
|
||||||
|
Content-Type: application/pdf; name="test.pdf"
|
||||||
|
Content-Disposition: attachment; filename="test.pdf"
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
|
||||||
|
JVBERi0xLjUKJeLjz9MKNyAwIG9iago8PAovVHlwZSAvRm9udERlc2NyaXB0b3IKL0ZvbnRO
|
||||||
|
MjM0NAolJUVPRgo=
|
||||||
|
--------------MQ01Z9UM8OaR9z39TvzDfdIq
|
||||||
|
Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;
|
||||||
|
name="test.xlsx"
|
||||||
|
Content-Disposition: attachment; filename="test.xlsx"
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
|
||||||
|
UEsDBBQABgAIAAAAIQBi7p1oXgEAAJAEAAATAAgCW0NvbnRlbnRfVHlwZXNdLnhtbCCiBAIo
|
||||||
|
UQIAABEAAAAAAAAAAAAAAAAARBcAAGRvY1Byb3BzL2NvcmUueG1sUEsBAi0AFAAGAAgAAAAh
|
||||||
|
AGFJCRCJAQAAEQMAABAAAAAAAAAAAAAAAAAAvBkAAGRvY1Byb3BzL2FwcC54bWxQSwUGAAAA
|
||||||
|
AAoACgCAAgAAexwAAAAA
|
||||||
|
--------------MQ01Z9UM8OaR9z39TvzDfdIq
|
||||||
|
Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document;
|
||||||
|
name="test.docx"
|
||||||
|
Content-Disposition: attachment; filename="test.docx"
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
|
||||||
|
UEsDBBQABgAIAAAAIQDfpNJsWgEAACAFAAATAAgCW0NvbnRlbnRfVHlwZXNdLnhtbCCiBAIo
|
||||||
|
cHAueG1sUEsBAi0AFAAGAAgAAAAhABA0tG9uAQAA4QIAABEAAAAAAAAAAAAAAAAA2xsAAGRv
|
||||||
|
Y1Byb3BzL2NvcmUueG1sUEsBAi0AFAAGAAgAAAAhAJ/mlBIqCwAAU3AAAA8AAAAAAAAAAAAA
|
||||||
|
AAAAgB4AAHdvcmQvc3R5bGVzLnhtbFBLBQYAAAAACwALAMECAADXKQAAAAA=
|
||||||
|
--------------MQ01Z9UM8OaR9z39TvzDfdIq
|
||||||
|
Content-Type: text/plain; charset=UTF-8; name="text file.txt"
|
||||||
|
Content-Disposition: attachment; filename="text file.txt"
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
|
||||||
|
dGV4dCBmaWxl
|
||||||
|
|
||||||
|
--------------MQ01Z9UM8OaR9z39TvzDfdIq--
|
||||||
|
|
||||||
|
"""
|
||||||
|
Then it succeeds
|
||||||
|
When user "[user:user]" connects and authenticates IMAP client "1"
|
||||||
|
Then IMAP client "1" eventually sees the following messages in "Sent":
|
||||||
|
| from | to | subject | X-Forwarded-Message-Id |
|
||||||
|
| [user:user]@[domain] | [user:user2]@[domain] | Fwd: Reply to this message, it has various attachments. | something@protonmail.ch |
|
||||||
|
And IMAP client "1" eventually sees 1 messages in "Sent"
|
||||||
|
When the user logs in with username "[user:user2]" and password "password"
|
||||||
|
And user "[user:user2]" connects and authenticates IMAP client "2"
|
||||||
|
And user "[user:user2]" finishes syncing
|
||||||
|
And it succeeds
|
||||||
|
Then IMAP client "2" eventually sees the following messages in "Inbox":
|
||||||
|
| from | to | subject | X-Forwarded-Message-Id |
|
||||||
|
| [user:user]@[domain] | [user:user2]@[domain] | Fwd: Reply to this message, it has various attachments. | something@protonmail.ch |
|
||||||
|
Then IMAP client "2" eventually sees the following message in "Inbox" with this structure:
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"from": "[user:user]@[domain]",
|
||||||
|
"to": "[user:user2]@[domain]",
|
||||||
|
"subject": "Fwd: Reply to this message, it has various attachments.",
|
||||||
|
"content": {
|
||||||
|
"content-type": "multipart/mixed",
|
||||||
|
"sections":[
|
||||||
|
{
|
||||||
|
"content-type": "text/plain",
|
||||||
|
"content-type-charset": "utf-8",
|
||||||
|
"transfer-encoding": "quoted-printable",
|
||||||
|
"body-is": "Forwarding a message with various attachments in it!\r\n\r\n\r\n\r\n-------- Forwarded Message --------\r\nSubject: \tReply to this message, it has various attachments.\r\nDate: \tThu, 26 Oct 2023 10:41:55 +0000\r\nFrom: \tGjorgji Testing <gorgitesting@protonmail.com>\r\nReply-To: \tGjorgji Testing <gorgitesting@protonmail.com>\r\nTo: \tGjorgji Test v3 <gorgitesting3@protonmail.com>\r\n\r\n\r\n\r\n\r\nFor real!\r\n\r\n*Gjorgji Testing\r\nTesASID <https://www.youtube.com/watch?v=3DMifXUbrjYr8>\r\n*"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content-type": "text/html",
|
||||||
|
"content-type-name": "index.html",
|
||||||
|
"content-disposition": "attachment",
|
||||||
|
"content-disposition-filename": "index.html",
|
||||||
|
"transfer-encoding": "base64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content-type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
"content-type-name": "test.docx",
|
||||||
|
"content-disposition": "attachment",
|
||||||
|
"content-disposition-filename": "test.docx",
|
||||||
|
"transfer-encoding": "base64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content-type": "application/pdf",
|
||||||
|
"content-type-name": "test.pdf",
|
||||||
|
"content-disposition": "attachment",
|
||||||
|
"content-disposition-filename": "test.pdf",
|
||||||
|
"transfer-encoding": "base64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content-type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
"content-type-name": "test.xlsx",
|
||||||
|
"content-disposition": "attachment",
|
||||||
|
"content-disposition-filename": "test.xlsx",
|
||||||
|
"transfer-encoding": "base64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content-type": "text/xml",
|
||||||
|
"content-type-name": "testxml.xml",
|
||||||
|
"content-disposition": "attachment",
|
||||||
|
"content-disposition-filename": "testxml.xml",
|
||||||
|
"transfer-encoding": "base64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content-type": "text/plain",
|
||||||
|
"content-type-name": "text file.txt",
|
||||||
|
"content-disposition": "attachment",
|
||||||
|
"content-disposition-filename": "text file.txt",
|
||||||
|
"transfer-encoding": "base64",
|
||||||
|
"body-is": "dGV4dCBmaWxl"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
2196
tests/features/smtp/send/html_to_internal.feature
Normal file
2196
tests/features/smtp/send/html_to_internal.feature
Normal file
File diff suppressed because one or more lines are too long
19
tests/features/user/account.feature
Normal file
19
tests/features/user/account.feature
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
Feature: Account settings
|
||||||
|
|
||||||
|
Background:
|
||||||
|
Given there exists an account with username "[user:user]" and password "password"
|
||||||
|
Then it succeeds
|
||||||
|
When bridge starts
|
||||||
|
|
||||||
|
Scenario: Check account default settings
|
||||||
|
Then the account "[user:user]" matches the following settings:
|
||||||
|
| DraftMIMEType | AttachPublicKey | Sign | PGPScheme |
|
||||||
|
| text/html | false | 0 | 0 |
|
||||||
|
When the account "[user:user]" has public key attachment "enabled"
|
||||||
|
And the account "[user:user]" has sign external messages "enabled"
|
||||||
|
And the account "[user:user]" has default draft format "plain"
|
||||||
|
And the account "[user:user]" has default PGP schema "inline"
|
||||||
|
Then the account "[user:user]" matches the following settings:
|
||||||
|
| DraftMIMEType | AttachPublicKey | Sign | PGPScheme |
|
||||||
|
| text/plain | true | 1 | 8 |
|
||||||
|
|
||||||
@ -41,7 +41,7 @@ Feature: The user reports a problem
|
|||||||
"Description": "Testing Description",
|
"Description": "Testing Description",
|
||||||
"Username": "[user:user]",
|
"Username": "[user:user]",
|
||||||
"Email": "[user:user]@[domain]",
|
"Email": "[user:user]@[domain]",
|
||||||
"Client": "Apple Mail"
|
"EmailClient": "Apple Mail"
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
Then the header in the "POST" multipart request to "/core/v4/reports/bug" has "Title" set to "[Bridge] Bug - Testing Title"
|
Then the header in the "POST" multipart request to "/core/v4/reports/bug" has "Title" set to "[Bridge] Bug - Testing Title"
|
||||||
|
|||||||
@ -114,6 +114,7 @@ func (s *scenario) steps(ctx *godog.ScenarioContext) {
|
|||||||
ctx.Step(`^the account "([^"]*)" has sign external messages "([^"]*)"`, s.accountHasSignExternalMessages)
|
ctx.Step(`^the account "([^"]*)" has sign external messages "([^"]*)"`, s.accountHasSignExternalMessages)
|
||||||
ctx.Step(`^the account "([^"]*)" has default draft format "([^"]*)"`, s.accountHasDefaultDraftFormat)
|
ctx.Step(`^the account "([^"]*)" has default draft format "([^"]*)"`, s.accountHasDefaultDraftFormat)
|
||||||
ctx.Step(`^the account "([^"]*)" has default PGP schema "([^"]*)"`, s.accountHasDefaultPGPSchema)
|
ctx.Step(`^the account "([^"]*)" has default PGP schema "([^"]*)"`, s.accountHasDefaultPGPSchema)
|
||||||
|
ctx.Step(`^the account "([^"]*)" matches the following settings:$`, s.accountMatchesSettings)
|
||||||
|
|
||||||
// ==== IMAP ====
|
// ==== IMAP ====
|
||||||
ctx.Step(`^user "([^"]*)" connects IMAP client "([^"]*)"$`, s.userConnectsIMAPClient)
|
ctx.Step(`^user "([^"]*)" connects IMAP client "([^"]*)"$`, s.userConnectsIMAPClient)
|
||||||
|
|||||||
27
tests/testdata/multipart/mixed_with_attachment_encoded.eml
vendored
Normal file
27
tests/testdata/multipart/mixed_with_attachment_encoded.eml
vendored
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
From: Bridge Test <bridgetest@pm.test>
|
||||||
|
Date: 01 Jan 1980 00:00:00 +0000
|
||||||
|
To: Internal Bridge <bridgetest@protonmail.com>
|
||||||
|
Subject: Message with attachment name
|
||||||
|
Content-type: multipart/mixed; boundary="boundary"
|
||||||
|
Received: by 2002:0:0:0:0:0:0:0 with SMTP id 0123456789abcdef; Wed, 30 Dec 2020 01:23:45 0000
|
||||||
|
|
||||||
|
This is a multi-part message in MIME format.
|
||||||
|
|
||||||
|
--boundary
|
||||||
|
Content-Type: text/plain
|
||||||
|
|
||||||
|
Hello
|
||||||
|
|
||||||
|
--boundary
|
||||||
|
Content-Type: text/html; charset=utf-8
|
||||||
|
Content-Transfer-Encoding: 7bit
|
||||||
|
|
||||||
|
<h1> HELLO </h1>
|
||||||
|
|
||||||
|
--boundary
|
||||||
|
Content-Type: application/pdf; name="=?US-ASCII?Q?filename?="
|
||||||
|
Content-Disposition: attachment; filename="=?US-ASCII?Q?filename?="
|
||||||
|
|
||||||
|
somebytes
|
||||||
|
|
||||||
|
--boundary--
|
||||||
27
tests/testdata/multipart/mixed_with_attachment_encoded_no_quote.eml
vendored
Normal file
27
tests/testdata/multipart/mixed_with_attachment_encoded_no_quote.eml
vendored
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
From: Bridge Test <bridgetest@pm.test>
|
||||||
|
Date: 01 Jan 1980 00:00:00 +0000
|
||||||
|
To: Internal Bridge <bridgetest@protonmail.com>
|
||||||
|
Subject: Message with attachment name
|
||||||
|
Content-type: multipart/mixed; boundary="boundary"
|
||||||
|
Received: by 2002:0:0:0:0:0:0:0 with SMTP id 0123456789abcdef; Wed, 30 Dec 2020 01:23:45 0000
|
||||||
|
|
||||||
|
This is a multi-part message in MIME format.
|
||||||
|
|
||||||
|
--boundary
|
||||||
|
Content-Type: text/plain
|
||||||
|
|
||||||
|
Hello
|
||||||
|
|
||||||
|
--boundary
|
||||||
|
Content-Type: text/html; charset=utf-8
|
||||||
|
Content-Transfer-Encoding: 7bit
|
||||||
|
|
||||||
|
<h1> HELLO </h1>
|
||||||
|
|
||||||
|
--boundary
|
||||||
|
Content-Type: application/pdf; name==?US-ASCII?Q?filename?=
|
||||||
|
Content-Disposition: attachment; filename==?US-ASCII?Q?filename?=
|
||||||
|
|
||||||
|
somebytes
|
||||||
|
|
||||||
|
--boundary--
|
||||||
27
tests/testdata/multipart/mixed_with_attachment_no_quote.eml
vendored
Normal file
27
tests/testdata/multipart/mixed_with_attachment_no_quote.eml
vendored
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
From: Bridge Test <bridgetest@pm.test>
|
||||||
|
Date: 01 Jan 1980 00:00:00 +0000
|
||||||
|
To: Internal Bridge <bridgetest@protonmail.com>
|
||||||
|
Subject: Message with attachment name
|
||||||
|
Content-type: multipart/mixed; boundary="boundary"
|
||||||
|
Received: by 2002:0:0:0:0:0:0:0 with SMTP id 0123456789abcdef; Wed, 30 Dec 2020 01:23:45 0000
|
||||||
|
|
||||||
|
This is a multi-part message in MIME format.
|
||||||
|
|
||||||
|
--boundary
|
||||||
|
Content-Type: text/plain
|
||||||
|
|
||||||
|
Hello
|
||||||
|
|
||||||
|
--boundary
|
||||||
|
Content-Type: text/html; charset=utf-8
|
||||||
|
Content-Transfer-Encoding: 7bit
|
||||||
|
|
||||||
|
<h1> HELLO </h1>
|
||||||
|
|
||||||
|
--boundary
|
||||||
|
Content-Type: application/pdf; name=filename
|
||||||
|
Content-Disposition: attachment; filename=filename
|
||||||
|
|
||||||
|
somebytes
|
||||||
|
|
||||||
|
--boundary--
|
||||||
@ -28,7 +28,10 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ProtonMail/gluon/rfc822"
|
"github.com/ProtonMail/gluon/rfc822"
|
||||||
|
"github.com/ProtonMail/go-proton-api"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/pkg/message"
|
"github.com/ProtonMail/proton-bridge/v3/pkg/message"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/pkg/message/parser"
|
||||||
|
pmmime "github.com/ProtonMail/proton-bridge/v3/pkg/mime"
|
||||||
"github.com/bradenaw/juniper/xslices"
|
"github.com/bradenaw/juniper/xslices"
|
||||||
"github.com/cucumber/messages-go/v16"
|
"github.com/cucumber/messages-go/v16"
|
||||||
"github.com/emersion/go-imap"
|
"github.com/emersion/go-imap"
|
||||||
@ -202,10 +205,16 @@ func newMessageStructFromIMAP(msg *imap.Message) MessageStruct {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
m, err := message.Parse(bytes.NewReader(literal))
|
parser, err := parser.New(bytes.NewReader(literal))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m, err := message.ParseWithParser(parser, true)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
var body string
|
var body string
|
||||||
switch {
|
switch {
|
||||||
case m.MIMEType == rfc822.TextPlain:
|
case m.MIMEType == rfc822.TextPlain:
|
||||||
@ -245,34 +254,23 @@ func formatAddressList(list []*imap.Address) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func parseMessageSection(literal []byte, body string) MessageSection {
|
func parseMessageSection(literal []byte, body string) MessageSection {
|
||||||
mimeType, boundary, charset, name := parseContentType(literal)
|
|
||||||
|
|
||||||
headers, err := rfc822.Parse(literal).ParseHeader()
|
headers, err := rfc822.Parse(literal).ParseHeader()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
msgSect := MessageSection{
|
mimeType, boundary, charset, name := parseContentType(headers.Get("Content-Type"))
|
||||||
ContentType: string(mimeType),
|
disp, filename := parseContentDisposition(headers.Get("Content-Disposition"))
|
||||||
ContentTypeBoundary: boundary,
|
|
||||||
ContentTypeCharset: charset,
|
|
||||||
ContentTypeName: name,
|
|
||||||
TransferEncoding: headers.Get("content-transfer-encoding"),
|
|
||||||
BodyIs: body,
|
|
||||||
}
|
|
||||||
|
|
||||||
contentDisposition := bytes.Split([]byte(headers.Get("content-disposition")), []byte(";"))
|
msgSect := MessageSection{
|
||||||
for id, value := range contentDisposition {
|
ContentType: mimeType,
|
||||||
if id == 0 {
|
ContentTypeBoundary: boundary,
|
||||||
msgSect.ContentDisposition = strings.TrimSpace(string(value))
|
ContentTypeCharset: charset,
|
||||||
continue
|
ContentTypeName: name,
|
||||||
}
|
ContentDisposition: disp,
|
||||||
param := bytes.Split(value, []byte("="))
|
ContentDispositionFilename: filename,
|
||||||
if strings.TrimSpace(string(param[0])) == "filename" && len(param) >= 2 {
|
TransferEncoding: headers.Get("content-transfer-encoding"),
|
||||||
_, filename, _ := strings.Cut(string(value), "filename=")
|
BodyIs: body,
|
||||||
filename = strings.Trim(filename, "\"")
|
|
||||||
msgSect.ContentDispositionFilename = strings.TrimSpace(filename)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if msgSect.ContentTypeBoundary != "" {
|
if msgSect.ContentTypeBoundary != "" {
|
||||||
@ -294,8 +292,8 @@ func parseMessageSection(literal []byte, body string) MessageSection {
|
|||||||
return msgSect
|
return msgSect
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseContentType(literal []byte) (rfc822.MIMEType, string, string, string) {
|
func parseContentType(contentType string) (string, string, string, string) {
|
||||||
mimeType, params, err := rfc822.Parse(literal).ContentType()
|
mimeType, params, err := pmmime.ParseMediaType(contentType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@ -314,6 +312,15 @@ func parseContentType(literal []byte) (rfc822.MIMEType, string, string, string)
|
|||||||
return mimeType, boundary, charset, name
|
return mimeType, boundary, charset, name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseContentDisposition(contentDisp string) (string, string) {
|
||||||
|
disp, params, _ := pmmime.ParseMediaType(contentDisp)
|
||||||
|
name, ok := params["filename"]
|
||||||
|
if !ok {
|
||||||
|
name = ""
|
||||||
|
}
|
||||||
|
return disp, name
|
||||||
|
}
|
||||||
|
|
||||||
func matchMessages(have, want []Message) error {
|
func matchMessages(have, want []Message) error {
|
||||||
slices.SortFunc(have, func(a, b Message) bool {
|
slices.SortFunc(have, func(a, b Message) bool {
|
||||||
return a.Subject < b.Subject
|
return a.Subject < b.Subject
|
||||||
@ -331,70 +338,77 @@ func matchMessages(have, want []Message) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func matchStructure(have []MessageStruct, want MessageStruct) error {
|
func matchStructure(have []MessageStruct, want MessageStruct) error {
|
||||||
|
mismatches := make([]string, 0)
|
||||||
for _, msg := range have {
|
for _, msg := range have {
|
||||||
if want.From != "" && msg.From != want.From {
|
if want.From != "" && msg.From != want.From {
|
||||||
|
mismatches = append(mismatches, "From")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if want.To != "" && msg.To != want.To {
|
if want.To != "" && msg.To != want.To {
|
||||||
|
mismatches = append(mismatches, "To")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if want.BCC != "" && msg.BCC != want.BCC {
|
if want.BCC != "" && msg.BCC != want.BCC {
|
||||||
|
mismatches = append(mismatches, "BCC")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if want.CC != "" && msg.CC != want.CC {
|
if want.CC != "" && msg.CC != want.CC {
|
||||||
|
mismatches = append(mismatches, "CC")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if want.Subject != "" && msg.Subject != want.Subject {
|
if want.Subject != "" && msg.Subject != want.Subject {
|
||||||
|
mismatches = append(mismatches, "Subject")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if want.Date != "" && want.Date != msg.Date {
|
if want.Date != "" && want.Date != msg.Date {
|
||||||
|
mismatches = append(mismatches, "Date")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if matchContent(msg.Content, want.Content) {
|
if ok, mismatch := matchContent(msg.Content, want.Content); !ok {
|
||||||
return nil
|
mismatches = append(mismatches, "Content: "+mismatch)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
return fmt.Errorf("missing messages: have %#v, want %#v", have, want)
|
return fmt.Errorf("missing messages: have %#v, want %#v with mismatch list %#v", have, want, mismatches)
|
||||||
}
|
}
|
||||||
|
|
||||||
func matchContent(have MessageSection, want MessageSection) bool {
|
func matchContent(have MessageSection, want MessageSection) (bool, string) {
|
||||||
if want.ContentType != "" && want.ContentType != have.ContentType {
|
if want.ContentType != "" && want.ContentType != have.ContentType {
|
||||||
return false
|
return false, "ContentType"
|
||||||
}
|
}
|
||||||
if want.ContentTypeBoundary != "" && want.ContentTypeBoundary != have.ContentTypeBoundary {
|
if want.ContentTypeBoundary != "" && want.ContentTypeBoundary != have.ContentTypeBoundary {
|
||||||
return false
|
return false, "ContentTypeBoundary"
|
||||||
}
|
}
|
||||||
if want.ContentTypeCharset != "" && want.ContentTypeCharset != have.ContentTypeCharset {
|
if want.ContentTypeCharset != "" && want.ContentTypeCharset != have.ContentTypeCharset {
|
||||||
return false
|
return false, "ContentTypeCharset"
|
||||||
}
|
}
|
||||||
if want.ContentTypeName != "" && want.ContentTypeName != have.ContentTypeName {
|
if want.ContentTypeName != "" && want.ContentTypeName != have.ContentTypeName {
|
||||||
return false
|
return false, "ContentTypeName"
|
||||||
}
|
}
|
||||||
if want.ContentDisposition != "" && want.ContentDisposition != have.ContentDisposition {
|
if want.ContentDisposition != "" && want.ContentDisposition != have.ContentDisposition {
|
||||||
return false
|
return false, "ContentDisposition"
|
||||||
}
|
}
|
||||||
if want.ContentDispositionFilename != "" && want.ContentDispositionFilename != have.ContentDispositionFilename {
|
if want.ContentDispositionFilename != "" && want.ContentDispositionFilename != have.ContentDispositionFilename {
|
||||||
return false
|
return false, "ContentDispositionFilename"
|
||||||
}
|
}
|
||||||
if want.TransferEncoding != "" && want.TransferEncoding != have.TransferEncoding {
|
if want.TransferEncoding != "" && want.TransferEncoding != have.TransferEncoding {
|
||||||
return false
|
return false, "TransferEncoding"
|
||||||
}
|
}
|
||||||
if want.BodyContains != "" && !strings.Contains(strings.TrimSpace(have.BodyIs), strings.TrimSpace(want.BodyContains)) {
|
if want.BodyContains != "" && !strings.Contains(strings.TrimSpace(have.BodyIs), strings.TrimSpace(want.BodyContains)) {
|
||||||
return false
|
return false, "BodyContains"
|
||||||
}
|
}
|
||||||
if want.BodyIs != "" && strings.TrimSpace(have.BodyIs) != strings.TrimSpace(want.BodyIs) {
|
if want.BodyIs != "" && strings.TrimSpace(have.BodyIs) != strings.TrimSpace(want.BodyIs) {
|
||||||
return false
|
return false, "BodyIs"
|
||||||
}
|
|
||||||
if len(have.Sections) != len(want.Sections) {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, section := range want.Sections {
|
for i, section := range want.Sections {
|
||||||
if !matchContent(have.Sections[i], section) {
|
if ok, mismatch := matchContent(have.Sections[i], section); !ok {
|
||||||
return false
|
return false, fmt.Sprintf("section %#v - %#v", i, mismatch)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mailbox struct {
|
type Mailbox struct {
|
||||||
@ -551,3 +565,10 @@ type Contact struct {
|
|||||||
Sign string `bdd:"signature"`
|
Sign string `bdd:"signature"`
|
||||||
Encrypt string `bdd:"encryption"`
|
Encrypt string `bdd:"encryption"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MailSettings struct {
|
||||||
|
DraftMIMEType rfc822.MIMEType `bdd:"DraftMIMEType"`
|
||||||
|
AttachPublicKey proton.Bool `bdd:"AttachPublicKey"`
|
||||||
|
Sign proton.SignExternalMessages `bdd:"Sign"`
|
||||||
|
PGPScheme proton.EncryptionScheme `bdd:"PGPScheme"`
|
||||||
|
}
|
||||||
|
|||||||
@ -643,3 +643,29 @@ func (s *scenario) accountHasDefaultPGPSchema(account, schema string) error {
|
|||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *scenario) accountMatchesSettings(account string, table *godog.Table) error {
|
||||||
|
return s.t.withClient(context.Background(), account, func(ctx context.Context, c *proton.Client) error {
|
||||||
|
wantSettings, err := unmarshalTable[MailSettings](table)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
settings, err := c.GetMailSettings(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(wantSettings) != 1 {
|
||||||
|
return errors.New("this step only supports one settings definition at a time")
|
||||||
|
}
|
||||||
|
|
||||||
|
return matchSettings(settings, wantSettings[0])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchSettings(have proton.MailSettings, want MailSettings) error {
|
||||||
|
if !IsSub(ToAny(have), ToAny(want)) {
|
||||||
|
return fmt.Errorf("missing mailsettings: have %#v, want %#v", have, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
37
utils/debug/debug_assemble.go
Normal file
37
utils/debug/debug_assemble.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
// Copyright (c) 2023 Proton AG
|
||||||
|
//
|
||||||
|
// This file is part of Proton Mail Bridge.Bridge.
|
||||||
|
//
|
||||||
|
// Proton Mail 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.
|
||||||
|
//
|
||||||
|
// Proton Mail 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 Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) < 2 {
|
||||||
|
fmt.Printf("Usage: %v <dump dir>\n", os.Args[0])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := user.TryBuildDebugMessage(os.Args[1]); err != nil {
|
||||||
|
fmt.Printf("%v\n", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user