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
135 changed files with 782 additions and 8272 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,7 +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)
* [go-smtp](https://github.com/ProtonMail/go-smtp) available under [license](https://github.com/ProtonMail/go-smtp/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,97 +3,12 @@
Changelog [format](http://keepachangelog.com/en/1.0.0/)
## Wakato Bridge 3.7.1
### Added
* Test(GODT-2740): Sending Plain text messages to internal recipient.
* Test(GODT-2892): Create fake log file.
* GODT-3122: Added test, changed interface for accessing display name.
### Changed
* Remove debug prints.
* GODT-2576: Forward and $Forward Flag Support.
* GODT-3053: Use smaller bridge window on small screens.
* GODT-3113: Only force UTF-8 charset for HTML part when needed.
* GODT-3113: Do not render HTML for attachment.
* GODT-3112: Replaced error message when bridge exists prematurely. Added a link to support form.
* GODT-2947: Remove 'blame it on the weather' error part from go-smtp.
* GODT-3010: Log MimeType parsing issue.
* GODT-3104: Added log entry for cert install status on startup on macOS.
* GODT-2277: Move Keychain helpers creation in main.
### Fixed
* GODT-3054: Only delete drafts after message has been Sent.
* GODT-2576: Correctly handle Forwarded messages from Thunderbird.
* GODT-3122: Use display name as 'Email Account Name' in macOS profile.
* GODT-3125: Heartbeat crash on exit.
* GODT-2617: Validate user can send from the SMTP sender address.
* GODT-3123: Trigger bad event on empty EventID on existing accounts.
* GODT-3118: Do not reset EventID when migrating sync settings.
* GODT-3116: Panic on closed channel.
* GODT-1623: Throttle SMTP failed requests.
* GODT-3047: Fixed 'disk full' error message.
* GODT-3054: Delete draft create from reply.
* GODT-3048: WKD Policy behavior.
## 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.1+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)
}
}

22
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.20231114153341-2ecbdd2739f7
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.20231116144214-8a47c8d92fbc
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,18 +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/emersion/go-smtp => github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865
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
)

50
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.20231114153341-2ecbdd2739f7 h1:w+VoSAq9FQvKMm3DlH1MIEZ1KGe7LJ+81EJFVwSV4VU=
github.com/ProtonMail/gluon v0.17.1-0.20231114153341-2ecbdd2739f7/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,14 +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.20231116144214-8a47c8d92fbc h1:GBRKoFAldApEMkMrsFN1ZxG0eG797w6LTv/dFMDcsqQ=
github.com/ProtonMail/go-proton-api v0.4.1-0.20231116144214-8a47c8d92fbc/go.mod h1:WEXJqj5DSc2YI77SgXdpMY0nk33Qy92Vu2r4tOEazA8=
github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865 h1:EP1gnxLL5Z7xBSymE9nSTM27nRYINuvssAtDmG0suD8=
github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
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=
@ -124,6 +120,8 @@ github.com/emersion/go-imap-id v0.0.0-20190926060100-f94a56b9ecde/go.mod h1:sPwp
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead h1:fI1Jck0vUrXT8bnphprS1EoVRe2Q5CKCX8iDlpqjQ/Y=
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-smtp v0.15.1-0.20221021114529-49b17434419d h1:hFRM6zCBSc+Xa0rBOqSlG6Qe9dKC/2vLhGAuZlWxTsc=
github.com/emersion/go-smtp v0.15.1-0.20221021114529-49b17434419d/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3 h1:hQ1wTMaKcGfobYRT88RM8NFNyX+IQHvagkm/tqViU98=
@ -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

