Compare commits

..

10 Commits

Author SHA1 Message Date
248fbf5e33 chore: Vasco da Gama Bridge 3.6.1 changelog. 2023-10-18 15:41:01 +02:00
8b12a454ea fix(GODT-3033): Unable to receive new mail
If the IMAP service happened to finish syncing and wanted to reset the
user event service at a time the latter was publishing an event a
deadlock would occur and the user would not receive any new messages.

This change puts the request to revert the event id in a separate
go-routine to avoid this situation from re-occurring. The operational
flow remains unchanged as the event service will only process this
request once the current set of events have been published.
2023-10-18 14:46:14 +02:00
8be4246f7e chore: Vasco da Gama Bridge 3.6.0 changelog. 2023-10-11 16:09:55 +02:00
e580f89106 feat(GODT-3004): update gopenpgp and dependencies. 2023-10-11 15:29:52 +02:00
275b30e518 chore: Vasco da Gama Bridge 3.6.0 changelog. 2023-10-10 11:29:36 +02:00
bf244e5c86 fix(GODT-3003): Ensure IMAP State is reset after vault corruption
After we detect that the user has suffered the GODT-3003 bug due the
vault corruption not ensuring that a previous sync state would be
erased, we patch the gluon db directly and then reset the sync state.

After the account is added, the sync is automatically triggered and the
account state fixes itself.
2023-10-10 11:24:06 +02:00
cf9651bb94 fix(GODT-3001): Only create system labels during system label sync 2023-10-10 11:23:32 +02:00
ba65ffdbc7 chore: Umshiang Bridge 3.5.2 changelog. 2023-10-10 11:22:41 +02:00
d3582fa981 chore: Vasco da Gama Bridge 3.6.0 changelog. 2023-10-03 16:43:33 +02:00
80c852a5b2 fix(GODT-2992): fix link in 'no account view' in main window after 2FA or TOTP are cancelled.
(cherry picked from commit 1c344211d1)
2023-10-03 11:08:52 +02:00
83 changed files with 415 additions and 5639 deletions

View File

@ -18,10 +18,6 @@
---
image: gitlab.protontech.ch:4567/go/bridge-internal:test-go1.20
default:
tags:
- shared-small
variables:
GOPRIVATE: gitlab.protontech.ch
GOMAXPROCS: $(( ${CI_TAG_CPU} / 2 ))
@ -122,7 +118,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 url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
tags:
- shared-large
- large
# Stage: TEST
@ -133,7 +129,7 @@ lint:
script:
- make lint
tags:
- shared-medium
- medium
bug-report-preview:
stage: test
@ -142,7 +138,7 @@ bug-report-preview:
script:
- make lint-bug-report-preview
tags:
- shared-medium
- medium
.script-test:
stage: test
@ -158,7 +154,7 @@ test-linux:
extends:
- .script-test
tags:
- shared-large
- large
fuzz-linux:
stage: test
@ -167,7 +163,7 @@ fuzz-linux:
script:
- make fuzz
tags:
- shared-large
- large
test-linux-race:
extends:
@ -222,7 +218,7 @@ test-coverage:
- test-integration
- test-integration-nightly
tags:
- shared-small
- small
artifacts:
paths:
- coverage*
@ -286,7 +282,7 @@ build-windows-qa:
variables:
BUILD_TAGS: "build_qa"
trigger-qa-installer:
trigeer-qa-installer:
stage: build
needs: ["lint"]
extends:

View File

@ -133,6 +133,5 @@ 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)
* [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)
* [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)
<!-- END AUTOGEN -->

View File

@ -3,63 +3,12 @@
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
### Added

View File

@ -11,7 +11,7 @@ ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
.PHONY: build build-gui build-nogui build-launcher versioner hasher
# Keep version hardcoded so app build works also without Git repository.
BRIDGE_APP_VERSION?=3.7.0+git
BRIDGE_APP_VERSION?=3.6.1+git
APP_VERSION:=${BRIDGE_APP_VERSION}
APP_FULL_NAME:=Proton Mail Bridge
APP_VENDOR:=Proton AG

View File

@ -23,6 +23,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/app"
"github.com/bradenaw/juniper/xslices"
"github.com/sirupsen/logrus"
)
/*
@ -43,5 +44,7 @@ import (
*/
func main() {
_ = app.New().Run(xslices.Filter(os.Args, func(arg string) bool { return !strings.Contains(arg, "-psn_") }))
if err := app.New().Run(xslices.Filter(os.Args, func(arg string) bool { return !strings.Contains(arg, "-psn_") })); err != nil {
logrus.Fatal(err)
}
}

21
go.mod
View File

