forked from Silverfish/proton-bridge
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9eb4703d7a | |||
| 105752fc65 | |||
| 2747e93316 | |||
| 9548f984eb | |||
| cb871ce4bc | |||
| 8ca849b7a8 | |||
| 4bb29b1b5c | |||
| e55e893c94 | |||
| 5ab63a290e | |||
| 7c3414b86f | |||
| cec8829032 | |||
| 78f9f49a8a | |||
| 5a7722fd18 | |||
| d111a979f7 | |||
| 31514c8e31 | |||
| af5ce101ef | |||
| 075da27d13 | |||
| 7b19fb44a4 | |||
| c991946ea7 | |||
| f960a3ae38 | |||
| 73f8811a4b | |||
| bc6ec2579a | |||
| 35bc7263da | |||
| cc3db00a06 | |||
| 7f7961ae0c | |||
| aae60b2ef8 | |||
| ab700543b9 | |||
| 413488f5f4 |
30
Changelog.md
30
Changelog.md
@ -2,10 +2,36 @@
|
|||||||
|
|
||||||
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||||
|
|
||||||
|
## [Bridge 2.4.5] Osney
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* GODT-2015: Bridge-gui logs to file until gRPC connection is established.
|
||||||
|
* GODT-2016: Added more logging of gRPC events at info level.
|
||||||
|
* GODT-2013: CLI flag for frontend is required.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* GODT-2020: Fix xdg_{home,cache}_home variables.
|
||||||
|
* GODT-2014: Bridge quit if gRPC client ends stream.
|
||||||
|
|
||||||
|
## [Bridge 2.4.4] Osney
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* GODT-1751: Switch from protonmail.com to proton.me domain.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* Other: Fix make run-cli for Darwin.
|
||||||
|
* GODT-1645: Fix CI pipeline.
|
||||||
|
* GODT-1938: Account details box values wrap.
|
||||||
|
* Other: Also install vcpkg ARM64 on Intel mac hosts.
|
||||||
|
* Other: Fix minor typo.
|
||||||
|
* GODT-1939: removed vertical overshoot when scrolling.
|
||||||
|
* GODT-1479: fix 'Open Bridge' button still hovered when status windows opens for Windows.
|
||||||
|
* GODT-1519: Move back to account view after sending bug report.
|
||||||
|
* Other: fix QML error with Qt 6.4 and a typo.
|
||||||
|
|
||||||
## [Bridge 2.4.3] Osney
|
## [Bridge 2.4.3] Osney
|
||||||
|
|
||||||
## Changed
|
### Changed
|
||||||
* Other: implemented tokens in bridge-gui-tester.
|
* Other: implemented tokens in bridge-gui-tester.
|
||||||
* GODT-1853:
|
* GODT-1853:
|
||||||
* Upgrade dependencies (including x/crypto).
|
* Upgrade dependencies (including x/crypto).
|
||||||
@ -28,7 +54,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Fixed
|
### Fixed
|
||||||
* GUI issues:
|
* GUI issues:
|
||||||
* GODT-1894: Fixed typo in alreadyLoggedIn event error message.
|
* GODT-1894: Fixed typo in alreadyLoggedIn event error message.
|
||||||
* GODT-1479: Fix hover on “Open Bridge” in status window on macOS.
|
* GODT-1479: Fix hover on “Open Bridge” in status window on macOS.
|
||||||
|
|||||||
13
Makefile
13
Makefile
@ -11,7 +11,7 @@ ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
|
|||||||
.PHONY: build build-gui build-nogui build-launcher versioner hasher
|
.PHONY: build build-gui build-nogui build-launcher versioner hasher
|
||||||
|
|
||||||
# Keep version hardcoded so app build works also without Git repository.
|
# Keep version hardcoded so app build works also without Git repository.
|
||||||
BRIDGE_APP_VERSION?=2.4.3+git
|
BRIDGE_APP_VERSION?=2.4.5+git
|
||||||
APP_VERSION:=${BRIDGE_APP_VERSION}
|
APP_VERSION:=${BRIDGE_APP_VERSION}
|
||||||
APP_FULL_NAME:=Proton Mail Bridge
|
APP_FULL_NAME:=Proton Mail Bridge
|
||||||
APP_VENDOR:=Proton AG
|
APP_VENDOR:=Proton AG
|
||||||
@ -80,6 +80,9 @@ build: build-gui
|
|||||||
build-gui: ${TGZ_TARGET}
|
build-gui: ${TGZ_TARGET}
|
||||||
|
|
||||||
build-nogui: ${EXE_NAME} build-launcher
|
build-nogui: ${EXE_NAME} build-launcher
|
||||||
|
ifeq "${TARGET_OS}" "darwin"
|
||||||
|
mv ${BRIDGE_EXE} ${BRIDGE_EXE_NAME}
|
||||||
|
endif
|
||||||
|
|
||||||
go-build=go build $(1) -o $(2) $(3)
|
go-build=go build $(1) -o $(2) $(3)
|
||||||
go-build-finalize=${go-build}
|
go-build-finalize=${go-build}
|
||||||
@ -254,6 +257,13 @@ lint-golang:
|
|||||||
$(info linting with GOMAXPROCS=${GOMAXPROCS})
|
$(info linting with GOMAXPROCS=${GOMAXPROCS})
|
||||||
golangci-lint run ./...
|
golangci-lint run ./...
|
||||||
|
|
||||||
|
gobinsec: gobinsec-cache.yml build
|
||||||
|
gobinsec -wait -cache -config utils/gobinsec_conf.yml ${EXE_TARGET} ${DEPLOY_DIR}/${TARGET_OS}/${LAUNCHER_EXE}
|
||||||
|
|
||||||
|
gobinsec-cache.yml:
|
||||||
|
./utils/gobinsec_update.sh
|
||||||
|
cp ./utils/gobinsec_update/gobinsec-cache-valid.yml ./gobinsec-cache.yml
|
||||||
|
|
||||||
updates: install-go-mod-outdated
|
updates: install-go-mod-outdated
|
||||||
# Uncomment the "-ci" to fail the job if something can be updated.
|
# Uncomment the "-ci" to fail the job if something can be updated.
|
||||||
go list -u -m -json all | go-mod-outdated -update -direct #-ci
|
go list -u -m -json all | go-mod-outdated -update -direct #-ci
|
||||||
@ -313,6 +323,7 @@ clean: clean-vendor clean-gui clean-vcpkg
|
|||||||
rm -f ./*.syso
|
rm -f ./*.syso
|
||||||
rm -f release-notes/bridge.html
|
rm -f release-notes/bridge.html
|
||||||
rm -f release-notes/import-export.html
|
rm -f release-notes/import-export.html
|
||||||
|
rm -f ${LAUNCHER_EXE} ${BRIDGE_EXE} ${BRIDGE_EXE_NAME}
|
||||||
|
|
||||||
|
|
||||||
.PHONY: generate
|
.PHONY: generate
|
||||||
|
|||||||
@ -22,7 +22,7 @@ to start Bridge on startup is enabled by default.
|
|||||||
When the main window is closed, Bridge will continue to run in the
|
When the main window is closed, Bridge will continue to run in the
|
||||||
background.
|
background.
|
||||||
|
|
||||||
More details [on the public website](https://protonmail.com/bridge).
|
More details [on the public website](https://proton.me/mail/bridge).
|
||||||
|
|
||||||
## Launchers
|
## Launchers
|
||||||
Launchers are binaries used to run the Proton Mail Bridge or Import-Export apps.
|
Launchers are binaries used to run the Proton Mail Bridge or Import-Export apps.
|
||||||
|
|||||||
8
go.mod
8
go.mod
@ -50,9 +50,9 @@ require (
|
|||||||
github.com/vmihailenco/msgpack/v5 v5.3.5
|
github.com/vmihailenco/msgpack/v5 v5.3.5
|
||||||
go.etcd.io/bbolt v1.3.6
|
go.etcd.io/bbolt v1.3.6
|
||||||
golang.org/x/exp v0.0.0-20220921164117-439092de6870
|
golang.org/x/exp v0.0.0-20220921164117-439092de6870
|
||||||
golang.org/x/net v0.0.0-20220921203646-d300de134e69
|
golang.org/x/net v0.1.0
|
||||||
golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8
|
golang.org/x/sys v0.1.0
|
||||||
golang.org/x/text v0.3.7
|
golang.org/x/text v0.4.0
|
||||||
google.golang.org/grpc v1.49.0
|
google.golang.org/grpc v1.49.0
|
||||||
google.golang.org/protobuf v1.28.1
|
google.golang.org/protobuf v1.28.1
|
||||||
howett.net/plist v1.0.0
|
howett.net/plist v1.0.0
|
||||||
@ -93,7 +93,7 @@ require (
|
|||||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||||
golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0 // indirect
|
golang.org/x/crypto v0.1.0 // indirect
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
|
||||||
golang.org/x/tools v0.1.12 // indirect
|
golang.org/x/tools v0.1.12 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20220921223823-23cae91e6737 // indirect
|
google.golang.org/genproto v0.0.0-20220921223823-23cae91e6737 // indirect
|
||||||
|
|||||||
16
go.sum
16
go.sum
@ -375,8 +375,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
|||||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0 h1:a5Yg6ylndHHYJqIPrdq0AhvR6KTvDTAvgBtaidhEevY=
|
golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
|
||||||
golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||||
@ -424,8 +424,8 @@ golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qx
|
|||||||
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20220921203646-d300de134e69 h1:hUJpGDpnfwdJW8iNypFjmSY0sCBEL+spFTZ2eO+Sfps=
|
golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0=
|
||||||
golang.org/x/net v0.0.0-20220921203646-d300de134e69/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
@ -465,8 +465,8 @@ golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 h1:h+EGohizhe9XlX18rfpa8k8RAc5XyaeamM+0VHRd4lc=
|
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
|
||||||
golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
@ -474,8 +474,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
|||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
|
||||||
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/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
|||||||
@ -69,12 +69,13 @@ const (
|
|||||||
flagMemProfileShort = "m"
|
flagMemProfileShort = "m"
|
||||||
flagLogLevel = "log-level"
|
flagLogLevel = "log-level"
|
||||||
flagLogLevelShort = "l"
|
flagLogLevelShort = "l"
|
||||||
// FlagCLI indicate to start with command line interface.
|
FlagGRPC = "grpc" // FlagGRPC starts the gRPC frontend
|
||||||
FlagCLI = "cli"
|
FlagGRPCShort = "g"
|
||||||
flagCLIShort = "c"
|
FlagCLI = "cli" // FlagCLI indicate to start with command line interface.
|
||||||
flagRestart = "restart"
|
flagCLIShort = "c"
|
||||||
FlagLauncher = "launcher"
|
flagRestart = "restart"
|
||||||
FlagNoWindow = "no-window"
|
FlagLauncher = "launcher"
|
||||||
|
FlagNoWindow = "no-window"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Base struct {
|
type Base struct {
|
||||||
@ -299,6 +300,11 @@ func (b *Base) NewApp(mainLoop func(*Base, *cli.Context) error) *cli.App {
|
|||||||
Aliases: []string{flagLogLevelShort},
|
Aliases: []string{flagLogLevelShort},
|
||||||
Usage: "Set the log level (one of panic, fatal, error, warn, info, debug)",
|
Usage: "Set the log level (one of panic, fatal, error, warn, info, debug)",
|
||||||
},
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: FlagGRPC,
|
||||||
|
Aliases: []string{FlagGRPCShort},
|
||||||
|
Usage: "Start the gRPC service",
|
||||||
|
},
|
||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
Name: FlagCLI,
|
Name: FlagCLI,
|
||||||
Aliases: []string{flagCLIShort},
|
Aliases: []string{flagCLIShort},
|
||||||
|
|||||||
@ -40,10 +40,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
flagLogIMAP = "log-imap"
|
flagLogIMAP = "log-imap"
|
||||||
flagLogSMTP = "log-smtp"
|
flagLogSMTP = "log-smtp"
|
||||||
flagNonInteractive = "noninteractive"
|
flagNonInteractive = "noninteractive"
|
||||||
|
flagNonInteractiveShort = "n"
|
||||||
// Memory cache was estimated by empirical usage in the past, and it was set to 100MB.
|
// Memory cache was estimated by empirical usage in the past, and it was set to 100MB.
|
||||||
// NOTE: This value must not be less than maximal size of one email (~30MB).
|
// NOTE: This value must not be less than maximal size of one email (~30MB).
|
||||||
inMemoryCacheLimit = 100 * (1 << 20)
|
inMemoryCacheLimit = 100 * (1 << 20)
|
||||||
@ -62,8 +62,9 @@ func New(base *base.Base) *cli.App {
|
|||||||
Usage: "Enable logging of SMTP communications (may contain decrypted data!)",
|
Usage: "Enable logging of SMTP communications (may contain decrypted data!)",
|
||||||
},
|
},
|
||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
Name: flagNonInteractive,
|
Name: flagNonInteractive,
|
||||||
Usage: "Start Bridge entirely non-interactively",
|
Aliases: []string{flagNonInteractiveShort},
|
||||||
|
Usage: "Start Bridge entirely non-interactively",
|
||||||
},
|
},
|
||||||
}...)
|
}...)
|
||||||
|
|
||||||
@ -72,6 +73,11 @@ func New(base *base.Base) *cli.App {
|
|||||||
|
|
||||||
func main(b *base.Base, c *cli.Context) error { //nolint:funlen
|
func main(b *base.Base, c *cli.Context) error { //nolint:funlen
|
||||||
frontendType := getFrontendTypeFromCLIParams(c)
|
frontendType := getFrontendTypeFromCLIParams(c)
|
||||||
|
if frontendType == frontend.Unknown {
|
||||||
|
_ = cli.ShowAppHelp(c)
|
||||||
|
return errors.New("no frontend was specified. Use --grpc, --cli or --noninteractive")
|
||||||
|
}
|
||||||
|
|
||||||
f := frontend.New(
|
f := frontend.New(
|
||||||
frontendType,
|
frontendType,
|
||||||
!c.Bool(base.FlagNoWindow),
|
!c.Bool(base.FlagNoWindow),
|
||||||
@ -171,12 +177,14 @@ func main(b *base.Base, c *cli.Context) error { //nolint:funlen
|
|||||||
|
|
||||||
func getFrontendTypeFromCLIParams(c *cli.Context) frontend.Type {
|
func getFrontendTypeFromCLIParams(c *cli.Context) frontend.Type {
|
||||||
switch {
|
switch {
|
||||||
|
case c.Bool(base.FlagGRPC):
|
||||||
|
return frontend.GRPC
|
||||||
case c.Bool(base.FlagCLI):
|
case c.Bool(base.FlagCLI):
|
||||||
return frontend.CLI
|
return frontend.CLI
|
||||||
case c.Bool(flagNonInteractive):
|
case c.Bool(flagNonInteractive):
|
||||||
return frontend.NonInteractive
|
return frontend.NonInteractive
|
||||||
default:
|
default:
|
||||||
return frontend.GRPC
|
return frontend.Unknown
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -43,7 +43,9 @@ void QMLBackend::init(GRPCConfig const &serviceConfig)
|
|||||||
{
|
{
|
||||||
users_ = new UserList(this);
|
users_ = new UserList(this);
|
||||||
|
|
||||||
app().grpc().setLog(&app().log());
|
Log& log = app().log();
|
||||||
|
log.info(QString("Connecting to gRPC service"));
|
||||||
|
app().grpc().setLog(&log);
|
||||||
this->connectGrpcEvents();
|
this->connectGrpcEvents();
|
||||||
|
|
||||||
QString error;
|
QString error;
|
||||||
|
|||||||
@ -54,7 +54,6 @@ BRIDGE_APP_FULL_NAME=${BRIDGE_APP_FULL_NAME:-"Proton Mail Bridge"}
|
|||||||
BRIDGE_VENDOR=${BRIDGE_VENDOR:-"Proton AG"}
|
BRIDGE_VENDOR=${BRIDGE_VENDOR:-"Proton AG"}
|
||||||
BUILD_CONFIG=${BRIDGE_GUI_BUILD_CONFIG:-Debug}
|
BUILD_CONFIG=${BRIDGE_GUI_BUILD_CONFIG:-Debug}
|
||||||
BUILD_DIR=$(echo "./cmake-build-${BUILD_CONFIG}" | tr '[:upper:]' '[:lower:]')
|
BUILD_DIR=$(echo "./cmake-build-${BUILD_CONFIG}" | tr '[:upper:]' '[:lower:]')
|
||||||
VCPKG_OSX_DEPLOYMENT_TARGET=11.0
|
|
||||||
VCPKG_ROOT="${BRIDGE_REPO_ROOT}/extern/vcpkg"
|
VCPKG_ROOT="${BRIDGE_REPO_ROOT}/extern/vcpkg"
|
||||||
|
|
||||||
git submodule update --init --recursive ${VCPKG_ROOT}
|
git submodule update --init --recursive ${VCPKG_ROOT}
|
||||||
@ -70,10 +69,8 @@ ${VCPKG_BOOTSTRAP} -disableMetrics
|
|||||||
check_exit "Failed to bootstrap vcpkg."
|
check_exit "Failed to bootstrap vcpkg."
|
||||||
|
|
||||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||||
if [[ "$(uname -m)" == "arm64" ]]; then
|
${VCPKG_EXE} install grpc:arm64-osx-min-11-0 --overlay-triplets=vcpkg/triplets --clean-after-build
|
||||||
${VCPKG_EXE} install grpc:arm64-osx-min-11-0 --overlay-triplets=vcpkg/triplets --clean-after-build
|
check_exit "Failed installing gRPC for macOS / Apple Silicon"
|
||||||
check_exit "Failed installing gRPC for macOS / Apple Silicon"
|
|
||||||
fi
|
|
||||||
${VCPKG_EXE} install grpc:x64-osx-min-11-0 --overlay-triplets=vcpkg/triplets --clean-after-build
|
${VCPKG_EXE} install grpc:x64-osx-min-11-0 --overlay-triplets=vcpkg/triplets --clean-after-build
|
||||||
check_exit "Failed installing gRPC for macOS / Intel x64"
|
check_exit "Failed installing gRPC for macOS / Intel x64"
|
||||||
elif [[ "$OSTYPE" == "linux"* ]]; then
|
elif [[ "$OSTYPE" == "linux"* ]]; then
|
||||||
|
|||||||
@ -80,6 +80,18 @@ Log &initLog()
|
|||||||
{
|
{
|
||||||
Log &log = app().log();
|
Log &log = app().log();
|
||||||
log.registerAsQtMessageHandler();
|
log.registerAsQtMessageHandler();
|
||||||
|
log.setEchoInConsole(true);
|
||||||
|
|
||||||
|
// remove old gui log files
|
||||||
|
QDir const logsDir(userLogsDir());
|
||||||
|
for (QFileInfo const fileInfo: logsDir.entryInfoList({ "gui_v*.log" }, QDir::Filter::Files)) // entryInfolist apparently only support wildcards, not regex.
|
||||||
|
QFile(fileInfo.absoluteFilePath()).remove();
|
||||||
|
|
||||||
|
// create new GUI log file
|
||||||
|
QString error;
|
||||||
|
if (!log.startWritingToFile(logsDir.absoluteFilePath(QString("gui_v%1_%2.log").arg(PROJECT_VER).arg(QDateTime::currentSecsSinceEpoch())), &error))
|
||||||
|
log.error(error);
|
||||||
|
|
||||||
return log;
|
return log;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -200,7 +212,7 @@ void launchBridge(QStringList const &args)
|
|||||||
else
|
else
|
||||||
app().log().debug(QString("Bridge executable path: %1").arg(QDir::toNativeSeparators(bridgeExePath)));
|
app().log().debug(QString("Bridge executable path: %1").arg(QDir::toNativeSeparators(bridgeExePath)));
|
||||||
|
|
||||||
overseer = std::make_unique<Overseer>(new ProcessMonitor(bridgeExePath, args, nullptr), nullptr);
|
overseer = std::make_unique<Overseer>(new ProcessMonitor(bridgeExePath, QStringList("--grpc") + args, nullptr), nullptr);
|
||||||
overseer->startWorker(true);
|
overseer->startWorker(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -258,7 +270,6 @@ int main(int argc, char *argv[])
|
|||||||
// In attached mode, we do not intercept stderr and stdout of bridge, as we did not launch it ourselves, so we output the log to the console.
|
// In attached mode, we do not intercept stderr and stdout of bridge, as we did not launch it ourselves, so we output the log to the console.
|
||||||
// When not in attached mode, log entries are forwarded to bridge, which output it on stdout/stderr. bridge-gui's process monitor intercept
|
// When not in attached mode, log entries are forwarded to bridge, which output it on stdout/stderr. bridge-gui's process monitor intercept
|
||||||
// these outputs and output them on the command-line.
|
// these outputs and output them on the command-line.
|
||||||
log.setEchoInConsole(attach);
|
|
||||||
log.setLevel(logLevel);
|
log.setLevel(logLevel);
|
||||||
|
|
||||||
if (!attach)
|
if (!attach)
|
||||||
@ -268,12 +279,17 @@ int main(int argc, char *argv[])
|
|||||||
launchBridge(args);
|
launchBridge(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.info(QString("Retrieving gRPC service configuration from '%1'").arg(QDir::toNativeSeparators(grpcServerConfigPath())));
|
||||||
log.debug(QString("Server configuration file will be loaded from '%1'").arg(QDir::toNativeSeparators(grpcServerConfigPath())));
|
|
||||||
app().backend().init(GRPCClient::waitAndRetrieveServiceConfig(attach ? 0 : grpcServiceConfigWaitDelayMs));
|
app().backend().init(GRPCClient::waitAndRetrieveServiceConfig(attach ? 0 : grpcServiceConfigWaitDelayMs));
|
||||||
if (!attach)
|
if (!attach)
|
||||||
GRPCClient::removeServiceConfigFile();
|
GRPCClient::removeServiceConfigFile();
|
||||||
log.debug("Backend was successfully initialized.");
|
|
||||||
|
// gRPC communication is established. From now on, log events will be sent to bridge via gRPC. bridge will write these to file,
|
||||||
|
// and will output then on console if appropriate. If we are not running in attached mode we intercept bridge stdout & stderr and
|
||||||
|
// display it in our own output and error, so we only continue to log directly to console if we are running in attached mode.
|
||||||
|
log.setEchoInConsole(attach);
|
||||||
|
log.info("Backend was successfully initialized.");
|
||||||
|
log.stopWritingToFile();
|
||||||
|
|
||||||
QQmlApplicationEngine engine;
|
QQmlApplicationEngine engine;
|
||||||
std::unique_ptr<QQmlComponent> rootComponent(createRootQmlComponent(engine));
|
std::unique_ptr<QQmlComponent> rootComponent(createRootQmlComponent(engine));
|
||||||
|
|||||||
@ -43,6 +43,7 @@ Item {
|
|||||||
clip: true
|
clip: true
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
Component.onCompleted: contentItem.boundsBehavior = Flickable.StopAtBounds // Disable the springy effect when scroll reaches top/bottom.
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
// can't use parent here because parent is not ScrollView (Flickable inside contentItem inside ScrollView)
|
// can't use parent here because parent is not ScrollView (Flickable inside contentItem inside ScrollView)
|
||||||
|
|||||||
@ -852,9 +852,9 @@ Window {
|
|||||||
property string version: "2.0.X-BridePreview"
|
property string version: "2.0.X-BridePreview"
|
||||||
property url logsPath: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
|
property url logsPath: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
|
||||||
property url licensePath: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
|
property url licensePath: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
|
||||||
property url releaseNotesLink: Qt.resolvedUrl("https://protonmail.com/download/bridge/early_releases.html")
|
property url releaseNotesLink: Qt.resolvedUrl("https://proton.me/download/bridge/early_releases.html")
|
||||||
property url dependencyLicensesLink: Qt.resolvedUrl("https://github.com/ProtonMail/proton-bridge/v2/blob/master/COPYING_NOTES.md#dependencies")
|
property url dependencyLicensesLink: Qt.resolvedUrl("https://github.com/ProtonMail/proton-bridge/v2/blob/master/COPYING_NOTES.md#dependencies")
|
||||||
property url landingPageLink: Qt.resolvedUrl("https://protonmail.com/bridge")
|
property url landingPageLink: Qt.resolvedUrl("https://proton.me/mail/bridge#download")
|
||||||
|
|
||||||
property string colorSchemeName: "light"
|
property string colorSchemeName: "light"
|
||||||
function changeColorScheme(newScheme){
|
function changeColorScheme(newScheme){
|
||||||
|
|||||||
@ -28,6 +28,8 @@ SettingsView {
|
|||||||
|
|
||||||
property var selectedAddress
|
property var selectedAddress
|
||||||
|
|
||||||
|
signal bugReportWasSent()
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
text: qsTr("Report a problem")
|
text: qsTr("Report a problem")
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
@ -61,7 +63,7 @@ SettingsView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onTextChanged: {
|
onTextChanged: {
|
||||||
// Rise max length error imidiatly while typing
|
// Rise max length error immediately while typing
|
||||||
if (description.text.length > description._maxLength) {
|
if (description.text.length > description._maxLength) {
|
||||||
validate()
|
validate()
|
||||||
}
|
}
|
||||||
@ -164,6 +166,7 @@ SettingsView {
|
|||||||
Connections {
|
Connections {
|
||||||
target: Backend
|
target: Backend
|
||||||
function onReportBugFinished() { sendButton.loading = false }
|
function onReportBugFinished() { sendButton.loading = false }
|
||||||
|
function onBugReportSendSuccess() { root.bugReportWasSent() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -53,6 +53,8 @@ Item {
|
|||||||
selectByMouse: true
|
selectByMouse: true
|
||||||
selectByKeyboard: true
|
selectByKeyboard: true
|
||||||
selectionColor: root.colorScheme.text_weak
|
selectionColor: root.colorScheme.text_weak
|
||||||
|
wrapMode: Text.WrapAnywhere
|
||||||
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -359,6 +359,10 @@ Item {
|
|||||||
onBack: {
|
onBack: {
|
||||||
rightContent.showHelpView()
|
rightContent.showHelpView()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onBugReportWasSent: {
|
||||||
|
rightContent.showAccount()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showAccount(index) {
|
function showAccount(index) {
|
||||||
|
|||||||
@ -41,7 +41,7 @@ SettingsView {
|
|||||||
actionIcon: "/qml/icons/ic-external-link.svg"
|
actionIcon: "/qml/icons/ic-external-link.svg"
|
||||||
description: qsTr("Get help setting up your client with our instructions and FAQs.")
|
description: qsTr("Get help setting up your client with our instructions and FAQs.")
|
||||||
type: SettingsItem.PrimaryButton
|
type: SettingsItem.PrimaryButton
|
||||||
onClicked: {Qt.openUrlExternally("https://protonmail.com/support/categories/bridge/")}
|
onClicked: {Qt.openUrlExternally("https://proton.me/support/mail")}
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|||||||
@ -289,7 +289,7 @@ QtObject {
|
|||||||
|
|
||||||
property Notification updateForceError: Notification {
|
property Notification updateForceError: Notification {
|
||||||
title: qsTr("Bridge coudn’t update")
|
title: qsTr("Bridge coudn’t update")
|
||||||
description: qsTr("You must update manually. Go to: https:/protonmail.com/bridge/download")
|
description: qsTr("You must update manually. Go to: https://proton.me/mail/bridge#download")
|
||||||
brief: title
|
brief: title
|
||||||
icon: "./icons/ic-exclamation-circle-filled.svg"
|
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||||
type: Notification.NotificationType.Danger
|
type: Notification.NotificationType.Danger
|
||||||
@ -997,7 +997,7 @@ QtObject {
|
|||||||
type: Notification.NotificationType.Danger
|
type: Notification.NotificationType.Danger
|
||||||
group: Notifications.Group.Dialogs | Notifications.Group.Configuration
|
group: Notifications.Group.Dialogs | Notifications.Group.Configuration
|
||||||
|
|
||||||
property var supportLink: "https://protonmail.com/support/knowledge-base/macos-keychain-corrupted"
|
property var supportLink: "https://proton.me/support/mail"
|
||||||
|
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// 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/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
import QtQml
|
import QtQml
|
||||||
|
|
||||||
QtObject {
|
QtObject {
|
||||||
@ -22,7 +23,7 @@ QtObject {
|
|||||||
property var prominent
|
property var prominent
|
||||||
|
|
||||||
// Primary
|
// Primary
|
||||||
property color primay_norm
|
property color primary_norm
|
||||||
|
|
||||||
// Interaction-norm
|
// Interaction-norm
|
||||||
property color interaction_norm
|
property color interaction_norm
|
||||||
|
|||||||
@ -30,7 +30,7 @@ QtObject {
|
|||||||
// https://doc.qt.io/qt-5/qtqml-documents-definetypes.html#inline-components
|
// https://doc.qt.io/qt-5/qtqml-documents-definetypes.html#inline-components
|
||||||
|
|
||||||
// component ColorScheme: QtObject {
|
// component ColorScheme: QtObject {
|
||||||
// property color primay_norm
|
// property color primary_norm
|
||||||
// ...
|
// ...
|
||||||
// }
|
// }
|
||||||
|
|
||||||
@ -40,7 +40,7 @@ QtObject {
|
|||||||
prominent: lightProminentStyle
|
prominent: lightProminentStyle
|
||||||
|
|
||||||
// Primary
|
// Primary
|
||||||
primay_norm: "#6D4AFF"
|
primary_norm: "#6D4AFF"
|
||||||
|
|
||||||
// Interaction-norm
|
// Interaction-norm
|
||||||
interaction_norm: "#6D4AFF"
|
interaction_norm: "#6D4AFF"
|
||||||
@ -115,7 +115,7 @@ QtObject {
|
|||||||
prominent: this
|
prominent: this
|
||||||
|
|
||||||
// Primary
|
// Primary
|
||||||
primay_norm: "#8A6EFF"
|
primary_norm: "#8A6EFF"
|
||||||
|
|
||||||
// Interaction-norm
|
// Interaction-norm
|
||||||
interaction_norm: "#6D4AFF"
|
interaction_norm: "#6D4AFF"
|
||||||
@ -190,7 +190,7 @@ QtObject {
|
|||||||
prominent: darkProminentStyle
|
prominent: darkProminentStyle
|
||||||
|
|
||||||
// Primary
|
// Primary
|
||||||
primay_norm: "#8A6EFF"
|
primary_norm: "#8A6EFF"
|
||||||
|
|
||||||
// Interaction-norm
|
// Interaction-norm
|
||||||
interaction_norm: "#6D4AFF"
|
interaction_norm: "#6D4AFF"
|
||||||
@ -265,7 +265,7 @@ QtObject {
|
|||||||
prominent: this
|
prominent: this
|
||||||
|
|
||||||
// Primary
|
// Primary
|
||||||
primay_norm: "#8A6EFF"
|
primary_norm: "#8A6EFF"
|
||||||
|
|
||||||
// Interaction-norm
|
// Interaction-norm
|
||||||
interaction_norm: "#6D4AFF"
|
interaction_norm: "#6D4AFF"
|
||||||
|
|||||||
@ -44,6 +44,7 @@ Item {
|
|||||||
clip: true
|
clip: true
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
Component.onCompleted: contentItem.boundsBehavior = Flickable.StopAtBounds // Disable the springy effect when scroll reaches top/bottom.
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
// can't use parent here because parent is not ScrollView (Flickable inside contentItem inside ScrollView)
|
// can't use parent here because parent is not ScrollView (Flickable inside contentItem inside ScrollView)
|
||||||
|
|||||||
@ -42,7 +42,7 @@ Item {
|
|||||||
property string name : "Apple Mail"
|
property string name : "Apple Mail"
|
||||||
property string iconSource : "/qml/icons/ic-apple-mail.svg"
|
property string iconSource : "/qml/icons/ic-apple-mail.svg"
|
||||||
property bool haveAutoSetup: true
|
property bool haveAutoSetup: true
|
||||||
property string link: "https://protonmail.com/bridge/applemail"
|
property string link: "https://proton.me/support/protonmail-bridge-clients-apple-mail"
|
||||||
|
|
||||||
Component.onCompleted : {
|
Component.onCompleted : {
|
||||||
if (Backend.goos == "darwin") {
|
if (Backend.goos == "darwin") {
|
||||||
@ -50,13 +50,13 @@ Item {
|
|||||||
"name" : "Apple Mail",
|
"name" : "Apple Mail",
|
||||||
"iconSource" : "/qml/icons/ic-apple-mail.svg",
|
"iconSource" : "/qml/icons/ic-apple-mail.svg",
|
||||||
"haveAutoSetup" : true,
|
"haveAutoSetup" : true,
|
||||||
"link" : "https://protonmail.com/bridge/applemail"
|
"link" : "https://proton.me/support/protonmail-bridge-clients-apple-mail"
|
||||||
})
|
})
|
||||||
append({
|
append({
|
||||||
"name" : "Microsoft Outlook",
|
"name" : "Microsoft Outlook",
|
||||||
"iconSource" : "/qml/icons/ic-microsoft-outlook.svg",
|
"iconSource" : "/qml/icons/ic-microsoft-outlook.svg",
|
||||||
"haveAutoSetup" : false,
|
"haveAutoSetup" : false,
|
||||||
"link" : "https://protonmail.com/bridge/outlook2019-mac"
|
"link" : "https://proton.me/support/protonmail-bridge-clients-macos-outlook-2019"
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (Backend.goos == "windows") {
|
if (Backend.goos == "windows") {
|
||||||
@ -64,7 +64,7 @@ Item {
|
|||||||
"name" : "Microsoft Outlook",
|
"name" : "Microsoft Outlook",
|
||||||
"iconSource" : "/qml/icons/ic-microsoft-outlook.svg",
|
"iconSource" : "/qml/icons/ic-microsoft-outlook.svg",
|
||||||
"haveAutoSetup" : false,
|
"haveAutoSetup" : false,
|
||||||
"link" : "https://protonmail.com/bridge/outlook2019"
|
"link" : "https://proton.me/support/protonmail-bridge-clients-windows-outlook-2019"
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,14 +72,14 @@ Item {
|
|||||||
"name" : "Mozilla Thunderbird",
|
"name" : "Mozilla Thunderbird",
|
||||||
"iconSource" : "/qml/icons/ic-mozilla-thunderbird.svg",
|
"iconSource" : "/qml/icons/ic-mozilla-thunderbird.svg",
|
||||||
"haveAutoSetup" : false,
|
"haveAutoSetup" : false,
|
||||||
"link" : "https://protonmail.com/bridge/thunderbird"
|
"link" : "https://proton.me/support/protonmail-bridge-clients-windows-thunderbird"
|
||||||
})
|
})
|
||||||
|
|
||||||
append({
|
append({
|
||||||
"name" : "Other",
|
"name" : "Other",
|
||||||
"iconSource" : "/qml/icons/ic-other-mail-clients.svg",
|
"iconSource" : "/qml/icons/ic-other-mail-clients.svg",
|
||||||
"haveAutoSetup" : false,
|
"haveAutoSetup" : false,
|
||||||
"link" : "https://protonmail.com/bridge/clients"
|
"link" : "https://proton.me/support/protonmail-bridge-configure-client"
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -288,7 +288,7 @@ FocusScope {
|
|||||||
Label {
|
Label {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
textFormat: Text.StyledText
|
textFormat: Text.StyledText
|
||||||
text: link("https://protonmail.com/signup", qsTr("Create or upgrade your account"))
|
text: link("https://proton.me/mail/pricing", qsTr("Create or upgrade your account"))
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
Layout.topMargin: 24
|
Layout.topMargin: 24
|
||||||
type: Label.LabelType.Body
|
type: Label.LabelType.Body
|
||||||
|
|||||||
@ -43,10 +43,23 @@ Window {
|
|||||||
signal showSignIn(string username)
|
signal showSignIn(string username)
|
||||||
signal quit()
|
signal quit()
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: mouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
}
|
||||||
|
|
||||||
|
function enableHoverOnOpenBridgeButton() {
|
||||||
|
openBridgeButton.hoverEnabled = true
|
||||||
|
mouseArea.positionChanged.disconnect(enableHoverOnOpenBridgeButton)
|
||||||
|
}
|
||||||
|
|
||||||
onVisibleChanged: {
|
onVisibleChanged: {
|
||||||
if (visible) { // GODT-1479 restore the hover-able status that may have been disabled when clicking on the 'Open Bridge' button.
|
if (visible) { // GODT-1479 To avoid a visual glitch where the 'Open bridge button' would appear hovered when the status windows opens,
|
||||||
openBridgeButton.hoverEnabled = true
|
// we've disabled hover on it when it was last closed. Re-enabling hover here will not work on all platforms. so we temporarily connect
|
||||||
|
// mouse move event over the window's mouseArea to a function that will re-enable hover on the open bridge button.
|
||||||
openBridgeButton.focus = false
|
openBridgeButton.focus = false
|
||||||
|
mouseArea.positionChanged.connect(enableHoverOnOpenBridgeButton)
|
||||||
} else {
|
} else {
|
||||||
menu.close()
|
menu.close()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -84,11 +84,13 @@ QString userConfigDir()
|
|||||||
dir += "/Library/Application Support";
|
dir += "/Library/Application Support";
|
||||||
#else
|
#else
|
||||||
dir = qgetenv ("XDG_CONFIG_HOME");
|
dir = qgetenv ("XDG_CONFIG_HOME");
|
||||||
if (dir.isEmpty())
|
if (dir.isEmpty())
|
||||||
dir = qgetenv ("HOME");
|
{
|
||||||
|
dir = qgetenv ("HOME");
|
||||||
if (dir.isEmpty())
|
if (dir.isEmpty())
|
||||||
throw Exception("neither $XDG_CONFIG_HOME nor $HOME are defined");
|
throw Exception("neither $XDG_CONFIG_HOME nor $HOME are defined");
|
||||||
dir += "/.config";
|
dir += "/.config";
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
QString const folder = QDir(dir).absoluteFilePath(configFolder);
|
QString const folder = QDir(dir).absoluteFilePath(configFolder);
|
||||||
QDir().mkpath(folder);
|
QDir().mkpath(folder);
|
||||||
@ -115,11 +117,13 @@ QString userCacheDir()
|
|||||||
dir += "/Library/Caches";
|
dir += "/Library/Caches";
|
||||||
#else
|
#else
|
||||||
dir = qgetenv ("XDG_CACHE_HOME");
|
dir = qgetenv ("XDG_CACHE_HOME");
|
||||||
if (dir.isEmpty())
|
if (dir.isEmpty())
|
||||||
dir = qgetenv ("HOME");
|
{
|
||||||
|
dir = qgetenv ("HOME");
|
||||||
if (dir.isEmpty())
|
if (dir.isEmpty())
|
||||||
throw Exception("neither XDG_CACHE_HOME nor $HOME are defined");
|
throw Exception("neither XDG_CACHE_HOME nor $HOME are defined");
|
||||||
dir += "/.cache";
|
dir += "/.cache";
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
QString const folder = QDir(dir).absoluteFilePath(configFolder);
|
QString const folder = QDir(dir).absoluteFilePath(configFolder);
|
||||||
@ -129,6 +133,17 @@ QString userCacheDir()
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \return user logs directory used by bridge.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
QString userLogsDir()
|
||||||
|
{
|
||||||
|
QString const path = QDir(userCacheDir()).absoluteFilePath("logs");
|
||||||
|
QDir().mkpath(path);
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \return The value GOOS would return for the current platform.
|
/// \return The value GOOS would return for the current platform.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
|
|||||||
@ -29,6 +29,7 @@ namespace bridgepp
|
|||||||
|
|
||||||
QString userConfigDir(); ///< Get the path of the user configuration folder.
|
QString userConfigDir(); ///< Get the path of the user configuration folder.
|
||||||
QString userCacheDir(); ///< Get the path of the user cache folder.
|
QString userCacheDir(); ///< Get the path of the user cache folder.
|
||||||
|
QString userLogsDir(); ///< Get the path of the user logs folder.
|
||||||
QString goos(); ///< return the value of Go's GOOS for the current platform ("darwin", "linux" and "windows" are supported).
|
QString goos(); ///< return the value of Go's GOOS for the current platform ("darwin", "linux" and "windows" are supported).
|
||||||
qint64 randN(qint64 n); ///< return a random integer in the half open range [0,n)
|
qint64 randN(qint64 n); ///< return a random integer in the half open range [0,n)
|
||||||
QString randomFirstName(); ///< Get a random first name from a pre-determined list.
|
QString randomFirstName(); ///< Get a random first name from a pre-determined list.
|
||||||
|
|||||||
@ -123,7 +123,7 @@ bool GRPCClient::connectToServer(GRPCConfig const &config, QString &outError)
|
|||||||
int i = 0;
|
int i = 0;
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
this->logDebug(QString("Connection to gRPC server at %1. attempt #%2").arg(address).arg(++i));
|
this->logInfo(QString("Connection to gRPC server at %1. attempt #%2").arg(address).arg(++i));
|
||||||
|
|
||||||
if (channel_->WaitForConnected(gpr_time_add(gpr_now(GPR_CLOCK_REALTIME), gpr_time_from_millis(grpcConnectionRetryDelayMs, GPR_TIMESPAN))))
|
if (channel_->WaitForConnected(gpr_time_add(gpr_now(GPR_CLOCK_REALTIME), gpr_time_from_millis(grpcConnectionRetryDelayMs, GPR_TIMESPAN))))
|
||||||
break; // connection established.
|
break; // connection established.
|
||||||
@ -135,13 +135,13 @@ bool GRPCClient::connectToServer(GRPCConfig const &config, QString &outError)
|
|||||||
if (channel_->GetState(true) != GRPC_CHANNEL_READY)
|
if (channel_->GetState(true) != GRPC_CHANNEL_READY)
|
||||||
throw Exception("connection check failed.");
|
throw Exception("connection check failed.");
|
||||||
|
|
||||||
this->logDebug("Successfully connected to gRPC server.");
|
this->logInfo("Successfully connected to gRPC server.");
|
||||||
|
|
||||||
QString const clientToken = QUuid::createUuid().toString();
|
QString const clientToken = QUuid::createUuid().toString();
|
||||||
QString clientConfigPath = createClientConfigFile(clientToken);
|
QString clientConfigPath = createClientConfigFile(clientToken);
|
||||||
if (clientConfigPath.isEmpty())
|
if (clientConfigPath.isEmpty())
|
||||||
throw Exception("gRPC client config could not be saved.");
|
throw Exception("gRPC client config could not be saved.");
|
||||||
this->logDebug(QString("Client config file was saved to '%1'").arg(QDir::toNativeSeparators(clientConfigPath)));
|
this->logInfo(QString("Client config file was saved to '%1'").arg(QDir::toNativeSeparators(clientConfigPath)));
|
||||||
|
|
||||||
QString returnedClientToken;
|
QString returnedClientToken;
|
||||||
grpc::Status status = this->checkTokens(QDir::toNativeSeparators(clientConfigPath), returnedClientToken);
|
grpc::Status status = this->checkTokens(QDir::toNativeSeparators(clientConfigPath), returnedClientToken);
|
||||||
@ -153,7 +153,7 @@ bool GRPCClient::connectToServer(GRPCConfig const &config, QString &outError)
|
|||||||
if (!status.ok())
|
if (!status.ok())
|
||||||
throw Exception(QString::fromStdString(status.error_message()));
|
throw Exception(QString::fromStdString(status.error_message()));
|
||||||
|
|
||||||
log_->debug("gRPC token was validated");
|
log_->info("gRPC token was validated");
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -933,6 +933,15 @@ void GRPCClient::logError(QString const &message)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \param[in] message The event message.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
void GRPCClient::logInfo(QString const &message)
|
||||||
|
{
|
||||||
|
this->log(Log::Level::Info, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \param[in] status The status
|
/// \param[in] status The status
|
||||||
/// \param[in] callName The call name.
|
/// \param[in] callName The call name.
|
||||||
|
|||||||
@ -211,6 +211,8 @@ private:
|
|||||||
void logTrace(QString const &message); ///< Log a trace event.
|
void logTrace(QString const &message); ///< Log a trace event.
|
||||||
void logDebug(QString const &message); ///< Log a debug event.
|
void logDebug(QString const &message); ///< Log a debug event.
|
||||||
void logError(QString const &message); ///< Log an error event.
|
void logError(QString const &message); ///< Log an error event.
|
||||||
|
void logInfo(QString const &message); ///< Log an info event.
|
||||||
|
|
||||||
grpc::Status logGRPCCallStatus(grpc::Status const &status, QString const &callName, QList<grpc::StatusCode> allowedErrors = {}); ///< Log the status of a gRPC code.
|
grpc::Status logGRPCCallStatus(grpc::Status const &status, QString const &callName, QList<grpc::StatusCode> allowedErrors = {}); ///< Log the status of a gRPC code.
|
||||||
grpc::Status simpleMethod(SimpleMethod method); ///< perform a gRPC call to a bool setter.
|
grpc::Status simpleMethod(SimpleMethod method); ///< perform a gRPC call to a bool setter.
|
||||||
grpc::Status setBool(BoolSetter setter, bool value); ///< perform a gRPC call to a bool setter.
|
grpc::Status setBool(BoolSetter setter, bool value); ///< perform a gRPC call to a bool setter.
|
||||||
|
|||||||
@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
|
|
||||||
#include "Log.h"
|
#include "Log.h"
|
||||||
|
#include "../Exception/Exception.h"
|
||||||
|
|
||||||
|
|
||||||
namespace bridgepp
|
namespace bridgepp
|
||||||
@ -204,6 +205,35 @@ bool Log::echoInConsole() const
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \param[in] path The path of the file to write to.
|
||||||
|
/// \param[out] outError if an error occurs and this pointer in not null, on exit it contains a description of the error.
|
||||||
|
/// \return true if and only if the operation was successful.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
bool Log::startWritingToFile(QString const &path, QString *outError)
|
||||||
|
{
|
||||||
|
QMutexLocker locker(&mutex_);
|
||||||
|
file_ = std::make_unique<QFile>(path);
|
||||||
|
if (file_->open(QIODevice::WriteOnly | QIODevice::Text))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (outError)
|
||||||
|
*outError = QString("Could not open log file '%1' for writing.");
|
||||||
|
file_.reset();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
///
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
void Log::stopWritingToFile()
|
||||||
|
{
|
||||||
|
QMutexLocker locker(&mutex_);
|
||||||
|
file_.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \param[in] message The message.
|
/// \param[in] message The message.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
@ -278,12 +308,22 @@ void Log::addEntry(Log::Level level, QString const &message)
|
|||||||
return;
|
return;
|
||||||
emit entryAdded(level, message);
|
emit entryAdded(level, message);
|
||||||
|
|
||||||
|
if (!(echoInConsole_ || file_))
|
||||||
|
return;
|
||||||
|
|
||||||
|
QString const entryStr = logEntryToString(level, message) + "\n";
|
||||||
if (echoInConsole_)
|
if (echoInConsole_)
|
||||||
{
|
{
|
||||||
QTextStream &stream = (qint32(level) <= (qint32(Level::Warn))) ? stderr_ : stdout_;
|
QTextStream &stream = (qint32(level) <= (qint32(Level::Warn))) ? stderr_ : stdout_;
|
||||||
stream << logEntryToString(level, message) << "\n";
|
stream << entryStr;
|
||||||
stream.flush();
|
stream.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (file_)
|
||||||
|
{
|
||||||
|
file_->write(entryStr.toLocal8Bit());
|
||||||
|
file_->flush();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -63,6 +63,8 @@ public: // member functions.
|
|||||||
Level level() const; ///< Get the log level.
|
Level level() const; ///< Get the log level.
|
||||||
void setEchoInConsole(bool value); ///< Set if the log entries should be echoed in STDOUT/STDERR.
|
void setEchoInConsole(bool value); ///< Set if the log entries should be echoed in STDOUT/STDERR.
|
||||||
bool echoInConsole() const; ///< Check if the log entries should be echoed in STDOUT/STDERR.
|
bool echoInConsole() const; ///< Check if the log entries should be echoed in STDOUT/STDERR.
|
||||||
|
bool startWritingToFile(QString const& path, QString *outError = nullptr); ///< Start writing the log to file. Concerns only future entries.
|
||||||
|
void stopWritingToFile();
|
||||||
void registerAsQtMessageHandler(); ///< Install the Qt message handler.
|
void registerAsQtMessageHandler(); ///< Install the Qt message handler.
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
@ -83,7 +85,7 @@ private: // data members
|
|||||||
mutable QMutex mutex_; ///< The mutex.
|
mutable QMutex mutex_; ///< The mutex.
|
||||||
Level level_ { defaultLevel }; ///< The log level
|
Level level_ { defaultLevel }; ///< The log level
|
||||||
bool echoInConsole_ { false }; ///< Set if the log messages should be sent to STDOUT/STDERR.
|
bool echoInConsole_ { false }; ///< Set if the log messages should be sent to STDOUT/STDERR.
|
||||||
|
std::unique_ptr<QFile> file_; ///< The file to write the log to.
|
||||||
QTextStream stdout_; ///< The stdout stream.
|
QTextStream stdout_; ///< The stdout stream.
|
||||||
QTextStream stderr_; ///< The stderr stream.
|
QTextStream stderr_; ///< The stderr stream.
|
||||||
};
|
};
|
||||||
|
|||||||
@ -25,6 +25,7 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v2/internal/locations"
|
"github.com/ProtonMail/proton-bridge/v2/internal/locations"
|
||||||
"github.com/ProtonMail/proton-bridge/v2/internal/updater"
|
"github.com/ProtonMail/proton-bridge/v2/internal/updater"
|
||||||
"github.com/ProtonMail/proton-bridge/v2/pkg/listener"
|
"github.com/ProtonMail/proton-bridge/v2/pkg/listener"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Type describes the available types of frontend.
|
// Type describes the available types of frontend.
|
||||||
@ -34,6 +35,7 @@ const (
|
|||||||
CLI Type = iota
|
CLI Type = iota
|
||||||
GRPC
|
GRPC
|
||||||
NonInteractive
|
NonInteractive
|
||||||
|
Unknown
|
||||||
)
|
)
|
||||||
|
|
||||||
type Frontend interface {
|
type Frontend interface {
|
||||||
@ -45,7 +47,7 @@ type Frontend interface {
|
|||||||
WaitUntilFrontendIsReady()
|
WaitUntilFrontendIsReady()
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns initialized frontend based on `frontendType`, which can be `CLI` or `GRPC`.
|
// New returns initialized frontend based on `frontendType`, which can be `CLI` or `GRPC`. Non-interactive will return a nil frontend.
|
||||||
func New(
|
func New(
|
||||||
frontendType Type,
|
frontendType Type,
|
||||||
showWindowOnStart bool,
|
showWindowOnStart bool,
|
||||||
@ -75,9 +77,13 @@ func New(
|
|||||||
)
|
)
|
||||||
|
|
||||||
case NonInteractive:
|
case NonInteractive:
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case Unknown:
|
||||||
fallthrough
|
fallthrough
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil
|
logrus.Panicf("Unexpected frontend value %v", frontendType)
|
||||||
|
return nil // return statement is required by compiler, although the above call will panic.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -59,12 +59,13 @@ const (
|
|||||||
// Service is the RPC service struct.
|
// Service is the RPC service struct.
|
||||||
type Service struct { // nolint:structcheck
|
type Service struct { // nolint:structcheck
|
||||||
UnimplementedBridgeServer
|
UnimplementedBridgeServer
|
||||||
grpcServer *grpc.Server // the gGRPC server
|
grpcServer *grpc.Server // the gGRPC server
|
||||||
listener net.Listener
|
listener net.Listener
|
||||||
eventStreamCh chan *StreamEvent
|
eventStreamCh chan *StreamEvent
|
||||||
eventStreamDoneCh chan struct{}
|
eventStreamChMutex sync.RWMutex
|
||||||
eventQueue []*StreamEvent
|
eventStreamDoneCh chan struct{}
|
||||||
eventQueueMutex sync.Mutex
|
eventQueue []*StreamEvent
|
||||||
|
eventQueueMutex sync.Mutex
|
||||||
|
|
||||||
panicHandler types.PanicHandler
|
panicHandler types.PanicHandler
|
||||||
eventListener listener.Listener
|
eventListener listener.Listener
|
||||||
@ -124,6 +125,7 @@ func NewService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) startGRPCServer() {
|
func (s *Service) startGRPCServer() {
|
||||||
|
s.log.Info("Starting gRPC server")
|
||||||
tlsConfig, pemCert, err := s.generateTLSConfig()
|
tlsConfig, pemCert, err := s.generateTLSConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.log.WithError(err).Panic("Could not generate gRPC TLS config")
|
s.log.WithError(err).Panic("Could not generate gRPC TLS config")
|
||||||
@ -147,7 +149,7 @@ func (s *Service) startGRPCServer() {
|
|||||||
if path, err := s.saveGRPCServerConfigFile(); err != nil {
|
if path, err := s.saveGRPCServerConfigFile(); err != nil {
|
||||||
s.log.WithError(err).WithField("path", path).Panic("Could not write gRPC service config file")
|
s.log.WithError(err).WithField("path", path).Panic("Could not write gRPC service config file")
|
||||||
} else {
|
} else {
|
||||||
s.log.WithField("path", path).Debug("Successfully saved gRPC service config file")
|
s.log.WithField("path", path).Info("Successfully saved gRPC service config file")
|
||||||
}
|
}
|
||||||
|
|
||||||
s.log.Info("gRPC server listening at ", s.listener.Addr())
|
s.log.Info("gRPC server listening at ", s.listener.Addr())
|
||||||
|
|||||||
@ -95,12 +95,15 @@ func (s *Service) GuiReady(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empt
|
|||||||
// Quit implement the Quit gRPC service call.
|
// Quit implement the Quit gRPC service call.
|
||||||
func (s *Service) Quit(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) {
|
func (s *Service) Quit(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) {
|
||||||
s.log.Debug("Quit")
|
s.log.Debug("Quit")
|
||||||
|
return &emptypb.Empty{}, s.quit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) quit() error {
|
||||||
// Windows is notably slow at Quitting. We do it in a goroutine to speed things up a bit.
|
// Windows is notably slow at Quitting. We do it in a goroutine to speed things up a bit.
|
||||||
go func() {
|
go func() {
|
||||||
var err error
|
var err error
|
||||||
if s.eventStreamCh != nil {
|
if s.isStreamingEvents() {
|
||||||
if _, err = s.StopEventStream(ctx, empty); err != nil {
|
if err = s.stopEventStream(); err != nil {
|
||||||
s.log.WithError(err).Error("Quit failed.")
|
s.log.WithError(err).Error("Quit failed.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -109,7 +112,7 @@ func (s *Service) Quit(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empt
|
|||||||
s.grpcServer.GracefulStop()
|
s.grpcServer.GracefulStop()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return &emptypb.Empty{}, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restart implement the Restart gRPC service call.
|
// Restart implement the Restart gRPC service call.
|
||||||
|
|||||||
@ -29,19 +29,19 @@ import (
|
|||||||
func (s *Service) RunEventStream(request *EventStreamRequest, server Bridge_RunEventStreamServer) error {
|
func (s *Service) RunEventStream(request *EventStreamRequest, server Bridge_RunEventStreamServer) error {
|
||||||
s.log.Debug("Starting Event stream")
|
s.log.Debug("Starting Event stream")
|
||||||
|
|
||||||
if s.eventStreamCh != nil {
|
if s.isStreamingEvents() {
|
||||||
return status.Errorf(codes.AlreadyExists, "the service is already streaming") // TO-DO GODT-1667 decide if we want to kill the existing stream.
|
return status.Errorf(codes.AlreadyExists, "the service is already streaming") // TO-DO GODT-1667 decide if we want to kill the existing stream.
|
||||||
}
|
}
|
||||||
|
|
||||||
s.bridge.SetCurrentPlatform(request.ClientPlatform)
|
s.bridge.SetCurrentPlatform(request.ClientPlatform)
|
||||||
|
|
||||||
s.eventStreamCh = make(chan *StreamEvent)
|
s.createEventStreamChannel()
|
||||||
s.eventStreamDoneCh = make(chan struct{})
|
s.eventStreamDoneCh = make(chan struct{})
|
||||||
|
|
||||||
// TO-DO GODT-1667 We should have a safer we to close this channel? What if an event occur while we are closing?
|
// TO-DO GODT-1667 We should have a safer we to close this channel? What if an event occur while we are closing?
|
||||||
defer func() {
|
defer func() {
|
||||||
close(s.eventStreamCh)
|
close(s.eventStreamCh)
|
||||||
s.eventStreamCh = nil
|
s.deleteEventStreamChannel()
|
||||||
close(s.eventStreamDoneCh)
|
close(s.eventStreamDoneCh)
|
||||||
s.eventStreamDoneCh = nil
|
s.eventStreamDoneCh = nil
|
||||||
}()
|
}()
|
||||||
@ -70,24 +70,34 @@ func (s *Service) RunEventStream(request *EventStreamRequest, server Bridge_RunE
|
|||||||
s.log.Debug("Stop Event stream")
|
s.log.Debug("Stop Event stream")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
case <-server.Context().Done():
|
||||||
|
s.log.Debug("Client closed the stream, exiting")
|
||||||
|
return s.quit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// StopEventStream stops the event stream.
|
// StopEventStream stops the event stream.
|
||||||
func (s *Service) StopEventStream(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
|
func (s *Service) StopEventStream(_ context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
|
||||||
|
return &emptypb.Empty{}, s.stopEventStream()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) stopEventStream() error {
|
||||||
|
s.eventStreamChMutex.RLock()
|
||||||
|
defer s.eventStreamChMutex.RUnlock()
|
||||||
|
|
||||||
if s.eventStreamCh == nil {
|
if s.eventStreamCh == nil {
|
||||||
return nil, status.Errorf(codes.NotFound, "The service is not streaming")
|
return status.Errorf(codes.NotFound, "The service is not streaming")
|
||||||
}
|
}
|
||||||
|
|
||||||
s.eventStreamDoneCh <- struct{}{}
|
s.eventStreamDoneCh <- struct{}{}
|
||||||
|
|
||||||
return &emptypb.Empty{}, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendEvent sends an event to the via the gRPC event stream.
|
// SendEvent sends an event to the via the gRPC event stream.
|
||||||
func (s *Service) SendEvent(event *StreamEvent) error {
|
func (s *Service) SendEvent(event *StreamEvent) error {
|
||||||
if s.eventStreamCh == nil { // nobody is connected to the event stream, we queue events
|
if !s.isStreamingEvents() { // nobody is connected to the event stream, we queue events
|
||||||
s.queueEvent(event)
|
s.queueEvent(event)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -174,3 +184,24 @@ func (s *Service) queueEvent(event *StreamEvent) {
|
|||||||
s.eventQueue = append(s.eventQueue, event)
|
s.eventQueue = append(s.eventQueue, event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) isStreamingEvents() bool {
|
||||||
|
s.eventStreamChMutex.RLock()
|
||||||
|
defer s.eventStreamChMutex.RUnlock()
|
||||||
|
|
||||||
|
return s.eventStreamCh != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) createEventStreamChannel() {
|
||||||
|
s.eventStreamChMutex.Lock()
|
||||||
|
defer s.eventStreamChMutex.Unlock()
|
||||||
|
|
||||||
|
s.eventStreamCh = make(chan *StreamEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) deleteEventStreamChannel() {
|
||||||
|
s.eventStreamChMutex.Lock()
|
||||||
|
defer s.eventStreamChMutex.Unlock()
|
||||||
|
|
||||||
|
s.eventStreamCh = nil
|
||||||
|
}
|
||||||
|
|||||||
@ -89,7 +89,7 @@ func newGoIMAPServer(tls *tls.Config, backend backend.Backend, address string, u
|
|||||||
serverID := imapid.ID{
|
serverID := imapid.ID{
|
||||||
imapid.FieldName: "Proton Mail Bridge",
|
imapid.FieldName: "Proton Mail Bridge",
|
||||||
imapid.FieldVendor: "Proton AG",
|
imapid.FieldVendor: "Proton AG",
|
||||||
imapid.FieldSupportURL: "https://protonmail.com/support",
|
imapid.FieldSupportURL: "https://proton.me/support/mail",
|
||||||
}
|
}
|
||||||
|
|
||||||
server.EnableAuth(sasl.Login, func(conn imapserver.Conn) sasl.Server {
|
server.EnableAuth(sasl.Login, func(conn imapserver.Conn) sasl.Server {
|
||||||
|
|||||||
@ -91,7 +91,7 @@ func (c *controller) ListenAndServe() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.WithError(err).Error("Cannot start listner.")
|
l.WithError(err).Error("Cannot start listener.")
|
||||||
c.signals.Emit(events.ErrorEvent, string(c.server.Protocol())+" failed: "+err.Error())
|
c.signals.Emit(events.ErrorEvent, string(c.server.Protocol())+" failed: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -153,7 +153,7 @@ func (q *sendRecorder) deleteExpiredKeys() {
|
|||||||
for key, value := range q.hashes {
|
for key, value := range q.hashes {
|
||||||
// It's hard to find a good expiration time.
|
// It's hard to find a good expiration time.
|
||||||
// On the one hand, a user could set up some cron job sending the same message over and over again (heartbeat).
|
// On the one hand, a user could set up some cron job sending the same message over and over again (heartbeat).
|
||||||
// On the the other, a user could put the device into sleep mode while sending.
|
// On the other, a user could put the device into sleep mode while sending.
|
||||||
// Changing the expiration time will always make one of the edge cases worse.
|
// Changing the expiration time will always make one of the edge cases worse.
|
||||||
// But both edge cases are something we don't care much about. Important thing is we don't send the same message many times.
|
// But both edge cases are something we don't care much about. Important thing is we don't send the same message many times.
|
||||||
if time.Since(value.time) > 30*time.Minute {
|
if time.Since(value.time) > 30*time.Minute {
|
||||||
|
|||||||
@ -245,7 +245,7 @@ func (store *Store) createOrUpdateMessageEvent(msg *pmapi.Message) error {
|
|||||||
func (store *Store) createOrUpdateMessagesEvent(msgs []*pmapi.Message) error { //nolint:funlen
|
func (store *Store) createOrUpdateMessagesEvent(msgs []*pmapi.Message) error { //nolint:funlen
|
||||||
store.log.WithField("msgs", msgs).Trace("Creating or updating messages in the store")
|
store.log.WithField("msgs", msgs).Trace("Creating or updating messages in the store")
|
||||||
|
|
||||||
// Strip non meta first to reduce memory (no need to keep all old msg ID data during update).
|
// Strip non-meta first to reduce memory (no need to keep all old msg ID data during update).
|
||||||
err := store.db.View(func(tx *bolt.Tx) error {
|
err := store.db.View(func(tx *bolt.Tx) error {
|
||||||
b := tx.Bucket(metadataBucket)
|
b := tx.Bucket(metadataBucket)
|
||||||
for _, msg := range msgs {
|
for _, msg := range msgs {
|
||||||
@ -321,7 +321,7 @@ func clearNonMetadata(onlyMeta *pmapi.Message) {
|
|||||||
onlyMeta.Attachments = nil
|
onlyMeta.Attachments = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// txUpdateMetadataFromDB changes the the onlyMeta data.
|
// txUpdateMetadataFromDB changes the onlyMeta data.
|
||||||
// If there is stored message in metaBucket the size, header and MIMEType are
|
// If there is stored message in metaBucket the size, header and MIMEType are
|
||||||
// not changed if already set. To change these:
|
// not changed if already set. To change these:
|
||||||
// * size must be updated by Message.SetSize
|
// * size must be updated by Message.SetSize
|
||||||
|
|||||||
@ -20,4 +20,4 @@
|
|||||||
|
|
||||||
package updater
|
package updater
|
||||||
|
|
||||||
const Host = "https://protonmail.com/download"
|
const Host = "https://proton.me/download"
|
||||||
|
|||||||
@ -38,7 +38,7 @@ type VersionInfo struct {
|
|||||||
// Installers are the locations of installer files (for manual installation).
|
// Installers are the locations of installer files (for manual installation).
|
||||||
Installers []string
|
Installers []string
|
||||||
|
|
||||||
// LandingPage is the address of the app landing page on protonmail.com.
|
// LandingPage is the address of the app landing page on proton.me
|
||||||
LandingPage string
|
LandingPage string
|
||||||
|
|
||||||
// ReleaseNotesPage is the address of the page containing the release notes.
|
// ReleaseNotesPage is the address of the page containing the release notes.
|
||||||
@ -54,26 +54,26 @@ type VersionInfo struct {
|
|||||||
// {
|
// {
|
||||||
// "stable": {
|
// "stable": {
|
||||||
// "Version": "2.3.4",
|
// "Version": "2.3.4",
|
||||||
// "Package": "https://protonmail.com/.../bridge_2.3.4_linux.tgz",
|
// "Package": "https://proton.me/.../bridge_2.3.4_linux.tgz",
|
||||||
// "Installers": [
|
// "Installers": [
|
||||||
// "https://protonmail.com/.../something.deb",
|
// "https://proton.me/.../something.deb",
|
||||||
// "https://protonmail.com/.../something.rpm",
|
// "https://proton.me/.../something.rpm",
|
||||||
// "https://protonmail.com/.../PKGBUILD"
|
// "https://proton.me/.../PKGBUILD"
|
||||||
// ],
|
// ],
|
||||||
// "LandingPage": "https://protonmail.com/bridge",
|
// "LandingPage": "https://proton.me/mail/bridge#download",
|
||||||
// "ReleaseNotesPage": "https://protonmail.com/.../release_notes.html",
|
// "ReleaseNotesPage": "https://proton.me/download/{ie,bridge}/{stable,early}_releases.html",
|
||||||
// "RolloutProportion": 0.5
|
// "RolloutProportion": 0.5
|
||||||
// },
|
// },
|
||||||
// "early": {
|
// "early": {
|
||||||
// "Version": "2.4.0",
|
// "Version": "2.4.0",
|
||||||
// "Package": "https://protonmail.com/.../bridge_2.4.0_linux.tgz",
|
// "Package": "https://proton.me/.../bridge_2.4.0_linux.tgz",
|
||||||
// "Installers": [
|
// "Installers": [
|
||||||
// "https://protonmail.com/.../something.deb",
|
// "https://proton.me/.../something.deb",
|
||||||
// "https://protonmail.com/.../something.rpm",
|
// "https://proton.me/.../something.rpm",
|
||||||
// "https://protonmail.com/.../PKGBUILD"
|
// "https://proton.me/.../PKGBUILD"
|
||||||
// ],
|
// ],
|
||||||
// "LandingPage": "https://protonmail.com/bridge",
|
// "LandingPage": "https://proton.me/mail/bridge#download",
|
||||||
// "ReleaseNotesPage": "https://protonmail.com/.../release_notes.html",
|
// "ReleaseNotesPage": "https://proton.me/download/{ie,bridge}/{stable,early}_releases.html",
|
||||||
// "RolloutProportion": 0.5
|
// "RolloutProportion": 0.5
|
||||||
// },
|
// },
|
||||||
// "...": {
|
// "...": {
|
||||||
@ -84,8 +84,8 @@ type VersionMap map[string]VersionInfo
|
|||||||
|
|
||||||
// getVersionFileURL returns the URL of the version file.
|
// getVersionFileURL returns the URL of the version file.
|
||||||
// For example:
|
// For example:
|
||||||
// - https://protonmail.com/download/bridge/version_linux.json
|
// - https://proton.me/download/bridge/version_linux.json
|
||||||
// - https://protonmail.com/download/ie/version_linux.json
|
// - https://proton.me/download/ie/version_linux.json
|
||||||
func (u *Updater) getVersionFileURL() string {
|
func (u *Updater) getVersionFileURL() string {
|
||||||
return fmt.Sprintf("%v/%v/version_%v.json", Host, u.updateURLName, u.platform)
|
return fmt.Sprintf("%v/%v/version_%v.json", Host, u.updateURLName, u.platform)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,10 +33,16 @@ var TrustedAPIPins = []string{ //nolint:gochecknoglobals
|
|||||||
`pin-sha256="AfMENBVvOS8MnISprtvyPsjKlPooqh8nMB/pvCrpJpw="`, // cold backup
|
`pin-sha256="AfMENBVvOS8MnISprtvyPsjKlPooqh8nMB/pvCrpJpw="`, // cold backup
|
||||||
|
|
||||||
// protonmail.com
|
// protonmail.com
|
||||||
|
// \todo remove when sure no one is using it.
|
||||||
`pin-sha256="8joiNBdqaYiQpKskgtkJsqRxF7zN0C0aqfi8DacknnI="`, // current
|
`pin-sha256="8joiNBdqaYiQpKskgtkJsqRxF7zN0C0aqfi8DacknnI="`, // current
|
||||||
`pin-sha256="JMI8yrbc6jB1FYGyyWRLFTmDNgIszrNEMGlgy972e7w="`, // hot backup
|
`pin-sha256="JMI8yrbc6jB1FYGyyWRLFTmDNgIszrNEMGlgy972e7w="`, // hot backup
|
||||||
`pin-sha256="Iu44zU84EOCZ9vx/vz67/MRVrxF1IO4i4NIa8ETwiIY="`, // cold backup
|
`pin-sha256="Iu44zU84EOCZ9vx/vz67/MRVrxF1IO4i4NIa8ETwiIY="`, // cold backup
|
||||||
|
|
||||||
|
// proton.me
|
||||||
|
`pin-sha256="CT56BhOTmj5ZIPgb/xD5mH8rY3BLo/MlhP7oPyJUEDo="`, // current
|
||||||
|
`pin-sha256="35Dx28/uzN3LeltkCBQ8RHK0tlNSa2kCpCRGNp34Gxc="`, // hot backup
|
||||||
|
`pin-sha256="qYIukVc63DEITct8sFT7ebIq5qsWmuscaIKeJx+5J5A="`, // col backup
|
||||||
|
|
||||||
// proxies
|
// proxies
|
||||||
`pin-sha256="EU6TS9MO0L/GsDHvVc9D5fChYLNy5JdGYpJw0ccgetM="`, // main
|
`pin-sha256="EU6TS9MO0L/GsDHvVc9D5fChYLNy5JdGYpJw0ccgetM="`, // main
|
||||||
`pin-sha256="iKPIHPnDNqdkvOnTClQ8zQAIKG0XavaPkcEo0LBAABA="`, // backup 1
|
`pin-sha256="iKPIHPnDNqdkvOnTClQ8zQAIKG0XavaPkcEo0LBAABA="`, // backup 1
|
||||||
|
|||||||
@ -88,7 +88,7 @@ func TestTLSSignedCertTrustedPublicKey(t *testing.T) {
|
|||||||
|
|
||||||
_, dialer, _ := createClientWithPinningDialer("")
|
_, dialer, _ := createClientWithPinningDialer("")
|
||||||
copyTrustedPins(dialer.pinChecker)
|
copyTrustedPins(dialer.pinChecker)
|
||||||
dialer.pinChecker.trustedPins = append(dialer.pinChecker.trustedPins, `pin-sha256="SA4v9d2YY4vX5YQOQ1qZHYTBMCTSD/sxPvyj+JL6+vI="`)
|
dialer.pinChecker.trustedPins = append(dialer.pinChecker.trustedPins, `pin-sha256="LwnIKjNLV3z243ap8y0yXNPghsqE76J08Eq3COvUt2E="`)
|
||||||
_, err := dialer.DialTLS("tcp", "rsa4096.badssl.com:443")
|
_, err := dialer.DialTLS("tcp", "rsa4096.badssl.com:443")
|
||||||
r.NoError(t, err, "expected dial to succeed because public key is known and cert is signed by CA")
|
r.NoError(t, err, "expected dial to succeed because public key is known and cert is signed by CA")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -85,7 +85,7 @@ func newManager(cfg Config) *manager {
|
|||||||
// The resty is increasing the delay between retries up to 1 minute
|
// The resty is increasing the delay between retries up to 1 minute
|
||||||
// (SetRetryMaxWaitTime) so for 10 retries the cumulative delay can be
|
// (SetRetryMaxWaitTime) so for 10 retries the cumulative delay can be
|
||||||
// up to 5min.
|
// up to 5min.
|
||||||
m.rc.SetRetryCount(5)
|
m.rc.SetRetryCount(3)
|
||||||
m.rc.SetRetryMaxWaitTime(time.Minute)
|
m.rc.SetRetryMaxWaitTime(time.Minute)
|
||||||
m.rc.SetRetryAfter(catchRetryAfter)
|
m.rc.SetRetryAfter(catchRetryAfter)
|
||||||
m.rc.AddRetryCondition(m.shouldRetry)
|
m.rc.AddRetryCondition(m.shouldRetry)
|
||||||
|
|||||||
@ -62,6 +62,10 @@ func (m *manager) NewClientWithLogin(ctx context.Context, username string, passw
|
|||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Do not retry requests after this point. The ephemeral from auth info
|
||||||
|
// won't be valid any more
|
||||||
|
ctx = ContextWithoutRetry(ctx)
|
||||||
|
|
||||||
auth, err := m.auth(ctx, AuthReq{
|
auth, err := m.auth(ctx, AuthReq{
|
||||||
Username: username,
|
Username: username,
|
||||||
ClientProof: base64.StdEncoding.EncodeToString(proofs.ClientProof),
|
ClientProof: base64.StdEncoding.EncodeToString(proofs.ClientProof),
|
||||||
|
|||||||
@ -105,17 +105,27 @@ func logConnReuse(_ *resty.Client, res *resty.Response) error {
|
|||||||
func catchRetryAfter(_ *resty.Client, res *resty.Response) (time.Duration, error) {
|
func catchRetryAfter(_ *resty.Client, res *resty.Response) (time.Duration, error) {
|
||||||
if res.StatusCode() == http.StatusTooManyRequests {
|
if res.StatusCode() == http.StatusTooManyRequests {
|
||||||
if after := res.Header().Get("Retry-After"); after != "" {
|
if after := res.Header().Get("Retry-After"); after != "" {
|
||||||
|
l := log.
|
||||||
|
WithField("statusCode", res.StatusCode()).
|
||||||
|
WithField("url", res.Request.URL).
|
||||||
|
WithField("verb", res.Request.Method)
|
||||||
|
|
||||||
seconds, err := strconv.Atoi(after)
|
seconds, err := strconv.Atoi(after)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Warning("Cannot convert Retry-After to number")
|
l.WithError(err).Warning("Cannot convert Retry-After to number")
|
||||||
seconds = 10
|
seconds = 10
|
||||||
}
|
}
|
||||||
|
|
||||||
// To avoid spikes when all clients retry at the same time, we add some random wait.
|
// To avoid spikes when all clients retry at the same time, we add some random wait.
|
||||||
seconds += rand.Intn(10) //nolint:gosec // It is OK to use weak random number generator here.
|
seconds += rand.Intn(10) //nolint:gosec // It is OK to use weak random number generator here.
|
||||||
|
l = l.WithField("seconds", seconds).WithField("start", time.Now().Unix())
|
||||||
|
|
||||||
log.Warningf("Retrying %s after %ds induced by http code %d", res.Request.URL, seconds, res.StatusCode())
|
// Maximum retry time in client is is one minute. But
|
||||||
return time.Duration(seconds) * time.Second, nil
|
// here wait times can be longer e.g. high API load
|
||||||
|
l.Warn("Retrying after induced by http code. Waiting now...")
|
||||||
|
time.Sleep(time.Duration(seconds) * time.Second)
|
||||||
|
l.Warn("Wait done")
|
||||||
|
return 0, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
.PHONY: check-go check-godog install-godog test test-bridge test-live test-live-bridge test-stage test-debug test-live-debug bench
|
.PHONY: check-go check-godog install-godog test test-bridge test-live test-live-bridge test-stage test-debug test-live-debug bench
|
||||||
|
|
||||||
export GO111MODULE=on
|
export GO111MODULE=on
|
||||||
export BRIDGE_VERSION:=2.4.3+integrationtests
|
export BRIDGE_VERSION:=2.4.5+integrationtests
|
||||||
export VERBOSITY?=fatal
|
export VERBOSITY?=fatal
|
||||||
export TEST_DATA=testdata
|
export TEST_DATA=testdata
|
||||||
|
|
||||||
@ -14,6 +14,7 @@ check-godog:
|
|||||||
@which godog || $(MAKE) install-godog
|
@which godog || $(MAKE) install-godog
|
||||||
install-godog: check-go
|
install-godog: check-go
|
||||||
go install github.com/cucumber/godog/cmd/godog@v0.12.5
|
go install github.com/cucumber/godog/cmd/godog@v0.12.5
|
||||||
|
go install github.com/cucumber/godog/cmd/godog@upd-go1.18
|
||||||
|
|
||||||
test: test-bridge
|
test: test-bridge
|
||||||
test-bridge: FEATURES ?= features
|
test-bridge: FEATURES ?= features
|
||||||
@ -25,7 +26,11 @@ test-bridge: check-godog
|
|||||||
test-live: test-live-bridge test-live-ie
|
test-live: test-live-bridge test-live-ie
|
||||||
test-live-bridge: FEATURES ?= features
|
test-live-bridge: FEATURES ?= features
|
||||||
test-live-bridge: check-godog
|
test-live-bridge: check-godog
|
||||||
TEST_ENV=live godog --tags="~@ignore && ~@ignore-live" $(FEATURES)
|
TEST_ENV=live godog --tags="~@ignore && ~@ignore-live && ~@ignore-live-auth" $(FEATURES)
|
||||||
|
|
||||||
|
test-live-bridge-auth: check-godog
|
||||||
|
TEST_ENV=live godog --tags="@ignore-live-auth" $(FEATURES)
|
||||||
|
|
||||||
|
|
||||||
# Doesn't work in parallel!
|
# Doesn't work in parallel!
|
||||||
# Provide TEST_ACCOUNTS with your accounts.
|
# Provide TEST_ACCOUNTS with your accounts.
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -29,6 +30,7 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v2/internal/users"
|
"github.com/ProtonMail/proton-bridge/v2/internal/users"
|
||||||
"github.com/ProtonMail/proton-bridge/v2/pkg/pmapi"
|
"github.com/ProtonMail/proton-bridge/v2/pkg/pmapi"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -57,9 +59,15 @@ func (ctx *TestContext) LoginUser(username string, password, mailboxPassword []b
|
|||||||
return errors.Wrap(err, "failed to finish login")
|
return errors.Wrap(err, "failed to finish login")
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.addCleanupChecked(func() error {
|
ctx.addCleanupChecked(
|
||||||
return ctx.bridge.LogoutUser(userID)
|
func() error {
|
||||||
}, "Logging out user")
|
if os.Getenv(EnvName) == EnvLive {
|
||||||
|
logrus.Warn("Pausing user.Logout by 2 minutes to not hit issues with too many login attempts.")
|
||||||
|
time.Sleep(2 * time.Minute)
|
||||||
|
}
|
||||||
|
return ctx.bridge.LogoutUser(userID)
|
||||||
|
}, "Logging out user",
|
||||||
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,11 +9,13 @@ Feature: IMAP auth
|
|||||||
When IMAP client authenticates "user" with bad password
|
When IMAP client authenticates "user" with bad password
|
||||||
Then IMAP response is "IMAP error: NO backend/credentials: incorrect password"
|
Then IMAP response is "IMAP error: NO backend/credentials: incorrect password"
|
||||||
|
|
||||||
|
@ignore-live-auth
|
||||||
Scenario: Authenticates with disconnected user
|
Scenario: Authenticates with disconnected user
|
||||||
Given there is disconnected user "user"
|
Given there is disconnected user "user"
|
||||||
When IMAP client authenticates "user"
|
When IMAP client authenticates "user"
|
||||||
Then IMAP response is "IMAP error: NO account is logged out, use the app to login again"
|
Then IMAP response is "IMAP error: NO account is logged out, use the app to login again"
|
||||||
|
|
||||||
|
@ignore-live-auth
|
||||||
Scenario: Authenticates with connected user that was loaded without internet
|
Scenario: Authenticates with connected user that was loaded without internet
|
||||||
Given there is user "user" which just logged in
|
Given there is user "user" which just logged in
|
||||||
And there is no internet connection
|
And there is no internet connection
|
||||||
@ -27,12 +29,14 @@ Feature: IMAP auth
|
|||||||
And 2 seconds pass
|
And 2 seconds pass
|
||||||
Then "user" is connected
|
Then "user" is connected
|
||||||
|
|
||||||
|
@ignore-live-auth
|
||||||
Scenario: Authenticates with freshly logged-out user
|
Scenario: Authenticates with freshly logged-out user
|
||||||
Given there is user "user" which just logged in
|
Given there is user "user" which just logged in
|
||||||
When "user" logs out
|
When "user" logs out
|
||||||
And IMAP client authenticates "user"
|
And IMAP client authenticates "user"
|
||||||
Then IMAP response is "IMAP error: NO account is logged out, use the app to login again"
|
Then IMAP response is "IMAP error: NO account is logged out, use the app to login again"
|
||||||
|
|
||||||
|
@ignore-live-auth
|
||||||
Scenario: Authenticates user which was re-logged in
|
Scenario: Authenticates user which was re-logged in
|
||||||
Given there is user "user" which just logged in
|
Given there is user "user" which just logged in
|
||||||
When "user" logs out
|
When "user" logs out
|
||||||
|
|||||||
@ -125,6 +125,7 @@ Feature: IMAP fetch messages
|
|||||||
| 1:* |
|
| 1:* |
|
||||||
| * |
|
| * |
|
||||||
|
|
||||||
|
@ignore-live
|
||||||
Scenario: Fetch of big mailbox
|
Scenario: Fetch of big mailbox
|
||||||
Given there are 100 messages in mailbox "Folders/mbox" for "user"
|
Given there are 100 messages in mailbox "Folders/mbox" for "user"
|
||||||
And there is IMAP client logged in as "user"
|
And there is IMAP client logged in as "user"
|
||||||
|
|||||||
@ -1,34 +1,43 @@
|
|||||||
Feature: Delete user
|
Feature: Delete user
|
||||||
|
@ignore-live-auth
|
||||||
Scenario: Deleting connected user
|
Scenario: Deleting connected user
|
||||||
Given there is user "user" which just logged in
|
Given there is user "user" which just logged in
|
||||||
When user deletes "user"
|
When user deletes "user"
|
||||||
Then last response is "OK"
|
Then last response is "OK"
|
||||||
And "user" has database file
|
And "user" has database file
|
||||||
|
|
||||||
|
@ignore-live-auth
|
||||||
Scenario: Deleting connected user with cache
|
Scenario: Deleting connected user with cache
|
||||||
Given there is user "user" which just logged in
|
Given there is user "user" which just logged in
|
||||||
When user deletes "user" with cache
|
When user deletes "user" with cache
|
||||||
Then last response is "OK"
|
Then last response is "OK"
|
||||||
And "user" does not have database file
|
And "user" does not have database file
|
||||||
|
|
||||||
|
@ignore-live-auth
|
||||||
Scenario: Deleting connected user without database file
|
Scenario: Deleting connected user without database file
|
||||||
Given there is user "user" which just logged in
|
Given there is user "user" which just logged in
|
||||||
And there is no database file for "user"
|
And there is no database file for "user"
|
||||||
When user deletes "user" with cache
|
When user deletes "user" with cache
|
||||||
Then last response is "OK"
|
Then last response is "OK"
|
||||||
|
|
||||||
|
# THIS IS BLOCKED BY ANTI-ABUSE
|
||||||
|
@ignore-live
|
||||||
Scenario: Deleting disconnected user
|
Scenario: Deleting disconnected user
|
||||||
Given there is disconnected user "user"
|
Given there is disconnected user "user"
|
||||||
When user deletes "user"
|
When user deletes "user"
|
||||||
Then last response is "OK"
|
Then last response is "OK"
|
||||||
And "user" has database file
|
And "user" has database file
|
||||||
|
|
||||||
|
# THIS IS BLOCKED BY ANTI-ABUSE
|
||||||
|
@ignore-live
|
||||||
Scenario: Deleting disconnected user with cache
|
Scenario: Deleting disconnected user with cache
|
||||||
Given there is disconnected user "user"
|
Given there is disconnected user "user"
|
||||||
When user deletes "user" with cache
|
When user deletes "user" with cache
|
||||||
Then last response is "OK"
|
Then last response is "OK"
|
||||||
And "user" does not have database file
|
And "user" does not have database file
|
||||||
|
|
||||||
|
# THIS IS BLOCKED BY ANTI-ABUSE
|
||||||
|
@ignore-live
|
||||||
Scenario: Deleting disconnected user without database file
|
Scenario: Deleting disconnected user without database file
|
||||||
Given there is disconnected user "user"
|
Given there is disconnected user "user"
|
||||||
And there is no database file for "user"
|
And there is no database file for "user"
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
Feature: Login for the first time
|
Feature: Login for the first time
|
||||||
|
@ignore-live-auth
|
||||||
Scenario: Normal login
|
Scenario: Normal login
|
||||||
Given there is user "user"
|
Given there is user "user"
|
||||||
When "user" logs in
|
When "user" logs in
|
||||||
@ -19,6 +20,7 @@ Feature: Login for the first time
|
|||||||
When "user" logs in with bad password
|
When "user" logs in with bad password
|
||||||
Then last response is "failed to login: Incorrect login credentials. Please try again"
|
Then last response is "failed to login: Incorrect login credentials. Please try again"
|
||||||
|
|
||||||
|
@ignore-live-auth
|
||||||
Scenario: Login without internet connection
|
Scenario: Login without internet connection
|
||||||
Given there is no internet connection
|
Given there is no internet connection
|
||||||
When "user" logs in
|
When "user" logs in
|
||||||
@ -34,6 +36,7 @@ Feature: Login for the first time
|
|||||||
And "user2fa" has running event loop
|
And "user2fa" has running event loop
|
||||||
And "user2fa" has non-zero space
|
And "user2fa" has non-zero space
|
||||||
|
|
||||||
|
@ignore-live-auth
|
||||||
Scenario: Login user with capital letters in address
|
Scenario: Login user with capital letters in address
|
||||||
Given there is user "userAddressWithCapitalLetter"
|
Given there is user "userAddressWithCapitalLetter"
|
||||||
When "userAddressWithCapitalLetter" logs in
|
When "userAddressWithCapitalLetter" logs in
|
||||||
@ -43,6 +46,7 @@ Feature: Login for the first time
|
|||||||
And "userAddressWithCapitalLetter" has running event loop
|
And "userAddressWithCapitalLetter" has running event loop
|
||||||
And "userAddressWithCapitalLetter" has non-zero space
|
And "userAddressWithCapitalLetter" has non-zero space
|
||||||
|
|
||||||
|
@ignore-live-auth
|
||||||
Scenario: Login user with more addresses
|
Scenario: Login user with more addresses
|
||||||
Given there is user "userMoreAddresses"
|
Given there is user "userMoreAddresses"
|
||||||
When "userMoreAddresses" logs in
|
When "userMoreAddresses" logs in
|
||||||
@ -62,6 +66,7 @@ Feature: Login for the first time
|
|||||||
And "userDisabledPrimaryAddress" has running event loop
|
And "userDisabledPrimaryAddress" has running event loop
|
||||||
And "userDisabledPrimaryAddress" has non-zero space
|
And "userDisabledPrimaryAddress" has non-zero space
|
||||||
|
|
||||||
|
@ignore-live-auth
|
||||||
Scenario: Login two users
|
Scenario: Login two users
|
||||||
Given there is user "user"
|
Given there is user "user"
|
||||||
And there is user "userMoreAddresses"
|
And there is user "userMoreAddresses"
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
Feature: Re-login
|
Feature: Re-login
|
||||||
|
@ignore-live-auth
|
||||||
Scenario: Re-login with connected user and database file
|
Scenario: Re-login with connected user and database file
|
||||||
Given there is user "user" which just logged in
|
Given there is user "user" which just logged in
|
||||||
And there is database file for "user"
|
And there is database file for "user"
|
||||||
@ -18,6 +19,7 @@ Feature: Re-login
|
|||||||
And "user" has database file
|
And "user" has database file
|
||||||
And "user" has running event loop
|
And "user" has running event loop
|
||||||
|
|
||||||
|
@ignore-live-auth
|
||||||
Scenario: Re-login with disconnected user and database file
|
Scenario: Re-login with disconnected user and database file
|
||||||
Given there is disconnected user "user"
|
Given there is disconnected user "user"
|
||||||
And there is database file for "user"
|
And there is database file for "user"
|
||||||
@ -27,6 +29,7 @@ Feature: Re-login
|
|||||||
And "user" has running event loop
|
And "user" has running event loop
|
||||||
And "user" has non-zero space
|
And "user" has non-zero space
|
||||||
|
|
||||||
|
@ignore-live-auth
|
||||||
Scenario: Re-login with disconnected user and no database file
|
Scenario: Re-login with disconnected user and no database file
|
||||||
Given there is disconnected user "user"
|
Given there is disconnected user "user"
|
||||||
And there is no database file for "user"
|
And there is no database file for "user"
|
||||||
|
|||||||
@ -1,11 +1,21 @@
|
|||||||
---
|
---
|
||||||
|
|
||||||
wait: true
|
wait: true
|
||||||
|
strict: true
|
||||||
|
|
||||||
file:
|
file:
|
||||||
name: "./gobinsec-cache.yml"
|
name: "./gobinsec-cache.yml"
|
||||||
expiration: 24h
|
expiration: "24h"
|
||||||
|
|
||||||
ignore:
|
ignore:
|
||||||
# golang.org/x/net wrong match, we are using 2871e0cb, fixed by 37e1c6af
|
# golang.org/x/net wrong match, we are using v0.1.0, fixed by 37e1c6af in v0.0.xxx
|
||||||
- "CVE-2021-33194"
|
- "CVE-2021-33194"
|
||||||
|
# golang.org/x/crypto wrong match, we are using v0.1.0 all of this have been fixed in vO.O.xx
|
||||||
|
- "CVE-2019-11840"
|
||||||
|
- "CVE-2020-29652"
|
||||||
|
- "CVE-2021-43565"
|
||||||
|
- "CVE-2022-27191"
|
||||||
|
- "CVE-2020-9283"
|
||||||
|
- "CVE-2017-3204"
|
||||||
|
# golang.org/x/text wrong match, we are using v0.4.0, fixed in a previous version
|
||||||
|
- "CVE-2020-14040"
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
##!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Copyright (c) 2022 Proton AG
|
# Copyright (c) 2022 Proton AG
|
||||||
#
|
#
|
||||||
|
|||||||
@ -7,9 +7,9 @@ require github.com/intercloud/gobinsec v0.10.2
|
|||||||
require (
|
require (
|
||||||
github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d // indirect
|
github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d // indirect
|
||||||
github.com/fatih/color v1.13.0 // indirect
|
github.com/fatih/color v1.13.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||||
github.com/memcachier/gomemcache v0.0.0-20170425125614-d027381f7653 // indirect
|
github.com/memcachier/gomemcache v0.0.0-20170425125614-d027381f7653 // indirect
|
||||||
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 // indirect
|
golang.org/x/sys v0.1.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@ -7,9 +7,13 @@ github.com/intercloud/gobinsec v0.10.2/go.mod h1:Y/AMKT0aQM40WDkTqlEe18W/IL6ZUuu
|
|||||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||||
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
||||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||||
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||||
|
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/memcachier/gomemcache v0.0.0-20170425125614-d027381f7653 h1:222emoxOt/bCmNHp8Xt0Pr5Am3gIbqRKFpb4CQ9O2SI=
|
github.com/memcachier/gomemcache v0.0.0-20170425125614-d027381f7653 h1:222emoxOt/bCmNHp8Xt0Pr5Am3gIbqRKFpb4CQ9O2SI=
|
||||||
github.com/memcachier/gomemcache v0.0.0-20170425125614-d027381f7653/go.mod h1:KoYVbOQexD45AOLfn+gsFB6c3o4ANzP1QKzjE6tZbK0=
|
github.com/memcachier/gomemcache v0.0.0-20170425125614-d027381f7653/go.mod h1:KoYVbOQexD45AOLfn+gsFB6c3o4ANzP1QKzjE6tZbK0=
|
||||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@ -18,7 +22,12 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 h1:xHms4gcpe1YE7A3yIllJXP16CMAGuqwO2lX1mTyyRRc=
|
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 h1:xHms4gcpe1YE7A3yIllJXP16CMAGuqwO2lX1mTyyRRc=
|
||||||
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
|
||||||
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
|
|
||||||
# Generate HTML release notes
|
# Generate HTML release notes
|
||||||
# hosted at https://protonmail.com/download/{ie,bridge}/{stable,early}_releases.html
|
# hosted at https://proton.me/download/{ie,bridge}/{stable,early}_releases.html
|
||||||
INFILE=$1
|
INFILE=$1
|
||||||
OUTFILE=${INFILE//.md/.html}
|
OUTFILE=${INFILE//.md/.html}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user