@ -41,7 +41,6 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/sentry"
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
"github.com/ProtonMail/proton-bridge/v3/pkg/restarter"
"github.com/pkg/profile"
"github.com/sirupsen/logrus"
@ -205,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()
@ -235,56 +234,53 @@ func run(c *cli.Context) error {
}
return withSingleInstance(settings, locations.GetLockFile(), version, func() error {
// Look for available keychains
return WithKeychainList(func(keychains *keychain.List) error {
// Unlock the encrypted vault.
return WithVault(locations, keychains, crashHandler, func(v *vault.Vault, insecure, corrupt bool) error {
if !v.Migrated() {
// Migrate old settings into the vault.
if err := migrateOldSettings(v); err != nil {
logrus.WithError(err).Error("Failed to migrate old settings")
}
// Migrate old accounts into the vault.
if err := migrateOldAccounts(locations, keychains, v); err != nil {
logrus.WithError(err).Error("Failed to migrate old accounts")
}
// The vault has been migrated.
if err := v.SetMigrated(); err != nil {
logrus.WithError(err).Error("Failed to mark vault as migrated")
}
// Unlock the encrypted vault.
return WithVault(locations, crashHandler, func(v *vault.Vault, insecure, corrupt bool) error {
if !v.Migrated() {
// Migrate old settings into the vault.
if err := migrateOldSettings(v); err != nil {
logrus.WithError(err).Error("Failed to migrate old settings")
}
logrus.WithFields(logrus.Fields{
"lastVersion": v.GetLastVersion().String(),
"showAllMail": v.GetShowAllMail(),
"updateCh": v.GetUpdateChannel(),
"autoUpdate": v.GetAutoUpdate(),
"rollout": v.GetUpdateRollout(),
"DoH": v.GetProxyAllowed(),
}).Info("Vault loaded")
// Migrate old accounts into the vault.
if err := migrateOldAccounts(locations, v); err != nil {
logrus.WithError(err).Error("Failed to migrate old accounts")
}
// Load the cookies from the vault.
return withCookieJar(v, func(cookieJar http.CookieJar) error {
// Create a new bridge instance.
return withBridge(c, exe, locations, version, identifier, crashHandler, reporter, v, cookieJar, keychains, func(b *bridge.Bridge, eventCh <-chan events.Event) error {
if insecure {
logrus.Warn("The vault key could not be retrieved; the vault will not be encrypted")
b.PushError(bridge.ErrVaultInsecure)
}
// The vault has been migrated.
if err := v.SetMigrated(); err != nil {
logrus.WithError(err).Error("Failed to mark vault as migrated")
}
}
if corrupt {
logrus.Warn("The vault is corrupt and has been wiped")
b.PushError(bridge.ErrVaultCorrupt)
}
logrus.WithFields(logrus.Fields{
"lastVersion": v.GetLastVersion().String(),
"showAllMail": v.GetShowAllMail(),
"updateCh": v.GetUpdateChannel(),
"autoUpdate": v.GetAutoUpdate(),
"rollout": v.GetUpdateRollout(),
"DoH": v.GetProxyAllowed(),
}).Info("Vault loaded")
// Remove old updates files
b.RemoveOldUpdates()
// Load the cookies from the vault.
return withCookieJar(v, func(cookieJar http.CookieJar) error {
// Create a new bridge instance.
return withBridge(c, exe, locations, version, identifier, crashHandler, reporter, v, cookieJar, func(b *bridge.Bridge, eventCh <-chan events.Event) error {
if insecure {
logrus.Warn("The vault key could not be retrieved; the vault will not be encrypted")
b.PushError(bridge.ErrVaultInsecure)
}
// Run the frontend.
return runFrontend(c, crashHandler, restarter, locations, b, eventCh, quitCh, c.Int(flagParentPID))
})
if corrupt {
logrus.Warn("The vault is corrupt and has been wiped")
b.PushError(bridge.ErrVaultCorrupt)
}
// Start telemetry heartbeat process
b.StartHeartbeat(b)
// Run the frontend.
return runFrontend(c, crashHandler, restarter, locations, b, eventCh, quitCh, c.Int(flagParentPID))
})
})
})
@ -294,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.
@ -481,13 +470,6 @@ func withCookieJar(vault *vault.Vault, fn func(http.CookieJar) error) error {
return fn(persister)
}
// WithKeychainList init the list of usable keychains.
func WithKeychainList(fn func(*keychain.List) error) error {
logrus.Debug("Creating keychain list")
defer logrus.Debug("Keychain list stop")
return fn(keychain.NewList())
}
func setDeviceCookies(jar *cookies.Jar) error {
url, err := url.Parse(constants.APIHost)
if err != nil {

View File

@ -37,7 +37,6 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/internal/versioner"
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
@ -56,7 +55,6 @@ func withBridge(
reporter *sentry.Reporter,
vault *vault.Vault,
cookieJar http.CookieJar,
keychains *keychain.List,
fn func(*bridge.Bridge, <-chan events.Event) error,
) error {
logrus.Debug("Creating bridge")
@ -99,7 +97,6 @@ func withBridge(
autostarter,
updater,
version,
keychains,
// The API stuff.
constants.APIHost,
@ -113,7 +110,6 @@ func withBridge(
crashHandler,
reporter,
imap.DefaultEpochUIDValidityGenerator(),
nil,
// The logging stuff.
c.String(flagLogIMAP) == "client" || c.String(flagLogIMAP) == "all",
@ -159,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

@ -122,7 +122,7 @@ func migrateOldSettingsWithDir(configDir string, v *vault.Vault) error {
return v.SetBridgeTLSCertKey(certPEM, keyPEM)
}
func migrateOldAccounts(locations *locations.Locations, keychains *keychain.List, v *vault.Vault) error {
func migrateOldAccounts(locations *locations.Locations, v *vault.Vault) error {
logrus.Info("Migrating accounts")
settings, err := locations.ProvideSettingsPath()
@ -134,7 +134,8 @@ func migrateOldAccounts(locations *locations.Locations, keychains *keychain.List
if err != nil {
return fmt.Errorf("failed to get helper: %w", err)
}
keychain, err := keychain.NewKeychain(helper, "bridge", keychains.GetHelpers(), keychains.GetDefaultHelper())
keychain, err := keychain.NewKeychain(helper, "bridge")
if err != nil {
return fmt.Errorf("failed to create keychain: %w", err)
}

View File

@ -35,6 +35,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/pkg/algo"
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
dockerCredentials "github.com/docker/docker-credential-helpers/credentials"
"github.com/stretchr/testify/require"
)
@ -132,9 +133,11 @@ func TestKeychainMigration(t *testing.T) {
}
func TestUserMigration(t *testing.T) {
kcl := keychain.NewTestKeychainsList()
keychainHelper := keychain.NewTestHelper()
kc, err := keychain.NewKeychain("mock", "bridge", kcl.GetHelpers(), kcl.GetDefaultHelper())
keychain.Helpers["mock"] = func(string) (dockerCredentials.Helper, error) { return keychainHelper, nil }
kc, err := keychain.NewKeychain("mock", "bridge")
require.NoError(t, err)
require.NoError(t, kc.Put("brokenID", "broken"))
@ -175,7 +178,7 @@ func TestUserMigration(t *testing.T) {
require.NoError(t, err)
require.False(t, corrupt)
require.NoError(t, migrateOldAccounts(locations, kcl, v))
require.NoError(t, migrateOldAccounts(locations, v))
require.Equal(t, []string{wantCredentials.UserID}, v.GetUserIDs())
require.NoError(t, v.GetUser(wantCredentials.UserID, func(u *vault.User) {

View File

@ -22,7 +22,6 @@ import (
"path"
"github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/proton-bridge/v3/internal/certs"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
@ -30,12 +29,12 @@ import (
"github.com/sirupsen/logrus"
)
func WithVault(locations *locations.Locations, keychains *keychain.List, panicHandler async.PanicHandler, fn func(*vault.Vault, bool, bool) error) error {
func WithVault(locations *locations.Locations, panicHandler async.PanicHandler, fn func(*vault.Vault, bool, bool) error) error {
logrus.Debug("Creating vault")
defer logrus.Debug("Vault stopped")
// Create the encVault.
encVault, insecure, corrupt, err := newVault(locations, keychains, panicHandler)
encVault, insecure, corrupt, err := newVault(locations, panicHandler)
if err != nil {
return fmt.Errorf("could not create vault: %w", err)
}
@ -45,15 +44,12 @@ func WithVault(locations *locations.Locations, keychains *keychain.List, panicHa
"corrupt": corrupt,
}).Debug("Vault created")
cert, _ := encVault.GetBridgeTLSCert()
certs.NewInstaller().LogCertInstallStatus(cert)
// GODT-1950: Add teardown actions (e.g. to close the vault).
return fn(encVault, insecure, corrupt)
}
func newVault(locations *locations.Locations, keychains *keychain.List, panicHandler async.PanicHandler) (*vault.Vault, bool, bool, error) {
func newVault(locations *locations.Locations, panicHandler async.PanicHandler) (*vault.Vault, bool, bool, error) {
vaultDir, err := locations.ProvideSettingsPath()
if err != nil {
return nil, false, false, fmt.Errorf("could not get vault dir: %w", err)
@ -66,7 +62,7 @@ func newVault(locations *locations.Locations, keychains *keychain.List, panicHan
insecure bool
)
if key, err := loadVaultKey(vaultDir, keychains); err != nil {
if key, err := loadVaultKey(vaultDir); err != nil {
logrus.WithError(err).Error("Could not load/create vault key")
insecure = true
@ -89,13 +85,13 @@ func newVault(locations *locations.Locations, keychains *keychain.List, panicHan
return vault, insecure, corrupt, nil
}
func loadVaultKey(vaultDir string, keychains *keychain.List) ([]byte, error) {
func loadVaultKey(vaultDir string) ([]byte, error) {
helper, err := vault.GetHelper(vaultDir)
if err != nil {
return nil, fmt.Errorf("could not get keychain helper: %w", err)
}
kc, err := keychain.NewKeychain(helper, constants.KeyChainName, keychains.GetHelpers(), keychains.GetDefaultHelper())
kc, err := keychain.NewKeychain(helper, constants.KeyChainName)
if err != nil {
return nil, fmt.Errorf("could not create keychain: %w", err)
}

View File

@ -45,7 +45,6 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/telemetry"
"github.com/ProtonMail/proton-bridge/v3/internal/user"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
"github.com/bradenaw/juniper/xslices"
"github.com/go-resty/resty/v2"
"github.com/sirupsen/logrus"
@ -75,7 +74,7 @@ type Bridge struct {
installCh chan installJob
// heartbeat is the telemetry heartbeat for metrics.
heartbeat *heartBeatState
heartbeat telemetry.Heartbeat
// curVersion is the current version of the bridge,
// newVersion is the version that was installed by the updater.
@ -83,9 +82,6 @@ type Bridge struct {
newVersion *semver.Version
newVersionLock safe.RWMutex
// keychains is the utils that own usable keychains found in the OS.
keychains *keychain.List
// focusService is used to raise the bridge window when needed.
focusService *focus.Service
@ -128,6 +124,9 @@ type Bridge struct {
// goUpdate triggers a check/install of updates.
goUpdate func()
// goHeartbeat triggers a check/sending if heartbeat is needed.
goHeartbeat func()
serverManager *imapsmtpserver.Service
syncService *syncservice.Service
}
@ -139,7 +138,6 @@ func New(
autostarter Autostarter, // the autostarter to manage autostart settings
updater Updater, // the updater to fetch and install updates
curVersion *semver.Version, // the current version of the bridge
keychains *keychain.List, // usable keychains
apiURL string, // the URL of the API to use
cookieJar http.CookieJar, // the cookie jar to use
@ -150,7 +148,6 @@ func New(
panicHandler async.PanicHandler,
reporter reporter.Reporter,
uidValidityGenerator imap.UIDValidityGenerator,
heartBeatManager telemetry.HeartbeatManager,
logIMAPClient, logIMAPServer bool, // whether to log IMAP client/server activity
logSMTP bool, // whether to log SMTP activity
@ -166,7 +163,6 @@ func New(
// bridge is the bridge.
bridge, err := newBridge(
context.Background(),
tasks,
imapEventCh,
@ -175,7 +171,6 @@ func New(
autostarter,
updater,
curVersion,
keychains,
panicHandler,
reporter,
@ -183,7 +178,6 @@ func New(
identifier,
proxyCtl,
uidValidityGenerator,
heartBeatManager,
logIMAPClient, logIMAPServer, logSMTP,
)
if err != nil {
@ -202,7 +196,6 @@ func New(
}
func newBridge(
ctx context.Context,
tasks *async.Group,
imapEventCh chan imapEvents.Event,
@ -211,7 +204,6 @@ func newBridge(
autostarter Autostarter,
updater Updater,
curVersion *semver.Version,
keychains *keychain.List,
panicHandler async.PanicHandler,
reporter reporter.Reporter,
@ -219,7 +211,6 @@ func newBridge(
identifier identifier.Identifier,
proxyCtl ProxyController,
uidValidityGenerator imap.UIDValidityGenerator,
heartbeatManager telemetry.HeartbeatManager,
logIMAPClient, logIMAPServer, logSMTP bool,
) (*Bridge, error) {
@ -265,13 +256,9 @@ func newBridge(
newVersion: curVersion,
newVersionLock: safe.NewRWMutex(),
keychains: keychains,
panicHandler: panicHandler,
reporter: reporter,
heartbeat: newHeartBeatState(ctx, panicHandler),
focusService: focusService,
autostarter: autostarter,
locator: locator,
@ -301,12 +288,6 @@ func newBridge(
return nil, err
}
if heartbeatManager == nil {
bridge.heartbeat.init(bridge, bridge)
} else {
bridge.heartbeat.init(bridge, heartbeatManager)
}
bridge.syncService.Run(bridge.tasks)
return bridge, nil
@ -436,9 +417,6 @@ func (bridge *Bridge) GetErrors() []error {
func (bridge *Bridge) Close(ctx context.Context) {
logrus.Info("Closing bridge")
// Stop heart beat before closing users.
bridge.heartbeat.stop()
// Close all users.
safe.Lock(func() {
for _, user := range bridge.users {
@ -509,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

@ -49,7 +49,6 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/user"
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
"github.com/ProtonMail/proton-bridge/v3/tests"
"github.com/bradenaw/juniper/xslices"
imapid "github.com/emersion/go-imap-id"
@ -951,7 +950,6 @@ func withBridgeNoMocks(
mocks.Autostarter,
mocks.Updater,
v2_3_0,
keychain.NewTestKeychainsList(),
// The API stuff.
apiURL,
@ -963,7 +961,6 @@ func withBridgeNoMocks(
mocks.CrashHandler,
mocks.Reporter,
testUIDValidityGenerator,
mocks.Heartbeat,
// The logging stuff.
os.Getenv("BRIDGE_LOG_IMAP_CLIENT") == "1",
@ -973,6 +970,9 @@ func withBridgeNoMocks(
require.NoError(t, err)
require.Empty(t, bridge.GetErrors())
// Start the Heartbeat process.
bridge.StartHeartbeat(mocks.Heartbeat)
// Wait for bridge to finish loading users.
waitForEvent(t, eventCh, events.AllUsersLoaded{})

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

@ -19,7 +19,6 @@ package bridge
import (
"context"
"errors"
"strings"
"github.com/ProtonMail/proton-bridge/v3/internal/clientconfig"
@ -31,8 +30,8 @@ import (
"github.com/sirupsen/logrus"
)
// ConfigureAppleMail configures Apple Mail for the given userID and address.
// If configuring Apple Mail for Catalina or newer, it ensures Bridge is using SSL.
// ConfigureAppleMail configures apple mail for the given userID and address.
// If configuring apple mail for Catalina or newer, it ensures Bridge is using SSL.
func (bridge *Bridge) ConfigureAppleMail(ctx context.Context, userID, address string) error {
logrus.WithFields(logrus.Fields{
"userID": userID,
@ -45,28 +44,16 @@ func (bridge *Bridge) ConfigureAppleMail(ctx context.Context, userID, address st
return ErrNoSuchUser
}
emails := user.Emails()
displayNames := user.DisplayNames()
if (len(emails) == 0) || (len(displayNames) == 0) {
return errors.New("could not retrieve user address info")
}
if address == "" {
address = emails[0]
address = user.Emails()[0]
}
var username, displayName, addresses string
username := address
addresses := address
if user.GetAddressMode() == vault.CombinedMode {
username = address
displayName = displayNames[username]
addresses = strings.Join(emails, ",")
} else {
username = address
addresses = address
displayName = displayNames[address]
if len(displayName) == 0 {
displayName = address
}
username = user.Emails()[0]
addresses = strings.Join(user.Emails(), ",")
}
if useragent.IsCatalinaOrNewer() && !bridge.vault.GetSMTPSSL() {
@ -82,7 +69,6 @@ func (bridge *Bridge) ConfigureAppleMail(ctx context.Context, userID, address st
bridge.vault.GetIMAPSSL(),
bridge.vault.GetSMTPSSL(),
username,
displayName,
addresses,
user.BridgePass(),
)

View File

@ -20,100 +20,18 @@ package bridge
import (
"context"
"encoding/json"
"sync"
"time"
"github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/telemetry"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
"github.com/sirupsen/logrus"
)
const HeartbeatCheckInterval = time.Hour
type heartBeatState struct {
task *async.Group
telemetry.Heartbeat
taskLock sync.Mutex
taskStarted bool
taskInterval time.Duration
}
func newHeartBeatState(ctx context.Context, panicHandler async.PanicHandler) *heartBeatState {
return &heartBeatState{
task: async.NewGroup(ctx, panicHandler),
}
}
func (h *heartBeatState) init(bridge *Bridge, manager telemetry.HeartbeatManager) {
h.Heartbeat = telemetry.NewHeartbeat(manager, 1143, 1025, bridge.GetGluonCacheDir(), bridge.keychains.GetDefaultHelper())
h.taskInterval = manager.GetHeartbeatPeriodicInterval()
h.SetRollout(bridge.GetUpdateRollout())
h.SetAutoStart(bridge.GetAutostart())
h.SetAutoUpdate(bridge.GetAutoUpdate())
h.SetBeta(bridge.GetUpdateChannel())
h.SetDoh(bridge.GetProxyAllowed())
h.SetShowAllMail(bridge.GetShowAllMail())
h.SetIMAPConnectionMode(bridge.GetIMAPSSL())
h.SetSMTPConnectionMode(bridge.GetSMTPSSL())
h.SetIMAPPort(bridge.GetIMAPPort())
h.SetSMTPPort(bridge.GetSMTPPort())
h.SetCacheLocation(bridge.GetGluonCacheDir())
if val, err := bridge.GetKeychainApp(); err != nil {
h.SetKeyChainPref(val)
} else {
h.SetKeyChainPref(bridge.keychains.GetDefaultHelper())
}
h.SetPrevVersion(bridge.GetLastVersion().String())
safe.RLock(func() {
var splitMode = false
for _, user := range bridge.users {
if user.GetAddressMode() == vault.SplitMode {
splitMode = true
break
}
}
var nbAccount = len(bridge.users)
h.SetNbAccount(nbAccount)
h.SetSplitMode(splitMode)
// Do not try to send if there is no user yet.
if nbAccount > 0 {
defer h.start()
}
}, bridge.usersLock)
}
func (h *heartBeatState) start() {
h.taskLock.Lock()
defer h.taskLock.Unlock()
if h.taskStarted {
return
}
h.taskStarted = true
h.task.PeriodicOrTrigger(h.taskInterval, 0, func(ctx context.Context) {
logrus.Debug("Checking for heartbeat")
h.TrySending(ctx)
})
}
func (h *heartBeatState) stop() {
h.taskLock.Lock()
defer h.taskLock.Unlock()
if !h.taskStarted {
return
}
h.task.CancelAndWait()
h.taskStarted = false
}
func (bridge *Bridge) IsTelemetryAvailable(ctx context.Context) bool {
var flag = true
if bridge.GetTelemetryDisabled() {
@ -162,6 +80,49 @@ func (bridge *Bridge) SetLastHeartbeatSent(timestamp time.Time) error {
return bridge.vault.SetLastHeartbeatSent(timestamp)
}
func (bridge *Bridge) GetHeartbeatPeriodicInterval() time.Duration {
return HeartbeatCheckInterval
func (bridge *Bridge) StartHeartbeat(manager telemetry.HeartbeatManager) {
bridge.heartbeat = telemetry.NewHeartbeat(manager, 1143, 1025, bridge.GetGluonCacheDir(), keychain.DefaultHelper)
// Check for heartbeat when triggered.
bridge.goHeartbeat = bridge.tasks.PeriodicOrTrigger(HeartbeatCheckInterval, 0, func(ctx context.Context) {
logrus.Debug("Checking for heartbeat")
bridge.heartbeat.TrySending(ctx)
})
bridge.heartbeat.SetRollout(bridge.GetUpdateRollout())
bridge.heartbeat.SetAutoStart(bridge.GetAutostart())
bridge.heartbeat.SetAutoUpdate(bridge.GetAutoUpdate())
bridge.heartbeat.SetBeta(bridge.GetUpdateChannel())
bridge.heartbeat.SetDoh(bridge.GetProxyAllowed())
bridge.heartbeat.SetShowAllMail(bridge.GetShowAllMail())
bridge.heartbeat.SetIMAPConnectionMode(bridge.GetIMAPSSL())
bridge.heartbeat.SetSMTPConnectionMode(bridge.GetSMTPSSL())
bridge.heartbeat.SetIMAPPort(bridge.GetIMAPPort())
bridge.heartbeat.SetSMTPPort(bridge.GetSMTPPort())
bridge.heartbeat.SetCacheLocation(bridge.GetGluonCacheDir())
if val, err := bridge.GetKeychainApp(); err != nil {
bridge.heartbeat.SetKeyChainPref(val)
} else {
bridge.heartbeat.SetKeyChainPref(keychain.DefaultHelper)
}
bridge.heartbeat.SetPrevVersion(bridge.GetLastVersion().String())
safe.RLock(func() {
var splitMode = false
for _, user := range bridge.users {
if user.GetAddressMode() == vault.SplitMode {
splitMode = true
break
}
}
var nbAccount = len(bridge.users)
bridge.heartbeat.SetNbAccount(nbAccount)
bridge.heartbeat.SetSplitMode(splitMode)
// Do not try to send if there is no user yet.
if nbAccount > 0 {
defer bridge.goHeartbeat()
}
}, bridge.usersLock)
}

View File

@ -1,24 +0,0 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail 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 bridge
import "golang.org/x/exp/maps"
func (bridge *Bridge) GetHelpersNames() []string {
return maps.Keys(bridge.keychains.GetHelpers())
}

View File

@ -7,7 +7,6 @@ import (
"os"
"sync"
"testing"
"time"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/proton-bridge/v3/internal/bridge/mocks"
@ -52,7 +51,6 @@ func NewMocks(tb testing.TB, version, minAuto *semver.Version) *Mocks {
// this is called at start of heartbeat process.
mocks.Heartbeat.EXPECT().IsTelemetryAvailable(gomock.Any()).AnyTimes()
mocks.Heartbeat.EXPECT().GetHeartbeatPeriodicInterval().AnyTimes().Return(500 * time.Millisecond)
return mocks
}
@ -156,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

@ -36,20 +36,6 @@ func (m *MockHeartbeatManager) EXPECT() *MockHeartbeatManagerMockRecorder {
return m.recorder
}
// GetHeartbeatPeriodicInterval mocks base method.
func (m *MockHeartbeatManager) GetHeartbeatPeriodicInterval() time.Duration {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetHeartbeatPeriodicInterval")
ret0, _ := ret[0].(time.Duration)
return ret0
}
// GetHeartbeatPeriodicInterval indicates an expected call of GetHeartbeatPeriodicInterval.
func (mr *MockHeartbeatManagerMockRecorder) GetHeartbeatPeriodicInterval() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHeartbeatPeriodicInterval", reflect.TypeOf((*MockHeartbeatManager)(nil).GetHeartbeatPeriodicInterval))
}
// GetLastHeartbeatSent mocks base method.
func (m *MockHeartbeatManager) GetLastHeartbeatSent() time.Time {
m.ctrl.T.Helper()

View File

@ -33,7 +33,6 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
smtpservice "github.com/ProtonMail/proton-bridge/v3/internal/services/smtp"
"github.com/emersion/go-imap"
"github.com/emersion/go-sasl"
"github.com/emersion/go-smtp"
@ -337,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
@ -347,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"
@ -364,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"
@ -524,234 +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)
})
})
}
func TestBridge_SendAddressDisabled(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
recipientUserID, _, err := s.CreateUser("recipient", password)
require.NoError(t, err)
senderUserID, addrID, err := s.CreateUser("sender", password)
require.NoError(t, err)
require.NoError(t, s.ChangeAddressAllowSend(senderUserID, addrID, false))
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, "sender", password, nil, nil)
require.NoError(t, err)
_, err = bridge.LoginFull(ctx, "recipient", password, nil, nil)
require.NoError(t, err)
smtpWaiter.Wait()
recipientInfo, err := bridge.GetUserInfo(recipientUserID)
require.NoError(t, err)
senderInfo, err := bridge.GetUserInfo(senderUserID)
require.NoError(t, err)
// 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}))
require.NoError(t, client.Auth(sasl.NewLoginClient(
senderInfo.Addresses[0],
string(senderInfo.BridgePass)),
))
// Send the message.
err = client.SendMail(
senderInfo.Addresses[0],
[]string{recipientInfo.Addresses[0]},
strings.NewReader("Subject: Test 1\r\n\r\nHello world!"),
)
smtpErr := smtpservice.NewErrCanNotSendOnAddress(senderInfo.Addresses[0])
require.Equal(t, fmt.Sprintf("Error: %v", smtpErr.Error()), err.Error())
})
})
}

View File

@ -261,12 +261,9 @@ func (bridge *Bridge) SetTelemetryDisabled(isDisabled bool) error {
return err
}
// If telemetry is re-enabled locally, try to send the heartbeat.
if isDisabled {
bridge.heartbeat.stop()
} else {
bridge.heartbeat.start()
if !isDisabled {
defer bridge.goHeartbeat()
}
return nil
}

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 {
@ -494,7 +479,7 @@ func (bridge *Bridge) addUser(
return fmt.Errorf("failed to add vault user: %w", err)
}
if err := bridge.addUserWithVault(ctx, client, apiUser, vaultUser, isNew); err != nil {
if err := bridge.addUserWithVault(ctx, client, apiUser, vaultUser); err != nil {
if _, ok := err.(*resty.ResponseError); ok || isLogin {
logrus.WithError(err).Error("Failed to add user, clearing its secrets from vault")
@ -529,7 +514,6 @@ func (bridge *Bridge) addUserWithVault(
client *proton.Client,
apiUser proton.User,
vault *vault.User,
isNew bool,
) error {
statsPath, err := bridge.locator.ProvideStatsPath()
if err != nil {
@ -557,7 +541,6 @@ func (bridge *Bridge) addUserWithVault(
&bridgeEventSubscription{b: bridge},
bridge.syncService,
syncSettingsPath,
isNew,
)
if err != nil {
return fmt.Errorf("failed to create user: %w", err)
@ -594,7 +577,7 @@ func (bridge *Bridge) addUserWithVault(
}, bridge.usersLock)
// As we need at least one user to send heartbeat, try to send it.
bridge.heartbeat.start()
defer bridge.goHeartbeat()
return 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

@ -356,10 +356,6 @@ func removeCertTrustCGo(buffer *C.char, size C.ulonglong) error {
}
}
func osSupportCertInstall() bool {
return true
}
// installCert installs a certificate in the keychain. The certificate is added to the keychain and it is set as trusted.
// This function will trigger a security prompt from the system, unless the certificate is already trusted in the user keychain.
func installCert(certPEM []byte) error {

View File

@ -28,7 +28,6 @@ import (
func TestCertInKeychain(t *testing.T) {
// no trust settings change is performed, so this test will not trigger an OS security prompt.
certPEM := generatePEMCertificate(t)
require.True(t, osSupportCertInstall())
require.False(t, isCertInKeychain(certPEM))
require.NoError(t, addCertToKeychain(certPEM))
require.True(t, isCertInKeychain(certPEM))

View File

@ -17,10 +17,6 @@
package certs
func osSupportCertInstall() bool {
return false
}
func installCert([]byte) error {
return nil // Linux doesn't have a root cert store.
}

View File

@ -17,10 +17,6 @@
package certs
func osSupportCertInstall() bool {
return false
}
func installCert([]byte) error {
return nil // NOTE(GODT-986): Install certs to root cert store?
}

View File

@ -37,10 +37,6 @@ func NewInstaller() *Installer {
}
}
func (installer *Installer) OSSupportCertInstall() bool {
return osSupportCertInstall()
}
func (installer *Installer) InstallCert(certPEM []byte) error {
installer.log.Info("Installing the Bridge TLS certificate in the OS keychain")
@ -68,15 +64,3 @@ func (installer *Installer) UninstallCert(certPEM []byte) error {
func (installer *Installer) IsCertInstalled(certPEM []byte) bool {
return isCertInstalled(certPEM)
}
// LogCertInstallStatus reports the current status of the certificate installation in the log.
// If certificate installation is not supported on the platform, this function does nothing.
func (installer *Installer) LogCertInstallStatus(certPEM []byte) {
if installer.OSSupportCertInstall() {
if installer.IsCertInstalled(certPEM) {
installer.log.Info("The Bridge TLS certificate is installed in the OS keychain")
} else {
installer.log.Info("The Bridge TLS certificate is not installed in the OS keychain")
}
}
}

View File

@ -39,10 +39,10 @@ func (c *AppleMail) Configure(
hostname string,
imapPort, smtpPort int,
imapSSL, smtpSSL bool,
username, displayName, addresses string,
username, addresses string,
password []byte,
) error {
mc := prepareMobileConfig(hostname, imapPort, smtpPort, imapSSL, smtpSSL, username, displayName, addresses, password)
mc := prepareMobileConfig(hostname, imapPort, smtpPort, imapSSL, smtpSSL, username, addresses, password)
confPath, err := saveConfigTemporarily(mc)
if err != nil {
@ -66,13 +66,13 @@ func prepareMobileConfig(
hostname string,
imapPort, smtpPort int,
imapSSL, smtpSSL bool,
username, displayName, addresses string,
username, addresses string,
password []byte,
) *mobileconfig.Config {
return &mobileconfig.Config{
DisplayName: username,
EmailAddress: addresses,
AccountName: displayName,
AccountName: username,
AccountDescription: username,
Identifier: "protonmail " + username + strconv.FormatInt(time.Now().Unix(), 10),
IMAP: &mobileconfig.IMAP{

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

@ -415,11 +415,7 @@ int main(int argc, char *argv[]) {
}
catch (Exception const &e) {
sentry_uuid_s const uuid = reportSentryException("Exception occurred during main", e);
QString message = e.qwhat();
if (e.showSupportLink()) {
message += R"(<br/><br/>If the issue persists, please contact our <a href="https://proton.me/support/contact">customer support</a>.)";
}
QMessageBox::critical(nullptr, "Error", message);
QMessageBox::critical(nullptr, "Error", e.qwhat());
QTextStream(stderr) << "reportID: " << QByteArray(uuid.bytes, 16).toHex() << " Captured exception :" << e.detailedWhat() << "\n";
return EXIT_FAILURE;
}

View File

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

View File

@ -69,8 +69,8 @@ ApplicationWindow {
}
colorScheme: ProtonStyle.currentStyle
height: screen.height < ProtonStyle.window_default_height + 100 ? ProtonStyle.window_minimum_height : ProtonStyle.window_default_height
minimumHeight: ProtonStyle.window_minimum_height
height: ProtonStyle.window_default_height
minimumHeight:ProtonStyle.window_minimum_height
minimumWidth: ProtonStyle.window_minimum_width
visible: true
width: ProtonStyle.window_default_width

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

@ -380,7 +380,7 @@ QtObject {
}
property Notification diskFull: Notification {
brief: title
description: qsTr("Quit Bridge and free disk space or move the local cache to another disk.")
description: qsTr("Quit Bridge and free disk space or disable the local cache (not recommended).")
group: Notifications.Group.Configuration | Notifications.Group.Dialogs
icon: "./icons/ic-exclamation-circle-filled.svg"
title: qsTr("Your disk is almost full")
@ -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

@ -26,16 +26,14 @@ namespace bridgepp {
/// \param[in] what A description of the exception.
/// \param[in] details The optional details for the exception.
/// \param[in] function The name of the calling function.
/// \param[in] showSupportLink Should a link to the support web form be included in GUI message.
//****************************************************************************************************************************************************
Exception::Exception(QString qwhat, QString details, QString function, QByteArray attachment, bool showSupportLink) noexcept
Exception::Exception(QString qwhat, QString details, QString function, QByteArray attachment) noexcept
: std::exception()
, qwhat_(std::move(qwhat))
, what_(qwhat_.toLocal8Bit())
, details_(std::move(details))
, function_(std::move(function))
, attachment_(std::move(attachment))
, showSupportLink_(showSupportLink) {
, attachment_(std::move(attachment)) {
}
@ -48,8 +46,7 @@ Exception::Exception(Exception const &ref) noexcept
, what_(ref.what_)
, details_(ref.details_)
, function_(ref.function_)
, attachment_(ref.attachment_)
, showSupportLink_(ref.showSupportLink_) {
, attachment_(ref.attachment_) {
}
@ -62,8 +59,7 @@ Exception::Exception(Exception &&ref) noexcept
, what_(ref.what_)
, details_(ref.details_)
, function_(ref.function_)
, attachment_(ref.attachment_)
, showSupportLink_(ref.showSupportLink_) {
, attachment_(ref.attachment_) {
}
@ -122,12 +118,4 @@ QString Exception::detailedWhat() const {
}
//****************************************************************************************************************************************************
/// \return true iff A link to the support page should shown in the GUI message box.
//****************************************************************************************************************************************************
bool Exception::showSupportLink() const {
return showSupportLink_;
}
} // namespace bridgepp

View File

@ -33,7 +33,7 @@ namespace bridgepp {
class Exception : public std::exception {
public: // member functions
explicit Exception(QString qwhat = QString(), QString details = QString(), QString function = QString(),
QByteArray attachment = QByteArray(), bool showSupportLink = false) noexcept; ///< Constructor
QByteArray attachment = QByteArray()) noexcept; ///< Constructor
Exception(Exception const &ref) noexcept; ///< copy constructor
Exception(Exception &&ref) noexcept; ///< copy constructor
Exception &operator=(Exception const &) = delete; ///< Disabled assignment operator
@ -45,7 +45,6 @@ public: // member functions
QString function() const noexcept; ///< Return the function that threw the exception.
QByteArray attachment() const noexcept; ///< Return the attachment for the exception.
QString detailedWhat() const; ///< Return the detailed description of the message (i.e. including the function name and the details).
bool showSupportLink() const; ///< Return the value for the 'Show support link' option.
public: // static data members
static qsizetype const attachmentMaxLength {25 * 1024}; ///< The maximum length text attachment sent in Sentry reports, in bytes.
@ -56,7 +55,6 @@ private: // data members
QString const details_; ///< The optional details for the exception.
QString const function_; ///< The name of the function that created the exception.
QByteArray const attachment_; ///< The attachment to add to the exception.
bool const showSupportLink_; ///< Should the GUI feedback include a link to support.
};

View File

@ -72,8 +72,8 @@ GRPCConfig GRPCClient::waitAndRetrieveServiceConfig(QString const & sessionID, Q
bool found = false;
while (true) {
if (serverProcess && serverProcess->getStatus().ended) {
throw Exception("Bridge failed to start.", "Bridge application exited before providing a gRPC service configuration file", __FUNCTION__,
tailOfLatestBridgeLog(sessionID), true);
throw Exception("Bridge application exited before providing a gRPC service configuration file.", QString(), __FUNCTION__,
tailOfLatestBridgeLog(sessionID));
}
if (file.exists()) {

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

@ -33,8 +33,10 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/service"
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
"github.com/ProtonMail/proton-bridge/v3/pkg/ports"
"github.com/sirupsen/logrus"
"golang.org/x/exp/maps"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/runtime/protoimpl"
@ -337,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
@ -381,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")
@ -710,7 +712,7 @@ func (s *Service) IsPortFree(_ context.Context, port *wrapperspb.Int32Value) (*w
func (s *Service) AvailableKeychains(_ context.Context, _ *emptypb.Empty) (*AvailableKeychainsResponse, error) {
s.log.Debug("AvailableKeychains")
return &AvailableKeychainsResponse{Keychains: s.bridge.GetHelpersNames()}, nil
return &AvailableKeychainsResponse{Keychains: maps.Keys(keychain.Helpers)}, nil
}
func (s *Service) SetCurrentKeychain(ctx context.Context, keychain *wrapperspb.StringValue) (*emptypb.Empty, error) {

View File

@ -48,6 +48,4 @@ type APIClient interface {
DeleteMessage(ctx context.Context, messageIDs ...string) error
MarkMessagesRead(ctx context.Context, messageIDs ...string) error
MarkMessagesUnread(ctx context.Context, messageIDs ...string) error
MarkMessagesForwarded(ctx context.Context, messageIDs ...string) error
MarkMessagesUnForwarded(ctx context.Context, messageIDs ...string) error
}

View File

@ -84,9 +84,9 @@ func NewConnector(
identityState: identityState,
addrID: addrID,
showAllMail: b32(showAllMail),
flags: defaultMailboxFlags(),
permFlags: defaultMailboxPermanentFlags(),
attrs: defaultMailboxAttributes(),
flags: defaultFlags,
permFlags: defaultPermanentFlags,
attrs: defaultAttributes,
client: apiClient,
telemetry: telemetry,
@ -144,18 +144,6 @@ func (s *Connector) Init(ctx context.Context, cache connector.IMAPState) error {
}
}
}
// Retroactively apply the forwarded flags to existing mailboxes so that the IMAP clients can recognize
// that they can store these flags now.
if err := write.AddFlagsToAllMailboxes(ctx, imap.ForwardFlagList...); err != nil {
return fmt.Errorf("failed to add \\Forward flag to all mailboxes:%w", err)
}
// Add forwarded flag as perm flags to all mailboxes.
if err := write.AddPermFlagsToAllMailboxes(ctx, imap.ForwardFlagList...); err != nil {
return fmt.Errorf("failed to add \\Forward permanent flag to all mailboxes:%w", err)
}
return nil
})
}
@ -499,14 +487,6 @@ func (s *Connector) MarkMessagesFlagged(ctx context.Context, _ connector.IMAPSta
return s.client.UnlabelMessages(ctx, usertypes.MapTo[imap.MessageID, string](messageIDs), proton.StarredLabel)
}
func (s *Connector) MarkMessagesForwarded(ctx context.Context, _ connector.IMAPStateWrite, messageIDs []imap.MessageID, flagged bool) error {
if flagged {
return s.client.MarkMessagesForwarded(ctx, usertypes.MapTo[imap.MessageID, string](messageIDs)...)
}
return s.client.MarkMessagesUnForwarded(ctx, usertypes.MapTo[imap.MessageID, string](messageIDs)...)
}
func (s *Connector) GetUpdates() <-chan imap.Update {
return s.updateCh.GetChannel()
}
@ -521,6 +501,12 @@ func (s *Connector) ShowAllMail(v bool) {
atomic.StoreUint32(&s.showAllMail, b32(v))
}
var (
defaultFlags = imap.NewFlagSet(imap.FlagSeen, imap.FlagFlagged, imap.FlagDeleted) // nolint:gochecknoglobals
defaultPermanentFlags = imap.NewFlagSet(imap.FlagSeen, imap.FlagFlagged, imap.FlagDeleted) // nolint:gochecknoglobals
defaultAttributes = imap.NewFlagSet() // nolint:gochecknoglobals
)
const (
folderPrefix = "Folders"
labelPrefix = "Labels"
@ -826,18 +812,3 @@ func fixGODT3003Labels(
return applied, nil
}
func defaultMailboxFlags() imap.FlagSet {
f := imap.NewFlagSet(imap.FlagSeen, imap.FlagFlagged, imap.FlagDeleted)
f.AddToSelf(imap.ForwardFlagList...)
return f
}
func defaultMailboxPermanentFlags() imap.FlagSet {
return defaultMailboxFlags()
}
func defaultMailboxAttributes() imap.FlagSet {
return imap.NewFlagSet()
}

View File

@ -68,10 +68,6 @@ func BuildFlagSetFromMessageMetadata(message proton.MessageMetadata) imap.FlagSe
flags.AddToSelf(imap.FlagAnswered)
}
if message.IsForwarded {
flags.AddToSelf(imap.ForwardFlagList...)
}
return flags
}

View File

@ -32,8 +32,8 @@ func newSystemMailboxCreatedUpdate(labelID imap.MailboxID, labelName string) *im
}
attrs := imap.NewFlagSet(imap.AttrNoInferiors)
permanentFlags := defaultMailboxPermanentFlags()
flags := defaultMailboxFlags()
permanentFlags := defaultPermanentFlags
flags := defaultFlags
switch labelID {
case proton.TrashLabel:
@ -86,8 +86,8 @@ func newPlaceHolderMailboxCreatedUpdate(labelName string) *imap.MailboxCreated {
return imap.NewMailboxCreated(imap.Mailbox{
ID: imap.MailboxID(labelName),
Name: []string{labelName},
Flags: defaultMailboxFlags(),
PermanentFlags: defaultMailboxPermanentFlags(),
Flags: defaultFlags,
PermanentFlags: defaultPermanentFlags,
Attributes: imap.NewFlagSet(imap.AttrNoSelect),
})
}
@ -96,8 +96,8 @@ func newMailboxCreatedUpdate(labelID imap.MailboxID, labelName []string) *imap.M
return imap.NewMailboxCreated(imap.Mailbox{
ID: labelID,
Name: labelName,
Flags: defaultMailboxFlags(),
PermanentFlags: defaultMailboxPermanentFlags(),
Flags: defaultFlags,
PermanentFlags: defaultPermanentFlags,
Attributes: imap.NewFlagSet(),
})
}

View File

@ -35,44 +35,6 @@ func (m *MockIMAPStateWrite) EXPECT() *MockIMAPStateWriteMockRecorder {
return m.recorder
}
// AddFlagsToAllMailboxes mocks base method.
func (m *MockIMAPStateWrite) AddFlagsToAllMailboxes(arg0 context.Context, arg1 ...string) error {
m.ctrl.T.Helper()
varargs := []interface{}{arg0}
for _, a := range arg1 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "AddFlagsToAllMailboxes", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// AddFlagsToAllMailboxes indicates an expected call of AddFlagsToAllMailboxes.
func (mr *MockIMAPStateWriteMockRecorder) AddFlagsToAllMailboxes(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0}, arg1...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddFlagsToAllMailboxes", reflect.TypeOf((*MockIMAPStateWrite)(nil).AddFlagsToAllMailboxes), varargs...)
}
// AddPermFlagsToAllMailboxes mocks base method.
func (m *MockIMAPStateWrite) AddPermFlagsToAllMailboxes(arg0 context.Context, arg1 ...string) error {
m.ctrl.T.Helper()
varargs := []interface{}{arg0}
for _, a := range arg1 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "AddPermFlagsToAllMailboxes", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// AddPermFlagsToAllMailboxes indicates an expected call of AddPermFlagsToAllMailboxes.
func (mr *MockIMAPStateWriteMockRecorder) AddPermFlagsToAllMailboxes(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0}, arg1...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddPermFlagsToAllMailboxes", reflect.TypeOf((*MockIMAPStateWrite)(nil).AddPermFlagsToAllMailboxes), varargs...)
}
// CreateMailbox mocks base method.
func (m *MockIMAPStateWrite) CreateMailbox(arg0 context.Context, arg1 imap.Mailbox) error {
m.ctrl.T.Helper()

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))
@ -395,11 +411,6 @@ func (s *Service) run(ctx context.Context) { //nolint gocyclo
// event service is unable to reply to the request until the events have been processed.
s.log.Info("Sync complete, starting API event stream")
go func() {
// If context cancelled do not do anything
if ctx.Err() != nil {
return
}
if err := s.eventProvider.RewindEventID(ctx, s.lastHandledEventID); err != nil {
if errors.Is(err, context.Canceled) {
return
@ -465,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()
}
}
}
@ -632,6 +632,10 @@ func (s *Service) cancelSync() {
type resyncReq struct{}
type cancelSyncReq struct{}
type resumeSyncReq struct{}
type getLabelsReq struct{}
type onBadEventReq struct{}

View File

@ -21,21 +21,16 @@ import (
"context"
"io"
"sync"
"time"
)
type Accounts struct {
accountsLock sync.RWMutex
accounts map[string]*smtpAccountState
accounts map[string]*Service
}
const maxFailedCommands = 3
const defaultErrTimeout = 20 * time.Second
const successiveErrInterval = time.Second
func NewAccounts() *Accounts {
return &Accounts{
accounts: make(map[string]*smtpAccountState),
accounts: make(map[string]*Service),
}
}
@ -43,10 +38,7 @@ func (s *Accounts) AddAccount(account *Service) {
s.accountsLock.Lock()
defer s.accountsLock.Unlock()
s.accounts[account.UserID()] = &smtpAccountState{
service: account,
errTimeout: defaultErrTimeout,
}
s.accounts[account.UserID()] = account
}
func (s *Accounts) RemoveAccount(account *Service) {
@ -60,18 +52,18 @@ func (s *Accounts) CheckAuth(user string, password []byte) (string, string, erro
s.accountsLock.RLock()
defer s.accountsLock.RUnlock()
for id, account := range s.accounts {
addrID, err := account.service.checkAuth(context.Background(), user, password)
for id, service := range s.accounts {
addrID, err := service.checkAuth(context.Background(), user, password)
if err != nil {
continue
}
account.service.telemetry.ReportSMTPAuthSuccess(context.Background())
service.telemetry.ReportSMTPAuthSuccess(context.Background())
return id, addrID, nil
}
for _, service := range s.accounts {
service.service.telemetry.ReportSMTPAuthFailed(user)
service.telemetry.ReportSMTPAuthFailed(user)
}
return "", "", ErrNoSuchUser
@ -85,57 +77,10 @@ func (s *Accounts) SendMail(ctx context.Context, userID, addrID, from string, to
s.accountsLock.RLock()
defer s.accountsLock.RUnlock()
requestTime := time.Now()
account, ok := s.accounts[userID]
service, ok := s.accounts[userID]
if !ok {
return ErrNoSuchUser
}
if err := account.canMakeRequest(requestTime); err != nil {
return err
}
err := account.service.SendMail(ctx, addrID, from, to, r)
account.handleSMTPErr(requestTime, err)
return err
}
type smtpAccountState struct {
service *Service
errTimeout time.Duration
errLock sync.Mutex
errCounter int
lastRequest time.Time
}
func (s *smtpAccountState) canMakeRequest(requestTime time.Time) error {
s.errLock.Lock()
defer s.errLock.Unlock()
if s.errCounter >= maxFailedCommands {
if requestTime.Sub(s.lastRequest) >= s.errTimeout {
s.errCounter = 0
return nil
}
return ErrTooManyErrors
}
return nil
}
func (s *smtpAccountState) handleSMTPErr(requestTime time.Time, err error) {
s.errLock.Lock()
defer s.errLock.Unlock()
if err == nil || requestTime.Sub(s.lastRequest) > successiveErrInterval {
s.errCounter = 0
} else {
s.errCounter++
}
s.lastRequest = requestTime
return service.SendMail(ctx, addrID, from, to, r)
}

View File

@ -1,46 +0,0 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail 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 smtp
import (
"errors"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestAccountTimeout(t *testing.T) {
account := smtpAccountState{errTimeout: 5 * time.Second}
err := errors.New("fail")
for i := 0; i <= maxFailedCommands; i++ {
requestTime := time.Now()
assert.Nil(t, account.canMakeRequest(requestTime))
account.handleSMTPErr(requestTime, err)
}
{
requestTime := time.Now()
assert.ErrorIs(t, account.canMakeRequest(requestTime), ErrTooManyErrors)
}
assert.Eventually(t, func() bool {
requestTime := time.Now()
return account.canMakeRequest(requestTime) == nil
}, 10*time.Second, time.Second)
}

View File

@ -17,24 +17,8 @@
package smtp
import (
"errors"
"fmt"
)
import "errors"
var ErrInvalidRecipient = errors.New("invalid recipient")
var ErrInvalidReturnPath = errors.New("invalid return path")
var ErrNoSuchUser = errors.New("no such user")
var ErrTooManyErrors = errors.New("too many failed requests, please try again later")
type ErrCanNotSendOnAddress struct {
address string
}
func NewErrCanNotSendOnAddress(address string) *ErrCanNotSendOnAddress {
return &ErrCanNotSendOnAddress{address: address}
}
func (e ErrCanNotSendOnAddress) Error() string {
return fmt.Sprintf("can't send on address: %v", e.address)
}

View File

@ -95,16 +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 for from address %v", sender)
return ErrInvalidReturnPath
}
}
if !fromAddr.Send {
s.log.Errorf("Can't send emails on address: %v", fromAddr.Email)
return &ErrCanNotSendOnAddress{address: fromAddr.Email}
}
// Load the user's mail settings.
@ -191,7 +181,7 @@ func (s *Service) sendWithKey(
if message.InReplyTo != "" {
references = append(references, message.InReplyTo)
}
parentID, draftsToDelete, err := getParentID(ctx, s.client, authAddrID, addrMode, references)
parentID, err := getParentID(ctx, s.client, authAddrID, addrMode, references)
if err != nil {
if err := s.reporter.ReportMessageWithContext("Failed to get parent ID", reporter.Context{
"error": err,
@ -217,7 +207,7 @@ func (s *Service) sendWithKey(
return proton.Message{}, fmt.Errorf("unsupported MIME type: %v", message.MIMEType)
}
draft, err := s.createDraft(ctx, addrKR, emails, from, to, parentID, message.InReplyTo, message.XForward, proton.DraftTemplate{
draft, err := s.createDraft(ctx, addrKR, emails, from, to, parentID, message.InReplyTo, proton.DraftTemplate{
Subject: message.Subject,
Body: decBody,
MIMEType: message.MIMEType,
@ -230,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)
@ -253,13 +243,6 @@ func (s *Service) sendWithKey(
return proton.Message{}, fmt.Errorf("failed to send draft: %w", err)
}
// Only delete the drafts, if any, after message was successfully sent.
if len(draftsToDelete) != 0 {
if err := s.client.DeleteMessage(ctx, draftsToDelete...); err != nil {
s.log.WithField("ids", draftsToDelete).WithError(err).Errorf("Failed to delete requested messages from Drafts")
}
}
return res, nil
}
@ -269,12 +252,11 @@ func getParentID(
authAddrID string,
addrMode usertypes.AddressMode,
references []string,
) (string, []string, error) {
) (string, error) {
var (
parentID string
internal []string
external []string
draftsToDelete []string
parentID string
internal []string
external []string
)
// Collect all the internal and external references of the message.
@ -299,18 +281,14 @@ func getParentID(
AddressID: addrID,
})
if err != nil {
return "", nil, fmt.Errorf("failed to get message metadata: %w", err)
return "", fmt.Errorf("failed to get message metadata: %w", err)
}
for _, metadata := range metadata {
if !metadata.IsDraft() {
parentID = metadata.ID
} else {
// We need to record this ID to delete later after the message has been sent successfully. This is
// required for Apple Mail to correctly delete a draft when a draft is created in Apple Mail, then
// edited on the web, edited again in Apple Mail and then Send from Apple Mail. If we don't
// delete the referenced draft it is never deleted from the drafts folder.
draftsToDelete = append(draftsToDelete, metadata.ID)
} else if err := client.DeleteMessage(ctx, metadata.ID); err != nil {
return "", fmt.Errorf("failed to delete message: %w", err)
}
}
}
@ -331,17 +309,13 @@ func getParentID(
AddressID: addrID,
})
if err != nil {
return "", nil, fmt.Errorf("failed to get message metadata: %w", err)
return "", fmt.Errorf("failed to get message metadata: %w", err)
}
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:
@ -356,7 +330,7 @@ func getParentID(
}
}
return parentID, draftsToDelete, nil
return parentID, nil
}
func (s *Service) createDraft(
@ -367,7 +341,6 @@ func (s *Service) createDraft(
to []string,
parentID string,
replyToID string,
xForwardID string,
template proton.DraftTemplate,
) (proton.Message, error) {
// Check sender: set the sender if it's missing.
@ -403,12 +376,7 @@ func (s *Service) createDraft(
var action proton.CreateDraftAction
if len(replyToID) > 0 {
// Thunderbird fills both ReplyTo and adds an X-Forwarded-Message-Id header when forwarding.
if replyToID == xForwardID {
action = proton.ForwardAction
} else {
action = proton.ReplyAction
}
action = proton.ReplyAction
} else {
action = proton.ForwardAction
}

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 (
@ -34,14 +33,13 @@ const (
)
type contactSettings struct {
Email string
Keys []string
Scheme string
Sign bool
SignIsSet bool
Encrypt bool
EncryptUntrusted bool
MIMEType rfc822.MIMEType
Email string
Keys []string
Scheme string
Sign bool
SignIsSet bool
Encrypt bool
MIMEType rfc822.MIMEType
}
// newContactSettings converts the API settings into our local settings.
@ -62,12 +60,6 @@ func newContactSettings(settings proton.ContactSettings) *contactSettings {
metadata.Encrypt = *settings.Encrypt
}
if settings.EncryptUntrusted != nil {
metadata.EncryptUntrusted = *settings.EncryptUntrusted
} else {
metadata.EncryptUntrusted = true
}
if settings.Scheme != nil {
switch *settings.Scheme { // nolint:exhaustive
case proton.PGPMIMEScheme:
@ -433,12 +425,9 @@ func (b *sendPrefsBuilder) setExternalPGPSettingsWithWKDKeys(
return errors.New("an API key is necessary but wasn't provided")
}
b.withEncrypt(vCardData.EncryptUntrusted)
if vCardData.EncryptUntrusted {
b.withSign(true)
} else if vCardData.SignIsSet {
b.withSign(vCardData.Sign)
}
// We always encrypt and sign external mail if WKD keys are present.
b.withEncrypt(true)
b.withSign(true)
// If the contact has a specific Scheme preference, we set it (otherwise we
// leave it unset to allow it to be filled in with the default value later).
@ -558,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

@ -110,22 +110,7 @@ func TestPreferencesBuilder(t *testing.T) {
{
name: "wkd-external",
contactMeta: &contactSettings{EncryptUntrusted: true},
receivedKeys: []proton.PublicKey{{PublicKey: testPublicKey}},
isInternal: false,
mailSettings: proton.MailSettings{PGPScheme: proton.PGPMIMEScheme, DraftMIMEType: "text/html"},
wantEncrypt: true,
wantSign: proton.DetachedSignature,
wantScheme: proton.PGPMIMEScheme,
wantMIMEType: "multipart/mixed",
wantPublicKey: testPublicKey,
},
{
name: "wkd-external",
contactMeta: &contactSettings{EncryptUntrusted: true},
contactMeta: &contactSettings{},
receivedKeys: []proton.PublicKey{{PublicKey: testPublicKey}},
isInternal: false,
mailSettings: proton.MailSettings{PGPScheme: proton.PGPMIMEScheme, DraftMIMEType: "text/html"},
@ -140,7 +125,7 @@ func TestPreferencesBuilder(t *testing.T) {
{
name: "wkd-external with contact-specific email format",
contactMeta: &contactSettings{MIMEType: "text/plain", EncryptUntrusted: true},
contactMeta: &contactSettings{MIMEType: "text/plain"},
receivedKeys: []proton.PublicKey{{PublicKey: testPublicKey}},
isInternal: false,
mailSettings: proton.MailSettings{PGPScheme: proton.PGPMIMEScheme, DraftMIMEType: "text/html"},
@ -155,7 +140,7 @@ func TestPreferencesBuilder(t *testing.T) {
{
name: "wkd-external with global pgp-inline scheme",
contactMeta: &contactSettings{EncryptUntrusted: true},
contactMeta: &contactSettings{},
receivedKeys: []proton.PublicKey{{PublicKey: testPublicKey}},
isInternal: false,
mailSettings: proton.MailSettings{PGPScheme: proton.PGPInlineScheme, DraftMIMEType: "text/html"},
@ -170,7 +155,7 @@ func TestPreferencesBuilder(t *testing.T) {
{
name: "wkd-external with contact-specific pgp-inline scheme overriding global pgp-mime setting",
contactMeta: &contactSettings{Scheme: pgpInline, EncryptUntrusted: true},
contactMeta: &contactSettings{Scheme: pgpInline},
receivedKeys: []proton.PublicKey{{PublicKey: testPublicKey}},
isInternal: false,
mailSettings: proton.MailSettings{PGPScheme: proton.PGPMIMEScheme, DraftMIMEType: "text/html"},
@ -185,7 +170,7 @@ func TestPreferencesBuilder(t *testing.T) {
{
name: "wkd-external with contact-specific pgp-mime scheme overriding global pgp-inline setting",
contactMeta: &contactSettings{Scheme: pgpMIME, EncryptUntrusted: true},
contactMeta: &contactSettings{Scheme: pgpMIME},
receivedKeys: []proton.PublicKey{{PublicKey: testPublicKey}},
isInternal: false,
mailSettings: proton.MailSettings{PGPScheme: proton.PGPInlineScheme, DraftMIMEType: "text/html"},
@ -200,7 +185,7 @@ func TestPreferencesBuilder(t *testing.T) {
{
name: "wkd-external with additional pinned contact public key",
contactMeta: &contactSettings{Keys: []string{testContactKey}, EncryptUntrusted: true},
contactMeta: &contactSettings{Keys: []string{testContactKey}},
receivedKeys: []proton.PublicKey{{PublicKey: testPublicKey}},
isInternal: false,
mailSettings: proton.MailSettings{PGPScheme: proton.PGPMIMEScheme, DraftMIMEType: "text/html"},
@ -216,7 +201,7 @@ func TestPreferencesBuilder(t *testing.T) {
// NOTE: Need to figured out how to test that this calls the frontend to check for user confirmation.
name: "wkd-external with additional conflicting contact public key",
contactMeta: &contactSettings{Keys: []string{testOtherContactKey}, EncryptUntrusted: true},
contactMeta: &contactSettings{Keys: []string{testOtherContactKey}},
receivedKeys: []proton.PublicKey{{PublicKey: testPublicKey}},
isInternal: false,
mailSettings: proton.MailSettings{PGPScheme: proton.PGPMIMEScheme, DraftMIMEType: "text/html"},
@ -228,51 +213,6 @@ func TestPreferencesBuilder(t *testing.T) {
wantPublicKey: testPublicKey,
},
{
name: "wkd-external-with-encrypt-and-sign-disabled",
contactMeta: &contactSettings{EncryptUntrusted: false},
receivedKeys: []proton.PublicKey{{PublicKey: testPublicKey}},
isInternal: false,
mailSettings: proton.MailSettings{PGPScheme: proton.PGPMIMEScheme, DraftMIMEType: "text/html"},
wantEncrypt: false,
wantSign: proton.NoSignature,
wantScheme: proton.ClearScheme,
wantMIMEType: "text/html",
wantPublicKey: testPublicKey,
},
{
name: "wkd-external-with-encrypt-and-sign-disabled-plain-text",
contactMeta: &contactSettings{EncryptUntrusted: false},
receivedKeys: []proton.PublicKey{{PublicKey: testPublicKey}},
isInternal: false,
mailSettings: proton.MailSettings{PGPScheme: proton.PGPMIMEScheme, DraftMIMEType: "text/plain"},
wantEncrypt: false,
wantSign: proton.NoSignature,
wantScheme: proton.ClearScheme,
wantMIMEType: "text/plain",
wantPublicKey: testPublicKey,
},
{
name: "wkd-external-with-encrypt-disabled-sign-enabled",
contactMeta: &contactSettings{EncryptUntrusted: false, Sign: true, SignIsSet: true},
receivedKeys: []proton.PublicKey{{PublicKey: testPublicKey}},
isInternal: false,
mailSettings: proton.MailSettings{PGPScheme: proton.PGPMIMEScheme, DraftMIMEType: "text/html"},
wantEncrypt: false,
wantSign: proton.DetachedSignature,
wantScheme: proton.ClearMIMEScheme,
wantMIMEType: "multipart/mixed",
wantPublicKey: testPublicKey,
},
{
name: "external",

View File

@ -58,10 +58,6 @@ func (s Status) IsComplete() bool {
return s.HasLabels && s.HasMessages
}
func (s Status) InProgress() bool {
return s.HasLabels || s.HasMessageCount
}
// Regulator is an abstraction for the sync service, since it regulates the number of concurrent sync activities.
type Regulator interface {
Sync(ctx context.Context, stage *Job)

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

@ -36,20 +36,6 @@ func (m *MockHeartbeatManager) EXPECT() *MockHeartbeatManagerMockRecorder {
return m.recorder
}
// GetHeartbeatPeriodicInterval mocks base method.
func (m *MockHeartbeatManager) GetHeartbeatPeriodicInterval() time.Duration {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetHeartbeatPeriodicInterval")
ret0, _ := ret[0].(time.Duration)
return ret0
}
// GetHeartbeatPeriodicInterval indicates an expected call of GetHeartbeatPeriodicInterval.
func (mr *MockHeartbeatManagerMockRecorder) GetHeartbeatPeriodicInterval() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHeartbeatPeriodicInterval", reflect.TypeOf((*MockHeartbeatManager)(nil).GetHeartbeatPeriodicInterval))
}
// GetLastHeartbeatSent mocks base method.
func (m *MockHeartbeatManager) GetLastHeartbeatSent() time.Time {
m.ctrl.T.Helper()

View File

@ -42,7 +42,6 @@ type HeartbeatManager interface {
SendHeartbeat(ctx context.Context, heartbeat *HeartbeatData) bool
GetLastHeartbeatSent() time.Time
SetLastHeartbeatSent(time.Time) error
GetHeartbeatPeriodicInterval() time.Duration
}
type HeartbeatValues struct {

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

@ -33,7 +33,7 @@ func migrateSyncStatusFromVault(encVault *vault.User, syncConfigDir string, user
}
if migrated {
if err := encVault.ClearSyncStatusWithoutEventID(); err != nil {
if err := encVault.ClearSyncStatus(); err != nil {
return fmt.Errorf("failed to clear sync settings from vault: %w", err)
}
}

View File

@ -105,7 +105,6 @@ func New(
eventSubscription events.Subscription,
syncService syncservice.Regulator,
syncConfigDir string,
isNew bool,
) (*User, error) {
user, err := newImpl(
ctx,
@ -123,7 +122,6 @@ func New(
eventSubscription,
syncService,
syncConfigDir,
isNew,
)
if err != nil {
// Cleanup any pending resources on error
@ -154,7 +152,6 @@ func newImpl(
eventSubscription events.Subscription,
syncService syncservice.Regulator,
syncConfigDir string,
isNew bool,
) (*User, error) {
logrus.WithField("userID", apiUser.ID).Info("Creating new user")
@ -226,7 +223,6 @@ func newImpl(
EventJitter,
5*time.Minute,
crashHandler,
eventSubscription,
)
addressMode := usertypes.VaultToAddressMode(encVault.AddressMode())
@ -298,14 +294,6 @@ func newImpl(
return nil
})
// If it's not a fresh user check the eventID and evaluate whether it is valid. If it's a new user, we don't
// need to perform this check.
if !isNew {
if err := checkIrrecoverableEventID(ctx, encVault.EventID(), apiUser.ID, syncConfigDir, user); err != nil {
return nil, err
}
}
// Start Event Service
lastEventID, err := user.eventService.Start(ctx, user.serviceGroup)
if err != nil {
@ -379,29 +367,25 @@ func (user *User) Match(query string) bool {
return false
}
// DisplayNames returns a map of the email addresses and their associated display names.
func (user *User) DisplayNames() map[string]string {
addresses := user.protonAddresses()
if addresses == nil {
return nil
}
result := make(map[string]string)
for _, address := range addresses {
result[address.Email] = address.DisplayName
}
return result
}
// Emails returns all the user's active email addresses.
// It returns them in sorted order; the user's primary address is first.
func (user *User) Emails() []string {
addresses := user.protonAddresses()
if addresses == nil {
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Minute))
defer cancel()
apiAddrs, err := user.identityService.GetAddresses(ctx)
if err != nil {
return nil
}
addresses := xslices.Filter(maps.Values(apiAddrs), func(addr proton.Address) bool {
return addr.Status == proton.AddressStatusEnabled && addr.Type != proton.AddressTypeExternal
})
slices.SortFunc(addresses, func(a, b proton.Address) bool {
return a.Order < b.Order
})
return xslices.Map(addresses, func(addr proton.Address) string {
return addr.Email
})
@ -531,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()
@ -544,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()
@ -570,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")
@ -697,23 +702,3 @@ func (user *User) PauseEventLoopWithWaiter() *userevents.EventPollWaiter {
func (user *User) ResumeEventLoop() {
user.eventService.Resume()
}
func (user *User) protonAddresses() []proton.Address {
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Minute))
defer cancel()
apiAddrs, err := user.identityService.GetAddresses(ctx)
if err != nil {
return nil
}
addresses := xslices.Filter(maps.Values(apiAddrs), func(addr proton.Address) bool {
return addr.Status == proton.AddressStatusEnabled && addr.Type != proton.AddressTypeExternal
})
slices.SortFunc(addresses, func(a, b proton.Address) bool {
return a.Order < b.Order
})
return addresses
}

View File

@ -1,68 +0,0 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail 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 user
import (
"context"
"fmt"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice"
)
func checkIrrecoverableEventID(
ctx context.Context,
lastEventID,
userID,
syncConfigDir string,
publisher events.EventPublisher,
) error {
// If we detect that the event ID stored in the vault got reset, the user is not a new account and
// we have started or finished syncing: this is an irrecoverable state and we should produce a bad event.
if lastEventID != "" {
return nil
}
syncConfigPath := imapservice.GetSyncConfigPath(syncConfigDir, userID)
syncState, err := imapservice.NewSyncState(syncConfigPath)
if err != nil {
return fmt.Errorf("failed to read imap sync state: %w", err)
}
syncStatus, err := syncState.GetSyncStatus(ctx)
if err != nil {
return fmt.Errorf("failed to imap sync status: %w", err)
}
if syncStatus.IsComplete() || syncStatus.InProgress() {
publisher.PublishEvent(ctx, newEmptyEventIDBadEvent(userID))
}
return nil
}
func newEmptyEventIDBadEvent(userID string) events.UserBadEvent {
return events.UserBadEvent{
UserID: userID,
OldEventID: "",
NewEventID: "",
EventInfo: "EventID missing from vault",
Error: fmt.Errorf("eventID in vault is empty, when it shouldn't be"),
}
}

View File

@ -1,104 +0,0 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail 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 user
import (
"context"
"testing"
"github.com/ProtonMail/proton-bridge/v3/internal/events/mocks"
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
)
func TestCheckIrrecoverableEventID_EventIDIsEmptyButNoSyncStarted(t *testing.T) {
tmpDir := t.TempDir()
userID := "foo"
mockCtrl := gomock.NewController(t)
publisher := mocks.NewMockEventPublisher(mockCtrl)
require.NoError(t, checkIrrecoverableEventID(context.Background(), "", userID, tmpDir, publisher))
}
func TestCheckIrrecoverableEventID_EventIDIsNotEmptyButNoSyncStarted(t *testing.T) {
tmpDir := t.TempDir()
userID := "foo"
mockCtrl := gomock.NewController(t)
publisher := mocks.NewMockEventPublisher(mockCtrl)
require.NoError(t, checkIrrecoverableEventID(context.Background(), "ffoofo", userID, tmpDir, publisher))
}
func TestCheckIrrecoverableEventID_EventIDIsEmptyButSyncStarted(t *testing.T) {
tmpDir := t.TempDir()
userID := "foo"
mockCtrl := gomock.NewController(t)
publisher := mocks.NewMockEventPublisher(mockCtrl)
publisher.EXPECT().PublishEvent(gomock.Any(), gomock.Eq(newEmptyEventIDBadEvent(userID)))
require.NoError(t, genSyncState(context.Background(), userID, tmpDir, false))
require.NoError(t, checkIrrecoverableEventID(context.Background(), "", userID, tmpDir, publisher))
}
func TestCheckIrrecoverableEventID_EventIDIsEmptyButSyncFinished(t *testing.T) {
tmpDir := t.TempDir()
userID := "foo"
mockCtrl := gomock.NewController(t)
publisher := mocks.NewMockEventPublisher(mockCtrl)
publisher.EXPECT().PublishEvent(gomock.Any(), gomock.Eq(newEmptyEventIDBadEvent(userID)))
require.NoError(t, genSyncState(context.Background(), userID, tmpDir, true))
require.NoError(t, checkIrrecoverableEventID(context.Background(), "", userID, tmpDir, publisher))
}
func TestCheckIrrecoverableEventID_EventIDIsNotEmptyButSyncFinished(t *testing.T) {
tmpDir := t.TempDir()
userID := "foo"
mockCtrl := gomock.NewController(t)
publisher := mocks.NewMockEventPublisher(mockCtrl)
require.NoError(t, genSyncState(context.Background(), userID, tmpDir, true))
require.NoError(t, checkIrrecoverableEventID(context.Background(), "some event", userID, tmpDir, publisher))
}
func genSyncState(ctx context.Context, userID, dir string, finished bool) error {
s, err := imapservice.NewSyncState(imapservice.GetSyncConfigPath(dir, userID))
if err != nil {
return err
}
if finished {
if err := s.SetHasLabels(ctx, true); err != nil {
return err
}
if err := s.SetHasMessages(ctx, true); err != nil {
return err
}
if err := s.SetMessageCount(ctx, 10); err != nil {
return err
}
} else {
if err := s.SetHasLabels(ctx, true); err != nil {
return err
}
}
return nil
}

View File

@ -19,7 +19,6 @@ package user
import (
"context"
"reflect"
"testing"
"time"
@ -59,12 +58,8 @@ func TestUser_Info(t *testing.T) {
// User's name should be correct.
require.Equal(t, "username", user.Name())
// User's emails should be correct and their associated display names should be correct
// User's email should be correct.
require.ElementsMatch(t, []string{"username@" + s.GetDomain(), "alias@pm.me"}, user.Emails())
require.True(t, reflect.DeepEqual(map[string]string{
"username@" + s.GetDomain(): "username" + " (Display Name)",
"alias@pm.me": "alias@pm.me (Display Name)",
}, user.DisplayNames()))
// By default, user should be in combined mode.
require.Equal(t, vault.CombinedMode, user.GetAddressMode())
@ -103,14 +98,12 @@ func withAPI(_ testing.TB, ctx context.Context, fn func(context.Context, *server
func withAccount(tb testing.TB, s *server.Server, username, password string, aliases []string, fn func(string, []string)) { //nolint:unparam
userID, addrID, err := s.CreateUser(username, []byte(password))
require.NoError(tb, err)
require.NoError(tb, s.ChangeAddressDisplayName(userID, addrID, username+" (Display Name)"))
addrIDs := []string{addrID}
for _, email := range aliases {
addrID, err := s.CreateAddress(userID, email, []byte(password))
require.NoError(tb, err)
require.NoError(tb, s.ChangeAddressDisplayName(userID, addrID, email+" (Display Name)"))
addrIDs = append(addrIDs, addrID)
}
@ -165,7 +158,6 @@ func withUser(tb testing.TB, ctx context.Context, _ *server.Server, m *proton.Ma
nullEventSubscription,
nil,
"",
true,
)
require.NoError(tb, err)
defer user.Close()

View File

@ -186,13 +186,13 @@ func (user *User) RemFailedMessageID(messageID string) error {
})
}
// GetSyncStatusDeprecated returns the user's sync status.
func (user *User) GetSyncStatusDeprecated() SyncStatus {
// GetSyncStatus returns the user's sync status.
func (user *User) GetSyncStatus() SyncStatus {
return user.vault.getUser(user.userID).SyncStatus
}
// ClearSyncStatusDeprecated clears the user's sync status.
func (user *User) ClearSyncStatusDeprecated() error {
// ClearSyncStatus clears the user's sync status.
func (user *User) ClearSyncStatus() error {
return user.vault.modUser(user.userID, func(data *UserData) {
data.SyncStatus = SyncStatus{}
@ -200,13 +200,6 @@ func (user *User) ClearSyncStatusDeprecated() error {
})
}
// ClearSyncStatusWithoutEventID clears the user's sync status without modifying EventID.
func (user *User) ClearSyncStatusWithoutEventID() error {
return user.vault.modUser(user.userID, func(data *UserData) {
data.SyncStatus = SyncStatus{}
})
}
// EventID returns the last processed event ID of the user.
func (user *User) EventID() string {
return user.vault.getUser(user.userID).EventID

View File

@ -137,7 +137,7 @@ func TestUser_SyncStatus(t *testing.T) {
require.True(t, user.SyncStatus().HasMessages)
// Clear the sync status.
require.NoError(t, user.ClearSyncStatusDeprecated())
require.NoError(t, user.ClearSyncStatus())
// Check the user's cleared sync status.
require.False(t, user.SyncStatus().HasLabels)
@ -145,31 +145,6 @@ func TestUser_SyncStatus(t *testing.T) {
require.Empty(t, user.SyncStatus().LastMessageID)
}
func TestUser_ClearSyncStatusWithoutEventID(t *testing.T) {
// Create a new test vault.
s := newVault(t)
// Create a new user.
user, err := s.AddUser("userID", "username", "username@pm.me", "authUID", "authRef", []byte("keyPass"))
require.NoError(t, err)
// Simulate finishing the sync.
require.NoError(t, user.SetHasLabels(true))
require.NoError(t, user.SetHasMessages(true))
require.True(t, user.SyncStatus().HasLabels)
require.True(t, user.SyncStatus().HasMessages)
require.NoError(t, user.SetEventID("foo"))
// Clear the sync status.
require.NoError(t, user.ClearSyncStatusWithoutEventID())
// Check the user's cleared sync status.
require.False(t, user.SyncStatus().HasLabels)
require.False(t, user.SyncStatus().HasMessages)
require.Empty(t, user.SyncStatus().LastMessageID)
require.Equal(t, "foo", user.EventID())
}
func TestUser_PrimaryEmail(t *testing.T) {
// Create a new test vault.
s := newVault(t)

View File

@ -31,18 +31,14 @@ const (
MacOSKeychain = "macos-keychain"
)
func listHelpers() (Helpers, string) {
helpers := make(Helpers)
func init() { //nolint:gochecknoinits
Helpers = make(map[string]helperConstructor)
// MacOS always provides a keychain.
if isUsable(newMacOSHelper("")) {
helpers[MacOSKeychain] = newMacOSHelper
} else {
logrus.WithField("keychain", "MacOSKeychain").Warn("Keychain is not available.")
}
Helpers[MacOSKeychain] = newMacOSHelper
// Use MacOSKeychain by default.
return helpers, MacOSKeychain
DefaultHelper = MacOSKeychain
}
func parseError(original error) error {

View File

@ -18,6 +18,8 @@
package keychain
import (
"reflect"
"github.com/docker/docker-credential-helpers/credentials"
"github.com/docker/docker-credential-helpers/pass"
"github.com/docker/docker-credential-helpers/secretservice"
@ -31,37 +33,30 @@ const (
SecretServiceDBus = "secret-service-dbus"
)
func listHelpers() (Helpers, string) {
helpers := make(Helpers)
func init() { //nolint:gochecknoinits
Helpers = make(map[string]helperConstructor)
if isUsable(newDBusHelper("")) {
helpers[SecretServiceDBus] = newDBusHelper
} else {
logrus.WithField("keychain", "SecretServiceDBus").Warn("Keychain is not available.")
Helpers[SecretServiceDBus] = newDBusHelper
}
if _, err := execabs.LookPath("gnome-keyring"); err == nil && isUsable(newSecretServiceHelper("")) {
helpers[SecretService] = newSecretServiceHelper
} else {
logrus.WithField("keychain", "SecretService").Warn("Keychain is not available.")
Helpers[SecretService] = newSecretServiceHelper
}
if _, err := execabs.LookPath("pass"); err == nil && isUsable(newPassHelper("")) {
helpers[Pass] = newPassHelper
} else {
logrus.WithField("keychain", "Pass").Warn("Keychain is not available.")
Helpers[Pass] = newPassHelper
}
defaultHelper := SecretServiceDBus
DefaultHelper = SecretServiceDBus
// If Pass is available, use it by default.
// Otherwise, if SecretService is available, use it by default.
if _, ok := helpers[Pass]; ok {
defaultHelper = Pass
} else if _, ok := helpers[SecretService]; ok {
defaultHelper = SecretService
if _, ok := Helpers[Pass]; ok {
DefaultHelper = Pass
} else if _, ok := Helpers[SecretService]; ok {
DefaultHelper = SecretService
}
return helpers, defaultHelper
}
func newDBusHelper(string) (credentials.Helper, error) {
@ -75,3 +70,36 @@ func newPassHelper(string) (credentials.Helper, error) {
func newSecretServiceHelper(string) (credentials.Helper, error) {
return &secretservice.Secretservice{}, nil
}
// isUsable returns whether the credentials helper is usable.
func isUsable(helper credentials.Helper, err error) bool {
l := logrus.WithField("helper", reflect.TypeOf(helper))
if err != nil {
l.WithError(err).Warn("Keychain helper couldn't be created")
return false
}
creds := &credentials.Credentials{
ServerURL: "bridge/check",
Username: "check",
Secret: "check",
}
if err := helper.Add(creds); err != nil {
l.WithError(err).Warn("Failed to add test credentials to keychain")
return false
}
if _, _, err := helper.Get(creds.ServerURL); err != nil {
l.WithError(err).Warn("Failed to get test credentials from keychain")
return false
}
if err := helper.Delete(creds.ServerURL); err != nil {
l.WithError(err).Warn("Failed to delete test credentials from keychain")
return false
}
return true
}

View File

@ -20,21 +20,18 @@ package keychain
import (
"github.com/docker/docker-credential-helpers/credentials"
"github.com/docker/docker-credential-helpers/wincred"
"github.com/sirupsen/logrus"
)
const WindowsCredentials = "windows-credentials"
func listHelpers() (Helpers, string) {
helpers := make(Helpers)
func init() { //nolint:gochecknoinits
Helpers = make(map[string]helperConstructor)
// Windows always provides a keychain.
if isUsable(newWinCredHelper("")) {
helpers[WindowsCredentials] = newWinCredHelper
} else {
logrus.WithField("keychain", "WindowsCredentials").Warn("Keychain is not available.")
}
Helpers[WindowsCredentials] = newWinCredHelper
// Use WindowsCredentials by default.
return helpers, WindowsCredentials
DefaultHelper = WindowsCredentials
}
func newWinCredHelper(string) (credentials.Helper, error) {

View File

@ -21,12 +21,9 @@ package keychain
import (
"errors"
"fmt"
"reflect"
"sync"
"time"
"github.com/docker/docker-credential-helpers/credentials"
"github.com/sirupsen/logrus"
)
// helperConstructor constructs a keychain helperConstructor.
@ -41,53 +38,28 @@ var (
// ErrMacKeychainRebuild is returned on macOS with blocked or corrupted keychain.
ErrMacKeychainRebuild = errors.New("keychain error -25293")
// Helpers holds all discovered keychain helpers. It is populated in init().
Helpers map[string]helperConstructor //nolint:gochecknoglobals
// DefaultHelper is the default helper to use if the user hasn't yet set a preference.
DefaultHelper string //nolint:gochecknoglobals
)
type Helpers map[string]helperConstructor
type List struct {
helpers Helpers
defaultHelper string
locker sync.Locker
}
// NewList checks availability of every keychains detected on the User Operating System
// This will ask the user to unlock keychain(s) to check their usability.
// This should only be called once.
func NewList() *List {
var list = List{locker: &sync.Mutex{}}
list.helpers, list.defaultHelper = listHelpers()
return &list
}
func (kcl *List) GetHelpers() Helpers {
kcl.locker.Lock()
defer kcl.locker.Unlock()
return kcl.helpers
}
func (kcl *List) GetDefaultHelper() string {
kcl.locker.Lock()
defer kcl.locker.Unlock()
return kcl.defaultHelper
}
// NewKeychain creates a new native keychain.
func NewKeychain(preferred, keychainName string, helpers Helpers, defaultHelper string) (*Keychain, error) {
func NewKeychain(preferred, keychainName string) (*Keychain, error) {
// There must be at least one keychain helper available.
if len(helpers) < 1 {
if len(Helpers) < 1 {
return nil, ErrNoKeychain
}
// If the preferred keychain is unsupported, fallback to the default one.
if _, ok := helpers[preferred]; !ok {
preferred = defaultHelper
if _, ok := Helpers[preferred]; !ok {
preferred = DefaultHelper
}
// Load the user's preferred keychain helper.
helperConstructor, ok := helpers[preferred]
helperConstructor, ok := Helpers[preferred]
if !ok {
return nil, ErrNoKeychain
}
@ -191,49 +163,3 @@ func (kc *Keychain) Put(userID, secret string) error {
func (kc *Keychain) secretURL(userID string) string {
return fmt.Sprintf("%v/%v", kc.url, userID)
}
// isUsable returns whether the credentials helper is usable.
func isUsable(helper credentials.Helper, err error) bool {
l := logrus.WithField("helper", reflect.TypeOf(helper))
if err != nil {
l.WithError(err).Warn("Keychain helper couldn't be created")
return false
}
creds := &credentials.Credentials{
ServerURL: "bridge/check",
Username: "check",
Secret: "check",
}
if err := retry(func() error {
return helper.Add(creds)
}); err != nil {
l.WithError(err).Warn("Failed to add test credentials to keychain")
return false
}
if _, _, err := helper.Get(creds.ServerURL); err != nil {
l.WithError(err).Warn("Failed to get test credentials from keychain")
return false
}
if err := helper.Delete(creds.ServerURL); err != nil {
l.WithError(err).Warn("Failed to delete test credentials from keychain")
return false
}
return true
}
func retry(condition func() error) error {
var maxRetry = 5
for r := 0; ; r++ {
err := condition()
if err == nil || r >= maxRetry {
return err
}
time.Sleep(200 * time.Millisecond)
}
}

View File

@ -17,22 +17,10 @@
package keychain
import (
"sync"
"github.com/docker/docker-credential-helpers/credentials"
)
import "github.com/docker/docker-credential-helpers/credentials"
type TestHelper map[string]*credentials.Credentials
func NewTestKeychainsList() *List {
keychainHelper := NewTestHelper()
helpers := make(Helpers)
helpers["mock"] = func(string) (credentials.Helper, error) { return keychainHelper, nil }
var list = List{helpers: helpers, defaultHelper: "mock", locker: &sync.Mutex{}}
return &list
}
func NewTestHelper() TestHelper {
return make(TestHelper)
}

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"
@ -60,7 +59,6 @@ type Message struct {
References []string
ExternalID string
InReplyTo string
XForward string
}
type Attachment struct {
@ -118,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")
@ -148,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")
}
@ -314,14 +308,24 @@ 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) {
if p.IsAttachment() {
disp, _, err := p.Header.ContentDisposition()
if err != nil {
disp = ""
}
if disp == "attachment" {
return parser.Parts{}, nil
}
return parser.Parts{p}, nil
}).
RegisterRule("text/html", func(p *parser.Part, visit parser.Visit) (interface{}, error) {
if p.IsAttachment() {
disp, _, err := p.Header.ContentDisposition()
if err != nil {
disp = ""
}
if disp == "attachment" {
return parser.Parts{}, nil
}
@ -396,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().
@ -405,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
}
@ -521,9 +525,6 @@ func parseMessageHeader(h message.Header, allowInvalidAddressLists bool) (Messag
case "in-reply-to":
m.InReplyTo = regexp.MustCompile("<(.*)>").ReplaceAllString(fields.Value(), "$1")
case "x-forwarded-message-id":
m.XForward = regexp.MustCompile("<(.*)>").ReplaceAllString(fields.Value(), "$1")
case "references":
for _, ref := range strings.Fields(fields.Value()) {
for _, ref := range strings.Split(ref, ",") {
@ -546,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
}
@ -557,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 {
@ -585,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
}
@ -635,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
}
}

Some files were not shown because too many files have changed in this diff Show More