@ -5,10 +5,10 @@ go 1.20
require (
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
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.20231009084701-3af0474b0b3c
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
github.com/ProtonMail/go-proton-api v0.4.1-0.20231106093533-5f248dfc820d
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton
github.com/ProtonMail/go-proton-api v0.4.1-0.20231011132529-24b5b817ee1f
github.com/ProtonMail/gopenpgp/v2 v2.7.3-proton
github.com/PuerkitoBio/goquery v1.8.1
github.com/abiosoft/ishell v2.0.0+incompatible
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37
@ -43,10 +43,10 @@ require (
github.com/vmihailenco/msgpack/v5 v5.3.5
go.uber.org/goleak v1.2.1
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
golang.org/x/net v0.17.0
golang.org/x/sys v0.13.0
golang.org/x/text v0.13.0
google.golang.org/grpc v1.56.3
golang.org/x/net v0.10.0
golang.org/x/sys v0.8.0
golang.org/x/text v0.9.0
google.golang.org/grpc v1.53.0
google.golang.org/protobuf v1.30.0
howett.net/plist v1.0.0
)
@ -79,7 +79,7 @@ require (
github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/gofrs/uuid v4.3.0+incompatible // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
@ -110,17 +110,16 @@ require (
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/crypto v0.9.0 // indirect
golang.org/x/mod v0.8.0 // indirect
golang.org/x/sync v0.2.0 // indirect
golang.org/x/tools v0.6.0 // indirect
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
google.golang.org/genproto v0.0.0-20230221151758-ace64dc21148 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace (
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/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
)

46
go.sum
View File

@ -15,8 +15,6 @@ 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/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/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/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
@ -25,8 +23,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/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/gluon v0.17.1-0.20231025125916-5c7941465df8 h1:sG0o5pEoS2z2jNR9zK7Juq5Tr3X+GfHmQ8L99RPowaE=
github.com/ProtonMail/gluon v0.17.1-0.20231025125916-5c7941465df8/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo=
github.com/ProtonMail/gluon v0.17.1-0.20231009084701-3af0474b0b3c h1:gUDu4pOswgbou0QczfreNiXQFrmvVlpSh8Q+vft/JvI=
github.com/ProtonMail/gluon v0.17.1-0.20231009084701-3af0474b0b3c/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/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
@ -36,12 +34,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-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-proton-api v0.4.1-0.20231106093533-5f248dfc820d h1:LI2kvxBisX19f7lyMh0H6NcAHHg/Y7/x/xZWtxVrXOc=
github.com/ProtonMail/go-proton-api v0.4.1-0.20231106093533-5f248dfc820d/go.mod h1:WEXJqj5DSc2YI77SgXdpMY0nk33Qy92Vu2r4tOEazA8=
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.20231011132529-24b5b817ee1f/go.mod h1:ZmvQMA8hanLiD1tFsvu9+qGBcuxbIRfch/4z/nqBhXA=
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/gopenpgp/v2 v2.7.4-proton h1:8tqHYM6IGsdEc6Vxf1TWiwpHNj8yIEQNACPhxsDagrk=
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton/go.mod h1:omVkSsfPAhmptzPF/piMXb16wKIWUvVhZbVW7sJKh0A=
github.com/ProtonMail/gopenpgp/v2 v2.7.3-proton h1:wuAxBUU9qF2wyDVJprn/2xPDx000eol5gwlKbOUYY88=
github.com/ProtonMail/gopenpgp/v2 v2.7.3-proton/go.mod h1:omVkSsfPAhmptzPF/piMXb16wKIWUvVhZbVW7sJKh0A=
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw=
@ -157,6 +155,8 @@ 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/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-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/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
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.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.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
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/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.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.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
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-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-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-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.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.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.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.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-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -521,17 +521,14 @@ 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.6.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.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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
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.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
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.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
@ -542,13 +539,10 @@ 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.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.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
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-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-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -594,13 +588,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-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-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
google.golang.org/genproto v0.0.0-20230221151758-ace64dc21148 h1:muK+gVBJBfFb4SejshDBlN2/UgxCCOKH9Y34ljqEGOc=
google.golang.org/genproto v0.0.0-20230221151758-ace64dc21148/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw=
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.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc=
google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc=
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
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.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=

View File

@ -204,7 +204,7 @@ func run(c *cli.Context) error {
}()
// Restart the app if requested.
err = withRestarter(exe, func(restarter *restarter.Restarter) error {
return withRestarter(exe, func(restarter *restarter.Restarter) error {
// Handle crashes with various actions.
return withCrashHandler(restarter, reporter, func(crashHandler *crash.Handler, quitCh <-chan struct{}) error {
migrationErr := migrateOldVersions()
@ -276,9 +276,6 @@ func run(c *cli.Context) error {
b.PushError(bridge.ErrVaultCorrupt)
}
// Remove old updates files
b.RemoveOldUpdates()
// Start telemetry heartbeat process
b.StartHeartbeat(b)
@ -293,13 +290,6 @@ 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.

View File

@ -155,7 +155,7 @@ func newUpdater(locations *locations.Locations) (*updater.Updater, error) {
}
return updater.NewUpdater(
versioner.New(updatesDir),
updater.NewInstaller(versioner.New(updatesDir)),
verifier,
constants.UpdateName,
runtime.GOOS,

View File

@ -487,15 +487,27 @@ func (bridge *Bridge) remWatcher(watcher *watcher.Watcher[events.Event]) {
watcher.Close()
}
func (bridge *Bridge) onStatusUp(_ context.Context) {
func (bridge *Bridge) onStatusUp(ctx context.Context) {
logrus.Info("Handling API status up")
safe.RLock(func() {
for _, user := range bridge.users {
user.OnStatusUp(ctx)
}
}, bridge.usersLock)
bridge.goLoad()
}
func (bridge *Bridge) onStatusDown(ctx context.Context) {
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) {
select {
case <-ctx.Done():

View File

@ -19,7 +19,6 @@ package bridge
import (
"context"
"errors"
"io"
"github.com/ProtonMail/go-proton-api"
@ -34,133 +33,63 @@ const (
DefaultMaxSessionCountForBugReport = 10
)
type ReportBugReq struct {
OSType string
OSVersion string
Title string
Description string
Username string
Email string
EmailClient string
IncludeLogs bool
}
func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, title, description, username, email, client string, attachLogs bool) error {
var account = username
func (bridge *Bridge) ReportBug(ctx context.Context, report *ReportBugReq) error {
if info, err := bridge.QueryUserInfo(report.Username); err == nil {
report.Username = info.Username
if info, err := bridge.QueryUserInfo(username); err == nil {
account = info.Username
} else if userIDs := bridge.GetUserIDs(); len(userIDs) > 0 {
if err := bridge.vault.GetUser(userIDs[0], func(user *vault.User) {
report.Username = user.Username()
account = user.Username()
}); err != nil {
return err
}
}
var attachments []proton.ReportBugAttachment
if report.IncludeLogs {
logs, err := bridge.CollectLogs()
var attachment []proton.ReportBugAttachment
if attachLogs {
logsPath, err := bridge.locator.ProvideLogsPath()
if err != nil {
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,
})
}
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() {
safe.Lock(func() {
for _, user := range bridge.users {
user.ReportBugSent()
}
}, bridge.usersLock)
// if we have a token we can append more attachment to the bugReport
for i, att := range attachments {
if i == 0 && report.IncludeLogs {
continue
}
err := bridge.appendComment(ctx, token, att)
if err != nil {
return err
}
}
return err
}
return bridge.api.ReportBug(ctx, proton.ReportBugReq{
OS: osType,
OSVersion: osVersion,
func (bridge *Bridge) CollectLogs() (proton.ReportBugAttachment, error) {
logsPath, err := bridge.locator.ProvideLogsPath()
if err != nil {
return proton.ReportBugAttachment{}, err
}
Title: "[Bridge] Bug - " + title,
Description: description,
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,
Client: client,
ClientType: proton.ClientTypeEmail,
ClientVersion: constants.AppVersion(bridge.curVersion.Original()),
Username: report.Username,
Email: report.Email,
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...)
Username: account,
Email: email,
}, attachment...)
}

View File

@ -22,7 +22,7 @@ import (
)
func (bridge *Bridge) ReportBugClicked() {
safe.RLock(func() {
safe.Lock(func() {
for _, user := range bridge.users {
user.ReportBugClicked()
}
@ -30,7 +30,7 @@ func (bridge *Bridge) ReportBugClicked() {
}
func (bridge *Bridge) AutoconfigUsed(client string) {
safe.RLock(func() {
safe.Lock(func() {
for _, user := range bridge.users {
user.AutoconfigUsed(client)
}
@ -38,7 +38,7 @@ func (bridge *Bridge) AutoconfigUsed(client string) {
}
func (bridge *Bridge) KBArticleOpened(article string) {
safe.RLock(func() {
safe.Lock(func() {
for _, user := range bridge.users {
user.KBArticleOpened(article)
}

View File

@ -154,7 +154,3 @@ func (testUpdater *TestUpdater) GetVersionInfo(_ context.Context, _ updater.Down
func (testUpdater *TestUpdater) InstallUpdate(_ context.Context, _ updater.Downloader, _ updater.VersionInfo) error {
return nil
}
func (testUpdater *TestUpdater) RemoveOldUpdates() error {
return nil
}

View File

@ -336,9 +336,6 @@ func TestBridge_SendInvite(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;
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
Subject: A new message
@ -346,7 +343,7 @@ Date: Mon, 13 Mar 2023 16:06:16 +0100
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
Content-Disposition: attachment;
Content-Disposition: inline;
filename=Cat_August_2010-4.jpeg
Content-Type: image/jpeg;
name="Cat_August_2010-4.jpeg"
@ -363,7 +360,7 @@ Subject: A new message Part2
Date: Mon, 13 Mar 2023 16:06:16 +0100
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
Content-Disposition: attachment;
Content-Disposition: inline;
filename=Cat_August_2010-4.jpeg
Content-Type: image/jpeg;
name="Cat_August_2010-4.jpeg"
@ -523,181 +520,3 @@ 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)
})
})
}

View File

@ -53,5 +53,4 @@ type Autostarter interface {
type Updater interface {
GetVersionInfo(context.Context, updater.Downloader, updater.Channel) (updater.VersionInfo, error)
InstallUpdate(context.Context, updater.Downloader, updater.VersionInfo) error
RemoveOldUpdates() error
}

View File

@ -139,9 +139,3 @@ func (bridge *Bridge) installUpdate(ctx context.Context, job installJob) {
}
}, bridge.newVersionLock)
}
func (bridge *Bridge) RemoveOldUpdates() {
if err := bridge.updater.RemoveOldUpdates(); err != nil {
logrus.WithError(err).Error("Remove old updates fails")
}
}

View File

@ -46,8 +46,6 @@ const (
Connected
)
var ErrFailedToUnlock = errors.New("failed to unlock user keys")
type UserInfo struct {
// UserID is the user's API ID.
UserID string
@ -68,10 +66,10 @@ type UserInfo struct {
BridgePass []byte
// UsedSpace is the amount of space used by the user.
UsedSpace uint64
UsedSpace int
// MaxSpace is the total amount of space available to the user.
MaxSpace uint64
MaxSpace int
}
// GetUserIDs returns the IDs of all known users (authorized or not).
@ -159,15 +157,11 @@ func (bridge *Bridge) LoginUser(
func() (string, error) {
return bridge.loginUser(ctx, client, auth.UID, auth.RefreshToken, keyPass)
},
func() error {
return client.AuthDelete(ctx)
},
)
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)
}
@ -223,16 +217,7 @@ func (bridge *Bridge) LoginFull(
keyPass = password
}
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
return bridge.LoginUser(ctx, client, auth, keyPass)
}
// LogoutUser logs out the given user.
@ -329,7 +314,7 @@ func (bridge *Bridge) SetAddressMode(ctx context.Context, userID string, mode va
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")
return safe.RLockRet(func() error {
return safe.LockRet(func() error {
ctx := context.Background()
user, ok := bridge.users[userID]
@ -389,9 +374,9 @@ func (bridge *Bridge) loginUser(ctx context.Context, client *proton.Client, auth
}
if userKR, err := apiUser.Keys.Unlock(saltedKeyPass, nil); err != nil {
return "", fmt.Errorf("%w: %w", ErrFailedToUnlock, err)
return "", fmt.Errorf("failed to unlock user keys: %w", err)
} else if userKR.CountDecryptionEntities() == 0 {
return "", ErrFailedToUnlock
return "", fmt.Errorf("failed to unlock user keys")
}
if err := bridge.addUser(ctx, client, apiUser, authUID, authRef, saltedKeyPass, true); err != nil {

View File

@ -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) {
safe.RLock(func() {
safe.Lock(func() {
if rerr := bridge.reporter.ReportMessageWithContext("Failed to handle event", reporter.Context{
"user_id": user.ID(),
"old_event_id": event.OldEventID,

View File

@ -95,13 +95,6 @@ func (status *ConfigurationStatus) IsPending() bool {
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 {
status.DataLock.RLock()
defer status.DataLock.RUnlock()

View File

@ -19,6 +19,7 @@ package configstatus
import (
"strconv"
"time"
)
type ConfigAbortValues struct {
@ -40,20 +41,17 @@ type ConfigAbortData struct {
type ConfigAbortBuilder struct{}
func (*ConfigAbortBuilder) New(config *ConfigurationStatus) ConfigAbortData {
config.DataLock.RLock()
defer config.DataLock.RUnlock()
func (*ConfigAbortBuilder) New(data *ConfigurationStatusData) ConfigAbortData {
return ConfigAbortData{
MeasurementGroup: "bridge.any.configuration",
Event: "bridge_config_abort",
Values: ConfigSuccessValues{
Duration: config.isPendingSinceMin(),
Duration: int(time.Since(data.DataV1.PendingSince).Minutes()),
},
Dimensions: ConfigSuccessDimensions{
ReportClick: strconv.FormatBool(config.Data.DataV1.ReportClick),
ReportSent: strconv.FormatBool(config.Data.DataV1.ReportSent),
ClickedLink: config.Data.clickedLinkToString(),
ReportClick: strconv.FormatBool(data.DataV1.ReportClick),
ReportSent: strconv.FormatBool(data.DataV1.ReportSent),
ClickedLink: data.clickedLinkToString(),
},
}
}

View File

@ -33,7 +33,7 @@ func TestConfigurationAbort_default(t *testing.T) {
require.NoError(t, err)
var builder = configstatus.ConfigAbortBuilder{}
req := builder.New(config)
req := builder.New(config.Data)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_abort", req.Event)
@ -64,7 +64,7 @@ func TestConfigurationAbort_fed(t *testing.T) {
require.NoError(t, err)
var builder = configstatus.ConfigAbortBuilder{}
req := builder.New(config)
req := builder.New(config.Data)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_abort", req.Event)

View File

@ -33,16 +33,13 @@ type ConfigProgressData struct {
type ConfigProgressBuilder struct{}
func (*ConfigProgressBuilder) New(config *ConfigurationStatus) ConfigProgressData {
config.DataLock.RLock()
defer config.DataLock.RUnlock()
func (*ConfigProgressBuilder) New(data *ConfigurationStatusData) ConfigProgressData {
return ConfigProgressData{
MeasurementGroup: "bridge.any.configuration",
Event: "bridge_config_progress",
Values: ConfigProgressValues{
NbDay: numberOfDay(time.Now(), config.Data.DataV1.PendingSince),
NbDaySinceLast: numberOfDay(time.Now(), config.Data.DataV1.LastProgress),
NbDay: numberOfDay(time.Now(), data.DataV1.PendingSince),
NbDaySinceLast: numberOfDay(time.Now(), data.DataV1.LastProgress),
},
}
}

View File

@ -33,7 +33,7 @@ func TestConfigurationProgress_default(t *testing.T) {
require.NoError(t, err)
var builder = configstatus.ConfigProgressBuilder{}
req := builder.New(config)
req := builder.New(config.Data)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_progress", req.Event)
@ -62,7 +62,7 @@ func TestConfigurationProgress_fed(t *testing.T) {
require.NoError(t, err)
var builder = configstatus.ConfigProgressBuilder{}
req := builder.New(config)
req := builder.New(config.Data)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_progress", req.Event)

View File

@ -19,6 +19,7 @@ package configstatus
import (
"strconv"
"time"
)
type ConfigRecoveryValues struct {
@ -42,22 +43,19 @@ type ConfigRecoveryData struct {
type ConfigRecoveryBuilder struct{}
func (*ConfigRecoveryBuilder) New(config *ConfigurationStatus) ConfigRecoveryData {
config.DataLock.RLock()
defer config.DataLock.RUnlock()
func (*ConfigRecoveryBuilder) New(data *ConfigurationStatusData) ConfigRecoveryData {
return ConfigRecoveryData{
MeasurementGroup: "bridge.any.configuration",
Event: "bridge_config_recovery",
Values: ConfigRecoveryValues{
Duration: config.isPendingSinceMin(),
Duration: int(time.Since(data.DataV1.PendingSince).Minutes()),
},
Dimensions: ConfigRecoveryDimensions{
Autoconf: config.Data.DataV1.Autoconf,
ReportClick: strconv.FormatBool(config.Data.DataV1.ReportClick),
ReportSent: strconv.FormatBool(config.Data.DataV1.ReportSent),
ClickedLink: config.Data.clickedLinkToString(),
FailureDetails: config.Data.DataV1.FailureDetails,
Autoconf: data.DataV1.Autoconf,
ReportClick: strconv.FormatBool(data.DataV1.ReportClick),
ReportSent: strconv.FormatBool(data.DataV1.ReportSent),
ClickedLink: data.clickedLinkToString(),
FailureDetails: data.DataV1.FailureDetails,
},
}
}

View File

@ -33,7 +33,7 @@ func TestConfigurationRecovery_default(t *testing.T) {
require.NoError(t, err)
var builder = configstatus.ConfigRecoveryBuilder{}
req := builder.New(config)
req := builder.New(config.Data)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_recovery", req.Event)
@ -66,7 +66,7 @@ func TestConfigurationRecovery_fed(t *testing.T) {
require.NoError(t, err)
var builder = configstatus.ConfigRecoveryBuilder{}
req := builder.New(config)
req := builder.New(config.Data)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_recovery", req.Event)

View File

@ -19,6 +19,7 @@ package configstatus
import (
"strconv"
"time"
)
type ConfigSuccessValues struct {
@ -41,21 +42,18 @@ type ConfigSuccessData struct {
type ConfigSuccessBuilder struct{}
func (*ConfigSuccessBuilder) New(config *ConfigurationStatus) ConfigSuccessData {
config.DataLock.RLock()
defer config.DataLock.RUnlock()
func (*ConfigSuccessBuilder) New(data *ConfigurationStatusData) ConfigSuccessData {
return ConfigSuccessData{
MeasurementGroup: "bridge.any.configuration",
Event: "bridge_config_success",
Values: ConfigSuccessValues{
Duration: config.isPendingSinceMin(),
Duration: int(time.Since(data.DataV1.PendingSince).Minutes()),
},
Dimensions: ConfigSuccessDimensions{
Autoconf: config.Data.DataV1.Autoconf,
ReportClick: strconv.FormatBool(config.Data.DataV1.ReportClick),
ReportSent: strconv.FormatBool(config.Data.DataV1.ReportSent),
ClickedLink: config.Data.clickedLinkToString(),
Autoconf: data.DataV1.Autoconf,
ReportClick: strconv.FormatBool(data.DataV1.ReportClick),
ReportSent: strconv.FormatBool(data.DataV1.ReportSent),
ClickedLink: data.clickedLinkToString(),
},
}
}

View File

@ -33,7 +33,7 @@ func TestConfigurationSuccess_default(t *testing.T) {
require.NoError(t, err)
var builder = configstatus.ConfigSuccessBuilder{}
req := builder.New(config)
req := builder.New(config.Data)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_success", req.Event)
@ -65,7 +65,7 @@ func TestConfigurationSuccess_fed(t *testing.T) {
require.NoError(t, err)
var builder = configstatus.ConfigSuccessBuilder{}
req := builder.New(config)
req := builder.New(config.Data)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_success", req.Event)

View File

@ -175,7 +175,7 @@ type UsedSpaceChanged struct {
UserID string
UsedSpace uint64
UsedSpace int
}
func (event UsedSpaceChanged) String() string {

View File

@ -368,7 +368,7 @@ Item {
currentIndex: hasAccount() ? 1 : 0
NoAccountView {
colorScheme: root.colorScheme
onStartSetup: {
onLinkClicked: function() {
root.showLogin("")
}
}

View File

@ -23,7 +23,7 @@ Rectangle {
color: root.colorScheme.background_norm
signal startSetup()
signal linkClicked()
ColumnLayout {
anchors.fill: parent
@ -38,10 +38,8 @@ Rectangle {
wizard: setupWizard
Component.onCompleted: {
showNoAccount();
}
onStartSetup: {
root.startSetup();
showOnboarding();
link1.setCallback(root.linkClicked, "Start setup", false)
}
}
Image {

View File

@ -728,12 +728,10 @@ QtObject {
}
property Notification noKeychain: Notification {
brief: title
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.")
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.")
group: Notifications.Group.Dialogs | Notifications.Group.Configuration
icon: "./icons/ic-exclamation-circle-filled.svg"
title: Backend.goos === "darwin" ? qsTr("Cannot access keychain") : qsTr("No keychain available")
title: qsTr("No keychain available")
type: Notification.NotificationType.Danger
action: [

View File

@ -114,9 +114,6 @@ FocusScope {
function getText(start, end) {
control.getText(start, end);
}
function hidePassword() {
eyeButton.checked = false;
}
function insert(position, text) {
control.insert(position, text);
}
@ -150,9 +147,6 @@ FocusScope {
function selectWord() {
control.selectWord();
}
function showPassword() {
eyeButton.checked = true;
}
function undo() {
control.undo();
}

View File

@ -18,21 +18,14 @@ import QtQuick.Controls
Item {
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 string iconSource
property int iconWidth
property var wizard
property ColorScheme colorScheme
property var _colorScheme: wizard ? wizard.colorScheme : colorScheme
signal startSetup()
property var link1: linkLabel1
property var link2: linkLabel2
function showAppleMailAutoconfigCertificateInstall() {
showAppleMailAutoconfigCommon();
@ -72,25 +65,14 @@ Item {
function showLoginMailboxPassword() {
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() {
titleLabel.text = (Backend.users.count === 0) ? welcomeTitle : addAccountTitle;
descriptionLabel.text = welcomeDescription
titleLabel.text = (Backend.users.count === 0) ? qsTr("Welcome to\nProton Mail Bridge") : qsTr("Add a Proton Mail account");
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. ");
linkLabel1.setCallback(function() { Backend.openKBArticle("https://proton.me/support/why-you-need-bridge"); }, qsTr("Why do I need Bridge?"), true);
linkLabel2.clear();
root.iconSource = welcomeImage;
root.iconHeight = welcomeImageHeight;
root.iconWidth = welcomeImageWidth;
root.iconSource = "/qml/icons/img-welcome.svg";
root.iconHeight = 148;
root.iconWidth = 265;
}
ColumnLayout {

View File

@ -44,8 +44,6 @@ FocusScope {
} else {
passwordTextField.forceActiveFocus();
}
passwordTextField.hidePassword();
secondPasswordTextField.hidePassword();
}
StackLayout {

View File

@ -309,8 +309,6 @@ void User::setIsSyncing(bool syncing) {
}
isSyncing_ = syncing;
syncProgress_ = 0;
emit isSyncingChanged(syncing);
}

View File

@ -193,7 +193,7 @@ func NewUserBadEvent(userID string, errorMessage string) *StreamEvent {
return userEvent(&UserEvent{Event: &UserEvent_UserBadEvent{UserBadEvent: &UserBadEvent{UserID: userID, ErrorMessage: errorMessage}}})
}
func NewUsedBytesChangedEvent(userID string, usedBytes uint64) *StreamEvent {
func NewUsedBytesChangedEvent(userID string, usedBytes int) *StreamEvent {
return userEvent(&UserEvent{Event: &UserEvent_UsedBytesChangedEvent{UsedBytesChangedEvent: &UsedBytesChangedEvent{UserID: userID, UsedBytes: int64(usedBytes)}}})
}

View File

@ -54,9 +54,8 @@ import (
)
const (
serverConfigFileName = "grpcServerConfig.json"
serverTokenMetadataKey = "server-token"
twoPasswordsMaxAttemptCount = 3 // The number of attempts allowed for the mailbox password.
serverConfigFileName = "grpcServerConfig.json"
serverTokenMetadataKey = "server-token"
)
// Service is the RPC service struct.
@ -83,10 +82,9 @@ type Service struct { // nolint:structcheck
target updater.VersionInfo
targetLock safe.RWMutex
authClient *proton.Client
auth proton.Auth
password []byte
twoPasswordAttemptCount int
authClient *proton.Client
auth proton.Auth
password []byte
log *logrus.Entry
initializing sync.WaitGroup
@ -340,11 +338,6 @@ func (s *Service) watchEvents() {
case events.SyncFinished:
_ = s.SendEvent(NewSyncFinishedEvent(event.UserID))
case events.SyncFailed:
if errors.Is(event.Error, context.Canceled) {
_ = s.SendEvent(NewSyncFinishedEvent(event.UserID))
}
case events.SyncProgress:
_ = s.SendEvent(NewSyncProgressEvent(event.UserID, event.Progress, event.Elapsed.Milliseconds(), event.Remaining.Milliseconds()))
@ -415,12 +408,7 @@ func (s *Service) loginClean() {
}
func (s *Service) finishLogin() {
performCleanup := true
defer func() {
if performCleanup {
s.loginClean()
}
}()
defer s.loginClean()
wasSignedOut := s.bridge.HasUser(s.auth.UserID)
@ -438,24 +426,10 @@ func (s *Service) finishLogin() {
eventCh, done := s.bridge.GetEvents(events.UserLoggedIn{})
defer done()
ctx := context.Background()
userID, err := s.bridge.LoginUser(ctx, s.authClient, s.auth, s.password)
userID, err := s.bridge.LoginUser(context.Background(), s.authClient, s.auth, s.password)
if err != nil {
s.log.WithError(err).Errorf("Finish login failed")
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()))
_ = s.SendEvent(NewLoginError(LoginErrorType_TWO_PASSWORDS_ABORT, err.Error()))
return
}

View File

@ -339,17 +339,18 @@ func (s *Service) ReportBug(_ context.Context, report *ReportBugRequest) (*empty
defer async.HandlePanic(s.panicHandler)
defer func() { _ = s.SendEvent(NewReportBugFinishedEvent()) }()
reportReq := bridge.ReportBugReq{
OSType: report.OsType,
OSVersion: report.OsVersion,
Title: report.Title,
Description: report.Description,
Username: report.Address,
Email: report.Address,
EmailClient: report.EmailClient,
IncludeLogs: report.IncludeLogs,
}
if err := s.bridge.ReportBug(context.Background(), &reportReq); err != nil {
if err := s.bridge.ReportBug(
context.Background(),
report.OsType,
report.OsVersion,
report.Title,
report.Description,
report.Address,
report.Address,
report.EmailClient,
report.IncludeLogs,
); err != nil {
s.log.WithError(err).Error("Failed to report bug")
_ = s.SendEvent(NewReportBugErrorEvent())
return
@ -383,7 +384,6 @@ func (s *Service) Login(_ context.Context, login *LoginRequest) (*emptypb.Empty,
go func() {
defer async.HandlePanic(s.panicHandler)
s.twoPasswordAttemptCount = 0
password, err := base64Decode(login.Password)
if err != nil {
s.log.WithError(err).Error("Cannot decode password")

View File

@ -152,7 +152,7 @@ func NewService(
connectors: make(map[string]*Connector),
maxSyncMemory: maxSyncMemory,
eventWatcher: subscription.Add(events.IMAPServerCreated{}, events.ConnStatusUp{}, events.ConnStatusDown{}),
eventWatcher: subscription.Add(events.IMAPServerCreated{}),
eventSubscription: subscription,
showAllMail: showAllMail,
@ -218,6 +218,18 @@ func (s *Service) Resync(ctx context.Context) error {
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 {
_, err := s.cpc.Send(ctx, &onBadEventReq{})
@ -330,7 +342,6 @@ func (s *Service) run(ctx context.Context) { //nolint gocyclo
}
switch r := req.Value().(type) {
case *setAddressModeReq:
s.log.Debug("Set Address Mode Request")
err := s.setAddressMode(ctx, r.mode)
req.Reply(ctx, nil, err)
@ -340,33 +351,38 @@ func (s *Service) run(ctx context.Context) { //nolint gocyclo
req.Reply(ctx, nil, err)
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:
s.log.Debug("Get labels Request")
labels := s.labels.GetLabelMap()
req.Reply(ctx, labels, nil)
case *onBadEventReq:
s.log.Debug("Bad Event Request")
err := s.removeConnectorsFromServer(ctx, s.connectors, false)
req.Reply(ctx, nil, err)
case *onBadEventResyncReq:
s.log.Debug("Bad Event Resync Request")
err := s.addConnectorsToServer(ctx, s.connectors)
req.Reply(ctx, nil, err)
case *onLogoutReq:
s.log.Debug("Logout Request")
err := s.removeConnectorsFromServer(ctx, s.connectors, false)
req.Reply(ctx, nil, err)
case *showAllMailReq:
s.log.Debug("Show all mail request")
req.Reply(ctx, nil, nil)
s.setShowAllMail(r.v)
case *getSyncFailedMessagesReq:
s.log.Debug("Get sync failed messages Request")
status, err := s.syncStateProvider.GetSyncStatus(ctx)
if err != nil {
req.Reply(ctx, nil, fmt.Errorf("failed to get sync status: %w", err))
@ -460,21 +476,10 @@ func (s *Service) run(ctx context.Context) { //nolint gocyclo
continue
}
switch e.(type) {
case events.IMAPServerCreated:
s.log.Debug("On IMAPServerCreated")
if _, ok := e.(events.IMAPServerCreated); ok {
if err := s.addConnectorsToServer(ctx, s.connectors); err != nil {
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()
}
}
}
@ -627,6 +632,10 @@ func (s *Service) cancelSync() {
type resyncReq struct{}
type cancelSyncReq struct{}
type resumeSyncReq struct{}
type getLabelsReq struct{}
type onBadEventReq struct{}

View File

@ -95,11 +95,6 @@ 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 sender, ok := getMessageSender(parser); ok {
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.
@ -225,7 +220,7 @@ func (s *Service) sendWithKey(
ExternalID: message.ExternalID,
})
if err != nil {
return proton.Message{}, fmt.Errorf("failed to create draft: %w", err)
return proton.Message{}, fmt.Errorf("failed to create attachments: %w", err)
}
attKeys, err := s.createAttachments(ctx, s.client, addrKR, draft.ID, message.Attachments)
@ -320,11 +315,7 @@ func getParentID(
switch len(metadata) {
case 1:
// found exactly one parent
// 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
}
parentID = metadata[0].ID
case 0:
// found no parents
default:

View File

@ -18,14 +18,11 @@
package smtp
import (
"fmt"
"github.com/ProtonMail/gluon/rfc822"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/v3/pkg/message"
"github.com/bradenaw/juniper/xslices"
"github.com/sirupsen/logrus"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
)
@ -46,9 +43,6 @@ func createSendReq(
}
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 err := req.AddTextPackage(kr, string(richBody), rfc822.TextHTML, recs, attKeys); err != nil {
return proton.SendDraftReq{}, err
@ -60,10 +54,6 @@ func createSendReq(
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

File diff suppressed because it is too large Load Diff

View File

@ -24,7 +24,6 @@ import (
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
const (
@ -548,7 +547,6 @@ func (b *sendPrefsBuilder) setEncryptionPreferences(mailSettings proton.MailSett
// Otherwise keep the defined value.
switch mailSettings.PGPScheme {
case proton.PGPInlineScheme:
logrus.WithFields(logrus.Fields{"service": "smtp", "settings": "account"}).Warn("PGPInline scheme used. Planed to be deprecated.")
b.withSchemeDefault(pgpInline)
case proton.PGPMIMEScheme:
b.withSchemeDefault(pgpMIME)

View File

@ -296,7 +296,7 @@ func (m *MockUserUsedSpaceEventHandler) EXPECT() *MockUserUsedSpaceEventHandlerM
}
// HandleUsedSpaceEvent mocks base method.
func (m *MockUserUsedSpaceEventHandler) HandleUsedSpaceEvent(arg0 context.Context, arg1 int64) error {
func (m *MockUserUsedSpaceEventHandler) HandleUsedSpaceEvent(arg0 context.Context, arg1 int) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HandleUsedSpaceEvent", arg0, arg1)
ret0, _ := ret[0].(error)

View File

@ -29,7 +29,6 @@ import (
"time"
"github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/gluon/watcher"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/proton-bridge/v3/internal"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
@ -68,8 +67,6 @@ type Service struct {
eventPollWaiters []*EventPollWaiter
eventPollWaitersLock sync.Mutex
eventSubscription events.Subscription
eventWatcher *watcher.Watcher[events.Event]
}
func NewService(
@ -81,7 +78,6 @@ func NewService(
jitter time.Duration,
eventTimeout time.Duration,
panicHandler async.PanicHandler,
eventSubscription events.Subscription,
) *Service {
return &Service{
cpc: cpc.NewCPC(),
@ -92,13 +88,11 @@ func NewService(
"service": "user-events",
"user": userID,
}),
eventPublisher: eventPublisher,
timer: proton.NewTicker(pollPeriod, jitter, panicHandler),
paused: 1,
eventTimeout: eventTimeout,
panicHandler: panicHandler,
eventSubscription: eventSubscription,
eventWatcher: eventSubscription.Add(events.ConnStatusDown{}, events.ConnStatusUp{}),
eventPublisher: eventPublisher,
timer: proton.NewTicker(pollPeriod, jitter, panicHandler),
paused: 1,
eventTimeout: eventTimeout,
panicHandler: panicHandler,
}
}
@ -230,19 +224,6 @@ func (s *Service) run(ctx context.Context, lastEventID string) {
}
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.
@ -314,11 +295,6 @@ 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.
func (s *Service) Close() {
if s.eventSubscription != nil {
s.eventSubscription.Remove(s.eventWatcher)
s.eventSubscription = nil
}
s.pendingSubscriptionsLock.Lock()
defer s.pendingSubscriptionsLock.Unlock()

View File

@ -48,7 +48,6 @@ func TestServiceHandleEventError_SubscriberEventUnwrapping(t *testing.T) {
time.Millisecond,
time.Second,
async.NoopPanicHandler{},
events.NewNullSubscription(),
)
lastEventID := "PrevEvent"
@ -86,7 +85,6 @@ func TestServiceHandleEventError_BadEventPutsServiceOnPause(t *testing.T) {
time.Millisecond,
time.Second,
async.NoopPanicHandler{},
events.NewNullSubscription(),
)
service.Resume()
lastEventID := "PrevEvent"
@ -120,7 +118,6 @@ func TestServiceHandleEventError_BadEventFromPublishTimeout(t *testing.T) {
time.Millisecond,
time.Second,
async.NoopPanicHandler{},
events.NewNullSubscription(),
)
lastEventID := "PrevEvent"
event := proton.Event{EventID: "MyEvent"}
@ -151,7 +148,6 @@ func TestServiceHandleEventError_NoBadEventCheck(t *testing.T) {
time.Millisecond,
time.Second,
async.NoopPanicHandler{},
events.NewNullSubscription(),
)
lastEventID := "PrevEvent"
event := proton.Event{EventID: "MyEvent"}
@ -177,7 +173,6 @@ func TestServiceHandleEventError_JsonUnmarshalEventProducesUncategorizedErrorEve
time.Millisecond,
time.Second,
async.NoopPanicHandler{},
events.NewNullSubscription(),
)
lastEventID := "PrevEvent"
event := proton.Event{EventID: "MyEvent"}

View File

@ -26,7 +26,6 @@ import (
"github.com/ProtonMail/gluon/async"
"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/golang/mock/gomock"
"github.com/stretchr/testify/require"
@ -68,7 +67,6 @@ func TestServiceHandleEvent_CheckEventCategoriesHandledInOrder(t *testing.T) {
time.Millisecond,
10*time.Second,
async.NoopPanicHandler{},
events.NewNullSubscription(),
)
subscription := NewCallbackSubscriber("test", EventHandler{
@ -86,7 +84,7 @@ func TestServiceHandleEvent_CheckEventCategoriesHandledInOrder(t *testing.T) {
require.NoError(t, service.handleEvent(context.Background(), "", proton.Event{Refresh: proton.RefreshMail}))
// Simulate Regular event.
usedSpace := int64(20)
usedSpace := 20
require.NoError(t, service.handleEvent(context.Background(), "", proton.Event{
User: new(proton.User),
Addresses: []proton.AddressEvent{
@ -129,7 +127,6 @@ func TestServiceHandleEvent_CheckEventFailureCausesError(t *testing.T) {
time.Millisecond,
time.Second,
async.NoopPanicHandler{},
events.NewNullSubscription(),
)
subscription := NewCallbackSubscriber("test", EventHandler{
@ -167,7 +164,6 @@ func TestServiceHandleEvent_CheckEventFailureCausesErrorParallel(t *testing.T) {
time.Millisecond,
time.Second,
async.NoopPanicHandler{},
events.NewNullSubscription(),
)
subscription := NewCallbackSubscriber("test", EventHandler{

View File

@ -75,7 +75,6 @@ func TestService_EventIDLoadStore(t *testing.T) {
time.Millisecond,
time.Second,
async.NoopPanicHandler{},
events.NewNullSubscription(),
)
_, err := service.Start(context.Background(), group)
@ -131,7 +130,6 @@ func TestService_RetryEventOnNonCatastrophicFailure(t *testing.T) {
time.Millisecond,
time.Second,
async.NoopPanicHandler{},
events.NewNullSubscription(),
)
service.Subscribe(NewCallbackSubscriber("foo", EventHandler{MessageHandler: subscriber}))
@ -181,7 +179,6 @@ func TestService_OnBadEventServiceIsPaused(t *testing.T) {
time.Millisecond,
time.Second,
async.NoopPanicHandler{},
events.NewNullSubscription(),
)
// Event publisher expectations.
@ -248,7 +245,6 @@ func TestService_UnsubscribeDuringEventHandlingDoesNotCauseDeadlock(t *testing.T
time.Millisecond,
time.Second,
async.NoopPanicHandler{},
events.NewNullSubscription(),
)
subscription := NewCallbackSubscriber("foo", EventHandler{MessageHandler: subscriber})
@ -308,7 +304,6 @@ func TestService_UnsubscribeBeforeHandlingEventIsNotConsideredError(t *testing.T
time.Millisecond,
time.Second,
async.NoopPanicHandler{},
events.NewNullSubscription(),
)
subscription := NewEventSubscriber("Foo")
@ -368,7 +363,6 @@ func TestService_WaitOnEventPublishAfterPause(t *testing.T) {
time.Millisecond,
time.Second,
async.NoopPanicHandler{},
events.NewNullSubscription(),
)
subscriber.EXPECT().HandleMessageEvents(gomock.Any(), gomock.Eq(messageEvents)).Times(1).DoAndReturn(func(_ context.Context, _ []proton.MessageEvent) error {
@ -441,7 +435,6 @@ func TestService_EventRewind(t *testing.T) {
time.Millisecond,
time.Second,
async.NoopPanicHandler{},
events.NewNullSubscription(),
)
_, err := service.Start(context.Background(), group)

View File

@ -98,7 +98,7 @@ type UserEventHandler interface {
}
type UserUsedSpaceEventHandler interface {
HandleUsedSpaceEvent(ctx context.Context, newSpace int64) error
HandleUsedSpaceEvent(ctx context.Context, newSpace int) error
}
type UserSettingsHandler interface {

View File

@ -102,13 +102,13 @@ func (s *Service) CheckAuth(ctx context.Context, email string, password []byte)
})
}
func (s *Service) HandleUsedSpaceEvent(ctx context.Context, newSpace int64) error {
func (s *Service) HandleUsedSpaceEvent(ctx context.Context, newSpace int) error {
s.log.Info("Handling User Space Changed event")
if s.identity.OnUserSpaceChanged(uint64(newSpace)) {
if s.identity.OnUserSpaceChanged(newSpace) {
s.eventPublisher.PublishEvent(ctx, events.UsedSpaceChanged{
UserID: s.identity.User.ID,
UsedSpace: uint64(newSpace),
UsedSpace: newSpace,
})
}

View File

@ -54,7 +54,7 @@ func TestService_OnUserSpaceChanged(t *testing.T) {
// New value, event should be published.
require.NoError(t, service.HandleUsedSpaceEvent(context.Background(), 1024))
require.Equal(t, uint64(1024), service.identity.User.UsedSpace)
require.Equal(t, 1024, service.identity.User.UsedSpace)
}
func TestService_OnRefreshEvent(t *testing.T) {

View File

@ -119,7 +119,7 @@ func (s *State) OnRefreshEvent(ctx context.Context) error {
return nil
}
func (s *State) OnUserSpaceChanged(value uint64) bool {
func (s *State) OnUserSpaceChanged(value int) bool {
if s.User.UsedSpace == value {
return false
}

View File

@ -26,7 +26,6 @@ import (
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/v3/internal/versioner"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
@ -47,17 +46,15 @@ type Installer interface {
}
type Updater struct {
versioner *versioner.Versioner
installer Installer
verifier *crypto.KeyRing
product string
platform string
}
func NewUpdater(ver *versioner.Versioner, verifier *crypto.KeyRing, product, platform string) *Updater {
func NewUpdater(installer Installer, verifier *crypto.KeyRing, product, platform string) *Updater {
return &Updater{
versioner: ver,
installer: NewInstaller(ver),
installer: installer,
verifier: verifier,
product: product,
platform: platform,
@ -112,10 +109,6 @@ func (u *Updater) InstallUpdate(ctx context.Context, downloader Downloader, upda
return nil
}
func (u *Updater) RemoveOldUpdates() error {
return u.versioner.RemoveOldVersions()
}
// getVersionFileURL returns the URL of the version file.
// For example:
// - https://protonmail.com/download/bridge/version_linux.json

View File

@ -38,7 +38,7 @@ func (user *User) SendConfigStatusSuccess(ctx context.Context) {
}
var builder configstatus.ConfigSuccessBuilder
success := builder.New(user.configStatus)
success := builder.New(user.configStatus.Data)
data, err := json.Marshal(success)
if err != nil {
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
}
var builder configstatus.ConfigAbortBuilder
abort := builder.New(user.configStatus)
abort := builder.New(user.configStatus.Data)
data, err := json.Marshal(abort)
if err != nil {
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
success := builder.New(user.configStatus)
success := builder.New(user.configStatus.Data)
data, err := json.Marshal(success)
if err != nil {
if err := user.reporter.ReportMessageWithContext("Cannot parse config_recovery data.", reporter.Context{
@ -125,7 +125,7 @@ func (user *User) SendConfigStatusProgress(ctx context.Context) {
return
}
var builder configstatus.ConfigProgressBuilder
progress := builder.New(user.configStatus)
progress := builder.New(user.configStatus.Data)
if progress.Values.NbDay == 0 {
return
}

View File

@ -22,7 +22,6 @@ import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"mime"
@ -38,7 +37,6 @@ import (
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/vault"
bmessage "github.com/ProtonMail/proton-bridge/v3/pkg/message"
"github.com/bradenaw/juniper/xmaps"
"github.com/bradenaw/juniper/xslices"
"github.com/emersion/go-message"
@ -226,55 +224,6 @@ func (user *User) DebugDownloadMessages(
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 {
return filepath.Join(path, "body.txt")
}
@ -348,16 +297,16 @@ func decodeSimpleMessage(outPath string, kr *crypto.KeyRing, msg proton.Message)
return nil
}
type DebugMetadata struct {
proton.MessageMetadata
Header string
ParsedHeaders proton.Headers
MIMEType rfc822.MIMEType
Attachments []proton.Attachment
}
func writeMetadata(outPath string, msg proton.Message) error {
metadata := DebugMetadata{
type CustomMetadata struct {
proton.MessageMetadata
Header string
ParsedHeaders proton.Headers
MIMEType rfc822.MIMEType
Attachments []proton.Attachment
}
metadata := CustomMetadata{
MessageMetadata: msg.MessageMetadata,
Header: msg.Header,
ParsedHeaders: msg.ParsedHeaders,
@ -484,78 +433,3 @@ func writeCustomAttachmentPart(
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.
}
}

View File

@ -223,7 +223,6 @@ func newImpl(
EventJitter,
5*time.Minute,
crashHandler,
eventSubscription,
)
addressMode := usertypes.VaultToAddressMode(encVault.AddressMode())
@ -516,7 +515,7 @@ func (user *User) BridgePass() []byte {
}
// UsedSpace returns the total space used by the user on the API.
func (user *User) UsedSpace() uint64 {
func (user *User) UsedSpace() int {
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Minute))
defer cancel()
@ -529,7 +528,7 @@ func (user *User) UsedSpace() uint64 {
}
// MaxSpace returns the amount of space the user can use on the API.
func (user *User) MaxSpace() uint64 {
func (user *User) MaxSpace() int {
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Minute))
defer cancel()
@ -555,6 +554,27 @@ func (user *User) CheckAuth(email string, password []byte) (string, error) {
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.
func (user *User) Logout(ctx context.Context, withAPI bool) error {
user.log.WithField("withAPI", withAPI).Info("Logging out user")

View File

@ -32,7 +32,6 @@ import (
"github.com/ProtonMail/proton-bridge/v3/pkg/message/parser"
pmmime "github.com/ProtonMail/proton-bridge/v3/pkg/mime"
"github.com/emersion/go-message"
"github.com/google/uuid"
"github.com/jaytaylor/html2text"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
@ -117,10 +116,6 @@ func parse(p *parser.Parser, allowInvalidAddressLists bool) (Message, error) {
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)
if err != nil {
return Message{}, errors.Wrap(err, "failed to parse message header")
@ -147,7 +142,7 @@ func parse(p *parser.Parser, allowInvalidAddressLists bool) (Message, error) {
m.PlainBody = Body(plainBody)
m.MIMEBody = MIMEBody(mimeBody)
mimeType, err := determineBodyMIMEType(p)
mimeType, err := determineMIMEType(p)
if err != nil {
return Message{}, errors.Wrap(err, "failed to get mime type")
}
@ -313,7 +308,7 @@ func collectBodyParts(p *parser.Parser, preferredContentType string) (parser.Par
return bestChoice(childParts, preferredContentType), nil
}).
RegisterRule("text/plain", func(p *parser.Part, visit parser.Visit) (interface{}, error) {
disp, _, err := p.ContentDisposition()
disp, _, err := p.Header.ContentDisposition()
if err != nil {
disp = ""
}
@ -325,7 +320,7 @@ func collectBodyParts(p *parser.Parser, preferredContentType string) (parser.Par
return parser.Parts{p}, nil
}).
RegisterRule("text/html", func(p *parser.Part, visit parser.Visit) (interface{}, error) {
disp, _, err := p.ContentDisposition()
disp, _, err := p.Header.ContentDisposition()
if err != nil {
disp = ""
}
@ -405,7 +400,7 @@ func allPartsHaveContentType(parts parser.Parts, contentType string) bool {
return true
}
func determineBodyMIMEType(p *parser.Parser) (string, error) {
func determineMIMEType(p *parser.Parser) (string, error) {
var isHTML bool
w := p.NewWalker().
@ -414,7 +409,7 @@ func determineBodyMIMEType(p *parser.Parser) (string, error) {
return
})
if err := w.WalkSkipAttachment(); err != nil {
if err := w.Walk(); err != nil {
return "", err
}
@ -552,8 +547,8 @@ func parseAttachment(h message.Header, body []byte) (Attachment, error) {
return Attachment{}, err
}
att.Header = mimeHeader
mimeType, mimeTypeParams, err := pmmime.ParseMediaType(h.Get("Content-Type"))
mimeType, mimeTypeParams, err := h.ContentType()
if err != nil {
return Attachment{}, err
}
@ -563,8 +558,7 @@ func parseAttachment(h message.Header, body []byte) (Attachment, error) {
// Prefer attachment name from filename param in content disposition.
// If not available, try to get it from name param in content type.
// Otherwise fallback to attachment.bin.
disp, dispParams, err := pmmime.ParseMediaType(h.Get("Content-Disposition"))
if err == nil {
if disp, dispParams, err := h.ContentDisposition(); err == nil {
att.Disposition = proton.Disposition(disp)
if filename, ok := dispParams["filename"]; ok {
@ -591,7 +585,7 @@ func parseAttachment(h message.Header, body []byte) (Attachment, error) {
// (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.)
if h.Has("Content-Disposition") {
disp, _, err := pmmime.ParseMediaType(h.Get("Content-Disposition"))
disp, _, err := h.ContentDisposition()
if err != nil {
return Attachment{}, err
}
@ -641,168 +635,3 @@ func forEachDecodedHeaderField(h message.Header, fn func(string, string) error)
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
}
}

View File

@ -32,10 +32,6 @@ func (h *handler) matchPart(p *Part) bool {
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 {
if h.typeRegExp == nil {
return false
@ -54,7 +50,7 @@ func (h *handler) matchDisp(p *Part) bool {
return false
}
disp, _, err := p.ContentDisposition()
disp, _, err := p.Header.ContentDisposition()
if err != nil {
disp = ""
}

View File

@ -27,7 +27,6 @@ import (
"github.com/PuerkitoBio/goquery"
"github.com/emersion/go-message"
"github.com/sirupsen/logrus"
"golang.org/x/exp/slices"
"golang.org/x/net/html"
"golang.org/x/net/html/charset"
"golang.org/x/text/encoding"
@ -53,14 +52,6 @@ func (p *Part) ContentType() (string, map[string]string, error) {
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) {
if len(p.children) < n {
return nil, errors.New("no such part")
@ -90,14 +81,6 @@ 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 {
logrus.Trace("Converting part to utf-8")
@ -200,23 +183,6 @@ func (p *Part) isMultipartMixed() bool {
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 {
var res message.Header

View File

@ -1,86 +0,0 @@
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--

View File

@ -33,12 +33,9 @@ func newWalker(root *Part) *Walker {
}
}
func (w *Walker) Walk() error {
func (w *Walker) Walk() (err error) {
return w.walkOverPart(w.root)
}
func (w *Walker) WalkSkipAttachment() error {
return w.walkOverPartSkipAttachment(w.root)
}
func (w *Walker) walkOverPart(p *Part) error {
if err := w.getHandlerFunc(p)(p); err != nil {
@ -54,20 +51,6 @@ func (w *Walker) walkOverPart(p *Part) error {
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
// that doesn't match a registered content type/disposition handler.
func (w *Walker) RegisterDefaultHandler(fn HandlerFunc) *Walker {
@ -108,13 +91,3 @@ func (w *Walker) getHandlerFunc(p *Part) HandlerFunc {
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
}

View File

@ -60,27 +60,6 @@ func TestWalkerTypeHandler(t *testing.T) {
}, 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) {
p := newTestParser(t, "text_html_octet_attachment.eml")

View File

@ -19,7 +19,6 @@ package message
import (
"bytes"
"fmt"
"image/png"
"io"
"os"
@ -313,13 +312,11 @@ func TestParseTextPlainWithImageInline(t *testing.T) {
m, err := Parse(f)
require.NoError(t, err)
require.NotEmpty(t, m.Attachments[0].ContentID)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.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, 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.
require.Len(t, m.Attachments, 1)
@ -329,69 +326,6 @@ func TestParseTextPlainWithImageInline(t *testing.T) {
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) {
f := getFileReader("text_plain_duplicate_charset.eml")
@ -494,12 +428,11 @@ func TestParseTextHTMLWithImageInline(t *testing.T) {
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
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, "<html><head></head><body>This is body of <b>HTML mail</b> with attachment</body></html>", string(m.RichBody))
assert.Equal(t, "This is body of *HTML mail* with attachment", string(m.PlainBody))
// 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)
@ -606,18 +539,6 @@ func TestParseMultipartAlternativeLatin1(t *testing.T) {
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) {
f := getFileReader("text_html_trailing_end_of_mail.eml")
@ -786,23 +707,6 @@ func TestParseTextPlainWithDocxAttachmentCyrillic(t *testing.T) {
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 {
f, err := os.Open(filepath.Join("testdata", filename))
if err != nil {

View File

@ -1,27 +0,0 @@
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--

View File

@ -1,39 +0,0 @@
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--

View File

@ -1,35 +0,0 @@
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--

View File

@ -1,46 +0,0 @@
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--

View File

@ -256,10 +256,6 @@ 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.
func ParseMediaType(v string) (mediatype string, params map[string]string, err error) {
decoded, err := DecodeHeader(v)
if err != nil {
return "", nil, err
}
v, _ = changeEncodingAndKeepLastParamDefinition(decoded)
v, _ = changeEncodingAndKeepLastParamDefinition(v)
return mime.ParseMediaType(v)
}

View File

@ -155,29 +155,34 @@ func (s *scenario) theUserSetSMTPModeToSSL() error {
}
type testBugReport struct {
request bridge.ReportBugReq
bridge *bridge.Bridge
OSType string `json:"OS"`
OSVersion string `json:"OSVersion"`
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(br *bridge.Bridge) *testBugReport {
request := bridge.ReportBugReq{
func newTestBugReport(bridge *bridge.Bridge) *testBugReport {
return &testBugReport{
OSType: "osType",
OSVersion: "osVersion",
Title: "title",
Description: "description",
Username: "username",
Email: "email",
EmailClient: "client",
IncludeLogs: false,
}
return &testBugReport{
request: request,
bridge: br,
Client: "client",
Attachment: false,
bridge: bridge,
}
}
func (r *testBugReport) report() error {
return r.bridge.ReportBug(context.Background(), &r.request)
return r.bridge.ReportBug(context.Background(), r.OSType, r.OSVersion, r.Title, r.Description, r.Username, r.Email, r.Client, r.Attachment)
}
func (s *scenario) theUserReportsABug() error {
@ -189,25 +194,25 @@ func (s *scenario) theUserReportsABugWithSingleHeaderChange(key, value string) e
switch key {
case "osType":
bugReport.request.OSType = value
bugReport.OSType = value
case "osVersion":
bugReport.request.OSVersion = value
bugReport.OSVersion = value
case "Title":
bugReport.request.Title = value
bugReport.Title = value
case "Description":
bugReport.request.Description = value
bugReport.Description = value
case "Username":
bugReport.request.Username = value
bugReport.Username = value
case "Email":
bugReport.request.Email = value
bugReport.Email = value
case "Client":
bugReport.request.EmailClient = value
bugReport.Client = value
case "Attachment":
att, err := strconv.ParseBool(value)
if err != nil {
return fmt.Errorf("failed to parse bug report attachment preferences: %w", err)
}
bugReport.request.IncludeLogs = att
bugReport.Attachment = att
default:
return fmt.Errorf("Wrong header (\"%s\") is being checked", key)
}
@ -217,9 +222,10 @@ func (s *scenario) theUserReportsABugWithSingleHeaderChange(key, value string) e
func (s *scenario) theUserReportsABugWithDetails(value *godog.DocString) error {
bugReport := newTestBugReport(s.t.bridge)
if err := json.Unmarshal([]byte(value.Content), &bugReport.request); err != nil {
if err := json.Unmarshal([]byte(value.Content), &bugReport); err != nil {
return fmt.Errorf("cannot parse bug report details: %w", err)
}
return bugReport.report()
}
@ -293,16 +299,17 @@ func (s *scenario) bridgeSendsSyncStartedAndFinishedEventsForUser(username strin
break
}
for {
finishEvent, ok := awaitType(s.t.events, events.SyncFinished{}, 30*time.Second)
if !ok {
return errors.New("expected sync finished event, got none")
}
if wantUserID := s.t.getUserByName(username).getUserID(); finishEvent.UserID == wantUserID {
return nil
}
finishEvent, ok := awaitType(s.t.events, events.SyncFinished{}, 30*time.Second)
if !ok {
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 {

View File

@ -362,8 +362,8 @@ func createContact(ctx context.Context, c *proton.Client, contact, name string,
if err != nil {
return err
}
if res[0].Response.Code != proton.SuccessCode {
return errors.New("APIError " + res[0].Response.Message + " while creating contact")
if res[0].Response.APIError.Code != proton.SuccessCode {
return errors.New("APIError " + res[0].Response.APIError.Message + " while creating contact")
}
if settings != nil {

View File

@ -67,46 +67,57 @@ Feature: IMAP import messages
"""
Scenario Outline: Import multipart message with attachment <message>
When IMAP client "1" appends <message> to "INBOX"
Scenario: Import message with attachment name encoded by RFC 2047 without quoting
When IMAP client "1" appends the following 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
And IMAP client "1" eventually sees the following message in "INBOX" with this structure:
"""
{
"from": "Bridge Test <bridgetest@pm.test>",
"date": "01 Jan 80 00:00 +0000",
"to": "Internal Bridge <bridgetest@protonmail.com>",
"subject": "Message with attachment name",
"body-contains": "Hello",
"content": {
"content-type": "multipart/mixed",
"sections":[
{
"content-type": "text/plain",
"body-is": "Hello"
},
{
"content-type": "text/html",
"content-type-charset": "utf-8",
"transfer-encoding": "7bit",
"body-contains": "HELLO"
},
{
"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" |
# And IMAP client "1" eventually sees the following message in "INBOX" with this structure:
# """
# {
# "from": "Bridge Test <bridgetest@pm.test>",
# "date": "01 Jan 80 00:00 +0000",
# "to": "Internal Bridge <bridgetest@protonmail.com>",
# "subject": "Message with attachment name encoded by RFC 2047 without quoting",
# "body-contains": "Hello",
# "content": {
# "content-type": "multipart/mixed; boundary=\"boundary\"",
# "sections":[
# {
# "content-type": "text/plain",
# "body-is": "Hello"
# },
# {
# "content-type": "application/pdf",
# "content-type-name": "=?US-ASCII?Q?filename?=",
# "content-disposition": "attachment",
# "content-disposition-filename": "=?US-ASCII?Q?filename?=",
# "body-is": "somebytes"
# }
# ]
# }
# }
# """
# The message is imported as UTF-8 and the content type is determined at build time.
@ -311,6 +322,8 @@ Feature: IMAP import messages
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
This is a multi-part message in MIME format.
--boundary
@ -353,6 +366,9 @@ Feature: IMAP import messages
"content": {
"content-type": "multipart/mixed",
"sections":[
{
"body-is": "This is a multi-part message in MIME format."
},
{
"content-type": "text/plain",
"content-type-charset": "utf-8",
@ -377,84 +393,3 @@ 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>"
}
]
}
}
"""

View File

@ -140,80 +140,4 @@ Feature: SMTP sending with 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 |
"""

View File

@ -1,11 +1,10 @@
Feature: SMTP sending of plain messages
Background:
Given there exists an account with username "[user:user]" and password "password"
And there exists an account with username "[user:user2]" and password "password"
And there exists an account with username "[user:to]" and password "password"
Then it succeeds
When bridge starts
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"
Then it succeeds
@ -124,45 +123,12 @@ 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
When SMTP client "1" sends the following message from "[user:user]@[domain]" to "[user:user2]@[domain]":
When SMTP client "1" sends the following message from "[user:user]@[domain]" to "[user:to]@[domain]":
"""
From: Bridge Test <[user:user]@[domain]>
To: Internal Bridge <[user:user2]@[domain]>
To: Internal Bridge <[user:to]@[domain]>
Subject: Html Inline Alternative Internal
Content-Disposition: inline
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Thunderbird/60.5.0
@ -226,7 +192,7 @@ Feature: SMTP sending of plain messages
When user "[user:user]" connects and authenticates IMAP client "1"
Then IMAP client "1" eventually sees the following messages in "Sent":
| from | to | subject |
| [user:user]@[domain] | [user:user2]@[domain] | Html Inline Alternative Internal |
| [user:user]@[domain] | [user:to]@[domain] | Html Inline Alternative Internal |
And the body in the "POST" request to "/mail/v4/messages" is:
"""
{
@ -237,7 +203,7 @@ Feature: SMTP sending of plain messages
},
"ToList": [
{
"Address": "[user:user2]@[domain]",
"Address": "[user:to]@[domain]",
"Name": "Internal Bridge"
}
],
@ -378,7 +344,7 @@ Feature: SMTP sending of plain messages
}
"""
Scenario: HTML message with Foreign/Nonascii chars in Subject and Body to external
Scenario: HTML message with Foreign/Nonascii chars in Subject and Body
When there exists an account 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"
@ -408,242 +374,3 @@ 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"
}
]
}
}
"""

File diff suppressed because one or more lines are too long

View File

@ -1,19 +0,0 @@
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 |

View File

@ -41,7 +41,7 @@ Feature: The user reports a problem
"Description": "Testing Description",
"Username": "[user:user]",
"Email": "[user:user]@[domain]",
"EmailClient": "Apple Mail"
"Client": "Apple Mail"
}
"""
Then the header in the "POST" multipart request to "/core/v4/reports/bug" has "Title" set to "[Bridge] Bug - Testing Title"

View File

@ -114,7 +114,6 @@ func (s *scenario) steps(ctx *godog.ScenarioContext) {
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 PGP schema "([^"]*)"`, s.accountHasDefaultPGPSchema)
ctx.Step(`^the account "([^"]*)" matches the following settings:$`, s.accountMatchesSettings)
// ==== IMAP ====
ctx.Step(`^user "([^"]*)" connects IMAP client "([^"]*)"$`, s.userConnectsIMAPClient)

View File

@ -1,27 +0,0 @@
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--

View File

@ -1,27 +0,0 @@
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--

View File

@ -1,27 +0,0 @@
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--

View File

@ -28,10 +28,7 @@ import (
"time"
"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/parser"
pmmime "github.com/ProtonMail/proton-bridge/v3/pkg/mime"
"github.com/bradenaw/juniper/xslices"
"github.com/cucumber/messages-go/v16"
"github.com/emersion/go-imap"
@ -205,16 +202,10 @@ func newMessageStructFromIMAP(msg *imap.Message) MessageStruct {
panic(err)
}
parser, err := parser.New(bytes.NewReader(literal))
m, err := message.Parse(bytes.NewReader(literal))
if err != nil {
panic(err)
}
m, err := message.ParseWithParser(parser, true)
if err != nil {
panic(err)
}
var body string
switch {
case m.MIMEType == rfc822.TextPlain:
@ -254,23 +245,34 @@ func formatAddressList(list []*imap.Address) string {
}
func parseMessageSection(literal []byte, body string) MessageSection {
mimeType, boundary, charset, name := parseContentType(literal)
headers, err := rfc822.Parse(literal).ParseHeader()
if err != nil {
panic(err)
}
mimeType, boundary, charset, name := parseContentType(headers.Get("Content-Type"))
disp, filename := parseContentDisposition(headers.Get("Content-Disposition"))
msgSect := MessageSection{
ContentType: mimeType,
ContentTypeBoundary: boundary,
ContentTypeCharset: charset,
ContentTypeName: name,
ContentDisposition: disp,
ContentDispositionFilename: filename,
TransferEncoding: headers.Get("content-transfer-encoding"),
BodyIs: body,
ContentType: string(mimeType),
ContentTypeBoundary: boundary,
ContentTypeCharset: charset,
ContentTypeName: name,
TransferEncoding: headers.Get("content-transfer-encoding"),
BodyIs: body,
}
contentDisposition := bytes.Split([]byte(headers.Get("content-disposition")), []byte(";"))
for id, value := range contentDisposition {
if id == 0 {
msgSect.ContentDisposition = strings.TrimSpace(string(value))
continue
}
param := bytes.Split(value, []byte("="))
if strings.TrimSpace(string(param[0])) == "filename" && len(param) >= 2 {
_, filename, _ := strings.Cut(string(value), "filename=")
filename = strings.Trim(filename, "\"")
msgSect.ContentDispositionFilename = strings.TrimSpace(filename)
}
}
if msgSect.ContentTypeBoundary != "" {
@ -292,8 +294,8 @@ func parseMessageSection(literal []byte, body string) MessageSection {
return msgSect
}
func parseContentType(contentType string) (string, string, string, string) {
mimeType, params, err := pmmime.ParseMediaType(contentType)
func parseContentType(literal []byte) (rfc822.MIMEType, string, string, string) {
mimeType, params, err := rfc822.Parse(literal).ContentType()
if err != nil {
panic(err)
}
@ -312,15 +314,6 @@ func parseContentType(contentType string) (string, string, string, string) {
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 {
slices.SortFunc(have, func(a, b Message) bool {
return a.Subject < b.Subject
@ -338,77 +331,70 @@ func matchMessages(have, want []Message) error {
}
func matchStructure(have []MessageStruct, want MessageStruct) error {
mismatches := make([]string, 0)
for _, msg := range have {
if want.From != "" && msg.From != want.From {
mismatches = append(mismatches, "From")
continue
}
if want.To != "" && msg.To != want.To {
mismatches = append(mismatches, "To")
continue
}
if want.BCC != "" && msg.BCC != want.BCC {
mismatches = append(mismatches, "BCC")
continue
}
if want.CC != "" && msg.CC != want.CC {
mismatches = append(mismatches, "CC")
continue
}
if want.Subject != "" && msg.Subject != want.Subject {
mismatches = append(mismatches, "Subject")
continue
}
if want.Date != "" && want.Date != msg.Date {
mismatches = append(mismatches, "Date")
continue
}
if ok, mismatch := matchContent(msg.Content, want.Content); !ok {
mismatches = append(mismatches, "Content: "+mismatch)
continue
if matchContent(msg.Content, want.Content) {
return nil
}
return nil
}
return fmt.Errorf("missing messages: have %#v, want %#v with mismatch list %#v", have, want, mismatches)
return fmt.Errorf("missing messages: have %#v, want %#v", have, want)
}
func matchContent(have MessageSection, want MessageSection) (bool, string) {
func matchContent(have MessageSection, want MessageSection) bool {
if want.ContentType != "" && want.ContentType != have.ContentType {
return false, "ContentType"
return false
}
if want.ContentTypeBoundary != "" && want.ContentTypeBoundary != have.ContentTypeBoundary {
return false, "ContentTypeBoundary"
return false
}
if want.ContentTypeCharset != "" && want.ContentTypeCharset != have.ContentTypeCharset {
return false, "ContentTypeCharset"
return false
}
if want.ContentTypeName != "" && want.ContentTypeName != have.ContentTypeName {
return false, "ContentTypeName"
return false
}
if want.ContentDisposition != "" && want.ContentDisposition != have.ContentDisposition {
return false, "ContentDisposition"
return false
}
if want.ContentDispositionFilename != "" && want.ContentDispositionFilename != have.ContentDispositionFilename {
return false, "ContentDispositionFilename"
return false
}
if want.TransferEncoding != "" && want.TransferEncoding != have.TransferEncoding {
return false, "TransferEncoding"
return false
}
if want.BodyContains != "" && !strings.Contains(strings.TrimSpace(have.BodyIs), strings.TrimSpace(want.BodyContains)) {
return false, "BodyContains"
return false
}
if want.BodyIs != "" && strings.TrimSpace(have.BodyIs) != strings.TrimSpace(want.BodyIs) {
return false, "BodyIs"
return false
}
if len(have.Sections) != len(want.Sections) {
return false
}
for i, section := range want.Sections {
if ok, mismatch := matchContent(have.Sections[i], section); !ok {
return false, fmt.Sprintf("section %#v - %#v", i, mismatch)
if !matchContent(have.Sections[i], section) {
return false
}
}
return true, ""
return true
}
type Mailbox struct {
@ -565,10 +551,3 @@ type Contact struct {
Sign string `bdd:"signature"`
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"`
}

View File

@ -643,29 +643,3 @@ func (s *scenario) accountHasDefaultPGPSchema(account, schema string) error {
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
}

View File

@ -1,37 +0,0 @@
// 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
}
}