Compare commits

..

24 Commits

Author SHA1 Message Date
05623a9e49 chore: Jubilee Bridge 3.20.0 changelog. 2025-04-24 13:40:43 +02:00
a305ee1113 chore: Infinity Bridge 3.19.0 changelog. 2025-04-24 11:13:17 +02:00
e38f7748d0 chore: bump GPA 2025-04-22 12:41:13 +00:00
92b2024e3e chore(BRIDGE-352): bump go to 1.24.2 2025-04-17 14:06:54 +00:00
37a8fc95d2 chore(BRIDGE-353): update x/net package to 0.38.0 2025-04-17 10:48:31 +02:00
0c63533aa7 fix(BRIDGE-351): allow draft creation and import to BYOE addresses in combined mode 2025-04-15 17:26:14 +02:00
af98bc2273 fix(BRIDGE-301): don't use external, non-BYOE addresses for imports 2025-04-10 09:51:31 +00:00
b37f2d138a feat(BRIDGE-348): display BYOE addresses in Bridge 2025-04-10 10:06:40 +02:00
7831a98e6c chore(BRIDGE-346): silence http/net vulnerability 2025-04-09 10:49:34 +02:00
4d415675e0 fix(BRIDGE-341): replace go-autostart with fork; added ability to create shortcuts with unicode chars 2025-04-02 11:56:58 +00:00
291f44d1b5 fix(BRIDGE-332): filter new line characters from username and password fields in GUI 2025-04-01 14:37:18 +02:00
a4b315d67a fix(BRIDGE-336): check and create all labels in Gluon on Bridge start 2025-03-25 15:24:59 +01:00
a15d4eb3ef ci: update CODEOWNERS 2025-03-24 15:38:03 +01:00
4e764fe93d feat(BRIDGE-340): additional logging for label operations & bad events 2025-03-24 14:30:19 +01:00
df409925ec fix(BRIDGE-335): store last sucessfully used keychain helper as user preference 2025-03-19 15:10:09 +01:00
e68f3441d7 fix(BRIDGE-196): bump badssl public key 2025-03-19 10:00:23 +00:00
899d3293bc feat(BRIDGE-324): added a log entry for the vault key hash 2025-03-18 11:21:12 +00:00
c66f0b800a fix(BRIDGE-333): ignore unkown label IDs during synchronization 2025-03-17 10:43:26 +01:00
b9c75d02b2 chore: stabilize windows tests 2025-03-14 11:56:42 +01:00
4b91e66505 chore(BRIDGE-315): remove silenced vulns 2025-03-06 14:49:03 +00:00
0cbcd0bf13 fix(BRIDGE-329): fix menu bar icons not displayin on macOS 2025-03-06 15:10:52 +01:00
5c12b00e70 chore: Helix Bridge 3.18.0 changelog. 2025-03-06 10:37:52 +01:00
6e7cdfcd68 feat(BRIDGE-316): Changes required for Qt 6.8.2 bump; bumped go to 1.24.0; changes to OS bundler configs; golangci-lint bump; 2025-03-05 14:27:33 +01:00
a75f84742b chore: remove redundant log entry 2025-02-24 10:58:16 +01:00
50 changed files with 929 additions and 213 deletions

View File

@ -1 +1 @@
* @go/bridge-ppl/devs
* inbox-desktop-approvers

View File

