mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-16 07:06:45 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 40f2d8b30f | |||
| 95a1acec0d | |||
| 5ff074cc49 | |||
| 4f0660bb8c | |||
| 708184439e | |||
| b8a33b9618 | |||
| 1c385d5c9b | |||
| 96773f3225 | |||
| 0f320dbd80 | |||
| 6cb233473a | |||
| 1ac4e70115 | |||
| 07f93d276b |
@ -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 -->
|
||||||
|
|||||||
57
Changelog.md
57
Changelog.md
@ -3,6 +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
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* 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.0+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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
3
go.mod
3
go.mod
@ -7,7 +7,7 @@ require (
|
|||||||
github.com/Masterminds/semver/v3 v3.2.0
|
github.com/Masterminds/semver/v3 v3.2.0
|
||||||
github.com/ProtonMail/gluon v0.17.1-0.20231025125916-5c7941465df8
|
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.20231030091225-8fc2478b27f4
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20231106093533-5f248dfc820d
|
||||||
github.com/ProtonMail/gopenpgp/v2 v2.7.4-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
|
||||||
@ -121,5 +121,6 @@ require (
|
|||||||
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
|
||||||
)
|
)
|
||||||
|
|||||||
15
go.sum
15
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=
|
||||||
@ -34,8 +36,8 @@ 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.20231030091225-8fc2478b27f4 h1:1XISoHi1FmaVW3vm/y5FuXmrSMo53U0sM3zZpgczWTc=
|
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.20231030091225-8fc2478b27f4/go.mod h1:qgCy0LgMJy3bfVYyLljPScdB1bybc2adEkMr9WhBB5c=
|
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.4-proton h1:8tqHYM6IGsdEc6Vxf1TWiwpHNj8yIEQNACPhxsDagrk=
|
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton h1:8tqHYM6IGsdEc6Vxf1TWiwpHNj8yIEQNACPhxsDagrk=
|
||||||
@ -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=
|
||||||
@ -465,13 +465,13 @@ 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/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
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=
|
||||||
@ -521,6 +521,7 @@ 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/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
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=
|
||||||
@ -529,6 +530,8 @@ 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=
|
||||||
@ -544,6 +547,8 @@ 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/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=
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -636,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)
|
||||||
@ -719,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 {
|
||||||
|
|||||||
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--
|
||||||
@ -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.Response.Code != proton.SuccessCode {
|
if res[0].Response.Code != proton.SuccessCode {
|
||||||
return errors.New("APIError " + res[0].Response.Response.Message + " while creating contact")
|
return errors.New("APIError " + res[0].Response.Message + " while creating contact")
|
||||||
}
|
}
|
||||||
|
|
||||||
if settings != nil {
|
if settings != nil {
|
||||||
|
|||||||
@ -480,3 +480,170 @@ Feature: SMTP sending of plain messages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
Reference in New Issue
Block a user