@ -3,14 +3,14 @@
## Prerequisites
* 64-bit OS:
- the go-rfc5322 module cannot currently be compiled for 32-bit OSes
* Go 1.23.4
* Go 1.24.0
* Bash with basic build utils: make, gcc, sed, find, grep, ...
- For Windows, it is recommended to use MinGW 64bit shell from [MSYS2](https://www.msys2.org/)
* GCC (Linux), msvc (Windows) or Xcode (macOS)
* Windres (Windows)
* libglvnd and libsecret development files (Linux)
* pkg-config (Linux)
* cmake, ninja-build and Qt 6.4.3 are required to build the graphical user interface. On Linux,
* cmake, ninja-build and Qt 6.8.2 are required to build the graphical user interface. On Linux,
the Mesa OpenGL development files are also needed.
To enable the sending of crash reports using Sentry please set the
@ -19,7 +19,7 @@ Otherwise, the sending of crash reports will be disabled.
## Build
In order to build Bridge app with Qt interface we are using
[Qt 6.4.3](https://doc.qt.io/qt-6/gettingstarted.html).
[Qt 6.8.2](https://doc.qt.io/qt-6/gettingstarted.html).
Please note that qmake path must be in your `PATH` to ensure Qt to be found.
Also, before you start build **on Windows**, please unset the `MSYSTEM` variable

View File

@ -141,6 +141,7 @@ Proton Mail Bridge includes the following 3rd party software:
* [appengine](https://google.golang.org/appengine) available under [license](https://pkg.go.dev/google.golang.org/appengine?tab=licenses)
* [genproto](https://google.golang.org/genproto) available under [license](https://pkg.go.dev/google.golang.org/genproto?tab=licenses)
* [yaml](https://gopkg.in/yaml.v3) available under [license](https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE) available under [license](https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE)
* [go-autostart](https://github.com/ElectroNafta/go-autostart) available under [license](https://github.com/ElectroNafta/go-autostart/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)

View File

@ -3,6 +3,33 @@
Changelog [format](http://keepachangelog.com/en/1.0.0/)
## Jubilee Bridge 3.20.0
### Added
* BRIDGE-348: Enable display of BYOE addresses in Bridge.
* BRIDGE-340: Added additional logging for label operations and related bad events.
* BRIDGE-324: Log a hash of the vault key on Bridge start.
### Changed
* BRIDGE-352: Chore: bump go to 1.24.2.
* BRIDGE-353: Chore: update x/net package to 0.38.0.
### Fixed
* BRIDGE-351: Allow draft creation and import to BYOE addresses in combined mode.
* BRIDGE-301: Prevent imports into non-BYOE external addresses.
* BRIDGE-341: Replaced go-autostart with a fork to support creating autostart shortcuts in directories with Unicode characters on Windows.
* BRIDGE-332: Strip newline characters from username and password fields in the Bridge GUI.
* BRIDGE-336: Ensure all remote labels are verified and created in Gluon at Bridge startup.
* BRIDGE-335: Persist the last successfully used keychain helper as a user preference on Linux.
* BRIDGE-333: Ignore unknown label IDs during Bridge synchronization.
## Infinity Bridge 3.19.0
### Changed
* BRIDGE-316: Update Qt to latest LTS version 6.8.2.
## Helix Bridge 3.18.0
### Changed

View File

@ -12,7 +12,7 @@ ROOT_DIR:=$(realpath .)
.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.18.0+git
BRIDGE_APP_VERSION?=3.20.0+git
APP_VERSION:=${BRIDGE_APP_VERSION}
APP_FULL_NAME:=Proton Mail Bridge
APP_VENDOR:=Proton AG
@ -189,7 +189,7 @@ ${RESOURCE_FILE}: ./dist/info.rc ./dist/${SRC_ICO} .FORCE
## Dev dependencies
.PHONY: install-devel-tools install-linter install-go-mod-outdated install-git-hooks
LINTVER:="v1.61.0"
LINTVER:="v1.64.6"
LINTSRC:="https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh"
install-dev-dependencies: install-devel-tools install-linter install-go-mod-outdated

19
go.mod
View File

@ -1,15 +1,15 @@
module github.com/ProtonMail/proton-bridge/v3
go 1.23
go 1.24
toolchain go1.23.4
toolchain go1.24.2
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.20250116113909-2ebd96ec0bc2
github.com/ProtonMail/gluon v0.17.1-0.20250324123053-2abce471ad71
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
github.com/ProtonMail/go-proton-api v0.4.1-0.20250217140732-2e531f21de4c
github.com/ProtonMail/go-proton-api v0.4.1-0.20250417134000-e624a080f7ba
github.com/ProtonMail/gopenpgp/v2 v2.8.2-proton
github.com/PuerkitoBio/goquery v1.8.1
github.com/abiosoft/ishell v2.0.0+incompatible
@ -46,10 +46,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.34.0
golang.org/x/net v0.38.0
golang.org/x/oauth2 v0.7.0
golang.org/x/sys v0.29.0
golang.org/x/text v0.21.0
golang.org/x/sys v0.31.0
golang.org/x/text v0.23.0
google.golang.org/api v0.114.0
google.golang.org/grpc v1.56.3
google.golang.org/protobuf v1.33.0
@ -121,9 +121,9 @@ require (
gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a // indirect
go.opencensus.io v0.24.0 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.32.0 // indirect
golang.org/x/crypto v0.36.0 // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sync v0.12.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
@ -131,6 +131,7 @@ require (
)
replace (
github.com/ProtonMail/go-autostart => github.com/ElectroNafta/go-autostart v0.0.0-20250402094843-326608c16033
github.com/emersion/go-message => github.com/ProtonMail/go-message v0.13.1-0.20240919135104-3bc88e6a9423
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-20231129100320-dddf8030d93a

28
go.sum
View File

@ -23,6 +23,8 @@ github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557 h1:l6surSnJ3RP4qA
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557/go.mod h1:sTrmvD/TxuypdOERsDOS7SndZg0rzzcCi1b6wQMXUYM=
github.com/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/ElectroNafta/go-autostart v0.0.0-20250402094843-326608c16033 h1:d2RB9rQmSusb0K+qSgB+DAY+8i+AXZ/o+oDHj2vAUaA=
github.com/ElectroNafta/go-autostart v0.0.0-20250402094843-326608c16033/go.mod h1:o0nKiWcK0e2G/90uL6akWRkzOV4mFcZmvpBPpigJvdw=
github.com/Kodeworks/golang-image-ico v0.0.0-20141118225523-73f0f4cfade9/go.mod h1:7uhhqiBaR4CpN0k9rMjOtjpcfGd6DG2m04zQxKnWQ0I=
github.com/LBeernaertProton/resty/v2 v2.0.0-20231129100320-dddf8030d93a h1:eQO/GF/+H8/9udc9QAgieFr+jr1tjXlJo35RAhsUbWY=
github.com/LBeernaertProton/resty/v2 v2.0.0-20231129100320-dddf8030d93a/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A=
@ -34,10 +36,8 @@ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAE
github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug=
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
github.com/ProtonMail/gluon v0.17.1-0.20250116113909-2ebd96ec0bc2 h1:lDgMidI/9j2eedavcy7YICv8+F73ooVTUoUGBE4dO0s=
github.com/ProtonMail/gluon v0.17.1-0.20250116113909-2ebd96ec0bc2/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
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/gluon v0.17.1-0.20250324123053-2abce471ad71 h1:UC8SLrS6QbBeOUM8FJugyNoeV5gRGoQCwNePAMxuM20=
github.com/ProtonMail/gluon v0.17.1-0.20250324123053-2abce471ad71/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
github.com/ProtonMail/go-crypto v1.1.4-proton h1:KIo9uNlk3vzlwI7o5VjhiEjI4Ld1TDixOMnoNZyfpFE=
github.com/ProtonMail/go-crypto v1.1.4-proton/go.mod h1:zNoyBJW3p/yVWiHNZgfTF9VsjwqYof5YY0M9kt2QaX0=
@ -45,10 +45,16 @@ github.com/ProtonMail/go-message v0.13.1-0.20240919135104-3bc88e6a9423 h1:p8nBDx
github.com/ProtonMail/go-message v0.13.1-0.20240919135104-3bc88e6a9423/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.20250121114701-67bd01ad0bc3 h1:YYnLBVcg7WrEbYVmF1PBr4AEQlob9rCphsMHAmF4CAo=
github.com/ProtonMail/go-proton-api v0.4.1-0.20250121114701-67bd01ad0bc3/go.mod h1:RYgagBFkA3zFrSt7/vviFFwjZxBo6pGzcTwFsLwsnyc=
github.com/ProtonMail/go-proton-api v0.4.1-0.20250217140732-2e531f21de4c h1:dxnbB+ov77BDj1LC35fKZ14hLoTpU6OTpZySwxarVx0=
github.com/ProtonMail/go-proton-api v0.4.1-0.20250217140732-2e531f21de4c/go.mod h1:RYgagBFkA3zFrSt7/vviFFwjZxBo6pGzcTwFsLwsnyc=
github.com/ProtonMail/go-proton-api v0.4.1-0.20250409092940-13ddc20a05a1 h1:u3G9UB8prOnzOneOf0JFCIVnMRLiK4QgEpPQVu9Y8Q4=
github.com/ProtonMail/go-proton-api v0.4.1-0.20250409092940-13ddc20a05a1/go.mod h1:RYgagBFkA3zFrSt7/vviFFwjZxBo6pGzcTwFsLwsnyc=
github.com/ProtonMail/go-proton-api v0.4.1-0.20250409131808-0bbc8e7c32db h1:mOtbY5BB2eNr2QmbZhFn5EnsJcimTntPB6akN2r+AuE=
github.com/ProtonMail/go-proton-api v0.4.1-0.20250409131808-0bbc8e7c32db/go.mod h1:RYgagBFkA3zFrSt7/vviFFwjZxBo6pGzcTwFsLwsnyc=
github.com/ProtonMail/go-proton-api v0.4.1-0.20250410050801-92de6e7c8517 h1:70JoDgXxfil4hbDoYGF98rMd47Rld6wXWyFAw4uFOTY=
github.com/ProtonMail/go-proton-api v0.4.1-0.20250410050801-92de6e7c8517/go.mod h1:RYgagBFkA3zFrSt7/vviFFwjZxBo6pGzcTwFsLwsnyc=
github.com/ProtonMail/go-proton-api v0.4.1-0.20250417134000-e624a080f7ba h1:DFBngZ7u/f69flRFzPp6Ipo6PKEyflJlA5OCh52yDB4=
github.com/ProtonMail/go-proton-api v0.4.1-0.20250417134000-e624a080f7ba/go.mod h1:eXIoLyIHxvPo8Kd9e1ygYIrAwbeWJhLi3vgSz2crlK4=
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-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
@ -502,6 +508,8 @@ golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
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=
@ -559,6 +567,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
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=
@ -575,6 +585,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -615,6 +627,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@ -636,6 +650,8 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
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=

View File

@ -138,7 +138,7 @@ 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", keychains.GetHelpers(), keychains.GetDefaultHelper())
if err != nil {
return fmt.Errorf("failed to create keychain: %w", err)
}

View File

@ -134,7 +134,7 @@ func TestKeychainMigration(t *testing.T) {
func TestUserMigration(t *testing.T) {
kcl := keychain.NewTestKeychainsList()
kc, err := keychain.NewKeychain("mock", "bridge", kcl.GetHelpers(), kcl.GetDefaultHelper())
kc, _, err := keychain.NewKeychain("mock", "bridge", kcl.GetHelpers(), kcl.GetDefaultHelper())
require.NoError(t, err)
require.NoError(t, kc.Put("brokenID", "broken"))

View File

@ -18,6 +18,8 @@
package app
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"path"
@ -67,11 +69,12 @@ func newVault(reporter *sentry.Reporter, locations *locations.Locations, keychai
logrus.WithField("vaultDir", vaultDir).Debug("Loading vault from directory")
var (
vaultKey []byte
insecure bool
vaultKey []byte
insecure bool
lastUsedHelper string
)
if key, err := loadVaultKey(vaultDir, keychains); err != nil {
if key, helper, err := loadVaultKey(vaultDir, keychains); err != nil {
if reporter != nil {
if rerr := reporter.ReportMessageWithContext("Could not load/create vault key", map[string]any{
"keychainDefaultHelper": keychains.GetDefaultHelper(),
@ -89,6 +92,8 @@ func newVault(reporter *sentry.Reporter, locations *locations.Locations, keychai
vaultDir = path.Join(vaultDir, "insecure")
} else {
vaultKey = key
lastUsedHelper = helper
logHashedVaultKey(vaultKey) // Log a hash of the vault key.
}
gluonCacheDir, err := locations.ProvideGluonCachePath()
@ -96,34 +101,47 @@ func newVault(reporter *sentry.Reporter, locations *locations.Locations, keychai
return nil, false, nil, fmt.Errorf("could not provide gluon path: %w", err)
}
vault, corrupt, err := vault.New(vaultDir, gluonCacheDir, vaultKey, panicHandler)
userVault, corrupt, err := vault.New(vaultDir, gluonCacheDir, vaultKey, panicHandler)
if err != nil {
return nil, false, corrupt, fmt.Errorf("could not create vault: %w", err)
}
return vault, insecure, corrupt, nil
// Remember the last successfully used keychain and store that as the user preference.
if err := vault.SetHelper(vaultDir, lastUsedHelper); err != nil {
logrus.WithError(err).Error("Could not store last used keychain helper")
}
return userVault, insecure, corrupt, nil
}
func loadVaultKey(vaultDir string, keychains *keychain.List) ([]byte, error) {
helper, err := vault.GetHelper(vaultDir)
// loadVaultKey - loads the key used to encrypt the vault alongside the keychain helper used to access it.
func loadVaultKey(vaultDir string, keychains *keychain.List) (key []byte, keychainHelper string, err error) {
keychainHelper, err = vault.GetHelper(vaultDir)
if err != nil {
return nil, fmt.Errorf("could not get keychain helper: %w", err)
return nil, keychainHelper, fmt.Errorf("could not get keychain helper: %w", err)
}
kc, err := keychain.NewKeychain(helper, constants.KeyChainName, keychains.GetHelpers(), keychains.GetDefaultHelper())
kc, keychainHelper, err := keychain.NewKeychain(keychainHelper, constants.KeyChainName, keychains.GetHelpers(), keychains.GetDefaultHelper())
if err != nil {
return nil, fmt.Errorf("could not create keychain: %w", err)
return nil, keychainHelper, fmt.Errorf("could not create keychain: %w", err)
}
key, err := vault.GetVaultKey(kc)
key, err = vault.GetVaultKey(kc)
if err != nil {
if keychain.IsErrKeychainNoItem(err) {
logrus.WithError(err).Warn("no vault key found, generating new")
return vault.NewVaultKey(kc)
key, err := vault.NewVaultKey(kc)
return key, keychainHelper, err
}
return nil, fmt.Errorf("could not check for vault key: %w", err)
return nil, keychainHelper, fmt.Errorf("could not check for vault key: %w", err)
}
return key, nil
return key, keychainHelper, nil
}
// logHashedVaultKey - computes a sha256 hash and encodes it to base 64. The resulting string is logged.
func logHashedVaultKey(vaultKey []byte) {
hashedKey := sha256.Sum256(vaultKey)
logrus.WithField("hashedKey", hex.EncodeToString(hashedKey[:])).Info("Found vault key")
}

View File

@ -720,13 +720,13 @@ func (bridge *Bridge) verifyUsernameChange() {
func GetUpdatedCachePath(gluonDBPath, gluonCachePath string) string {
// If gluon cache is moved to an external drive; regex find will fail; as is expected
cachePathMatches := usernameChangeRegex.FindStringSubmatch(gluonCachePath)
if cachePathMatches == nil || len(cachePathMatches) < 2 {
if len(cachePathMatches) < 2 {
return ""
}
cacheUsername := cachePathMatches[1]
dbPathMatches := usernameChangeRegex.FindStringSubmatch(gluonDBPath)
if dbPathMatches == nil || len(dbPathMatches) < 2 {
if len(dbPathMatches) < 2 {
return ""
}

View File

@ -618,7 +618,7 @@ func TestBridge_AddressWithoutKeys(t *testing.T) {
require.NoError(t, err)
// Create an additional address for the user; it will not have keys.
aliasAddrID, err := s.CreateAddress(userID, "alias@pm.me", []byte("password"))
aliasAddrID, err := s.CreateAddress(userID, "alias@pm.me", []byte("password"), true)
require.NoError(t, err)
// Create an API client so we can remove the address keys.
@ -785,7 +785,7 @@ func TestBridge_ChangeAddressOrder(t *testing.T) {
require.NoError(t, err)
// Create a second address for the user.
aliasID, err := s.CreateAddress(userID, "alias@"+s.GetDomain(), password)
aliasID, err := s.CreateAddress(userID, "alias@"+s.GetDomain(), password, true)
require.NoError(t, err)
// Create 10 messages for the user.

View File

@ -127,9 +127,9 @@ func TestBridge_Observability_UserMetric(t *testing.T) {
}
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
userMetricPeriod := time.Millisecond * 200
userMetricPeriod := time.Millisecond * 600
heartbeatPeriod := time.Second * 10
throttlePeriod := time.Millisecond * 100
throttlePeriod := time.Millisecond * 300
observability.ModifyUserMetricInterval(userMetricPeriod)
observability.ModifyThrottlePeriod(throttlePeriod)

View File

@ -355,7 +355,7 @@ func TestBridge_CanProcessEventsDuringSync(t *testing.T) {
// Create a new address
newAddress := "foo@proton.ch"
addrID, err := s.CreateAddress(userID, newAddress, password)
addrID, err := s.CreateAddress(userID, newAddress, password, true)
require.NoError(t, err)
event := <-addressCreatedCh
@ -430,7 +430,7 @@ func TestBridge_EventReplayAfterSyncHasFinished(t *testing.T) {
createNumMessages(ctx, t, c, addrID, labelID, numMsg)
})
addrID1, err := s.CreateAddress(userID, "foo@proton.ch", password)
addrID1, err := s.CreateAddress(userID, "foo@proton.ch", password, true)
require.NoError(t, err)
var allowSyncToProgress atomic.Bool
@ -469,7 +469,7 @@ func TestBridge_EventReplayAfterSyncHasFinished(t *testing.T) {
})
// User AddrID2 event as a check point to see when the new address was created.
addrID2, err := s.CreateAddress(userID, "bar@proton.ch", password)
addrID2, err := s.CreateAddress(userID, "bar@proton.ch", password, true)
require.NoError(t, err)
allowSyncToProgress.Store(true)
@ -552,7 +552,7 @@ func TestBridge_MessageCreateDuringSync(t *testing.T) {
})
// User AddrID2 event as a check point to see when the new address was created.
addrID, err := s.CreateAddress(userID, "bar@proton.ch", password)
addrID, err := s.CreateAddress(userID, "bar@proton.ch", password, true)
require.NoError(t, err)
// At most two events can be published, one for the first address, then for the second.
@ -663,7 +663,7 @@ func TestBridge_AddressOrderChangeDuringSyncInCombinedModeDoesNotTriggerBadEvent
require.Equal(t, 1, len(info.Addresses))
require.Equal(t, info.Addresses[0], "user@proton.local")
addrID2, err := s.CreateAddress(userID, "foo@"+s.GetDomain(), password)
addrID2, err := s.CreateAddress(userID, "foo@"+s.GetDomain(), password, true)
require.NoError(t, err)
require.NoError(t, s.SetAddressOrder(userID, []string{addrID2, addrID}))

View File

@ -304,7 +304,7 @@ func TestBridge_User_AddressEvents_NoBadEvent(t *testing.T) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
userLoginAndSync(ctx, t, bridge, "user", password)
addrID, err = s.CreateAddress(userID, "other@pm.me", password)
addrID, err = s.CreateAddress(userID, "other@pm.me", password, true)
require.NoError(t, err)
userContinueEventProcess(ctx, t, s, bridge)
@ -312,7 +312,7 @@ func TestBridge_User_AddressEvents_NoBadEvent(t *testing.T) {
userContinueEventProcess(ctx, t, s, bridge)
})
otherID, err := s.CreateAddress(userID, "another@pm.me", password)
otherID, err := s.CreateAddress(userID, "another@pm.me", password, true)
require.NoError(t, err)
require.NoError(t, s.RemoveAddress(userID, otherID))
@ -328,6 +328,87 @@ func TestBridge_User_AddressEvents_NoBadEvent(t *testing.T) {
})
}
func TestBridge_User_AddressEvents_BYOEAddressAdded(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
// Create a user.
userID, addrID, err := s.CreateUser("user", password)
require.NoError(t, err)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
userLoginAndSync(ctx, t, bridge, "user", password)
// Create an additional proton address
addrID, err = s.CreateAddress(userID, "other@pm.me", password, true)
require.NoError(t, err)
userContinueEventProcess(ctx, t, s, bridge)
require.NoError(t, s.AddAddressCreatedEvent(userID, addrID))
userContinueEventProcess(ctx, t, s, bridge)
userInfo, err := bridge.GetUserInfo(userID)
require.NoError(t, err)
require.Equal(t, 2, len(userInfo.Addresses))
// Create an external address with sending disabled.
externalID, err := s.CreateExternalAddress(userID, "another@yahoo.com", password, false)
require.NoError(t, err)
userContinueEventProcess(ctx, t, s, bridge)
require.NoError(t, s.AddAddressCreatedEvent(userID, externalID))
userContinueEventProcess(ctx, t, s, bridge)
// User addresses should still return 2, as we ignore the external address.
userInfo, err = bridge.GetUserInfo(userID)
require.NoError(t, err)
require.Equal(t, 2, len(userInfo.Addresses))
// Create an external address w. sending enabled. This is considered a BYOE address.
BYOEAddrID, err := s.CreateExternalAddress(userID, "other@yahoo.com", password, true)
require.NoError(t, err)
userContinueEventProcess(ctx, t, s, bridge)
require.NoError(t, s.AddAddressCreatedEvent(userID, BYOEAddrID))
userContinueEventProcess(ctx, t, s, bridge)
userInfo, err = bridge.GetUserInfo(userID)
require.NoError(t, err)
require.Equal(t, 3, len(userInfo.Addresses))
})
})
}
func TestBridge_User_AddressEvents_ExternalAddressSendChanged(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
userID, _, err := s.CreateUser("user", password)
require.NoError(t, err)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
userLoginAndSync(ctx, t, bridge, "user", password)
// Create an additional external address.
externalID, err := s.CreateExternalAddress(userID, "other@yahoo.me", password, false)
require.NoError(t, err)
userContinueEventProcess(ctx, t, s, bridge)
require.NoError(t, s.AddAddressCreatedEvent(userID, externalID))
userContinueEventProcess(ctx, t, s, bridge)
// We expect only one address, the external one without sending should not be considered a valid address.
userInfo, err := bridge.GetUserInfo(userID)
require.NoError(t, err)
require.Equal(t, 1, len(userInfo.Addresses))
// Change it to allow sending such that it becomes a BYOE address.
err = s.ChangeAddressAllowSend(userID, externalID, true)
require.NoError(t, err)
userContinueEventProcess(ctx, t, s, bridge)
require.NoError(t, s.AddAddressUpdatedEvent(userID, externalID))
userContinueEventProcess(ctx, t, s, bridge)
// We should now have 2 usable addresses listed.
userInfo, err = bridge.GetUserInfo(userID)
require.NoError(t, err)
require.Equal(t, 2, len(userInfo.Addresses))
})
})
}
func TestBridge_User_AddressEventUpdatedForAddressThatDoesNotExist_NoBadEvent(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
// Create a user.
@ -694,7 +775,7 @@ func TestBridge_User_DisableEnableAddress(t *testing.T) {
require.NoError(t, err)
// Create an additional address for the user.
aliasID, err := s.CreateAddress(userID, "alias@"+s.GetDomain(), password)
aliasID, err := s.CreateAddress(userID, "alias@"+s.GetDomain(), password, true)
require.NoError(t, err)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
@ -745,7 +826,7 @@ func TestBridge_User_CreateDisabledAddress(t *testing.T) {
require.NoError(t, err)
// Create an additional address for the user.
aliasID, err := s.CreateAddress(userID, "alias@"+s.GetDomain(), password)
aliasID, err := s.CreateAddress(userID, "alias@"+s.GetDomain(), password, true)
require.NoError(t, err)
// Immediately disable the address.

View File

@ -658,7 +658,7 @@ func TestBridge_UserInfo_Alias(t *testing.T) {
require.NoError(t, err)
// Give the new user an alias.
require.NoError(t, getErr(s.CreateAddress(userID, "alias@pm.me", []byte("password"))))
require.NoError(t, getErr(s.CreateAddress(userID, "alias@pm.me", []byte("password"), true)))
// Login the user.
require.NoError(t, getErr(bridge.LoginFull(ctx, "primary", []byte("password"), nil, nil)))
@ -706,7 +706,7 @@ func TestBridge_User_GetAddresses(t *testing.T) {
// Create a user.
userID, _, err := s.CreateUser("user", password)
require.NoError(t, err)
addrID2, err := s.CreateAddress(userID, "user@external.com", []byte("password"))
addrID2, err := s.CreateAddress(userID, "user@external.com", password, false)
require.NoError(t, err)
require.NoError(t, s.ChangeAddressType(userID, addrID2, proton.AddressTypeExternal))
@ -720,6 +720,29 @@ func TestBridge_User_GetAddresses(t *testing.T) {
})
}
func TestBridge_User_GetAddresses_BYOE(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
// Create a user.
userID, _, err := s.CreateUser("user", password)
require.NoError(t, err)
// Add a non-sending external address.
_, err = s.CreateExternalAddress(userID, "user@external.com", password, false)
require.NoError(t, err)
// Add a BYOE address.
_, err = s.CreateExternalAddress(userID, "user2@external.com", password, true)
require.NoError(t, err)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
userLoginAndSync(ctx, t, bridge, "user", password)
info, err := bridge.GetUserInfo(userID)
require.NoError(t, err)
require.Equal(t, 2, len(info.Addresses))
require.Equal(t, info.Addresses[0], "user@proton.local")
require.Equal(t, info.Addresses[1], "user2@external.com")
})
})
}
// getErr returns the error that was passed to it.
func getErr[T any](_ T, err error) error {
return err

View File

@ -91,13 +91,12 @@ func TestTLSSignedCertWrongPublicKey(t *testing.T) {
r.Error(t, err, "expected dial to fail because of wrong public key")
}
// GODT-2293 bump badssl cert and re enable this.
func _TestTLSSignedCertTrustedPublicKey(t *testing.T) { //nolint:unused,deadcode
func TestTLSSignedCertTrustedPublicKey(t *testing.T) {
skipIfProxyIsSet(t)
_, dialer, _, checker, _ := createClientWithPinningDialer("")
copyTrustedPins(checker)
checker.trustedPins = append(checker.trustedPins, `pin-sha256="LwnIKjNLV3z243ap8y0yXNPghsqE76J08Eq3COvUt2E="`)
checker.trustedPins = append(checker.trustedPins, `pin-sha256="hgraU1+uoS6kjiJaH5G+BiqQoyiIml1Nat+2FiUAcII="`)
_, err := dialer.DialTLSContext(context.Background(), "tcp", "rsa4096.badssl.com:443")
r.NoError(t, err, "expected dial to succeed because public key is known and cert is signed by CA")
}

View File

@ -29,7 +29,7 @@ using namespace bridgepp;
//****************************************************************************************************************************************************
BridgeApp::BridgeApp(int &argc, char **argv)
: QApplication(argc, argv) {
setAttribute(Qt::AA_DontShowIconsInMenus, false);
}

View File

@ -24,15 +24,33 @@ cmake_minimum_required(VERSION 3.22)
install(SCRIPT ${deploy_script})
# QML
install(DIRECTORY "${QT_DIR}/qml/Qt"
DESTINATION "${CMAKE_INSTALL_PREFIX}/bridge-gui.app/Contents/MacOS")
install(DIRECTORY "${QT_DIR}/qml/Qt/labs/platform"
DESTINATION "${CMAKE_INSTALL_PREFIX}/bridge-gui.app/Contents/MacOS/Qt/labs")
install(DIRECTORY "${QT_DIR}/qml/QtQml"
DESTINATION "${CMAKE_INSTALL_PREFIX}/bridge-gui.app/Contents/MacOS")
install(DIRECTORY "${QT_DIR}/qml/QtQuick"
DESTINATION "${CMAKE_INSTALL_PREFIX}/bridge-gui.app/Contents/MacOS")
DESTINATION "${CMAKE_INSTALL_PREFIX}/bridge-gui.app/Contents/MacOS"
PATTERN "VirtualKeyboard" EXCLUDE
PATTERN "Effects" EXCLUDE
PATTERN "LocalStorage" EXCLUDE
PATTERN "NativeStyle" EXCLUDE
PATTERN "Particles" EXCLUDE
PATTERN "Scene2D" EXCLUDE
PATTERN "Scene3D" EXCLUDE
PATTERN "Shapes" EXCLUDE
PATTERN "Timeline" EXCLUDE
PATTERN "VectorImage" EXCLUDE
PATTERN "Controls/FluentWinUI3" EXCLUDE
PATTERN "Controls/designer" EXCLUDE
PATTERN "Controls/Fusion" EXCLUDE
PATTERN "Controls/Imagine" EXCLUDE
PATTERN "Controls/Material" EXCLUDE
PATTERN "Controls/Universal" EXCLUDE
PATTERN "Controls/iOS" EXCLUDE
PATTERN "Controls/macOS" EXCLUDE)
# FRAMEWORKS
install(DIRECTORY "${QT_DIR}/lib/QtQmlWorkerScript.framework"
DESTINATION "${CMAKE_INSTALL_PREFIX}/bridge-gui.app/Contents/Frameworks")
install(DIRECTORY "${QT_DIR}/lib/QtQuickControls2Impl.framework"
DESTINATION "${CMAKE_INSTALL_PREFIX}/bridge-gui.app/Contents/Frameworks")
install(DIRECTORY "${QT_DIR}/lib/QtQuickLayouts.framework"
@ -43,6 +61,14 @@ install(DIRECTORY "${QT_DIR}/lib/QtQuickDialogs2QuickImpl.framework"
DESTINATION "${CMAKE_INSTALL_PREFIX}/bridge-gui.app/Contents/Frameworks")
install(DIRECTORY "${QT_DIR}/lib/QtQuickDialogs2Utils.framework"
DESTINATION "${CMAKE_INSTALL_PREFIX}/bridge-gui.app/Contents/Frameworks")
# ADDITIONAL FRAMEWORKS FOR Qt 6.8
install(DIRECTORY "${QT_DIR}/lib/QtQuickControls2Basic.framework"
DESTINATION "${CMAKE_INSTALL_PREFIX}/bridge-gui.app/Contents/Frameworks")
install(DIRECTORY "${QT_DIR}/lib/QtLabsPlatform.framework"
DESTINATION "${CMAKE_INSTALL_PREFIX}/bridge-gui.app/Contents/Frameworks")
install(DIRECTORY "${QT_DIR}/lib/QtQuickControls2BasicStyleImpl.framework"
DESTINATION "${CMAKE_INSTALL_PREFIX}/bridge-gui.app/Contents/Frameworks")
# PLUGINS
install(FILES "${QT_DIR}/plugins/imageformats/libqsvg.dylib"
DESTINATION "${CMAKE_INSTALL_PREFIX}/bridge-gui.app/Contents/PlugIns/imageformats")

View File

@ -54,9 +54,9 @@ AppendQt6Lib("libQt6Gui.so.6")
AppendQt6Lib("libQt6Core.so.6")
AppendQt6Lib("libQt6QuickTemplates2.so.6")
AppendQt6Lib("libQt6DBus.so.6")
AppendQt6Lib("libicui18n.so.56")
AppendQt6Lib("libicuuc.so.56")
AppendQt6Lib("libicudata.so.56")
AppendQt6Lib("libicui18n.so.73")
AppendQt6Lib("libicuuc.so.73")
AppendQt6Lib("libicudata.so.73")
AppendQt6Lib("libQt6XcbQpa.so.6")
AppendQt6Lib("libQt6WaylandClient.so.6")
AppendQt6Lib("libQt6WlShellIntegration.so.6")
@ -68,6 +68,10 @@ AppendQt6Lib("libQt6PrintSupport.so.6")
AppendQt6Lib("libQt6Xml.so.6")
AppendQt6Lib("libQt6OpenGLWidgets.so.6")
AppendQt6Lib("libQt6QuickWidgets.so.6")
AppendQt6Lib("libQt6QmlMeta.so.6")
AppendQt6Lib("libQt6LabsPlatform.so.6")
AppendQt6Lib("libQt6QuickControls2Basic.so.6")
AppendQt6Lib("libQt6QuickControls2BasicStyleImpl.so.6")
# QML dependencies
AppendQt6Lib("libQt6QmlWorkerScript.so.6")

View File

@ -57,20 +57,36 @@ AppendVCPKGLib("re2.dll")
AppendVCPKGLib("sentry.dll")
AppendVCPKGLib("zlib1.dll")
# QML DLLs
AppendQt6Lib("Qt6QmlWorkerScript.dll")
AppendQt6Lib("Qt6Widgets.dll")
AppendQt6Lib("Qt6QuickControls2Impl.dll")
AppendQt6Lib("Qt6QuickLayouts.dll")
AppendQt6Lib("Qt6QuickDialogs2.dll")
AppendQt6Lib("Qt6QuickDialogs2QuickImpl.dll")
AppendQt6Lib("Qt6QuickDialogs2Utils.dll")
AppendQt6Lib("Qt6LabsPlatform.dll")
AppendQt6Lib("Qt6QuickControls2.dll")
AppendQt6Lib("Qt6QuickControls2Basic.dll")
install(FILES ${DEPLOY_LIBS} DESTINATION "${CMAKE_INSTALL_PREFIX}")
# QML PlugIns
install(DIRECTORY ${QT_DIR}/qml/Qt/labs/platform DESTINATION "${CMAKE_INSTALL_PREFIX}/Qt/labs/")
install(DIRECTORY ${QT_DIR}/qml/QtQml DESTINATION "${CMAKE_INSTALL_PREFIX}")
install(DIRECTORY ${QT_DIR}/qml/QtQuick DESTINATION "${CMAKE_INSTALL_PREFIX}")
install(DIRECTORY ${QT_DIR}/qml/QtQuick DESTINATION "${CMAKE_INSTALL_PREFIX}"
PATTERN "Effects" EXCLUDE
PATTERN "LocalStorage" EXCLUDE
PATTERN "NativeStyle" EXCLUDE
PATTERN "Particles" EXCLUDE
PATTERN "Shapes" EXCLUDE
PATTERN "VectorImage" EXCLUDE
PATTERN "Controls/designer" EXCLUDE
PATTERN "Controls/FluentWinUI3" EXCLUDE
PATTERN "Controls/Fusion" EXCLUDE
PATTERN "Controls/Imagine" EXCLUDE
PATTERN "Controls/Material" EXCLUDE
PATTERN "Controls/Universal" EXCLUDE
PATTERN "Controls/Windows" EXCLUDE)
# crash handler utils
install(PROGRAMS "${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/tools/sentry-native/crashpad_handler.exe" DESTINATION "${CMAKE_INSTALL_PREFIX}")

View File

@ -58,9 +58,9 @@ Item {
}
ColorImage {
color: root.colorScheme.text_norm
height: root.colorScheme.body_font_size
height: ProtonStyle.body_font_size
source: "/qml/icons/ic-copy.svg"
sourceSize.height: root.colorScheme.body_font_size
sourceSize.height: ProtonStyle.body_font_size
MouseArea {
anchors.fill: parent

View File

@ -86,9 +86,9 @@ SettingsView {
ColorImage {
Layout.alignment: Qt.AlignCenter
color: root.colorScheme.interaction_norm
height: root.colorScheme.body_font_size
height: ProtonStyle.body_font_size
source: root._isAdvancedShown ? "/qml/icons/ic-chevron-down.svg" : "/qml/icons/ic-chevron-right.svg"
sourceSize.height: root.colorScheme.body_font_size
sourceSize.height: ProtonStyle.body_font_size
MouseArea {
anchors.fill: parent

View File

@ -72,9 +72,9 @@ Item {
ColorImage {
anchors.centerIn: parent
color: root.colorScheme.background_norm
height: root.colorScheme.body_font_size
height: ProtonStyle.body_font_size
source: "/qml/icons/ic-check.svg"
sourceSize.height: root.colorScheme.body_font_size
sourceSize.height: ProtonStyle.body_font_size
visible: root.checked
}
}
@ -82,9 +82,9 @@ Item {
id: loader
anchors.centerIn: parent
color: root.colorScheme.text_norm
height: root.colorScheme.body_font_size
height: ProtonStyle.body_font_size
source: "/qml/icons/Loader_16.svg"
sourceSize.height: root.colorScheme.body_font_size
sourceSize.height: ProtonStyle.body_font_size
visible: root.loading
RotationAnimation {

View File

@ -271,7 +271,10 @@ FocusScope {
usernameTextField.enabled = false;
passwordTextField.enabled = false;
loading = true;
Backend.login(usernameTextField.text, Qt.btoa(passwordTextField.text));
let usernameTextFiltered = usernameTextField.text.replace(/[\n\r]+$/, "");
let passwordTextFiltered = passwordTextField.text.replace(/[\n\r]+$/, "");
Backend.login(usernameTextFiltered, Qt.btoa(passwordTextFiltered));
}
Layout.fillWidth: true

View File

@ -212,7 +212,7 @@ func buildSessionInfoList(dir string) (map[SessionID]*sessionInfo, error) {
}
rx := regexp.MustCompile(`^(\d{8}_\d{9})_.*\.log$`)
match := rx.FindStringSubmatch(entry.Name())
if match == nil || len(match) < 2 {
if len(match) < 2 {
continue
}

View File

@ -257,7 +257,7 @@ func (s *Connector) DeleteMailbox(ctx context.Context, _ connector.IMAPStateWrit
wLabels := s.labels.Write()
defer wLabels.Close()
wLabels.Delete(string(mboxID))
wLabels.Delete(string(mboxID), "connectorDeleteMailbox")
return nil
}
@ -555,7 +555,7 @@ func (s *Connector) createLabel(ctx context.Context, name []string) (imap.Mailbo
wLabels := s.labels.Write()
defer wLabels.Close()
wLabels.SetLabel(label.ID, label)
wLabels.SetLabel(label.ID, label, "connectorCreateLabel")
return toIMAPMailbox(label, s.flags, s.permFlags, s.attrs), nil
}
@ -593,7 +593,7 @@ func (s *Connector) createFolder(ctx context.Context, name []string) (imap.Mailb
}
// Add label to list so subsequent sub folder create requests work correct.
wLabels.SetLabel(label.ID, label)
wLabels.SetLabel(label.ID, label, "connectorCreateFolder")
return toIMAPMailbox(label, s.flags, s.permFlags, s.attrs), nil
}
@ -619,7 +619,7 @@ func (s *Connector) updateLabel(ctx context.Context, labelID imap.MailboxID, nam
wLabels := s.labels.Write()
defer wLabels.Close()
wLabels.SetLabel(label.ID, update)
wLabels.SetLabel(label.ID, update, "connectorUpdateLabel")
return nil
}
@ -660,7 +660,7 @@ func (s *Connector) updateFolder(ctx context.Context, labelID imap.MailboxID, na
return err
}
wLabels.SetLabel(label.ID, update)
wLabels.SetLabel(label.ID, update, "connectorUpdateFolder")
return nil
}
@ -680,7 +680,7 @@ func (s *Connector) importMessage(
}
isDraft := slices.Contains(labelIDs, proton.DraftsLabel)
addr, err := s.getImportAddress(p, isDraft)
addr, err := getImportAddress(p, isDraft, s.addrID, s)
if err != nil {
return imap.Message{}, nil, err
}
@ -871,45 +871,6 @@ func equalAddresses(a, b string) bool {
return strings.EqualFold(stripPlusAlias(a), stripPlusAlias(b))
}
func (s *Connector) getImportAddress(p *parser.Parser, isDraft bool) (proton.Address, error) {
// addr is primary for combined mode or active for split mode
address, ok := s.identityState.GetAddress(s.addrID)
if !ok {
return proton.Address{}, errors.New("could not find account address")
}
inCombinedMode := s.addressMode == usertypes.AddressModeCombined
if !inCombinedMode {
return address, nil
}
senderAddr, err := s.getSenderProtonAddress(p)
if err != nil {
if !errors.Is(err, errNoSenderAddressMatch) {
s.log.WithError(err).Warn("Could not get import address")
}
// We did not find a match, so we use the default address.
return address, nil
}
if senderAddr.ID == address.ID {
return address, nil
}
// GODT-3185 / BRIDGE-120 In combined mode, in certain cases we adapt the address used for encryption.
// - draft with non-default address in combined mode: using sender address
// - import with non-default address in combined mode: using sender address
// - import with non-default disabled address in combined mode: using sender address
isSenderAddressDisabled := (!bool(senderAddr.Send)) || (senderAddr.Status != proton.AddressStatusEnabled)
if isDraft && isSenderAddressDisabled {
return address, nil
}
return senderAddr, nil
}
func (s *Connector) getSenderProtonAddress(p *parser.Parser) (proton.Address, error) {
// Step 1: extract sender email address from message
if (p == nil) || (p.Root() == nil) || (p.Root().Header.Len() == 0) {

View File

@ -43,7 +43,7 @@ func TestFixGODT3003Labels(t *testing.T) {
Path: []string{"bar", "Foo"},
Color: "",
Type: proton.LabelTypeFolder,
})
}, "")
wr.SetLabel("0", proton.Label{
ID: "0",
@ -52,7 +52,7 @@ func TestFixGODT3003Labels(t *testing.T) {
Path: []string{"Inbox"},
Color: "",
Type: proton.LabelTypeSystem,
})
}, "")
wr.SetLabel("bar", proton.Label{
ID: "bar",
@ -61,7 +61,7 @@ func TestFixGODT3003Labels(t *testing.T) {
Path: []string{"bar"},
Color: "",
Type: proton.LabelTypeFolder,
})
}, "")
wr.SetLabel("my_label", proton.Label{
ID: "my_label",
@ -70,7 +70,7 @@ func TestFixGODT3003Labels(t *testing.T) {
Path: []string{"MyLabel"},
Color: "",
Type: proton.LabelTypeLabel,
})
}, "")
wr.SetLabel("my_label2", proton.Label{
ID: "my_label2",
@ -79,7 +79,7 @@ func TestFixGODT3003Labels(t *testing.T) {
Path: []string{labelPrefix, "MyLabel2"},
Color: "",
Type: proton.LabelTypeLabel,
})
}, "")
wr.Close()
mboxs := []imap.MailboxNoAttrib{
@ -133,7 +133,7 @@ func TestFixGODT3003Labels_Noop(t *testing.T) {
Path: []string{folderPrefix, "bar", "Foo"},
Color: "",
Type: proton.LabelTypeFolder,
})
}, "")
wr.SetLabel("0", proton.Label{
ID: "0",
@ -142,7 +142,7 @@ func TestFixGODT3003Labels_Noop(t *testing.T) {
Path: []string{"Inbox"},
Color: "",
Type: proton.LabelTypeSystem,
})
}, "")
wr.SetLabel("bar", proton.Label{
ID: "bar",
@ -151,7 +151,7 @@ func TestFixGODT3003Labels_Noop(t *testing.T) {
Path: []string{folderPrefix, "bar"},
Color: "",
Type: proton.LabelTypeFolder,
})
}, "")
wr.SetLabel("my_label", proton.Label{
ID: "my_label",
@ -160,7 +160,7 @@ func TestFixGODT3003Labels_Noop(t *testing.T) {
Path: []string{labelPrefix, "MyLabel"},
Color: "",
Type: proton.LabelTypeLabel,
})
}, "")
wr.SetLabel("my_label2", proton.Label{
ID: "my_label2",
@ -169,7 +169,7 @@ func TestFixGODT3003Labels_Noop(t *testing.T) {
Path: []string{labelPrefix, "MyLabel2"},
Color: "",
Type: proton.LabelTypeLabel,
})
}, "")
wr.Close()
mboxs := []imap.MailboxNoAttrib{

View File

@ -34,6 +34,8 @@ type IMAPServerManager interface {
) error
RemoveIMAPUser(ctx context.Context, deleteData bool, provider GluonIDProvider, addrID ...string) error
LogRemoteLabelIDs(ctx context.Context, provider GluonIDProvider, addrID ...string) error
}
type NullIMAPServerManager struct{}
@ -57,6 +59,14 @@ func (n NullIMAPServerManager) RemoveIMAPUser(
return nil
}
func (n NullIMAPServerManager) LogRemoteLabelIDs(
_ context.Context,
_ GluonIDProvider,
_ ...string,
) error {
return nil
}
func NewNullIMAPServerManager() *NullIMAPServerManager {
return &NullIMAPServerManager{}
}

View File

@ -355,6 +355,12 @@ func (s *Service) run(ctx context.Context) { //nolint gocyclo
case *onBadEventReq:
s.log.Debug("Bad Event Request")
// // Log remote label IDs stored in the local labelMap.
s.labels.LogLabels()
// Log the remote label IDs store in Gluon.
if err := s.logRemoteMailboxIDsFromServer(ctx, s.connectors); err != nil {
s.log.Warnf("Could not obtain remote mailbox IDs from server: %v", err)
}
err := s.removeConnectorsFromServer(ctx, s.connectors, false)
req.Reply(ctx, nil, err)
@ -572,6 +578,16 @@ func (s *Service) addConnectorsToServer(ctx context.Context, connectors map[stri
return nil
}
func (s *Service) logRemoteMailboxIDsFromServer(ctx context.Context, connectors map[string]*Connector) error {
addrIDs := make([]string, 0, len(connectors))
for _, c := range connectors {
addrIDs = append(addrIDs, c.addrID)
}
return s.serverManager.LogRemoteLabelIDs(ctx, s.gluonIDProvider, addrIDs...)
}
func (s *Service) removeConnectorsFromServer(ctx context.Context, connectors map[string]*Connector, deleteData bool) error {
addrIDs := make([]string, 0, len(connectors))

View File

@ -85,7 +85,7 @@ func onLabelCreated(ctx context.Context, s *Service, event proton.LabelEvent) []
wr := s.labels.Write()
defer wr.Close()
wr.SetLabel(event.Label.ID, event.Label)
wr.SetLabel(event.Label.ID, event.Label, "onLabelCreated")
for _, updateCh := range maps.Values(s.connectors) {
update := newMailboxCreatedUpdate(imap.MailboxID(event.ID), GetMailboxName(event.Label))
@ -121,7 +121,7 @@ func onLabelUpdated(ctx context.Context, s *Service, event proton.LabelEvent) ([
// Only update the label if it exists; we don't want to create it as a client may have just deleted it.
if _, ok := wr.GetLabel(label.ID); ok {
wr.SetLabel(label.ID, event.Label)
wr.SetLabel(label.ID, event.Label, "onLabelUpdatedLabelEventID")
}
// API doesn't notify us that the path has changed. We need to fetch it again.
@ -134,7 +134,7 @@ func onLabelUpdated(ctx context.Context, s *Service, event proton.LabelEvent) ([
}
// Update the label in the map.
wr.SetLabel(apiLabel.ID, apiLabel)
wr.SetLabel(apiLabel.ID, apiLabel, "onLabelUpdatedApiID")
// Notify the IMAP clients.
for _, updateCh := range maps.Values(s.connectors) {
@ -176,7 +176,7 @@ func onLabelDeleted(ctx context.Context, s *Service, event proton.LabelEvent) []
wr := s.labels.Write()
wr.Close()
wr.Delete(event.ID)
wr.Delete(event.ID, "onLabelDeleted")
s.eventPublisher.PublishEvent(ctx, events.UserLabelDeleted{
UserID: s.identityState.UserID(),

View File

@ -256,8 +256,8 @@ func onMessageUpdateDraftOrSent(ctx context.Context, s *Service, event proton.Me
res.update.Literal,
res.update.MailboxIDs,
res.update.ParsedMessage,
true, // Is the message doesn't exist, silently create it.
false,
true, // Is the message doesn't exist, silently create it.
duringSync, // Ignore unknown labelIDs during sync.
)
didPublish, err := safePublishMessageUpdate(ctx, s, full.AddressID, update, duringSync)

View File

@ -113,7 +113,7 @@ func (s syncMessageEventHandler) HandleMessageEvents(ctx context.Context, events
if err := waitOnIMAPUpdates(ctx, updates); gluon.IsNoSuchMessage(err) {
logrus.WithError(err).Error("Failed to handle update message event in gluon, will try creating it (sync)")
updates, err := onMessageCreated(ctx, s.service, event.Message, false, true)
updates, err := onMessageCreated(ctx, s.service, event.Message, true, true)
if err != nil {
s.service.observabilitySender.AddDistinctMetrics(
observability.SyncError,

View File

@ -22,6 +22,8 @@ import (
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/proton-bridge/v3/internal/usertypes"
"github.com/bradenaw/juniper/xslices"
"github.com/sirupsen/logrus"
"golang.org/x/exp/maps"
)
@ -42,8 +44,8 @@ type labelsRead interface {
type labelsWrite interface {
labelsRead
SetLabel(id string, label proton.Label)
Delete(id string)
SetLabel(id string, label proton.Label, actionSource string)
Delete(id string, actionSource string)
}
type rwLabels struct {
@ -51,6 +53,22 @@ type rwLabels struct {
labels labelMap
}
func (r *rwLabels) LogLabels() {
r.lock.RLock()
defer r.lock.RUnlock()
remoteLabelIDs := make([]string, len(r.labels))
i := 0
for labelID := range r.labels {
remoteLabelIDs[i] = labelID
i++
}
logrus.WithFields(logrus.Fields{
"remoteLabelIDs": remoteLabelIDs,
}).Debug("Logging remote label IDs stored in labelMap")
}
func (r *rwLabels) Read() labelsRead {
r.lock.RLock()
return &rwLabelsRead{rw: r}
@ -75,6 +93,15 @@ func (r *rwLabels) SetLabels(labels []proton.Label) {
r.lock.Lock()
defer r.lock.Unlock()
labelIDs := xslices.Map(labels, func(label proton.Label) string {
return label.ID
})
logrus.WithFields(logrus.Fields{
"pkg": "rwLabels",
"labelIDs": labelIDs,
}).Info("Setting labels")
r.labels = usertypes.GroupBy(labels, func(label proton.Label) string { return label.ID })
}
@ -123,10 +150,20 @@ func (r rwLabelsWrite) GetLabels() []proton.Label {
return r.rw.getLabelsUnsafe()
}
func (r rwLabelsWrite) SetLabel(id string, label proton.Label) {
func (r rwLabelsWrite) SetLabel(id string, label proton.Label, actionSource string) {
logAction("SetLabel", actionSource, label.ID)
r.rw.labels[id] = label
}
func (r rwLabelsWrite) Delete(id string) {
func (r rwLabelsWrite) Delete(id string, actionSource string) {
logAction("Delete", actionSource, id)
delete(r.rw.labels, id)
}
func logAction(actionType, actionSource, labelID string) {
logrus.WithFields(logrus.Fields{
"pkg": "rwLabelsWrite",
"actionSource": actionSource,
"labelID": labelID,
}).Debug(actionType)
}

View File

@ -111,39 +111,6 @@ func (s *SyncUpdateApplier) ApplySyncUpdates(ctx context.Context, updates []sync
return nil
}
func (s *SyncUpdateApplier) SyncSystemLabelsOnly(ctx context.Context, labels map[string]proton.Label) error {
request := func(ctx context.Context, _ usertypes.AddressMode, connectors map[string]*Connector) ([]imap.Update, error) {
updates := make([]imap.Update, 0, len(labels)*len(connectors))
for _, label := range labels {
if !WantLabel(label) {
continue
}
if label.Type != proton.LabelTypeSystem {
continue
}
for _, c := range connectors {
update := newSystemMailboxCreatedUpdate(imap.MailboxID(label.ID), label.Name)
updates = append(updates, update)
c.publishUpdate(ctx, update)
}
}
return updates, nil
}
updates, err := s.sendRequest(ctx, request)
if err != nil {
return err
}
if err := waitOnIMAPUpdates(ctx, updates); err != nil {
return fmt.Errorf("could not sync system labels: %w", err)
}
return nil
}
func (s *SyncUpdateApplier) SyncLabels(ctx context.Context, labels map[string]proton.Label) error {
request := func(ctx context.Context, _ usertypes.AddressMode, connectors map[string]*Connector) ([]imap.Update, error) {
return syncLabels(ctx, labels, maps.Values(connectors))

View File

@ -0,0 +1,100 @@
// Copyright (c) 2025 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 imapservice
import (
"errors"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/proton-bridge/v3/internal/usertypes"
"github.com/ProtonMail/proton-bridge/v3/pkg/message/parser"
)
type connectorInterface interface {
getSenderProtonAddress(p *parser.Parser) (proton.Address, error)
getAddress(id string) (proton.Address, bool)
getPrimaryAddress() (proton.Address, error)
getAddressMode() usertypes.AddressMode
logError(err error, errMsg string)
}
func (s *Connector) logError(err error, errMsg string) {
s.log.WithError(err).Warn(errMsg)
}
func (s *Connector) getAddressMode() usertypes.AddressMode {
return s.addressMode
}
func (s *Connector) getPrimaryAddress() (proton.Address, error) {
return s.identityState.GetPrimaryAddress()
}
func (s *Connector) getAddress(id string) (proton.Address, bool) {
return s.identityState.GetAddress(id)
}
func getImportAddress(p *parser.Parser, isDraft bool, id string, conn connectorInterface) (proton.Address, error) {
// addr is primary for combined mode or active for split mode
address, ok := conn.getAddress(id)
if !ok {
return proton.Address{}, errors.New("could not find account address")
}
// If the address is external and not BYOE - with sending enabled, then use the primary address as an import target.
if address.Type == proton.AddressTypeExternal && !address.Send {
var err error
address, err = conn.getPrimaryAddress()
if err != nil {
return proton.Address{}, errors.New("could not get primary account address")
}
}
inCombinedMode := conn.getAddressMode() == usertypes.AddressModeCombined
if !inCombinedMode {
return address, nil
}
senderAddr, err := conn.getSenderProtonAddress(p)
if err != nil {
if !errors.Is(err, errNoSenderAddressMatch) {
conn.logError(err, "Could not get import address")
}
// We did not find a match, so we use the default address.
return address, nil
}
if senderAddr.ID == address.ID {
return address, nil
}
// GODT-3185 / BRIDGE-120 In combined mode, in certain cases we adapt the address used for encryption.
// - draft with non-default address in combined mode: using sender address
// - import with non-default address in combined mode: using sender address
// - import with non-default disabled address in combined mode: using sender address
isSenderAddressDisabled := (!bool(senderAddr.Send)) || (senderAddr.Status != proton.AddressStatusEnabled)
isSenderExternalNonBYOE := senderAddr.Type == proton.AddressTypeExternal && !bool(senderAddr.Send)
// Forbid drafts/imports for external non-BYOE addresses
if isSenderExternalNonBYOE || (isDraft && isSenderAddressDisabled) {
return address, nil
}
return senderAddr, nil
}

View File

@ -0,0 +1,380 @@
// Copyright (c) 2025 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 imapservice
import (
"errors"
"testing"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/proton-bridge/v3/internal/usertypes"
"github.com/ProtonMail/proton-bridge/v3/pkg/message/parser"
"github.com/stretchr/testify/require"
)
type testConnector struct {
addressMode usertypes.AddressMode
primaryAddress proton.Address
senderAddress proton.Address
imapAddress proton.Address
senderAddressError error
}
func (t *testConnector) getSenderProtonAddress(_ *parser.Parser) (proton.Address, error) {
return t.senderAddress, t.senderAddressError
}
func (t *testConnector) getAddress(_ string) (proton.Address, bool) {
return t.imapAddress, true
}
func (t *testConnector) getPrimaryAddress() (proton.Address, error) {
return t.primaryAddress, nil
}
func (t *testConnector) getAddressMode() usertypes.AddressMode {
return t.addressMode
}
func (t *testConnector) logError(_ error, _ string) {
}
func Test_GetImportAddress_SplitMode(t *testing.T) {
primaryAddress := proton.Address{
ID: "1",
Email: "primary@proton.me",
Send: true,
Receive: true,
Type: proton.AddressTypeOriginal,
Status: proton.AddressStatusEnabled,
}
imapAddressProton := proton.Address{
ID: "2",
Email: "imap@proton.me",
Send: true,
Receive: true,
Type: proton.AddressTypeOriginal,
}
testConn := &testConnector{
addressMode: usertypes.AddressModeSplit,
primaryAddress: primaryAddress,
imapAddress: imapAddressProton,
}
// Import address is internal, we're creating a draft.
// Expected: returned address is internal.
addr, err := getImportAddress(nil, true, imapAddressProton.ID, testConn)
require.NoError(t, err)
require.Equal(t, imapAddressProton.ID, addr.ID)
require.Equal(t, imapAddressProton.Email, addr.Email)
// Import address is internal, we're attempting to import a message.
// Expected: returned address is internal.
addr, err = getImportAddress(nil, false, imapAddressProton.ID, testConn)
require.NoError(t, err)
require.Equal(t, imapAddressProton.ID, addr.ID)
require.Equal(t, imapAddressProton.Email, addr.Email)
imapAddressBYOE := proton.Address{
ID: "3",
Email: "byoe@external.com",
Send: true,
Receive: true,
Type: proton.AddressTypeExternal,
}
// IMAP address is BYOE, we're creating a draft
// Expected: returned address is BYOE.
testConn.imapAddress = imapAddressBYOE
addr, err = getImportAddress(nil, true, imapAddressBYOE.ID, testConn)
require.NoError(t, err)
require.Equal(t, imapAddressBYOE.ID, addr.ID)
require.Equal(t, imapAddressBYOE.Email, addr.Email)
// IMAP address is BYOE, we're importing a message
// Expected: returned address is BYOE.
addr, err = getImportAddress(nil, false, imapAddressBYOE.ID, testConn)
require.NoError(t, err)
require.Equal(t, imapAddressBYOE.ID, addr.ID)
require.Equal(t, imapAddressBYOE.Email, addr.Email)
imapAddressExternal := proton.Address{
ID: "4",
Email: "external@external.com",
Send: false,
Receive: false,
Type: proton.AddressTypeExternal,
}
// IMAP address is external, we're creating a draft.
// Expected: returned address is primary.
testConn.imapAddress = imapAddressExternal
addr, err = getImportAddress(nil, true, imapAddressExternal.ID, testConn)
require.NoError(t, err)
require.Equal(t, primaryAddress.ID, addr.ID)
require.Equal(t, primaryAddress.Email, addr.Email)
// IMAP address is external, we're trying to import.
// Expected: returned address is primary.
addr, err = getImportAddress(nil, false, imapAddressExternal.ID, testConn)
require.NoError(t, err)
require.Equal(t, primaryAddress.ID, addr.ID)
require.Equal(t, primaryAddress.Email, addr.Email)
}
func Test_GetImportAddress_CombinedMode_ProtonAddresses(t *testing.T) {
primaryAddress := proton.Address{
ID: "1",
Email: "primary@proton.me",
Send: true,
Receive: true,
Type: proton.AddressTypeOriginal,
Status: proton.AddressStatusEnabled,
}
imapAddressProton := proton.Address{
ID: "2",
Email: "imap@proton.me",
Send: true,
Receive: true,
Type: proton.AddressTypeOriginal,
}
senderAddress := proton.Address{
ID: "3",
Email: "sender@proton.me",
Send: true,
Receive: true,
Type: proton.AddressTypeOriginal,
Status: proton.AddressStatusEnabled,
}
testConn := &testConnector{
addressMode: usertypes.AddressModeCombined,
primaryAddress: primaryAddress,
imapAddress: imapAddressProton,
senderAddress: senderAddress,
}
// Both the sender address and the imap address are the same. We're creating a draft.
// Expected: IMAP address is returned.
testConn.senderAddress = imapAddressProton
testConn.imapAddress = imapAddressProton
addr, err := getImportAddress(nil, true, imapAddressProton.ID, testConn)
require.NoError(t, err)
require.Equal(t, imapAddressProton.ID, addr.ID)
require.Equal(t, imapAddressProton.Email, addr.Email)
// Both the sender address and the imap address are the same. We're trying to import
// Expected: IMAP address is returned.
addr, err = getImportAddress(nil, false, imapAddressProton.ID, testConn)
require.NoError(t, err)
require.Equal(t, imapAddressProton.ID, addr.ID)
require.Equal(t, imapAddressProton.Email, addr.Email)
// Sender address and imap address are different. Sender address is enabled and has sending enabled.
// We're creating a draft.
// Expected: Sender address is returned.
testConn.senderAddress = senderAddress
testConn.imapAddress = imapAddressProton
addr, err = getImportAddress(nil, true, imapAddressProton.ID, testConn)
require.NoError(t, err)
require.Equal(t, senderAddress.ID, addr.ID)
require.Equal(t, senderAddress.Email, addr.Email)
// Sender address and imap address are different. Sender address is enabled and has sending enabled.
// We're importing a message.
// Expected: Sender address is returned.
addr, err = getImportAddress(nil, false, imapAddressProton.ID, testConn)
require.NoError(t, err)
require.Equal(t, senderAddress.ID, addr.ID)
require.Equal(t, senderAddress.Email, addr.Email)
// Sender address and imap address are different. Sender address is disabled, but has sending enabled.
// We're creating a draft message.
// Expected: IMAP address is returned.
senderAddress.Status = proton.AddressStatusDisabled
testConn.senderAddress = senderAddress
addr, err = getImportAddress(nil, true, imapAddressProton.ID, testConn)
require.NoError(t, err)
require.Equal(t, imapAddressProton.ID, addr.ID)
require.Equal(t, imapAddressProton.Email, addr.Email)
// Sender address and imap address are different. Sender address is disabled, but has sending enabled.
// We're importing a message.
// Expected: IMAP address is returned.
addr, err = getImportAddress(nil, false, imapAddressProton.ID, testConn)
require.NoError(t, err)
require.Equal(t, senderAddress.ID, addr.ID)
require.Equal(t, senderAddress.Email, addr.Email)
// Sender address and imap address are different. Sender address is enabled, but has sending disabled.
// We're creating a draft.
// Expected: IMAP address is returned.
senderAddress.Status = proton.AddressStatusEnabled
senderAddress.Send = false
testConn.senderAddress = senderAddress
addr, err = getImportAddress(nil, true, imapAddressProton.ID, testConn)
require.NoError(t, err)
require.Equal(t, imapAddressProton.ID, addr.ID)
require.Equal(t, imapAddressProton.Email, addr.Email)
// Sender address and imap address are different. Sender address is enabled, but has sending disabled.
// We're importing a message.
// Expected: IMAP address is returned.
addr, err = getImportAddress(nil, false, imapAddressProton.ID, testConn)
require.NoError(t, err)
require.Equal(t, senderAddress.ID, addr.ID)
require.Equal(t, senderAddress.Email, addr.Email)
// Sender address and imap address are different. But sender address is not an associated proton address.
// We're creating a draft.
// Expected: Sender address is returned.
testConn.senderAddressError = errors.New("sender address is not associated with the account")
addr, err = getImportAddress(nil, true, imapAddressProton.ID, testConn)
require.NoError(t, err)
require.Equal(t, imapAddressProton.ID, addr.ID)
require.Equal(t, imapAddressProton.Email, addr.Email)
// Sender address and imap address are different. But sender address is not an associated proton address.
// We're importing a message.
// Expected: Sender address is returned.
addr, err = getImportAddress(nil, false, imapAddressProton.ID, testConn)
require.NoError(t, err)
require.Equal(t, imapAddressProton.ID, addr.ID)
require.Equal(t, imapAddressProton.Email, addr.Email)
}
func Test_GetImportAddress_CombinedMode_ExternalAddresses(t *testing.T) {
primaryAddress := proton.Address{
ID: "1",
Email: "primary@proton.me",
Send: true,
Receive: true,
Type: proton.AddressTypeOriginal,
Status: proton.AddressStatusEnabled,
}
imapAddressProton := proton.Address{
ID: "2",
Email: "imap@proton.me",
Send: true,
Receive: true,
Type: proton.AddressTypeOriginal,
}
senderAddressExternal := proton.Address{
ID: "3",
Email: "sender@external.me",
Send: false,
Receive: false,
Type: proton.AddressTypeExternal,
Status: proton.AddressStatusEnabled,
}
senderAddressExternalSecondary := proton.Address{
ID: "4",
Email: "sender2@external.me",
Send: false,
Receive: false,
Type: proton.AddressTypeExternal,
Status: proton.AddressStatusEnabled,
}
testConn := &testConnector{
addressMode: usertypes.AddressModeCombined,
primaryAddress: primaryAddress,
imapAddress: imapAddressProton,
senderAddress: senderAddressExternal,
}
// Sender address is external, and we're creating a draft.
// Expected: IMAP address is returned.
testConn.senderAddress = senderAddressExternal
testConn.imapAddress = imapAddressProton
addr, err := getImportAddress(nil, true, imapAddressProton.ID, testConn)
require.NoError(t, err)
require.Equal(t, imapAddressProton.ID, addr.ID)
require.Equal(t, imapAddressProton.Email, addr.Email)
// Sender address is external, and we're importing a message.
// Expected: IMAP address is returned.
addr, err = getImportAddress(nil, false, imapAddressProton.ID, testConn)
require.NoError(t, err)
require.Equal(t, imapAddressProton.ID, addr.ID)
require.Equal(t, imapAddressProton.Email, addr.Email)
// Sender and IMAP address are external, and we're trying to import.
// Expected: Primary address is returned.
testConn.imapAddress = senderAddressExternalSecondary
addr, err = getImportAddress(nil, false, imapAddressProton.ID, testConn)
require.NoError(t, err)
require.Equal(t, primaryAddress.ID, addr.ID)
require.Equal(t, primaryAddress.Email, addr.Email)
// Sender and IMAP address are external, and we're trying to create a draft.
// Expected: Primary address is returned.
addr, err = getImportAddress(nil, true, imapAddressProton.ID, testConn)
require.NoError(t, err)
require.Equal(t, primaryAddress.ID, addr.ID)
require.Equal(t, primaryAddress.Email, addr.Email)
}
func Test_GetImportAddress_CombinedMode_BYOEAddresses(t *testing.T) {
primaryAddress := proton.Address{
ID: "1",
Email: "primary@proton.me",
Send: true,
Receive: true,
Type: proton.AddressTypeOriginal,
Status: proton.AddressStatusEnabled,
}
imapAddressProton := proton.Address{
ID: "2",
Email: "imap@proton.me",
Send: true,
Receive: true,
Type: proton.AddressTypeOriginal,
}
senderAddressBYOE := proton.Address{
ID: "3",
Email: "sender@external.me",
Send: true,
Receive: true,
Type: proton.AddressTypeExternal,
Status: proton.AddressStatusEnabled,
}
testConn := &testConnector{
addressMode: usertypes.AddressModeCombined,
primaryAddress: primaryAddress,
imapAddress: imapAddressProton,
senderAddress: senderAddressBYOE,
}
// Sender address is BYOE, and we're creating a draft.
// Expected: BYOE address is returned.
testConn.senderAddress = senderAddressBYOE
testConn.imapAddress = imapAddressProton
addr, err := getImportAddress(nil, true, imapAddressProton.ID, testConn)
require.NoError(t, err)
require.Equal(t, senderAddressBYOE.ID, addr.ID)
require.Equal(t, senderAddressBYOE.Email, addr.Email)
// Sender address is BYOE, and we're importing a message.
// Expected: BYOE address is returned.
addr, err = getImportAddress(nil, false, imapAddressProton.ID, testConn)
require.NoError(t, err)
require.Equal(t, senderAddressBYOE.ID, addr.ID)
require.Equal(t, senderAddressBYOE.Email, addr.Email)
}

View File

@ -170,6 +170,14 @@ func (sm *Service) SetGluonDir(ctx context.Context, gluonDir string) error {
return err
}
func (sm *Service) LogRemoteLabelIDs(ctx context.Context, provider imapservice.GluonIDProvider, addrID ...string) error {
_, err := sm.requests.Send(ctx, &smRequestLogRemoteMailboxIDs{
addrID: addrID,
idProvider: provider,
})
return err
}
func (sm *Service) RemoveIMAPUser(ctx context.Context, deleteData bool, provider imapservice.GluonIDProvider, addrID ...string) error {
_, err := sm.requests.Send(ctx, &smRequestRemoveIMAPUser{
withData: deleteData,
@ -244,6 +252,10 @@ func (sm *Service) run(ctx context.Context, subscription events.Subscription) {
sm.handleLoadedUserCountChange(ctx)
}
case *smRequestLogRemoteMailboxIDs:
err := sm.logRemoteLabelIDsFromServer(ctx, r.addrID, r.idProvider)
request.Reply(ctx, nil, err)
case *smRequestRemoveIMAPUser:
err := sm.handleRemoveIMAPUser(ctx, r.withData, r.idProvider, r.addrID...)
request.Reply(ctx, nil, err)
@ -311,6 +323,35 @@ func (sm *Service) handleAddIMAPUser(ctx context.Context,
return sm.handleAddIMAPUserImpl(ctx, connector, addrID, idProvider, syncStateProvider)
}
func (sm *Service) logRemoteLabelIDsFromServer(ctx context.Context, addrIDs []string, idProvider imapservice.GluonIDProvider) error {
if sm.imapServer == nil {
return fmt.Errorf("no imap server instance running")
}
for _, addrID := range addrIDs {
gluonID, ok := idProvider.GetGluonID(addrID)
if !ok {
sm.log.Warnf("Could not find Gluon ID for addrID %v", addrID)
continue
}
log := sm.log.WithFields(logrus.Fields{
"addrID": addrID,
"gluonID": gluonID,
})
remoteLabelIDs, err := sm.imapServer.GetAllMailboxRemoteIDsForUser(ctx, gluonID)
if err != nil {
log.WithError(err).Error("Could not obtain remote label IDs for user")
continue
}
log.WithField("remoteLabelIDs", remoteLabelIDs).Debug("Logging Gluon remote Label IDs")
}
return nil
}
func (sm *Service) handleAddIMAPUserImpl(ctx context.Context,
connector connector.Connector,
addrID string,
@ -723,3 +764,8 @@ type smRequestAddSMTPAccount struct {
type smRequestRemoveSMTPAccount struct {
account *bridgesmtp.Service
}
type smRequestLogRemoteMailboxIDs struct {
addrID []string
idProvider imapservice.GluonIDProvider
}

View File

@ -253,7 +253,6 @@ type sendMailReq struct {
func (s *Service) sendMail(ctx context.Context, req *sendMailReq) error {
defer async.HandlePanic(s.panicHandler)
start := time.Now()
s.log.Debug("Received send mail request")
defer func() {
end := time.Now()
s.log.Debugf("Send mail request finished in %v", end.Sub(start))

View File

@ -138,11 +138,10 @@ func (t *Handler) run(ctx context.Context,
}
if syncStatus.IsComplete() {
t.log.Info("Sync already complete, only system labels will be updated")
if err := updateApplier.SyncSystemLabelsOnly(ctx, labels); err != nil {
t.log.WithError(err).Error("Failed to sync system labels")
t.log.Info("Sync already complete, updating labels")
if err := updateApplier.SyncLabels(ctx, labels); err != nil {
t.log.WithError(err).Error("Failed to sync labels")
return err
}

View File

@ -74,8 +74,7 @@ func TestTask_NoStateAndSucceeds(t *testing.T) {
}
{
call1 := tt.updateApplier.EXPECT().SyncLabels(gomock.Any(), gomock.Eq(labels)).Times(1).Return(nil)
tt.updateApplier.EXPECT().SyncSystemLabelsOnly(gomock.Any(), gomock.Eq(labels)).After(call1).Times(1).Return(nil)
tt.updateApplier.EXPECT().SyncLabels(gomock.Any(), gomock.Eq(labels)).Times(2).Return(nil)
}
{
@ -203,7 +202,7 @@ func TestTask_StateHasSyncedState(t *testing.T) {
}, nil
})
tt.updateApplier.EXPECT().SyncSystemLabelsOnly(gomock.Any(), gomock.Eq(labels)).Return(nil)
tt.updateApplier.EXPECT().SyncLabels(gomock.Any(), gomock.Eq(labels)).Return(nil)
err := tt.task.run(context.Background(), tt.syncReporter, labels, tt.updateApplier, tt.messageBuilder)
require.NoError(t, err)

View File

@ -80,7 +80,6 @@ type MessageBuilder interface {
type UpdateApplier interface {
ApplySyncUpdates(ctx context.Context, updates []BuildResult) error
SyncSystemLabelsOnly(ctx context.Context, labels map[string]proton.Label) error
SyncLabels(ctx context.Context, labels map[string]proton.Label) error
}

View File

@ -548,20 +548,6 @@ func (mr *MockUpdateApplierMockRecorder) SyncLabels(arg0, arg1 interface{}) *gom
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SyncLabels", reflect.TypeOf((*MockUpdateApplier)(nil).SyncLabels), arg0, arg1)
}
// SyncSystemLabelsOnly mocks base method.
func (m *MockUpdateApplier) SyncSystemLabelsOnly(arg0 context.Context, arg1 map[string]proton.Label) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SyncSystemLabelsOnly", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// SyncSystemLabelsOnly indicates an expected call of SyncSystemLabelsOnly.
func (mr *MockUpdateApplierMockRecorder) SyncSystemLabelsOnly(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SyncSystemLabelsOnly", reflect.TypeOf((*MockUpdateApplier)(nil).SyncSystemLabelsOnly), arg0, arg1)
}
// MockMessageBuilder is a mock of MessageBuilder interface.
type MockMessageBuilder struct {
ctrl *gomock.Controller

View File

@ -739,7 +739,7 @@ func (user *User) protonAddresses() []proton.Address {
}
addresses := xslices.Filter(maps.Values(apiAddrs), func(addr proton.Address) bool {
return addr.Status == proton.AddressStatusEnabled && addr.Type != proton.AddressTypeExternal
return addr.Status == proton.AddressStatusEnabled && (addr.IsBYOEAddress() || addr.Type != proton.AddressTypeExternal)
})
slices.SortFunc(addresses, func(a, b proton.Address) bool {

View File

@ -110,7 +110,7 @@ func withAccount(tb testing.TB, s *server.Server, username, password string, ali
addrIDs := []string{addrID}
for _, email := range aliases {
addrID, err := s.CreateAddress(userID, email, []byte(password))
addrID, err := s.CreateAddress(userID, email, []byte(password), true)
require.NoError(tb, err)
require.NoError(tb, s.ChangeAddressDisplayName(userID, addrID, email+" (Display Name)"))

View File

@ -63,6 +63,10 @@ func GetHelper(vaultDir string) (string, error) {
}
func SetHelper(vaultDir, helper string) error {
if helper == "" {
return nil
}
settings, err := LoadKeychainSettings(vaultDir)
if err != nil {
return err

View File

@ -82,11 +82,11 @@ func (kcl *List) GetDefaultHelper() string {
return kcl.defaultHelper
}
// NewKeychain creates a new native keychain.
func NewKeychain(preferred, keychainName string, helpers Helpers, defaultHelper string) (*Keychain, error) {
// NewKeychain creates a new native keychain. It also returns the keychain helper used to access the keychain.
func NewKeychain(preferred, keychainName string, helpers Helpers, defaultHelper string) (kc *Keychain, usedKeychainHelper string, err error) {
// There must be at least one keychain helper available.
if len(helpers) < 1 {
return nil, ErrNoKeychain
return nil, "", ErrNoKeychain
}
// If the preferred keychain is unsupported, fallback to the default one.
@ -97,16 +97,16 @@ func NewKeychain(preferred, keychainName string, helpers Helpers, defaultHelper
// Load the user's preferred keychain helper.
helperConstructor, ok := helpers[preferred]
if !ok {
return nil, ErrNoKeychain
return nil, "", ErrNoKeychain
}
// Construct the keychain helper.
helper, err := helperConstructor(hostURL(keychainName))
if err != nil {
return nil, err
return nil, preferred, err
}
return newKeychain(helper, hostURL(keychainName)), nil
return newKeychain(helper, hostURL(keychainName)), preferred, nil
}
func newKeychain(helper credentials.Helper, url string) *Keychain {

View File

@ -120,7 +120,7 @@ func TestIsErrKeychainNoItem(t *testing.T) {
helpers := NewList().GetHelpers()
for helperName := range helpers {
kc, err := NewKeychain(helperName, "bridge-test", helpers, helperName)
kc, _, err := NewKeychain(helperName, "bridge-test", helpers, helperName)
r.NoError(err)
_, _, err = kc.Get("non-existing")

View File

@ -30,7 +30,7 @@ egrep $'^\t[^=>]*$' $LOCKFILE | sed -r 's/\t([^ ]*) v.*/\1/g' > $TEMPFILE1
egrep $'^\t.*=>.*v.*$' $LOCKFILE | sed -r 's/^.*=> ([^ ]*)( v.*)?/\1/g' >> $TEMPFILE1
cat $TEMPFILE1 | egrep -v 'therecipe/qt/internal|therecipe/env_.*_512|protontech' | sort | uniq > $TEMPFILE2
# Add non vendor credits
echo -e "\nQt 6.4.3 by Qt group\n" >> $TEMPFILE2
echo -e "\nQt 6.8.2 by Qt group\n" >> $TEMPFILE2
# join lines
sed -i -e ':a' -e 'N' -e '$!ba' -e 's|\n|;|g' $TEMPFILE2

View File

@ -28,9 +28,7 @@ main(){
jq -r '.finding | select( (.osv != null) and (.trace[0].function != null) ) | .osv ' < vulns.json > vulns_osv_ids.txt
ignore GO-2023-2328 "GODT-3124 RESTY race condition"
ignore GO-2025-3373 "BRIDGE-315 stdlib crypto/x509"
ignore GO-2025-3420 "BRIDGE-315 stdlib net/http"
ignore GO-2025-3447 "BRIDGE-315 stdlib crypto/internal/nistec"
ignore GO-2025-3563 "BRIDGE-346 net/http request smuggling"
has_vulns