forked from Silverfish/proton-bridge
Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ed5adb18fb | |||
| 85a91c5572 | |||
| 56d4bfbb71 | |||
| 48a75b0dd7 | |||
| 8688277ee6 | |||
| 63eb67760e | |||
| cffab028b2 | |||
| 8ea712b052 | |||
| ff0615167b | |||
| e2b361b9a6 | |||
| 1c6bbf1fae | |||
| e7713fa785 | |||
| 28ae54b5ca | |||
| 00aff40160 | |||
| ab289e6e01 | |||
| a28dc9f2f3 | |||
| 8a859082cd | |||
| 1d972835ff | |||
| 8469e0a661 | |||
| 6ea970bf97 | |||
| a05b90e803 | |||
| 239ad8b946 | |||
| d9fdbb35bc | |||
| 5769fb9466 | |||
| a4020cebd4 | |||
| 7a8760e2ef | |||
| 9552e72ba8 | |||
| c692c21b87 | |||
| bb15efa711 | |||
| e94d3be12d | |||
| 66569f71a0 | |||
| 9bfa79455e | |||
| 67e802e3a0 | |||
| 8a5e2007f6 | |||
| 5b92945626 | |||
| 4a8a7ef093 | |||
| 2cfda14b1a | |||
| 312993e08e | |||
| b1110b04c9 | |||
| d2bc60d9cb | |||
| 1d8f6c75c8 | |||
| 06daaf8d9f | |||
| cb436fff63 | |||
| 921a44f1a3 | |||
| d35af6b686 | |||
| 4cb938c57f | |||
| 232e98d812 | |||
| 6fadbde4a6 | |||
| d2fbbc3e25 | |||
| 1c7c342e19 |
@ -25,10 +25,14 @@ variables:
|
|||||||
GOMAXPROCS: $(( ${CI_TAG_CPU} / 2 ))
|
GOMAXPROCS: $(( ${CI_TAG_CPU} / 2 ))
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
- apt update && apt-get -y install libsecret-1-dev
|
- |
|
||||||
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
|
if [ "$CI_JOB_NAME" != "grype-scan-code-dependencies" ]; then
|
||||||
|
apt update && apt-get -y install libsecret-1-dev
|
||||||
|
git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
|
||||||
|
fi
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
|
- analyse
|
||||||
- test
|
- test
|
||||||
- build
|
- build
|
||||||
|
|
||||||
@ -38,4 +42,11 @@ include:
|
|||||||
- local: ci/env.yml
|
- local: ci/env.yml
|
||||||
- local: ci/test.yml
|
- local: ci/test.yml
|
||||||
- local: ci/build.yml
|
- local: ci/build.yml
|
||||||
|
- component: gitlab.protontech.ch/proton/devops/cicd-components/devsecops/gitleaks/scan-repository@~latest
|
||||||
|
inputs:
|
||||||
|
stage: analyse
|
||||||
|
cli-args: "--baseline-path $GITLEAKS_BASELINE"
|
||||||
|
- component: gitlab.protontech.ch/proton/devops/cicd-components/devsecops/grype/scan-code@~latest
|
||||||
|
inputs:
|
||||||
|
stage: analyse
|
||||||
|
|
||||||
|
|||||||
2
.grype.yaml
Normal file
2
.grype.yaml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# Check out for configuration details: https://github.com/anchore/grype?tab=readme-ov-file#configuration
|
||||||
|
fail-on-severity: "medium"
|
||||||
@ -3,7 +3,7 @@
|
|||||||
## Prerequisites
|
## Prerequisites
|
||||||
* 64-bit OS:
|
* 64-bit OS:
|
||||||
- the go-rfc5322 module cannot currently be compiled for 32-bit OSes
|
- the go-rfc5322 module cannot currently be compiled for 32-bit OSes
|
||||||
* Go 1.21.6
|
* Go 1.21.9
|
||||||
* Bash with basic build utils: make, gcc, sed, find, grep, ...
|
* Bash with basic build utils: make, gcc, sed, find, grep, ...
|
||||||
- For Windows, it is recommended to use MinGW 64bit shell from [MSYS2](https://www.msys2.org/)
|
- For Windows, it is recommended to use MinGW 64bit shell from [MSYS2](https://www.msys2.org/)
|
||||||
* GCC (Linux), msvc (Windows) or Xcode (macOS)
|
* GCC (Linux), msvc (Windows) or Xcode (macOS)
|
||||||
|
|||||||
63
Changelog.md
63
Changelog.md
@ -3,6 +3,69 @@
|
|||||||
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||||
|
|
||||||
|
|
||||||
|
## Bastei Bridge 3.12.0
|
||||||
|
|
||||||
|
### Added
|
||||||
|
* BRIDGE-75: Bridge repair button.
|
||||||
|
* BRIDGE-79: Add New Outlook for Mac KB disclaimer.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* BRIDGE-16: Bump version Go 1.21.9 Qt 6.4.3.
|
||||||
|
* BRIDGE-23: Update gluon to go 1.21.
|
||||||
|
* BRIDGE-22: Update gpa to go 1.21.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* BRIDGE-90: Disable repair button when bridge cannot connect to proton servers; bump GPA.
|
||||||
|
* BRIDGE-69: Explicitly handle semver panic for last bridge version from vault.
|
||||||
|
* BRIDGE-29: Bump gluon version.
|
||||||
|
* BRIDGE-49: Configure gitleaks baseline and grype config.
|
||||||
|
* BRIDGE-21: Missing panic handling.
|
||||||
|
* BRIDGE-17: Broken telemetry heartbeat test.
|
||||||
|
* BRIDGE-10: Bumped gluon version.
|
||||||
|
|
||||||
|
|
||||||
|
## Alcantara Bridge 3.11.1
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* BRIDGE-70: Hotfix for blocked smtp/imap port causing bridge to quit.
|
||||||
|
|
||||||
|
|
||||||
|
## Alcantara Bridge 3.11.0
|
||||||
|
|
||||||
|
### Added
|
||||||
|
* GODT-3185: Report cases which leads to wrong address key used.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* BRIDGE-14: HV3 implementation.
|
||||||
|
* BRIDGE-15: Certificate install is now also done during Outlook setup on macOS.
|
||||||
|
* GODT-3146: Start servers on startup, keep running even when no users are active.
|
||||||
|
* BRIDGE-19: Update checksum validation use warning instead of error on non-existing files.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* BRIDGE-8: Fix bridge double sessionID issue in logs.
|
||||||
|
* BRIDGE-7: Modify keychain test on macOS.
|
||||||
|
* BRIDGE-4: Logs not being created when invalid flag is passed.
|
||||||
|
* BRIDGE-5: Add tooltip to tray icon.
|
||||||
|
* GODT-3163: Filter MBOX format delimiter.
|
||||||
|
|
||||||
|
|
||||||
|
## Zaehringen Bridge 3.10.0
|
||||||
|
|
||||||
|
### Added
|
||||||
|
* GODT-3199: Add package log field.
|
||||||
|
* GODT-3220: Add more test scenarios.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* GODT-3193: Preserve attachment encoding.
|
||||||
|
* GODT-3214: Encrypt only with primary key.
|
||||||
|
* GODT-2662: Use tart runner for darwin jobs.
|
||||||
|
* GODT-1602: Test: run integration tests against black 🖤.
|
||||||
|
* GODT-3257: Test: quad9 provider test not working on CI.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* GODT-3290: Fix test failing because of leap day.
|
||||||
|
|
||||||
|
|
||||||
## Ypsilon Bridge 3.9.1
|
## Ypsilon Bridge 3.9.1
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
20
Makefile
20
Makefile
@ -1,17 +1,18 @@
|
|||||||
export GO111MODULE=on
|
export GO111MODULE=on
|
||||||
|
export CGO_ENABLED=1
|
||||||
|
|
||||||
# By default, the target OS is the same as the host OS,
|
# By default, the target OS is the same as the host OS,
|
||||||
# but this can be overridden by setting TARGET_OS to "windows"/"darwin"/"linux".
|
# but this can be overridden by setting TARGET_OS to "windows"/"darwin"/"linux".
|
||||||
GOOS:=$(shell go env GOOS)
|
GOOS:=$(shell go env GOOS)
|
||||||
TARGET_CMD?=Desktop-Bridge
|
TARGET_CMD?=Desktop-Bridge
|
||||||
TARGET_OS?=${GOOS}
|
TARGET_OS?=${GOOS}
|
||||||
ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
|
ROOT_DIR:=$(realpath .)
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
.PHONY: build build-gui build-nogui build-launcher versioner hasher
|
.PHONY: build build-gui build-nogui build-launcher versioner hasher
|
||||||
|
|
||||||
# Keep version hardcoded so app build works also without Git repository.
|
# Keep version hardcoded so app build works also without Git repository.
|
||||||
BRIDGE_APP_VERSION?=3.9.1+git
|
BRIDGE_APP_VERSION?=3.12.0+git
|
||||||
APP_VERSION:=${BRIDGE_APP_VERSION}
|
APP_VERSION:=${BRIDGE_APP_VERSION}
|
||||||
APP_FULL_NAME:=Proton Mail Bridge
|
APP_FULL_NAME:=Proton Mail Bridge
|
||||||
APP_VENDOR:=Proton AG
|
APP_VENDOR:=Proton AG
|
||||||
@ -19,8 +20,8 @@ SRC_ICO:=bridge.ico
|
|||||||
SRC_ICNS:=Bridge.icns
|
SRC_ICNS:=Bridge.icns
|
||||||
SRC_SVG:=bridge.svg
|
SRC_SVG:=bridge.svg
|
||||||
EXE_NAME:=proton-bridge
|
EXE_NAME:=proton-bridge
|
||||||
REVISION:=$(shell ./utils/get_revision.sh)
|
REVISION:=$(shell "${ROOT_DIR}/utils/get_revision.sh" rev)
|
||||||
TAG:=$(shell ./utils/get_revision.sh tag)
|
TAG:=$(shell "${ROOT_DIR}/utils/get_revision.sh" tag)
|
||||||
BUILD_TIME:=$(shell date +%FT%T%z)
|
BUILD_TIME:=$(shell date +%FT%T%z)
|
||||||
MACOS_MIN_VERSION_ARM64=11.0
|
MACOS_MIN_VERSION_ARM64=11.0
|
||||||
MACOS_MIN_VERSION_AMD64=10.15
|
MACOS_MIN_VERSION_AMD64=10.15
|
||||||
@ -101,9 +102,9 @@ endif
|
|||||||
|
|
||||||
ifeq "${GOOS}" "windows"
|
ifeq "${GOOS}" "windows"
|
||||||
go-build-finalize= \
|
go-build-finalize= \
|
||||||
$(if $(4),powershell Copy-Item ${ROOT_DIR}/${RESOURCE_FILE} ${4} &&,) \
|
$(if $(4),cp "${ROOT_DIR}/${RESOURCE_FILE}" ${4} &&,) \
|
||||||
$(call go-build,$(1),$(2),$(3)) \
|
$(call go-build,$(1),$(2),$(3)) \
|
||||||
$(if $(4), && powershell Remove-Item ${4} -Force,)
|
$(if $(4), && rm -f ${4},)
|
||||||
endif
|
endif
|
||||||
|
|
||||||
${EXE_NAME}: gofiles ${RESOURCE_FILE}
|
${EXE_NAME}: gofiles ${RESOURCE_FILE}
|
||||||
@ -117,7 +118,10 @@ versioner:
|
|||||||
go build ${BUILD_FLAGS} -o versioner utils/versioner/main.go
|
go build ${BUILD_FLAGS} -o versioner utils/versioner/main.go
|
||||||
|
|
||||||
vault-editor:
|
vault-editor:
|
||||||
$(call go-build-finalize,"-tags=debug","vault-editor","./utils/vault-editor/main.go")
|
$(call go-build-finalize,-tags=debug,"vault-editor","./utils/vault-editor/main.go")
|
||||||
|
|
||||||
|
bridge-rollout:
|
||||||
|
$(call go-build-finalize,, "bridge-rollout","./utils/bridge-rollout/bridge-rollout.go")
|
||||||
|
|
||||||
hasher:
|
hasher:
|
||||||
go build -o hasher utils/hasher/main.go
|
go build -o hasher utils/hasher/main.go
|
||||||
@ -164,7 +168,7 @@ ${EXE_TARGET}: check-build-essentials ${EXE_NAME}
|
|||||||
BRIDGE_BUILD_TIME=${BUILD_TIME} \
|
BRIDGE_BUILD_TIME=${BUILD_TIME} \
|
||||||
BRIDGE_GUI_BUILD_CONFIG=Release \
|
BRIDGE_GUI_BUILD_CONFIG=Release \
|
||||||
BRIDGE_BUILD_ENV=${BUILD_ENV} \
|
BRIDGE_BUILD_ENV=${BUILD_ENV} \
|
||||||
BRIDGE_INSTALL_PATH=${ROOT_DIR}/${DEPLOY_DIR}/${GOOS} \
|
BRIDGE_INSTALL_PATH="${ROOT_DIR}/${DEPLOY_DIR}/${GOOS}" \
|
||||||
./build.sh install
|
./build.sh install
|
||||||
mv "${ROOT_DIR}/${BRIDGE_EXE}" "$(ROOT_DIR)/${EXE_TARGET}"
|
mv "${ROOT_DIR}/${BRIDGE_EXE}" "$(ROOT_DIR)/${EXE_TARGET}"
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
.script-build:
|
.script-build:
|
||||||
@ -7,9 +6,14 @@
|
|||||||
extends:
|
extends:
|
||||||
- .rules-branch-and-MR-manual
|
- .rules-branch-and-MR-manual
|
||||||
script:
|
script:
|
||||||
|
- which go && go version
|
||||||
|
- which gcc && gcc --version
|
||||||
|
- which qmake && qmake --version
|
||||||
|
- git rev-parse --short=10 HEAD
|
||||||
- make build
|
- make build
|
||||||
- git diff && git diff-index --quiet HEAD
|
- git diff && git diff-index --quiet HEAD
|
||||||
- make vault-editor
|
- make vault-editor
|
||||||
|
- make bridge-rollout
|
||||||
artifacts:
|
artifacts:
|
||||||
expire_in: 1 day
|
expire_in: 1 day
|
||||||
when: always
|
when: always
|
||||||
@ -17,7 +21,7 @@
|
|||||||
paths:
|
paths:
|
||||||
- bridge_*.tgz
|
- bridge_*.tgz
|
||||||
- vault-editor
|
- vault-editor
|
||||||
|
- bridge-rollout
|
||||||
build-linux:
|
build-linux:
|
||||||
extends:
|
extends:
|
||||||
- .script-build
|
- .script-build
|
||||||
@ -66,4 +70,3 @@ trigger-qa-installer:
|
|||||||
trigger:
|
trigger:
|
||||||
project: "jcuth/bridge-release"
|
project: "jcuth/bridge-release"
|
||||||
branch: master
|
branch: master
|
||||||
|
|
||||||
|
|||||||
24
ci/env.yml
24
ci/env.yml
@ -2,27 +2,35 @@
|
|||||||
---
|
---
|
||||||
|
|
||||||
.env-windows:
|
.env-windows:
|
||||||
|
extends:
|
||||||
|
- .image-windows-virt-build
|
||||||
before_script:
|
before_script:
|
||||||
- export BRIDGE_SYNC_FORCE_MINIMUM_SPEC=1
|
- !reference [.before-script-windows-virt-build, before_script]
|
||||||
- !reference [.before-script-windows-aws-build, before_script]
|
|
||||||
- !reference [.before-script-git-config, before_script]
|
- !reference [.before-script-git-config, before_script]
|
||||||
- git config --global safe.directory '*'
|
- mkdir -p .cache/bin
|
||||||
- git status --porcelain
|
- export PATH=$(pwd)/.cache/bin:$PATH
|
||||||
cache: {}
|
- export GOPATH="$CI_PROJECT_DIR/.cache"
|
||||||
tags:
|
variables:
|
||||||
- windows-bridge
|
GOARCH: amd64
|
||||||
|
BRIDGE_SYNC_FORCE_MINIMUM_SPEC: 1
|
||||||
|
VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.cache
|
||||||
|
cache:
|
||||||
|
key: windows-vcpkg-go-0
|
||||||
|
paths:
|
||||||
|
- .cache
|
||||||
|
when: 'always'
|
||||||
|
|
||||||
.env-darwin:
|
.env-darwin:
|
||||||
extends:
|
extends:
|
||||||
- .image-darwin-build
|
- .image-darwin-build
|
||||||
before_script:
|
before_script:
|
||||||
- export BRIDGE_SYNC_FORCE_MINIMUM_SPEC=1
|
|
||||||
- !reference [.before-script-darwin-tart-build, before_script]
|
- !reference [.before-script-darwin-tart-build, before_script]
|
||||||
- !reference [.before-script-git-config, before_script]
|
- !reference [.before-script-git-config, before_script]
|
||||||
- mkdir -p .cache/bin
|
- mkdir -p .cache/bin
|
||||||
- export PATH=$(pwd)/.cache/bin:$PATH
|
- export PATH=$(pwd)/.cache/bin:$PATH
|
||||||
- export GOPATH="$CI_PROJECT_DIR/.cache"
|
- export GOPATH="$CI_PROJECT_DIR/.cache"
|
||||||
variables:
|
variables:
|
||||||
|
BRIDGE_SYNC_FORCE_MINIMUM_SPEC: 1
|
||||||
VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.cache
|
VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.cache
|
||||||
cache:
|
cache:
|
||||||
key: darwin-go-and-vcpkg
|
key: darwin-go-and-vcpkg
|
||||||
|
|||||||
10
ci/test.yml
10
ci/test.yml
@ -26,11 +26,15 @@ lint-bug-report-preview:
|
|||||||
extends:
|
extends:
|
||||||
- .rules-branch-manual-MR-and-devel-always
|
- .rules-branch-manual-MR-and-devel-always
|
||||||
script:
|
script:
|
||||||
|
- which go && go version
|
||||||
|
- which gcc && gcc --version
|
||||||
- make test
|
- make test
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- coverage/**
|
- coverage/**
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
test-linux:
|
test-linux:
|
||||||
extends:
|
extends:
|
||||||
- .image-linux-test
|
- .image-linux-test
|
||||||
@ -70,6 +74,9 @@ test-integration:
|
|||||||
- test-linux
|
- test-linux
|
||||||
script:
|
script:
|
||||||
- make test-integration | tee -a integration-job.log
|
- make test-integration | tee -a integration-job.log
|
||||||
|
after_script:
|
||||||
|
- |
|
||||||
|
grep "Error: " integration-job.log
|
||||||
artifacts:
|
artifacts:
|
||||||
when: always
|
when: always
|
||||||
paths:
|
paths:
|
||||||
@ -95,6 +102,9 @@ test-integration-nightly:
|
|||||||
- test-integration
|
- test-integration
|
||||||
script:
|
script:
|
||||||
- make test-integration-nightly | tee -a nightly-job.log
|
- make test-integration-nightly | tee -a nightly-job.log
|
||||||
|
after_script:
|
||||||
|
- |
|
||||||
|
grep "Error: " nightly-job.log
|
||||||
artifacts:
|
artifacts:
|
||||||
when: always
|
when: always
|
||||||
paths:
|
paths:
|
||||||
|
|||||||
@ -19,8 +19,15 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/sentry"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/app"
|
"github.com/ProtonMail/proton-bridge/v3/internal/app"
|
||||||
"github.com/bradenaw/juniper/xslices"
|
"github.com/bradenaw/juniper/xslices"
|
||||||
)
|
)
|
||||||
@ -43,5 +50,72 @@ import (
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
_ = app.New().Run(xslices.Filter(os.Args, func(arg string) bool { return !strings.Contains(arg, "-psn_") }))
|
appErr := app.New().Run(xslices.Filter(os.Args, func(arg string) bool { return !strings.Contains(arg, "-psn_") }))
|
||||||
|
if appErr != nil {
|
||||||
|
_ = app.WithLocations(func(l *locations.Locations) error {
|
||||||
|
logsPath, err := l.ProvideLogsPath()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the session ID if its specified
|
||||||
|
var sessionID logging.SessionID
|
||||||
|
if flagVal, found := getFlagValue(os.Args, app.FlagSessionID); found {
|
||||||
|
sessionID = logging.SessionID(flagVal)
|
||||||
|
} else {
|
||||||
|
sessionID = logging.NewSessionID()
|
||||||
|
}
|
||||||
|
|
||||||
|
closer, err := logging.Init(
|
||||||
|
logsPath,
|
||||||
|
sessionID,
|
||||||
|
logging.BridgeShortAppName,
|
||||||
|
logging.DefaultMaxLogFileSize,
|
||||||
|
logging.DefaultPruningSize,
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
_ = logging.Close(closer)
|
||||||
|
}()
|
||||||
|
|
||||||
|
logrus.
|
||||||
|
WithField("appName", constants.FullAppName).
|
||||||
|
WithField("version", constants.Version).
|
||||||
|
WithField("revision", constants.Revision).
|
||||||
|
WithField("tag", constants.Tag).
|
||||||
|
WithField("build", constants.BuildTime).
|
||||||
|
WithField("runtime", runtime.GOOS).
|
||||||
|
WithField("args", os.Args).
|
||||||
|
WithField("SentryID", sentry.GetProtectedHostname()).WithError(appErr).Error("Failed to initialize bridge")
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getFlagValue - obtains the value of a specified tag
|
||||||
|
// The flag can be of the following form `-flag value`, `--flag value`, `-flag=value` or `--flags=value`.
|
||||||
|
func getFlagValue(argList []string, flag string) (string, bool) {
|
||||||
|
eqPrefix1 := "-" + flag + "="
|
||||||
|
eqPrefix2 := "--" + flag + "="
|
||||||
|
|
||||||
|
for i := 0; i < len(argList); i++ {
|
||||||
|
arg := argList[i]
|
||||||
|
if strings.HasPrefix(arg, eqPrefix1) {
|
||||||
|
val := strings.TrimPrefix(arg, eqPrefix1)
|
||||||
|
return val, len(val) > 0
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(arg, eqPrefix2) {
|
||||||
|
val := strings.TrimPrefix(arg, eqPrefix2)
|
||||||
|
return val, len(val) > 0
|
||||||
|
}
|
||||||
|
if (arg == "-"+flag || arg == "--"+flag) && i+1 < len(argList) {
|
||||||
|
return argList[i+1], true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", false
|
||||||
}
|
}
|
||||||
|
|||||||
47
cmd/Desktop-Bridge/main_test.go
Normal file
47
cmd/Desktop-Bridge/main_test.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
// Copyright (c) 2024 Proton AG
|
||||||
|
//
|
||||||
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetFlagValue(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
args []string
|
||||||
|
flag string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{[]string{"session-id", ""}, "session-id", ""},
|
||||||
|
{[]string{"-session-id", ""}, "session-id", ""},
|
||||||
|
{[]string{"--session-id", ""}, "session-id", ""},
|
||||||
|
{[]string{"session-id", "test"}, "session-id", ""},
|
||||||
|
{[]string{"-session-id", "test"}, "session-id", "test"},
|
||||||
|
{[]string{"--session-id", "test"}, "session-id", "test"},
|
||||||
|
{[]string{"session-id=test"}, "session-id", ""},
|
||||||
|
{[]string{"-session-id=test"}, "session-id", "test"},
|
||||||
|
{[]string{"--session-id=test"}, "session-id", "test"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
val, _ := getFlagValue(tt.args, tt.flag)
|
||||||
|
require.Equal(t, val, tt.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -40,6 +40,7 @@ import (
|
|||||||
"github.com/elastic/go-sysinfo/types"
|
"github.com/elastic/go-sysinfo/types"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
"golang.org/x/sys/execabs"
|
"golang.org/x/sys/execabs"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -53,9 +54,12 @@ const (
|
|||||||
FlagCLIShort = "c"
|
FlagCLIShort = "c"
|
||||||
FlagNonInteractive = "noninteractive"
|
FlagNonInteractive = "noninteractive"
|
||||||
FlagNonInteractiveShort = "n"
|
FlagNonInteractiveShort = "n"
|
||||||
FlagLauncher = "--launcher"
|
FlagLauncher = "launcher"
|
||||||
FlagWait = "--wait"
|
FlagWait = "wait"
|
||||||
FlagSessionID = "--session-id"
|
FlagSessionID = "session-id"
|
||||||
|
HyphenatedFlagLauncher = "--" + FlagLauncher
|
||||||
|
HyphenatedFlagWait = "--" + FlagWait
|
||||||
|
HyphenatedFlagSessionID = "--" + FlagSessionID
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() { //nolint:funlen
|
func main() { //nolint:funlen
|
||||||
@ -151,7 +155,7 @@ func main() { //nolint:funlen
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := execabs.Command(exe, appendLauncherPath(launcher, append(args, FlagSessionID, string(sessionID)))...) //nolint:gosec
|
cmd := execabs.Command(exe, appendLauncherPath(launcher, appendOrModifySessionID(args, string(sessionID)))...) //nolint:gosec
|
||||||
|
|
||||||
cmd.Stdin = os.Stdin
|
cmd.Stdin = os.Stdin
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
@ -173,19 +177,14 @@ func main() { //nolint:funlen
|
|||||||
|
|
||||||
// appendLauncherPath add launcher path if missing.
|
// appendLauncherPath add launcher path if missing.
|
||||||
func appendLauncherPath(path string, args []string) []string {
|
func appendLauncherPath(path string, args []string) []string {
|
||||||
if !sliceContains(args, FlagLauncher) {
|
if !slices.Contains(args, HyphenatedFlagLauncher) {
|
||||||
res := append([]string{}, args...)
|
res := append([]string{}, args...)
|
||||||
res = append(res, FlagLauncher, path)
|
res = append(res, HyphenatedFlagLauncher, path)
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
return args
|
return args
|
||||||
}
|
}
|
||||||
|
|
||||||
// sliceContains checks if a value is present in a list.
|
|
||||||
func sliceContains[T comparable](list []T, s T) bool {
|
|
||||||
return xslices.Any(list, func(arg T) bool { return arg == s })
|
|
||||||
}
|
|
||||||
|
|
||||||
// inCLIMode detect if CLI mode is asked.
|
// inCLIMode detect if CLI mode is asked.
|
||||||
func inCLIMode(args []string) bool {
|
func inCLIMode(args []string) bool {
|
||||||
return hasFlag(args, FlagCLI) || hasFlag(args, FlagCLIShort) || hasFlag(args, FlagNonInteractive) || hasFlag(args, FlagNonInteractiveShort)
|
return hasFlag(args, FlagCLI) || hasFlag(args, FlagCLIShort) || hasFlag(args, FlagNonInteractive) || hasFlag(args, FlagNonInteractiveShort)
|
||||||
@ -193,7 +192,12 @@ func inCLIMode(args []string) bool {
|
|||||||
|
|
||||||
// hasFlag checks if a flag is present in a list.
|
// hasFlag checks if a flag is present in a list.
|
||||||
func hasFlag(args []string, flag string) bool {
|
func hasFlag(args []string, flag string) bool {
|
||||||
return xslices.Any(args, func(arg string) bool { return (arg == "-"+flag) || (arg == "--"+flag) })
|
return flagIndex(args, flag) >= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// flagIndex returns the position of the first occurrence of a flag int args, or -1 if the flag is not present.
|
||||||
|
func flagIndex(args []string, flag string) int {
|
||||||
|
return slices.IndexFunc(args, func(arg string) bool { return (arg == "-"+flag) || (arg == "--"+flag) })
|
||||||
}
|
}
|
||||||
|
|
||||||
// findAndStrip check if a value is present in s list and remove all occurrences of the value from this list.
|
// findAndStrip check if a value is present in s list and remove all occurrences of the value from this list.
|
||||||
@ -211,7 +215,7 @@ func findAndStripWait(args []string) ([]string, bool, []string) {
|
|||||||
hasFlag := false
|
hasFlag := false
|
||||||
values := make([]string, 0)
|
values := make([]string, 0)
|
||||||
for k, v := range res {
|
for k, v := range res {
|
||||||
if v != FlagWait {
|
if v != HyphenatedFlagWait {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if k+1 >= len(res) {
|
if k+1 >= len(res) {
|
||||||
@ -222,7 +226,7 @@ func findAndStripWait(args []string) ([]string, bool, []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if hasFlag {
|
if hasFlag {
|
||||||
res, _ = findAndStrip(res, FlagWait)
|
res, _ = findAndStrip(res, HyphenatedFlagWait)
|
||||||
for _, v := range values {
|
for _, v := range values {
|
||||||
res, _ = findAndStrip(res, v)
|
res, _ = findAndStrip(res, v)
|
||||||
}
|
}
|
||||||
@ -230,6 +234,23 @@ func findAndStripWait(args []string) ([]string, bool, []string) {
|
|||||||
return res, hasFlag, values
|
return res, hasFlag, values
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// return args with the sessionID flag and value added or modified. The original slice is not modified.
|
||||||
|
func appendOrModifySessionID(args []string, sessionID string) []string {
|
||||||
|
index := flagIndex(args, FlagSessionID)
|
||||||
|
if index < 0 {
|
||||||
|
return append(args, HyphenatedFlagSessionID, sessionID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if index == len(args)-1 {
|
||||||
|
return append(args, sessionID)
|
||||||
|
}
|
||||||
|
|
||||||
|
res := slices.Clone(args)
|
||||||
|
res[index+1] = sessionID
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
func getPathToUpdatedExecutable(
|
func getPathToUpdatedExecutable(
|
||||||
name string,
|
name string,
|
||||||
ver *versioner.Versioner,
|
ver *versioner.Versioner,
|
||||||
|
|||||||
@ -20,61 +20,62 @@ package main
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/bradenaw/juniper/xslices"
|
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSliceContains(t *testing.T) {
|
|
||||||
assert.True(t, sliceContains([]string{"a", "b", "c"}, "a"))
|
|
||||||
assert.True(t, sliceContains([]int{1, 2, 3}, 2))
|
|
||||||
assert.False(t, sliceContains([]string{"a", "b", "c"}, "A"))
|
|
||||||
assert.False(t, sliceContains([]int{1, 2, 3}, 4))
|
|
||||||
assert.False(t, sliceContains([]string{}, "a"))
|
|
||||||
assert.True(t, sliceContains([]string{"a", "a"}, "a"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFindAndStrip(t *testing.T) {
|
func TestFindAndStrip(t *testing.T) {
|
||||||
list := []string{"a", "b", "c", "c", "b", "c"}
|
list := []string{"a", "b", "c", "c", "b", "c"}
|
||||||
|
|
||||||
result, found := findAndStrip(list, "a")
|
result, found := findAndStrip(list, "a")
|
||||||
assert.True(t, found)
|
assert.True(t, found)
|
||||||
assert.True(t, xslices.Equal(result, []string{"b", "c", "c", "b", "c"}))
|
assert.Equal(t, result, []string{"b", "c", "c", "b", "c"})
|
||||||
|
|
||||||
result, found = findAndStrip(list, "c")
|
result, found = findAndStrip(list, "c")
|
||||||
assert.True(t, found)
|
assert.True(t, found)
|
||||||
assert.True(t, xslices.Equal(result, []string{"a", "b", "b"}))
|
assert.Equal(t, result, []string{"a", "b", "b"})
|
||||||
|
|
||||||
result, found = findAndStrip([]string{"c", "c", "c"}, "c")
|
result, found = findAndStrip([]string{"c", "c", "c"}, "c")
|
||||||
assert.True(t, found)
|
assert.True(t, found)
|
||||||
assert.True(t, xslices.Equal(result, []string{}))
|
assert.Equal(t, result, []string{})
|
||||||
|
|
||||||
result, found = findAndStrip(list, "A")
|
result, found = findAndStrip(list, "A")
|
||||||
assert.False(t, found)
|
assert.False(t, found)
|
||||||
assert.True(t, xslices.Equal(result, list))
|
assert.Equal(t, result, list)
|
||||||
|
|
||||||
result, found = findAndStrip([]string{}, "a")
|
result, found = findAndStrip([]string{}, "a")
|
||||||
assert.False(t, found)
|
assert.False(t, found)
|
||||||
assert.True(t, xslices.Equal(result, []string{}))
|
assert.Equal(t, result, []string{})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFindAndStripWait(t *testing.T) {
|
func TestFindAndStripWait(t *testing.T) {
|
||||||
result, found, values := findAndStripWait([]string{"a", "b", "c"})
|
result, found, values := findAndStripWait([]string{"a", "b", "c"})
|
||||||
assert.False(t, found)
|
assert.False(t, found)
|
||||||
assert.True(t, xslices.Equal(result, []string{"a", "b", "c"}))
|
assert.Equal(t, result, []string{"a", "b", "c"})
|
||||||
assert.True(t, xslices.Equal(values, []string{}))
|
assert.Equal(t, values, []string{})
|
||||||
|
|
||||||
result, found, values = findAndStripWait([]string{"a", "--wait", "b"})
|
result, found, values = findAndStripWait([]string{"a", "--wait", "b"})
|
||||||
assert.True(t, found)
|
assert.True(t, found)
|
||||||
assert.True(t, xslices.Equal(result, []string{"a"}))
|
assert.Equal(t, result, []string{"a"})
|
||||||
assert.True(t, xslices.Equal(values, []string{"b"}))
|
assert.Equal(t, values, []string{"b"})
|
||||||
|
|
||||||
result, found, values = findAndStripWait([]string{"a", "--wait", "b", "--wait", "c"})
|
result, found, values = findAndStripWait([]string{"a", "--wait", "b", "--wait", "c"})
|
||||||
assert.True(t, found)
|
assert.True(t, found)
|
||||||
assert.True(t, xslices.Equal(result, []string{"a"}))
|
assert.Equal(t, result, []string{"a"})
|
||||||
assert.True(t, xslices.Equal(values, []string{"b", "c"}))
|
assert.Equal(t, values, []string{"b", "c"})
|
||||||
|
|
||||||
result, found, values = findAndStripWait([]string{"a", "--wait", "b", "--wait", "c", "--wait", "d"})
|
result, found, values = findAndStripWait([]string{"a", "--wait", "b", "--wait", "c", "--wait", "d"})
|
||||||
assert.True(t, found)
|
assert.True(t, found)
|
||||||
assert.True(t, xslices.Equal(result, []string{"a"}))
|
assert.Equal(t, result, []string{"a"})
|
||||||
assert.True(t, xslices.Equal(values, []string{"b", "c", "d"}))
|
assert.Equal(t, values, []string{"b", "c", "d"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppendOrModifySessionID(t *testing.T) {
|
||||||
|
sessionID := string(logging.NewSessionID())
|
||||||
|
assert.Equal(t, appendOrModifySessionID(nil, sessionID), []string{"--session-id", sessionID})
|
||||||
|
assert.Equal(t, appendOrModifySessionID([]string{}, sessionID), []string{"--session-id", sessionID})
|
||||||
|
assert.Equal(t, appendOrModifySessionID([]string{"--cli"}, sessionID), []string{"--cli", "--session-id", sessionID})
|
||||||
|
assert.Equal(t, appendOrModifySessionID([]string{"--cli", "--session-id"}, sessionID), []string{"--cli", "--session-id", sessionID})
|
||||||
|
assert.Equal(t, appendOrModifySessionID([]string{"--cli", "--session-id"}, sessionID), []string{"--cli", "--session-id", sessionID})
|
||||||
|
assert.Equal(t, appendOrModifySessionID([]string{"--session-id", "<oldID>", "--cli"}, sessionID), []string{"--session-id", sessionID, "--cli"})
|
||||||
}
|
}
|
||||||
|
|||||||
2
extern/vcpkg
vendored
2
extern/vcpkg
vendored
Submodule extern/vcpkg updated: d4d39d71b3...fba75d0906
18
go.mod
18
go.mod
@ -2,12 +2,14 @@ module github.com/ProtonMail/proton-bridge/v3
|
|||||||
|
|
||||||
go 1.21
|
go 1.21
|
||||||
|
|
||||||
|
toolchain go1.21.9
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
|
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
|
||||||
github.com/Masterminds/semver/v3 v3.2.0
|
github.com/Masterminds/semver/v3 v3.2.0
|
||||||
github.com/ProtonMail/gluon v0.17.1-0.20240227105633-3734c7694bcd
|
github.com/ProtonMail/gluon v0.17.1-0.20240514133734-79cdd0fec41c
|
||||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
|
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
|
||||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20240226161523-ec58ed7ea4b9
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20240605113119-1a81ec7dc72d
|
||||||
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton
|
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton
|
||||||
github.com/PuerkitoBio/goquery v1.8.1
|
github.com/PuerkitoBio/goquery v1.8.1
|
||||||
github.com/abiosoft/ishell v2.0.0+incompatible
|
github.com/abiosoft/ishell v2.0.0+incompatible
|
||||||
@ -44,11 +46,11 @@ require (
|
|||||||
github.com/vmihailenco/msgpack/v5 v5.3.5
|
github.com/vmihailenco/msgpack/v5 v5.3.5
|
||||||
go.uber.org/goleak v1.2.1
|
go.uber.org/goleak v1.2.1
|
||||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
|
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
|
||||||
golang.org/x/net v0.17.0
|
golang.org/x/net v0.24.0
|
||||||
golang.org/x/sys v0.16.0
|
golang.org/x/sys v0.19.0
|
||||||
golang.org/x/text v0.14.0
|
golang.org/x/text v0.14.0
|
||||||
google.golang.org/grpc v1.56.3
|
google.golang.org/grpc v1.56.3
|
||||||
google.golang.org/protobuf v1.31.0
|
google.golang.org/protobuf v1.33.0
|
||||||
howett.net/plist v1.0.0
|
howett.net/plist v1.0.0
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -62,7 +64,7 @@ require (
|
|||||||
github.com/bytedance/sonic v1.9.1 // indirect
|
github.com/bytedance/sonic v1.9.1 // indirect
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||||
github.com/chzyer/test v1.0.0 // indirect
|
github.com/chzyer/test v1.0.0 // indirect
|
||||||
github.com/cloudflare/circl v1.3.3 // indirect
|
github.com/cloudflare/circl v1.3.7 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||||
github.com/cronokirby/saferith v0.33.0 // indirect
|
github.com/cronokirby/saferith v0.33.0 // indirect
|
||||||
github.com/cucumber/gherkin-go/v19 v19.0.3 // indirect
|
github.com/cucumber/gherkin-go/v19 v19.0.3 // indirect
|
||||||
@ -93,7 +95,7 @@ require (
|
|||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.14.17 // indirect
|
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||||
@ -111,7 +113,7 @@ require (
|
|||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||||
gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a // indirect
|
gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a // indirect
|
||||||
golang.org/x/arch v0.3.0 // indirect
|
golang.org/x/arch v0.3.0 // indirect
|
||||||
golang.org/x/crypto v0.18.0 // indirect
|
golang.org/x/crypto v0.22.0 // indirect
|
||||||
golang.org/x/mod v0.8.0 // indirect
|
golang.org/x/mod v0.8.0 // indirect
|
||||||
golang.org/x/sync v0.3.0 // indirect
|
golang.org/x/sync v0.3.0 // indirect
|
||||||
golang.org/x/tools v0.6.0 // indirect
|
golang.org/x/tools v0.6.0 // indirect
|
||||||
|
|||||||
34
go.sum
34
go.sum
@ -27,8 +27,10 @@ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAE
|
|||||||
github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
|
github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
|
||||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug=
|
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug=
|
||||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
|
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
|
||||||
github.com/ProtonMail/gluon v0.17.1-0.20240227105633-3734c7694bcd h1:AjJsf5xQGmZPg6GLn+wB+eBoGRopJlG70lQBfSyfX+M=
|
github.com/ProtonMail/gluon v0.17.1-0.20240423123310-0266b0f75d41 h1:Lu2hKO4fcHeMcbZOon129iM1dAy0ERwZkJtuNQCLlOQ=
|
||||||
github.com/ProtonMail/gluon v0.17.1-0.20240227105633-3734c7694bcd/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo=
|
github.com/ProtonMail/gluon v0.17.1-0.20240423123310-0266b0f75d41/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
|
||||||
|
github.com/ProtonMail/gluon v0.17.1-0.20240514133734-79cdd0fec41c h1:P3SvCACt13Zqdj0IRDB4bgwqI68+oMB2j0uVuPQyoTw=
|
||||||
|
github.com/ProtonMail/gluon v0.17.1-0.20240514133734-79cdd0fec41c/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
|
||||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
|
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
|
||||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
|
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
|
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
|
||||||
@ -38,8 +40,10 @@ github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7 h1:+j+Kd/
|
|||||||
github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
|
github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
|
||||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
|
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
|
||||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
|
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
|
||||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20240226161523-ec58ed7ea4b9 h1:tcQpGQljNsZmfuA6L4hAzio8/AIx5OXcU2JUdyX/qxw=
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20240423123404-a6163268401c h1:3U245DPGyL+LeAcJzFSg+E2lShXx+z/lBHM2v9P5mEg=
|
||||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20240226161523-ec58ed7ea4b9/go.mod h1:t+hb0BfkmZ9fpvzVRpHC7limoowym6ln/j0XL9a8DDw=
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20240423123404-a6163268401c/go.mod h1:3A0cpdo0BIenIPjTG6u8EbzJ8uuJy7rVvM/NaynjCKA=
|
||||||
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20240605113119-1a81ec7dc72d h1:B9/ZLubPWIY4uvATviFoCUoLauq98C3Bbt4v0A2VEdU=
|
||||||
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20240605113119-1a81ec7dc72d/go.mod h1:3A0cpdo0BIenIPjTG6u8EbzJ8uuJy7rVvM/NaynjCKA=
|
||||||
github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865 h1:EP1gnxLL5Z7xBSymE9nSTM27nRYINuvssAtDmG0suD8=
|
github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865 h1:EP1gnxLL5Z7xBSymE9nSTM27nRYINuvssAtDmG0suD8=
|
||||||
github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
|
github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
|
||||||
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
|
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
|
||||||
@ -87,8 +91,9 @@ github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
|
|||||||
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
|
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
|
||||||
github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
|
|
||||||
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
||||||
|
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
|
||||||
|
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
|
||||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||||
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||||
@ -312,8 +317,8 @@ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
|
|||||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||||
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||||
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||||
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||||
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
|
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
|
||||||
@ -465,8 +470,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
|
|||||||
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||||
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
|
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
|
||||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||||
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=
|
||||||
@ -520,8 +525,9 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
|||||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
|
||||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||||
|
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
||||||
|
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||||
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=
|
||||||
@ -575,8 +581,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
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/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||||
@ -660,8 +666,8 @@ google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc=
|
|||||||
google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
|
google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
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/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|||||||
@ -83,7 +83,7 @@ const (
|
|||||||
flagNoWindow = "no-window"
|
flagNoWindow = "no-window"
|
||||||
flagParentPID = "parent-pid"
|
flagParentPID = "parent-pid"
|
||||||
flagSoftwareRenderer = "software-renderer"
|
flagSoftwareRenderer = "software-renderer"
|
||||||
flagSessionID = "session-id"
|
FlagSessionID = "session-id"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -165,7 +165,7 @@ func New() *cli.App {
|
|||||||
Value: false,
|
Value: false,
|
||||||
},
|
},
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: flagSessionID,
|
Name: FlagSessionID,
|
||||||
Hidden: true,
|
Hidden: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -346,7 +346,7 @@ func withLogging(c *cli.Context, crashHandler *crash.Handler, locations *locatio
|
|||||||
logrus.WithField("path", logsPath).Debug("Received logs path")
|
logrus.WithField("path", logsPath).Debug("Received logs path")
|
||||||
|
|
||||||
// Initialize logging.
|
// Initialize logging.
|
||||||
sessionID := logging.NewSessionIDFromString(c.String(flagSessionID))
|
sessionID := logging.NewSessionIDFromString(c.String(FlagSessionID))
|
||||||
var closer io.Closer
|
var closer io.Closer
|
||||||
if closer, err = logging.Init(
|
if closer, err = logging.Init(
|
||||||
logsPath,
|
logsPath,
|
||||||
|
|||||||
@ -43,7 +43,7 @@ import (
|
|||||||
|
|
||||||
// nolint:gosec
|
// nolint:gosec
|
||||||
func migrateKeychainHelper(locations *locations.Locations) error {
|
func migrateKeychainHelper(locations *locations.Locations) error {
|
||||||
logrus.Info("Migrating keychain helper")
|
logrus.Trace("Checking if keychain helper needs to be migrated")
|
||||||
|
|
||||||
settings, err := locations.ProvideSettingsPath()
|
settings, err := locations.ProvideSettingsPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -75,7 +75,11 @@ func migrateKeychainHelper(locations *locations.Locations) error {
|
|||||||
return fmt.Errorf("failed to unmarshal old prefs file: %w", err)
|
return fmt.Errorf("failed to unmarshal old prefs file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return vault.SetHelper(settings, prefs.Helper)
|
err = vault.SetHelper(settings, prefs.Helper)
|
||||||
|
if err == nil {
|
||||||
|
logrus.Info("Keychain helper has been migrated")
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// nolint:gosec
|
// nolint:gosec
|
||||||
|
|||||||
@ -539,6 +539,49 @@ func (bridge *Bridge) onStatusDown(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) Repair() {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
userIDS := bridge.GetUserIDs()
|
||||||
|
|
||||||
|
for _, userID := range userIDS {
|
||||||
|
logPkg.Info("Initiating repair for userID:", userID)
|
||||||
|
|
||||||
|
userInfo, err := bridge.GetUserInfo(userID)
|
||||||
|
if err != nil {
|
||||||
|
logPkg.WithError(err).Error("Failed getting user info for repair; ID:", userID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if userInfo.State != Connected {
|
||||||
|
logPkg.Info("User is not connected. Repair will be executed on following successful log in.", userID)
|
||||||
|
if err := bridge.vault.GetUser(userID, func(user *vault.User) {
|
||||||
|
if err := user.SetShouldSync(true); err != nil {
|
||||||
|
logPkg.WithError(err).Error("Failed setting vault should sync for user:", userID)
|
||||||
|
}
|
||||||
|
}); err != nil {
|
||||||
|
logPkg.WithError(err).Error("Unable to get user vault when scheduling repair:", userID)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
bridgeUser, ok := bridge.users[userID]
|
||||||
|
if !ok {
|
||||||
|
logPkg.Info("UserID does not exist in bridge user map", userID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func(userID string) {
|
||||||
|
defer wg.Done()
|
||||||
|
if err = bridgeUser.TriggerRepair(); err != nil {
|
||||||
|
logPkg.WithError(err).Error("Failed re-syncing IMAP for userID", userID)
|
||||||
|
}
|
||||||
|
}(userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
func loadTLSConfig(vault *vault.Vault) (*tls.Config, error) {
|
func loadTLSConfig(vault *vault.Vault) (*tls.Config, error) {
|
||||||
cert, err := tls.X509KeyPair(vault.GetBridgeTLSCert())
|
cert, err := tls.X509KeyPair(vault.GetBridgeTLSCert())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -558,3 +601,7 @@ func min(a, b time.Duration) time.Duration {
|
|||||||
|
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) HasAPIConnection() bool {
|
||||||
|
return bridge.api.GetStatus() == proton.StatusUp
|
||||||
|
}
|
||||||
|
|||||||
@ -184,20 +184,11 @@ func TestBridge_UserAgent_Persistence(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
imapWaiter := waitForIMAPServerReady(b)
|
|
||||||
defer imapWaiter.Done()
|
|
||||||
|
|
||||||
smtpWaiter := waitForSMTPServerReady(b)
|
|
||||||
defer smtpWaiter.Done()
|
|
||||||
|
|
||||||
currentUserAgent := b.GetCurrentUserAgent()
|
currentUserAgent := b.GetCurrentUserAgent()
|
||||||
require.Contains(t, currentUserAgent, useragent.DefaultUserAgent)
|
require.Contains(t, currentUserAgent, useragent.DefaultUserAgent)
|
||||||
|
|
||||||
require.NoError(t, getErr(b.LoginFull(ctx, otherUser, otherPassword, nil, nil)))
|
require.NoError(t, getErr(b.LoginFull(ctx, otherUser, otherPassword, nil, nil)))
|
||||||
|
|
||||||
imapWaiter.Wait()
|
|
||||||
smtpWaiter.Wait()
|
|
||||||
|
|
||||||
imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer func() { _ = imapClient.Logout() }()
|
defer func() { _ = imapClient.Logout() }()
|
||||||
@ -235,21 +226,12 @@ func TestBridge_UserAgentFromUnknownClient(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
imapWaiter := waitForIMAPServerReady(b)
|
|
||||||
defer imapWaiter.Done()
|
|
||||||
|
|
||||||
smtpWaiter := waitForSMTPServerReady(b)
|
|
||||||
defer smtpWaiter.Done()
|
|
||||||
|
|
||||||
currentUserAgent := b.GetCurrentUserAgent()
|
currentUserAgent := b.GetCurrentUserAgent()
|
||||||
require.Contains(t, currentUserAgent, useragent.DefaultUserAgent)
|
require.Contains(t, currentUserAgent, useragent.DefaultUserAgent)
|
||||||
|
|
||||||
userID, err := b.LoginFull(context.Background(), username, password, nil, nil)
|
userID, err := b.LoginFull(context.Background(), username, password, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
imapWaiter.Wait()
|
|
||||||
smtpWaiter.Wait()
|
|
||||||
|
|
||||||
imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer func() { _ = imapClient.Logout() }()
|
defer func() { _ = imapClient.Logout() }()
|
||||||
@ -274,21 +256,12 @@ func TestBridge_UserAgentFromSMTPClient(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
imapWaiter := waitForIMAPServerReady(b)
|
|
||||||
defer imapWaiter.Done()
|
|
||||||
|
|
||||||
smtpWaiter := waitForSMTPServerReady(b)
|
|
||||||
defer smtpWaiter.Done()
|
|
||||||
|
|
||||||
currentUserAgent := b.GetCurrentUserAgent()
|
currentUserAgent := b.GetCurrentUserAgent()
|
||||||
require.Contains(t, currentUserAgent, useragent.DefaultUserAgent)
|
require.Contains(t, currentUserAgent, useragent.DefaultUserAgent)
|
||||||
|
|
||||||
userID, err := b.LoginFull(context.Background(), username, password, nil, nil)
|
userID, err := b.LoginFull(context.Background(), username, password, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
imapWaiter.Wait()
|
|
||||||
smtpWaiter.Wait()
|
|
||||||
|
|
||||||
client, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(b.GetSMTPPort())))
|
client, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(b.GetSMTPPort())))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer client.Close() //nolint:errcheck
|
defer client.Close() //nolint:errcheck
|
||||||
@ -333,17 +306,8 @@ func TestBridge_UserAgentFromIMAPID(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
imapWaiter := waitForIMAPServerReady(b)
|
|
||||||
defer imapWaiter.Done()
|
|
||||||
|
|
||||||
smtpWaiter := waitForSMTPServerReady(b)
|
|
||||||
defer smtpWaiter.Done()
|
|
||||||
|
|
||||||
require.NoError(t, getErr(b.LoginFull(ctx, otherUser, otherPassword, nil, nil)))
|
require.NoError(t, getErr(b.LoginFull(ctx, otherUser, otherPassword, nil, nil)))
|
||||||
|
|
||||||
imapWaiter.Wait()
|
|
||||||
smtpWaiter.Wait()
|
|
||||||
|
|
||||||
imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer func() { _ = imapClient.Logout() }()
|
defer func() { _ = imapClient.Logout() }()
|
||||||
@ -715,21 +679,12 @@ func TestBridge_InitGluonDirectory(t *testing.T) {
|
|||||||
func TestBridge_LoginFailed(t *testing.T) {
|
func TestBridge_LoginFailed(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
imapWaiter := waitForIMAPServerReady(bridge)
|
|
||||||
defer imapWaiter.Done()
|
|
||||||
|
|
||||||
smtpWaiter := waitForSMTPServerReady(bridge)
|
|
||||||
defer smtpWaiter.Done()
|
|
||||||
|
|
||||||
failCh, done := chToType[events.Event, events.IMAPLoginFailed](bridge.GetEvents(events.IMAPLoginFailed{}))
|
failCh, done := chToType[events.Event, events.IMAPLoginFailed](bridge.GetEvents(events.IMAPLoginFailed{}))
|
||||||
defer done()
|
defer done()
|
||||||
|
|
||||||
_, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
_, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
imapWaiter.Wait()
|
|
||||||
smtpWaiter.Wait()
|
|
||||||
|
|
||||||
imapClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
|
imapClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@ -757,12 +712,6 @@ func TestBridge_ChangeCacheDirectory(t *testing.T) {
|
|||||||
configDir, err := b.GetGluonDataDir()
|
configDir, err := b.GetGluonDataDir()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
imapWaiter := waitForIMAPServerReady(b)
|
|
||||||
defer imapWaiter.Done()
|
|
||||||
|
|
||||||
smtpWaiter := waitForSMTPServerReady(b)
|
|
||||||
defer smtpWaiter.Done()
|
|
||||||
|
|
||||||
// Login the user.
|
// Login the user.
|
||||||
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
|
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
|
||||||
defer done()
|
defer done()
|
||||||
@ -796,9 +745,6 @@ func TestBridge_ChangeCacheDirectory(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.True(t, info.State == bridge.Connected)
|
require.True(t, info.State == bridge.Connected)
|
||||||
|
|
||||||
imapWaiter.Wait()
|
|
||||||
smtpWaiter.Wait()
|
|
||||||
|
|
||||||
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||||
|
|||||||
@ -64,9 +64,6 @@ func TestBridge_HandleDraftsSendFromOtherClient(t *testing.T) {
|
|||||||
|
|
||||||
// The initial user should be fully synced.
|
// The initial user should be fully synced.
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
waiter := waitForIMAPServerReady(b)
|
|
||||||
defer waiter.Done()
|
|
||||||
|
|
||||||
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
|
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
|
||||||
defer done()
|
defer done()
|
||||||
|
|
||||||
@ -74,7 +71,6 @@ func TestBridge_HandleDraftsSendFromOtherClient(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.Equal(t, userID, (<-syncCh).UserID)
|
require.Equal(t, userID, (<-syncCh).UserID)
|
||||||
waiter.Wait()
|
|
||||||
|
|
||||||
info, err := b.GetUserInfo(userID)
|
info, err := b.GetUserInfo(userID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
@ -46,17 +46,12 @@ func TestBridge_Send(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
smtpWaiter := waitForSMTPServerReady(bridge)
|
|
||||||
defer smtpWaiter.Done()
|
|
||||||
|
|
||||||
senderUserID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
senderUserID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
recipientUserID, err := bridge.LoginFull(ctx, "recipient", password, nil, nil)
|
recipientUserID, err := bridge.LoginFull(ctx, "recipient", password, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
smtpWaiter.Wait()
|
|
||||||
|
|
||||||
senderInfo, err := bridge.GetUserInfo(senderUserID)
|
senderInfo, err := bridge.GetUserInfo(senderUserID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@ -409,9 +404,6 @@ SGVsbG8gd29ybGQK
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
smtpWaiter := waitForSMTPServerReady(bridge)
|
|
||||||
defer smtpWaiter.Done()
|
|
||||||
|
|
||||||
senderUserID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
senderUserID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@ -431,8 +423,6 @@ SGVsbG8gd29ybGQK
|
|||||||
messageMultipartWithoutTextWithTextAttachment,
|
messageMultipartWithoutTextWithTextAttachment,
|
||||||
}
|
}
|
||||||
|
|
||||||
smtpWaiter.Wait()
|
|
||||||
|
|
||||||
for _, m := range messages {
|
for _, m := range messages {
|
||||||
// Dial the server.
|
// Dial the server.
|
||||||
client, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
|
client, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
|
||||||
@ -617,9 +607,6 @@ Hello world
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
smtpWaiter := waitForSMTPServerReady(bridge)
|
|
||||||
defer smtpWaiter.Done()
|
|
||||||
|
|
||||||
senderUserID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
senderUserID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@ -639,8 +626,6 @@ Hello world
|
|||||||
messageInlineImageFollowedByText,
|
messageInlineImageFollowedByText,
|
||||||
}
|
}
|
||||||
|
|
||||||
smtpWaiter.Wait()
|
|
||||||
|
|
||||||
for _, m := range messages {
|
for _, m := range messages {
|
||||||
// Dial the server.
|
// Dial the server.
|
||||||
client, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
|
client, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
|
||||||
@ -714,17 +699,12 @@ func TestBridge_SendAddressDisabled(t *testing.T) {
|
|||||||
require.NoError(t, s.ChangeAddressAllowSend(senderUserID, addrID, false))
|
require.NoError(t, s.ChangeAddressAllowSend(senderUserID, addrID, false))
|
||||||
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
smtpWaiter := waitForSMTPServerReady(bridge)
|
|
||||||
defer smtpWaiter.Done()
|
|
||||||
|
|
||||||
senderUserID, err := bridge.LoginFull(ctx, "sender", password, nil, nil)
|
senderUserID, err := bridge.LoginFull(ctx, "sender", password, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
_, err = bridge.LoginFull(ctx, "recipient", password, nil, nil)
|
_, err = bridge.LoginFull(ctx, "recipient", password, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
smtpWaiter.Wait()
|
|
||||||
|
|
||||||
recipientInfo, err := bridge.GetUserInfo(recipientUserID)
|
recipientInfo, err := bridge.GetUserInfo(recipientUserID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@ -750,7 +730,7 @@ func TestBridge_SendAddressDisabled(t *testing.T) {
|
|||||||
strings.NewReader("Subject: Test 1\r\n\r\nHello world!"),
|
strings.NewReader("Subject: Test 1\r\n\r\nHello world!"),
|
||||||
)
|
)
|
||||||
|
|
||||||
smtpErr := smtpservice.NewErrCanNotSendOnAddress(senderInfo.Addresses[0])
|
smtpErr := smtpservice.NewErrCannotSendFromAddress(senderInfo.Addresses[0])
|
||||||
require.Equal(t, fmt.Sprintf("Error: %v", smtpErr.Error()), err.Error())
|
require.Equal(t, fmt.Sprintf("Error: %v", smtpErr.Error()), err.Error())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -36,9 +36,6 @@ import (
|
|||||||
func TestBridge_Report(t *testing.T) {
|
func TestBridge_Report(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
imapWaiter := waitForIMAPServerReady(b)
|
|
||||||
defer imapWaiter.Done()
|
|
||||||
|
|
||||||
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
|
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
|
||||||
defer done()
|
defer done()
|
||||||
|
|
||||||
@ -54,8 +51,6 @@ func TestBridge_Report(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.True(t, info.State == bridge.Connected)
|
require.True(t, info.State == bridge.Connected)
|
||||||
|
|
||||||
imapWaiter.Wait()
|
|
||||||
|
|
||||||
// Dial the IMAP port.
|
// Dial the IMAP port.
|
||||||
conn, err := net.Dial("tcp", fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
conn, err := net.Dial("tcp", fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
@ -20,6 +20,7 @@ package bridge_test
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
@ -27,57 +28,39 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
|
"github.com/emersion/go-smtp"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestServerManager_NoLoadedUsersNoServers(t *testing.T) {
|
func TestServerManager_ServersStartWithBridge(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
_, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
|
imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
|
||||||
require.Error(t, err)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestServerManager_ServersStartAfterFirstConnectedUser(t *testing.T) {
|
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
|
||||||
imapWaiter := waitForIMAPServerReady(bridge)
|
|
||||||
defer imapWaiter.Done()
|
|
||||||
|
|
||||||
smtpWaiter := waitForSMTPServerReady(bridge)
|
|
||||||
defer smtpWaiter.Done()
|
|
||||||
|
|
||||||
_, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, imapClient.Logout())
|
||||||
|
|
||||||
imapWaiter.Wait()
|
smtpClient, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
|
||||||
smtpWaiter.Wait()
|
require.NoError(t, err)
|
||||||
|
smtpClient.Close() //nolint:errcheck
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestServerManager_ServersStopsAfterUserLogsOut(t *testing.T) {
|
func TestServerManager_ServersKeepsRunningfterUserLogsOut(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
imapWaiter := waitForIMAPServerReady(bridge)
|
|
||||||
defer imapWaiter.Done()
|
|
||||||
|
|
||||||
smtpWaiter := waitForSMTPServerReady(bridge)
|
|
||||||
defer smtpWaiter.Done()
|
|
||||||
|
|
||||||
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
imapWaiter.Wait()
|
|
||||||
smtpWaiter.Wait()
|
|
||||||
|
|
||||||
imapWaiterStopped := waitForIMAPServerStopped(bridge)
|
|
||||||
defer imapWaiterStopped.Done()
|
|
||||||
|
|
||||||
require.NoError(t, bridge.LogoutUser(ctx, userID))
|
require.NoError(t, bridge.LogoutUser(ctx, userID))
|
||||||
|
|
||||||
imapWaiterStopped.Wait()
|
imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, imapClient.Logout())
|
||||||
|
|
||||||
|
smtpClient, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
|
||||||
|
require.NoError(t, err)
|
||||||
|
smtpClient.Close() //nolint:errcheck
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -90,21 +73,12 @@ func TestServerManager_ServersDoNotStopWhenThereIsStillOneActiveUser(t *testing.
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
imapWaiter := waitForIMAPServerReady(bridge)
|
|
||||||
defer imapWaiter.Done()
|
|
||||||
|
|
||||||
smtpWaiter := waitForSMTPServerReady(bridge)
|
|
||||||
defer smtpWaiter.Done()
|
|
||||||
|
|
||||||
_, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
_, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
userIDOther, err := bridge.LoginFull(ctx, otherUser, otherPassword, nil, nil)
|
userIDOther, err := bridge.LoginFull(ctx, otherUser, otherPassword, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
imapWaiter.Wait()
|
|
||||||
smtpWaiter.Wait()
|
|
||||||
|
|
||||||
evtCh, cancel := bridge.GetEvents(events.UserDeauth{})
|
evtCh, cancel := bridge.GetEvents(events.UserDeauth{})
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@ -115,31 +89,10 @@ func TestServerManager_ServersDoNotStopWhenThereIsStillOneActiveUser(t *testing.
|
|||||||
imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
|
imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NoError(t, imapClient.Logout())
|
require.NoError(t, imapClient.Logout())
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestServerManager_ServersStartIfAtLeastOneUserIsLoggedIn(t *testing.T) {
|
smtpClient, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
|
||||||
otherPassword := []byte("bar")
|
|
||||||
otherUser := "foo"
|
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
|
||||||
userIDOther, _, err := s.CreateUser(otherUser, otherPassword)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
|
||||||
_, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
smtpClient.Close() //nolint:errcheck
|
||||||
_, err = bridge.LoginFull(ctx, otherUser, otherPassword, nil, nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
require.NoError(t, s.RevokeUser(userIDOther))
|
|
||||||
|
|
||||||
withBridgeWaitForServers(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
|
||||||
imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NoError(t, imapClient.Logout())
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -162,8 +115,13 @@ func TestServerManager_NetworkLossStopsServers(t *testing.T) {
|
|||||||
_, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
_, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
imapWaiter.Wait()
|
imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
|
||||||
smtpWaiter.Wait()
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, imapClient.Logout())
|
||||||
|
|
||||||
|
smtpClient, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
|
||||||
|
require.NoError(t, err)
|
||||||
|
smtpClient.Close() //nolint:errcheck
|
||||||
|
|
||||||
netCtl.Disable()
|
netCtl.Disable()
|
||||||
|
|
||||||
|
|||||||
@ -28,6 +28,7 @@ import (
|
|||||||
"github.com/ProtonMail/gluon/reporter"
|
"github.com/ProtonMail/gluon/reporter"
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/hv"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice"
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice"
|
||||||
@ -123,15 +124,20 @@ func (bridge *Bridge) QueryUserInfo(query string) (UserInfo, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// LoginAuth begins the login process. It returns an authorized client that might need 2FA.
|
// LoginAuth begins the login process. It returns an authorized client that might need 2FA.
|
||||||
func (bridge *Bridge) LoginAuth(ctx context.Context, username string, password []byte) (*proton.Client, proton.Auth, error) {
|
func (bridge *Bridge) LoginAuth(ctx context.Context, username string, password []byte, hvDetails *proton.APIHVDetails) (*proton.Client, proton.Auth, error) {
|
||||||
logUser.WithField("username", logging.Sensitive(username)).Info("Authorizing user for login")
|
logUser.WithField("username", logging.Sensitive(username)).Info("Authorizing user for login")
|
||||||
|
|
||||||
if username == "crash@bandicoot" {
|
if username == "crash@bandicoot" {
|
||||||
panic("Your wish is my command.. I crash!")
|
panic("Your wish is my command.. I crash!")
|
||||||
}
|
}
|
||||||
|
client, auth, err := bridge.api.NewClientWithLoginWithHVToken(ctx, username, password, hvDetails)
|
||||||
client, auth, err := bridge.api.NewClientWithLogin(ctx, username, password)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if hv.IsHvRequest(err) {
|
||||||
|
logUser.WithFields(logrus.Fields{"username": logging.Sensitive(username),
|
||||||
|
"loginError": err.Error()}).Info("Human Verification requested for login")
|
||||||
|
return nil, proton.Auth{}, err
|
||||||
|
}
|
||||||
|
|
||||||
return nil, proton.Auth{}, fmt.Errorf("failed to create new API client: %w", err)
|
return nil, proton.Auth{}, fmt.Errorf("failed to create new API client: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,12 +160,13 @@ func (bridge *Bridge) LoginUser(
|
|||||||
client *proton.Client,
|
client *proton.Client,
|
||||||
auth proton.Auth,
|
auth proton.Auth,
|
||||||
keyPass []byte,
|
keyPass []byte,
|
||||||
|
hvDetails *proton.APIHVDetails,
|
||||||
) (string, error) {
|
) (string, error) {
|
||||||
logUser.WithField("userID", auth.UserID).Info("Logging in authorized user")
|
logUser.WithField("userID", auth.UserID).Info("Logging in authorized user")
|
||||||
|
|
||||||
userID, err := try.CatchVal(
|
userID, err := try.CatchVal(
|
||||||
func() (string, error) {
|
func() (string, error) {
|
||||||
return bridge.loginUser(ctx, client, auth.UID, auth.RefreshToken, keyPass)
|
return bridge.loginUser(ctx, client, auth.UID, auth.RefreshToken, keyPass, hvDetails)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -192,7 +199,8 @@ func (bridge *Bridge) LoginFull(
|
|||||||
) (string, error) {
|
) (string, error) {
|
||||||
logUser.WithField("username", logging.Sensitive(username)).Info("Performing full user login")
|
logUser.WithField("username", logging.Sensitive(username)).Info("Performing full user login")
|
||||||
|
|
||||||
client, auth, err := bridge.LoginAuth(ctx, username, password)
|
// (atanas) the following may need to be modified once HV is merged (its used only for testing; and depends on whether we will test HV related logic)
|
||||||
|
client, auth, err := bridge.LoginAuth(ctx, username, password, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to begin login process: %w", err)
|
return "", fmt.Errorf("failed to begin login process: %w", err)
|
||||||
}
|
}
|
||||||
@ -225,7 +233,7 @@ func (bridge *Bridge) LoginFull(
|
|||||||
keyPass = password
|
keyPass = password
|
||||||
}
|
}
|
||||||
|
|
||||||
userID, err := bridge.LoginUser(ctx, client, auth, keyPass)
|
userID, err := bridge.LoginUser(ctx, client, auth, keyPass, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if deleteErr := client.AuthDelete(ctx); deleteErr != nil {
|
if deleteErr := client.AuthDelete(ctx); deleteErr != nil {
|
||||||
logUser.WithError(err).Error("Failed to delete auth")
|
logUser.WithError(err).Error("Failed to delete auth")
|
||||||
@ -374,8 +382,8 @@ func (bridge *Bridge) SendBadEventUserFeedback(_ context.Context, userID string,
|
|||||||
}, bridge.usersLock)
|
}, bridge.usersLock)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) loginUser(ctx context.Context, client *proton.Client, authUID, authRef string, keyPass []byte) (string, error) {
|
func (bridge *Bridge) loginUser(ctx context.Context, client *proton.Client, authUID, authRef string, keyPass []byte, hvDetails *proton.APIHVDetails) (string, error) {
|
||||||
apiUser, err := client.GetUser(ctx)
|
apiUser, err := client.GetUserWithHV(ctx, hvDetails)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to get API user: %w", err)
|
return "", fmt.Errorf("failed to get API user: %w", err)
|
||||||
}
|
}
|
||||||
@ -598,6 +606,8 @@ func (bridge *Bridge) addUserWithVault(
|
|||||||
// As we need at least one user to send heartbeat, try to send it.
|
// As we need at least one user to send heartbeat, try to send it.
|
||||||
bridge.heartbeat.start()
|
bridge.heartbeat.start()
|
||||||
|
|
||||||
|
user.PublishEvent(ctx, events.UserLoadedCheckResync{UserID: user.ID()})
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -139,9 +139,6 @@ func test_badMessage_badEvent(userFeedback func(t *testing.T, ctx context.Contex
|
|||||||
})
|
})
|
||||||
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
smtpWaiter := waitForSMTPServerReady(bridge)
|
|
||||||
defer smtpWaiter.Done()
|
|
||||||
|
|
||||||
userLoginAndSync(ctx, t, bridge, "user", password)
|
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||||
|
|
||||||
var messageIDs []string
|
var messageIDs []string
|
||||||
@ -177,8 +174,6 @@ func test_badMessage_badEvent(userFeedback func(t *testing.T, ctx context.Contex
|
|||||||
|
|
||||||
userFeedback(t, ctx, bridge, badUserID)
|
userFeedback(t, ctx, bridge, badUserID)
|
||||||
|
|
||||||
smtpWaiter.Wait()
|
|
||||||
|
|
||||||
userContinueEventProcess(ctx, t, s, bridge)
|
userContinueEventProcess(ctx, t, s, bridge)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -197,9 +192,6 @@ func TestBridge_User_BadMessage_NoBadEvent(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
smtpWaiter := waitForSMTPServerReady(bridge)
|
|
||||||
defer smtpWaiter.Done()
|
|
||||||
|
|
||||||
userLoginAndSync(ctx, t, bridge, "user", password)
|
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||||
|
|
||||||
var messageIDs []string
|
var messageIDs []string
|
||||||
@ -223,7 +215,6 @@ func TestBridge_User_BadMessage_NoBadEvent(t *testing.T) {
|
|||||||
require.NoError(t, c.DeleteMessage(ctx, messageIDs...))
|
require.NoError(t, c.DeleteMessage(ctx, messageIDs...))
|
||||||
})
|
})
|
||||||
|
|
||||||
smtpWaiter.Wait()
|
|
||||||
userContinueEventProcess(ctx, t, s, bridge)
|
userContinueEventProcess(ctx, t, s, bridge)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -776,20 +767,11 @@ func TestBridge_User_CreateDisabledAddress(t *testing.T) {
|
|||||||
func TestBridge_User_HandleParentLabelRename(t *testing.T) {
|
func TestBridge_User_HandleParentLabelRename(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
imapWaiter := waitForIMAPServerReady(bridge)
|
|
||||||
defer imapWaiter.Done()
|
|
||||||
|
|
||||||
smtpWaiter := waitForSMTPServerReady(bridge)
|
|
||||||
defer smtpWaiter.Done()
|
|
||||||
|
|
||||||
require.NoError(t, getErr(bridge.LoginFull(ctx, username, password, nil, nil)))
|
require.NoError(t, getErr(bridge.LoginFull(ctx, username, password, nil, nil)))
|
||||||
|
|
||||||
info, err := bridge.QueryUserInfo(username)
|
info, err := bridge.QueryUserInfo(username)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
imapWaiter.Wait()
|
|
||||||
smtpWaiter.Wait()
|
|
||||||
|
|
||||||
cli, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
|
cli, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NoError(t, cli.Login(info.Addresses[0], string(info.BridgePass)))
|
require.NoError(t, cli.Login(info.Addresses[0], string(info.BridgePass)))
|
||||||
|
|||||||
@ -36,6 +36,9 @@ func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, even
|
|||||||
case events.UserBadEvent:
|
case events.UserBadEvent:
|
||||||
bridge.handleUserBadEvent(ctx, user, event)
|
bridge.handleUserBadEvent(ctx, user, event)
|
||||||
|
|
||||||
|
case events.UserLoadedCheckResync:
|
||||||
|
user.VerifyResyncAndExecute()
|
||||||
|
|
||||||
case events.UncategorizedEventError:
|
case events.UncategorizedEventError:
|
||||||
bridge.handleUncategorizedErrorEvent(event)
|
bridge.handleUncategorizedErrorEvent(event)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -64,7 +64,8 @@ func TestTLSPinInvalid(t *testing.T) {
|
|||||||
checkTLSIssueHandler(t, 1, called)
|
checkTLSIssueHandler(t, 1, called)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTLSPinNoMatch(t *testing.T) {
|
// Disabled for now we'll need to patch this up.
|
||||||
|
func _TestTLSPinNoMatch(t *testing.T) { //nolint:unused
|
||||||
skipIfProxyIsSet(t)
|
skipIfProxyIsSet(t)
|
||||||
|
|
||||||
called, _, reporter, checker, cm := createClientWithPinningDialer(getRootURL())
|
called, _, reporter, checker, cm := createClientWithPinningDialer(getRootURL())
|
||||||
|
|||||||
@ -202,3 +202,13 @@ type UncategorizedEventError struct {
|
|||||||
func (event UncategorizedEventError) String() string {
|
func (event UncategorizedEventError) String() string {
|
||||||
return fmt.Sprintf("UncategorizedEventError: UserID: %s, Source:%T, Error: %s", event.UserID, event.Error, event.Error)
|
return fmt.Sprintf("UncategorizedEventError: UserID: %s, Source:%T, Error: %s", event.UserID, event.Error, event.Error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UserLoadedCheckResync struct {
|
||||||
|
eventBase
|
||||||
|
|
||||||
|
UserID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (event UserLoadedCheckResync) String() string {
|
||||||
|
return fmt.Sprintf("UserLoadedCheckResync: UserID: %s", event.UserID)
|
||||||
|
}
|
||||||
|
|||||||
@ -29,13 +29,11 @@ using namespace bridgepp;
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
|
||||||
QString const defaultKeychain = "defaultKeychain"; ///< The default keychain.
|
QString const defaultKeychain = "defaultKeychain"; ///< The default keychain.
|
||||||
|
QString const HV_ERROR_TEMPLATE = "failed to create new API client: 422 POST https://mail-api.proton.me/auth/v4: CAPTCHA validation failed (Code=12087, Status=422)";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
//
|
//
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
@ -349,6 +347,7 @@ Status GRPCService::ForceLauncher(ServerContext *, StringValue const *request, E
|
|||||||
/// \return The status for the call.
|
/// \return The status for the call.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
Status GRPCService::SetMainExecutable(ServerContext *, StringValue const *request, Empty *) {
|
Status GRPCService::SetMainExecutable(ServerContext *, StringValue const *request, Empty *) {
|
||||||
|
resetHv();
|
||||||
app().log().debug(__FUNCTION__);
|
app().log().debug(__FUNCTION__);
|
||||||
app().log().info(QString("SetMainExecutable: %1").arg(QString::fromStdString(request->value())));
|
app().log().info(QString("SetMainExecutable: %1").arg(QString::fromStdString(request->value())));
|
||||||
return Status::OK;
|
return Status::OK;
|
||||||
@ -418,7 +417,19 @@ Status GRPCService::Login(ServerContext *, LoginRequest const *request, Empty *)
|
|||||||
return Status::OK;
|
return Status::OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (usersTab.nextUserHvRequired() && !hvWasRequested_ && previousHvUsername_ != QString::fromStdString(request->username())) {
|
||||||
|
hvWasRequested_ = true;
|
||||||
|
previousHvUsername_ = QString::fromStdString(request->username());
|
||||||
|
qtProxy_.sendDelayedEvent(newLoginHvRequestedEvent());
|
||||||
|
return Status::OK;
|
||||||
|
} else {
|
||||||
|
hvWasRequested_ = false;
|
||||||
|
previousHvUsername_ = "";
|
||||||
|
}
|
||||||
|
if (usersTab.nextUserHvError()) {
|
||||||
|
qtProxy_.sendDelayedEvent(newLoginError(LoginErrorType::HV_ERROR, HV_ERROR_TEMPLATE));
|
||||||
|
return Status::OK;
|
||||||
|
}
|
||||||
if (usersTab.nextUserUsernamePasswordError()) {
|
if (usersTab.nextUserUsernamePasswordError()) {
|
||||||
qtProxy_.sendDelayedEvent(newLoginError(LoginErrorType::USERNAME_PASSWORD_ERROR, usersTab.usernamePasswordErrorMessage()));
|
qtProxy_.sendDelayedEvent(newLoginError(LoginErrorType::USERNAME_PASSWORD_ERROR, usersTab.usernamePasswordErrorMessage()));
|
||||||
return Status::OK;
|
return Status::OK;
|
||||||
@ -495,6 +506,7 @@ Status GRPCService::Login2Passwords(ServerContext *, LoginRequest const *request
|
|||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
Status GRPCService::LoginAbort(ServerContext *, LoginAbortRequest const *request, Empty *) {
|
Status GRPCService::LoginAbort(ServerContext *, LoginAbortRequest const *request, Empty *) {
|
||||||
app().log().debug(__FUNCTION__);
|
app().log().debug(__FUNCTION__);
|
||||||
|
this->resetHv();
|
||||||
loginUsername_ = QString();
|
loginUsername_ = QString();
|
||||||
return Status::OK;
|
return Status::OK;
|
||||||
}
|
}
|
||||||
@ -953,3 +965,11 @@ void GRPCService::finishLogin() {
|
|||||||
|
|
||||||
qtProxy_.sendDelayedEvent(newLoginFinishedEvent(user->id(), alreadyExist));
|
qtProxy_.sendDelayedEvent(newLoginFinishedEvent(user->id(), alreadyExist));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
//
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
void GRPCService::resetHv() {
|
||||||
|
hvWasRequested_ = false;
|
||||||
|
previousHvUsername_ = "";
|
||||||
|
}
|
||||||
|
|||||||
@ -106,6 +106,7 @@ public: // member functions.
|
|||||||
|
|
||||||
private: // member functions
|
private: // member functions
|
||||||
void finishLogin(); ///< finish the login procedure once the credentials have been validated.
|
void finishLogin(); ///< finish the login procedure once the credentials have been validated.
|
||||||
|
void resetHv(); ///< Resets the human verification state.
|
||||||
|
|
||||||
private: // data member
|
private: // data member
|
||||||
mutable QMutex eventStreamMutex_; ///< Mutex used to access eventQueue_, isStreaming_ and shouldStopStreaming_;
|
mutable QMutex eventStreamMutex_; ///< Mutex used to access eventQueue_, isStreaming_ and shouldStopStreaming_;
|
||||||
@ -113,6 +114,8 @@ private: // data member
|
|||||||
bool isStreaming_; ///< Is the gRPC stream running. Access protected by eventStreamMutex_;
|
bool isStreaming_; ///< Is the gRPC stream running. Access protected by eventStreamMutex_;
|
||||||
bool eventStreamShouldStop_; ///< Should the stream be stopped? Access protected by eventStreamMutex
|
bool eventStreamShouldStop_; ///< Should the stream be stopped? Access protected by eventStreamMutex
|
||||||
QString loginUsername_; ///< The username used for the current login procedure.
|
QString loginUsername_; ///< The username used for the current login procedure.
|
||||||
|
QString previousHvUsername_; ///< The previous username used for HV.
|
||||||
|
bool hvWasRequested_ {false}; ///< Was human verification requested.
|
||||||
GRPCQtProxy qtProxy_; ///< Qt Proxy used to send signals, as this class is not a QObject.
|
GRPCQtProxy qtProxy_; ///< Qt Proxy used to send signals, as this class is not a QObject.
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -277,6 +277,22 @@ bridgepp::SPUser UsersTab::userWithUsernameOrEmail(QString const &username) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \return true if the next login attempt should trigger a human verification request
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
bool UsersTab::nextUserHvRequired() const {
|
||||||
|
return ui_.checkHV3Required->isChecked();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \return true if the next login attempt should trigger a human verification error
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
bool UsersTab::nextUserHvError() const {
|
||||||
|
return ui_.checkHV3Error->isChecked();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \return true iff the next login attempt should trigger a username/password error.
|
/// \return true iff the next login attempt should trigger a username/password error.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
|
|||||||
@ -39,6 +39,8 @@ public: // member functions.
|
|||||||
UserTable &userTable(); ///< Returns a reference to the user table.
|
UserTable &userTable(); ///< Returns a reference to the user table.
|
||||||
bridgepp::SPUser userWithID(QString const &userID); ///< Get the user with the given ID.
|
bridgepp::SPUser userWithID(QString const &userID); ///< Get the user with the given ID.
|
||||||
bridgepp::SPUser userWithUsernameOrEmail(QString const &username); ///< Get the user with the given username.
|
bridgepp::SPUser userWithUsernameOrEmail(QString const &username); ///< Get the user with the given username.
|
||||||
|
bool nextUserHvRequired() const; ///< Check if next user login should trigger HV
|
||||||
|
bool nextUserHvError() const; ///< Check if next user login should trigger HV error
|
||||||
bool nextUserUsernamePasswordError() const; ///< Check if next user login should trigger a username/password error.
|
bool nextUserUsernamePasswordError() const; ///< Check if next user login should trigger a username/password error.
|
||||||
bool nextUserFreeUserError() const; ///< Check if next user login should trigger a Free user error.
|
bool nextUserFreeUserError() const; ///< Check if next user login should trigger a Free user error.
|
||||||
bool nextUserTFARequired() const; ///< Check if next user login should requires 2FA.
|
bool nextUserTFARequired() const; ///< Check if next user login should requires 2FA.
|
||||||
|
|||||||
@ -290,6 +290,20 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QCheckBox" name="checkHV3Required">
|
||||||
|
<property name="text">
|
||||||
|
<string>HV3 required</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QCheckBox" name="checkHV3Error">
|
||||||
|
<property name="text">
|
||||||
|
<string>HV3 error</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QCheckBox" name="checkFreeUserError">
|
<widget class="QCheckBox" name="checkFreeUserError">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
|
|||||||
@ -140,7 +140,7 @@ if (WIN32) # on Windows, we add a (non-Qt) resource file that contains the appli
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
target_precompile_headers(bridge-gui PRIVATE Pch.h)
|
target_precompile_headers(bridge-gui PRIVATE Pch.h)
|
||||||
target_include_directories(bridge-gui PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${SENTRY_CONFIG_GENERATED_FILE_DIR})
|
target_include_directories(bridge-gui PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}" ${SENTRY_CONFIG_GENERATED_FILE_DIR})
|
||||||
target_link_libraries(bridge-gui
|
target_link_libraries(bridge-gui
|
||||||
Qt6::Widgets
|
Qt6::Widgets
|
||||||
Qt6::Core
|
Qt6::Core
|
||||||
|
|||||||
@ -15,113 +15,85 @@
|
|||||||
// 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/>.
|
||||||
|
|
||||||
|
|
||||||
#include "Pch.h"
|
#include "Pch.h"
|
||||||
#include "CommandLine.h"
|
#include "CommandLine.h"
|
||||||
#include "Settings.h"
|
#include "Settings.h"
|
||||||
|
#include <bridgepp/CLI/CLIUtils.h>
|
||||||
#include <bridgepp/SessionID/SessionID.h>
|
#include <bridgepp/SessionID/SessionID.h>
|
||||||
|
|
||||||
|
|
||||||
using namespace bridgepp;
|
using namespace bridgepp;
|
||||||
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
QString const hyphenatedLauncherFlag = "--launcher"; ///< launcher flag parameter used for bridge.
|
||||||
QString const launcherFlag = "--launcher"; ///< launcher flag parameter used for bridge.
|
QString const hyphenatedWindowFlag = "--no-window"; ///< The no-window command-line flag.
|
||||||
QString const noWindowFlag = "--no-window"; ///< The no-window command-line flag.
|
QString const hyphenatedSoftwareRendererFlag = "--software-renderer"; ///< The 'software-renderer' command-line flag. enable software rendering for a single execution
|
||||||
QString const softwareRendererFlag = "--software-renderer"; ///< The 'software-renderer' command-line flag. enable software rendering for a single execution
|
QString const hyphenatedSetSoftwareRendererFlag = "--set-software-renderer"; ///< The 'set-software-renderer' command-line flag. Software rendering will be used for all subsequent executions of the application.
|
||||||
QString const setSoftwareRendererFlag = "--set-software-renderer"; ///< The 'set-software-renderer' command-line flag. Software rendering will be used for all subsequent executions of the application.
|
QString const hyphenatedSetHardwareRendererFlag = "--set-hardware-renderer"; ///< The 'set-hardware-renderer' command-line flag. Hardware rendering will be used for all subsequent executions of the application.
|
||||||
QString const setHardwareRendererFlag = "--set-hardware-renderer"; ///< The 'set-hardware-renderer' command-line flag. Hardware rendering will be used for all subsequent executions of the application.
|
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
/// \brief parse a command-line string argument as expected by go's CLI package.
|
|
||||||
/// \param[in] argc The number of arguments passed to the application.
|
|
||||||
/// \param[in] argv The list of arguments passed to the application.
|
|
||||||
/// \param[in] paramNames the list of names for the parameter
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
QString parseGoCLIStringArgument(int argc, char *argv[], QStringList paramNames) {
|
|
||||||
// go cli package is pretty permissive when it comes to parsing arguments. For each name 'param', all the following seems to be accepted:
|
|
||||||
// -param value
|
|
||||||
// --param value
|
|
||||||
// -param=value
|
|
||||||
// --param=value
|
|
||||||
for (QString const ¶mName: paramNames) {
|
|
||||||
for (qsizetype i = 1; i < argc; ++i) {
|
|
||||||
QString const arg(QString::fromLocal8Bit(argv[i]));
|
|
||||||
if ((i < argc - 1) && ((arg == "-" + paramName) || (arg == "--" + paramName))) {
|
|
||||||
return QString(argv[i + 1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
QRegularExpressionMatch match = QRegularExpression(QString("^-{1,2}%1=(.+)$").arg(paramName)).match(arg);
|
|
||||||
if (match.hasMatch()) {
|
|
||||||
return match.captured(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return QString();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \brief Parse the log level from the command-line arguments.
|
/// \brief Parse the log level from the command-line arguments.
|
||||||
///
|
///
|
||||||
/// \param[in] argc The number of arguments passed to the application.
|
/// \param[in] args The command-line arguments.
|
||||||
/// \param[in] argv The list of arguments passed to the application.
|
|
||||||
/// \return The log level. if not specified on the command-line, the default log level is returned.
|
/// \return The log level. if not specified on the command-line, the default log level is returned.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
Log::Level parseLogLevel(int argc, char *argv[]) {
|
Log::Level parseLogLevel(QStringList const &args) {
|
||||||
QString levelStr = parseGoCLIStringArgument(argc, argv, { "l", "log-level" });
|
QStringList levelStr = parseGoCLIStringArgument(args, {"l", "log-level"});
|
||||||
if (levelStr.isEmpty()) {
|
if (levelStr.isEmpty()) {
|
||||||
return Log::defaultLevel;
|
return Log::defaultLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log::Level level = Log::defaultLevel;
|
Log::Level level = Log::defaultLevel;
|
||||||
Log::stringToLevel(levelStr, level);
|
Log::stringToLevel(levelStr.back(), level);
|
||||||
return level;
|
return level;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
} // anonymous namespace
|
} // anonymous namespace
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \param[in] argc number of arguments passed to the application.
|
/// \param[in] argv list of arguments passed to the application, including the exe name/path at index 0.
|
||||||
/// \param[in] argv list of arguments passed to the application.
|
|
||||||
/// \return The parsed options.
|
/// \return The parsed options.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
CommandLineOptions parseCommandLine(int argc, char *argv[]) {
|
CommandLineOptions parseCommandLine(QStringList const &argv) {
|
||||||
CommandLineOptions options;
|
CommandLineOptions options;
|
||||||
bool flagFound = false;
|
bool launcherFlagFound = false;
|
||||||
options.launcher = QString::fromLocal8Bit(argv[0]);
|
options.launcher = argv[0];
|
||||||
// for unknown reasons, on Windows QCoreApplication::arguments() frequently returns an empty list, which is incorrect, so we rebuild the argument
|
// for unknown reasons, on Windows QCoreApplication::arguments() frequently returns an empty list, which is incorrect, so we rebuild the argument
|
||||||
// list from the original argc and argv values.
|
// list from the original argc and argv values.
|
||||||
for (int i = 1; i < argc; i++) {
|
for (int i = 1; i < argv.count(); i++) {
|
||||||
QString const &arg = QString::fromLocal8Bit(argv[i]);
|
QString const &arg = argv[i];
|
||||||
// we can't use QCommandLineParser here since it will fail on unknown options.
|
// we can't use QCommandLineParser here since it will fail on unknown options.
|
||||||
|
|
||||||
|
// we skip session-id for now we'll process it later, with a special treatment for duplicates
|
||||||
|
if (arg == hyphenatedSessionIDFlag) {
|
||||||
|
i++; // we skip the next param, which if the flag's value.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (arg.startsWith(hyphenatedSessionIDFlag + "=")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Arguments may contain some bridge flags.
|
// Arguments may contain some bridge flags.
|
||||||
if (arg == softwareRendererFlag) {
|
if (arg == hyphenatedSoftwareRendererFlag) {
|
||||||
options.bridgeGuiArgs.append(arg);
|
options.bridgeGuiArgs.append(arg);
|
||||||
options.useSoftwareRenderer = true;
|
options.useSoftwareRenderer = true;
|
||||||
}
|
}
|
||||||
if (arg == setSoftwareRendererFlag) {
|
if (arg == hyphenatedSetSoftwareRendererFlag) {
|
||||||
app().settings().setUseSoftwareRenderer(true);
|
app().settings().setUseSoftwareRenderer(true);
|
||||||
continue; // setting is permanent. no need to keep/pass it to bridge for restart.
|
continue; // setting is permanent. no need to keep/pass it to bridge for restart.
|
||||||
}
|
}
|
||||||
if (arg == setHardwareRendererFlag) {
|
if (arg == hyphenatedSetHardwareRendererFlag) {
|
||||||
app().settings().setUseSoftwareRenderer(false);
|
app().settings().setUseSoftwareRenderer(false);
|
||||||
continue; // setting is permanent. no need to keep/pass it to bridge for restart.
|
continue; // setting is permanent. no need to keep/pass it to bridge for restart.
|
||||||
}
|
}
|
||||||
if (arg == noWindowFlag) {
|
if (arg == hyphenatedWindowFlag) {
|
||||||
options.noWindow = true;
|
options.noWindow = true;
|
||||||
}
|
}
|
||||||
if (arg == launcherFlag) {
|
if (arg == hyphenatedLauncherFlag) {
|
||||||
options.bridgeArgs.append(arg);
|
options.bridgeArgs.append(arg);
|
||||||
options.launcher = QString::fromLocal8Bit(argv[++i]);
|
options.launcher = argv[++i];
|
||||||
options.bridgeArgs.append(options.launcher);
|
options.bridgeArgs.append(options.launcher);
|
||||||
flagFound = true;
|
launcherFlagFound = true;
|
||||||
}
|
}
|
||||||
#ifdef QT_DEBUG
|
#ifdef QT_DEBUG
|
||||||
else if (arg == "--attach" || arg == "-a") {
|
else if (arg == "--attach" || arg == "-a") {
|
||||||
@ -135,22 +107,24 @@ CommandLineOptions parseCommandLine(int argc, char *argv[]) {
|
|||||||
options.bridgeGuiArgs.append(arg);
|
options.bridgeGuiArgs.append(arg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!flagFound) {
|
if (!launcherFlagFound) {
|
||||||
// add bridge-gui as launcher
|
// add bridge-gui as launcher
|
||||||
options.bridgeArgs.append(launcherFlag);
|
options.bridgeArgs.append(hyphenatedLauncherFlag);
|
||||||
options.bridgeArgs.append(options.launcher);
|
options.bridgeArgs.append(options.launcher);
|
||||||
}
|
}
|
||||||
|
|
||||||
options.logLevel = parseLogLevel(argc, argv);
|
QStringList args;
|
||||||
|
if (!argv.isEmpty()) {
|
||||||
QString sessionID = parseGoCLIStringArgument(argc, argv, { "session-id" });
|
args = argv.last(argv.count() - 1);
|
||||||
if (sessionID.isEmpty()) {
|
|
||||||
// The session ID was not passed to us on the command-line -> create one and add to the command-line for bridge
|
|
||||||
sessionID = newSessionID();
|
|
||||||
options.bridgeArgs.append("--session-id");
|
|
||||||
options.bridgeArgs.append(sessionID);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
options.logLevel = parseLogLevel(args);
|
||||||
|
|
||||||
|
QString const sessionID = mostRecentSessionID(args);
|
||||||
|
options.bridgeArgs.append(hyphenatedSessionIDFlag);
|
||||||
|
options.bridgeArgs.append(sessionID);
|
||||||
app().setSessionID(sessionID);
|
app().setSessionID(sessionID);
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -37,7 +37,7 @@ struct CommandLineOptions {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
CommandLineOptions parseCommandLine(int argc, char *argv[]); ///< Parse the command-line arguments
|
CommandLineOptions parseCommandLine(QStringList const &argv); ///< Parse the command-line arguments
|
||||||
|
|
||||||
|
|
||||||
#endif //BRIDGE_GUI_COMMAND_LINE_H
|
#endif //BRIDGE_GUI_COMMAND_LINE_H
|
||||||
|
|||||||
@ -31,7 +31,7 @@ macro( AppendLib LIB_NAME HINT_PATH)
|
|||||||
if( ${PATH_${UP_NAME}} STREQUAL "PATH_${UP_NAME}-NOTFOUND")
|
if( ${PATH_${UP_NAME}} STREQUAL "PATH_${UP_NAME}-NOTFOUND")
|
||||||
message(SEND_ERROR "${LIB_NAME} was not found in ${HINT_PATH}")
|
message(SEND_ERROR "${LIB_NAME} was not found in ${HINT_PATH}")
|
||||||
else()
|
else()
|
||||||
list(APPEND DEPLOY_LIBS ${PATH_${UP_NAME}})
|
list(APPEND DEPLOY_LIBS "${PATH_${UP_NAME}}")
|
||||||
endif()
|
endif()
|
||||||
endmacro()
|
endmacro()
|
||||||
|
|
||||||
|
|||||||
@ -810,6 +810,18 @@ void QMLBackend::login(QString const &username, QString const &password) const {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void QMLBackend::loginHv(QString const &username, QString const &password) const {
|
||||||
|
HANDLE_EXCEPTION(
|
||||||
|
if (username.compare("coco@bandicoot", Qt::CaseInsensitive) == 0) {
|
||||||
|
throw Exception("User requested bridge-gui to crash by trying to log as coco@bandicoot",
|
||||||
|
"This error exists for test purposes and should be ignored.", __func__, tailOfLatestBridgeLog(app().sessionID()));
|
||||||
|
}
|
||||||
|
app().grpc().loginHv(username, password);
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \param[in] username The username.
|
/// \param[in] username The username.
|
||||||
@ -1316,6 +1328,8 @@ void QMLBackend::connectGrpcEvents() {
|
|||||||
connect(client, &GRPCClient::certificateInstallFailed, this, &QMLBackend::certificateInstallFailed);
|
connect(client, &GRPCClient::certificateInstallFailed, this, &QMLBackend::certificateInstallFailed);
|
||||||
connect(client, &GRPCClient::showMainWindow, [&]() { this->showMainWindow("gRPC showMainWindow event"); });
|
connect(client, &GRPCClient::showMainWindow, [&]() { this->showMainWindow("gRPC showMainWindow event"); });
|
||||||
connect(client, &GRPCClient::knowledgeBasSuggestionsReceived, this, &QMLBackend::receivedKnowledgeBaseSuggestions);
|
connect(client, &GRPCClient::knowledgeBasSuggestionsReceived, this, &QMLBackend::receivedKnowledgeBaseSuggestions);
|
||||||
|
connect(client, &GRPCClient::repairStarted, this, &QMLBackend::repairStarted);
|
||||||
|
connect(client, &GRPCClient::allUsersLoaded, this, &QMLBackend::allUsersLoaded);
|
||||||
|
|
||||||
// cache events
|
// cache events
|
||||||
connect(client, &GRPCClient::cantMoveDiskCache, this, &QMLBackend::cantMoveDiskCache);
|
connect(client, &GRPCClient::cantMoveDiskCache, this, &QMLBackend::cantMoveDiskCache);
|
||||||
@ -1334,6 +1348,8 @@ void QMLBackend::connectGrpcEvents() {
|
|||||||
connect(client, &GRPCClient::login2PasswordErrorAbort, this, &QMLBackend::login2PasswordErrorAbort);
|
connect(client, &GRPCClient::login2PasswordErrorAbort, this, &QMLBackend::login2PasswordErrorAbort);
|
||||||
connect(client, &GRPCClient::loginFinished, this, &QMLBackend::onLoginFinished);
|
connect(client, &GRPCClient::loginFinished, this, &QMLBackend::onLoginFinished);
|
||||||
connect(client, &GRPCClient::loginAlreadyLoggedIn, this, &QMLBackend::onLoginAlreadyLoggedIn);
|
connect(client, &GRPCClient::loginAlreadyLoggedIn, this, &QMLBackend::onLoginAlreadyLoggedIn);
|
||||||
|
connect(client, &GRPCClient::loginHvRequested, this, &QMLBackend::loginHvRequested);
|
||||||
|
connect(client, &GRPCClient::loginHvError, this, &QMLBackend::loginHvError);
|
||||||
|
|
||||||
// update events
|
// update events
|
||||||
connect(client, &GRPCClient::updateManualError, this, &QMLBackend::updateManualError);
|
connect(client, &GRPCClient::updateManualError, this, &QMLBackend::updateManualError);
|
||||||
@ -1396,3 +1412,9 @@ void QMLBackend::displayBadEventDialog(QString const &userID) {
|
|||||||
emit showMainWindow();
|
emit showMainWindow();
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void QMLBackend::triggerRepair() const {
|
||||||
|
HANDLE_EXCEPTION(
|
||||||
|
app().grpc().triggerRepair();
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@ -183,6 +183,7 @@ public slots: // slot for signals received from QML -> To be forwarded to Bridge
|
|||||||
void changeColorScheme(QString const &scheme); ///< Slot for the change of the theme.
|
void changeColorScheme(QString const &scheme); ///< Slot for the change of the theme.
|
||||||
void setDiskCachePath(QUrl const &path) const; ///< Slot for the change of the disk cache path.
|
void setDiskCachePath(QUrl const &path) const; ///< Slot for the change of the disk cache path.
|
||||||
void login(QString const &username, QString const &password) const; ///< Slot for the login button (initial login).
|
void login(QString const &username, QString const &password) const; ///< Slot for the login button (initial login).
|
||||||
|
void loginHv(QString const &username, QString const &password) const; ///< Slot for the login button (after HV challenge completed).
|
||||||
void login2FA(QString const &username, QString const &code) const; ///< Slot for the login button (2FA login).
|
void login2FA(QString const &username, QString const &code) const; ///< Slot for the login button (2FA login).
|
||||||
void login2Password(QString const &username, QString const &password) const; ///< Slot for the login button (mailbox password login).
|
void login2Password(QString const &username, QString const &password) const; ///< Slot for the login button (mailbox password login).
|
||||||
void loginAbort(QString const &username) const; ///< Slot for the login abort procedure.
|
void loginAbort(QString const &username) const; ///< Slot for the login abort procedure.
|
||||||
@ -207,6 +208,7 @@ public slots: // slot for signals received from QML -> To be forwarded to Bridge
|
|||||||
void notifyReportBugClicked() const; ///< Slot for the ReportBugClicked gRPC event.
|
void notifyReportBugClicked() const; ///< Slot for the ReportBugClicked gRPC event.
|
||||||
void notifyAutoconfigClicked(QString const &client) const; ///< Slot for gAutoconfigClicked gRPC event.
|
void notifyAutoconfigClicked(QString const &client) const; ///< Slot for gAutoconfigClicked gRPC event.
|
||||||
void notifyExternalLinkClicked(QString const &article) const; ///< Slot for KBArticleClicked gRPC event.
|
void notifyExternalLinkClicked(QString const &article) const; ///< Slot for KBArticleClicked gRPC event.
|
||||||
|
void triggerRepair() const; ///< Slot for the triggering of the bridge repair function i.e. 'resync'.
|
||||||
|
|
||||||
public slots: // slots for functions that need to be processed locally.
|
public slots: // slots for functions that need to be processed locally.
|
||||||
void setNormalTrayIcon(); ///< Set the tray icon to normal.
|
void setNormalTrayIcon(); ///< Set the tray icon to normal.
|
||||||
@ -238,6 +240,8 @@ signals: // Signals received from the Go backend, to be forwarded to QML
|
|||||||
void login2PasswordErrorAbort(QString const &errorMsg); ///< Signal for the 'login2PasswordErrorAbort' gRPC stream event.
|
void login2PasswordErrorAbort(QString const &errorMsg); ///< Signal for the 'login2PasswordErrorAbort' gRPC stream event.
|
||||||
void loginFinished(int index, bool wasSignedOut); ///< Signal for the 'loginFinished' gRPC stream event.
|
void loginFinished(int index, bool wasSignedOut); ///< Signal for the 'loginFinished' gRPC stream event.
|
||||||
void loginAlreadyLoggedIn(int index); ///< Signal for the 'loginAlreadyLoggedIn' gRPC stream event.
|
void loginAlreadyLoggedIn(int index); ///< Signal for the 'loginAlreadyLoggedIn' gRPC stream event.
|
||||||
|
void loginHvRequested(QString const &hvUrl); ///< Signal for the 'loginHvRequested' gRPC stream event.
|
||||||
|
void loginHvError(QString const &errorMsg); ///< Signal for the 'loginHvError' gRPC stream event.
|
||||||
void updateManualReady(QString const &version); ///< Signal for the 'updateManualReady' gRPC stream event.
|
void updateManualReady(QString const &version); ///< Signal for the 'updateManualReady' gRPC stream event.
|
||||||
void updateManualRestartNeeded(); ///< Signal for the 'updateManualRestartNeeded' gRPC stream event.
|
void updateManualRestartNeeded(); ///< Signal for the 'updateManualRestartNeeded' gRPC stream event.
|
||||||
void updateManualError(); ///< Signal for the 'updateManualError' gRPC stream event.
|
void updateManualError(); ///< Signal for the 'updateManualError' gRPC stream event.
|
||||||
@ -279,7 +283,9 @@ signals: // Signals received from the Go backend, to be forwarded to QML
|
|||||||
void selectUser(QString const& userID, bool forceShowWindow); ///< Signal emitted in order to selected a user with a given ID in the list.
|
void selectUser(QString const& userID, bool forceShowWindow); ///< Signal emitted in order to selected a user with a given ID in the list.
|
||||||
void genericError(QString const &title, QString const &description); ///< Signal for the 'genericError' gRPC stream event.
|
void genericError(QString const &title, QString const &description); ///< Signal for the 'genericError' gRPC stream event.
|
||||||
void imapLoginWhileSignedOut(QString const& username); ///< Signal for the notification of IMAP login attempt on a signed out account.
|
void imapLoginWhileSignedOut(QString const& username); ///< Signal for the notification of IMAP login attempt on a signed out account.
|
||||||
void receivedKnowledgeBaseSuggestions(QList<bridgepp::KnowledgeBaseSuggestion> const& suggestions); ///< Signal for the reception of knowledgebase article suggestions.
|
void receivedKnowledgeBaseSuggestions(QList<bridgepp::KnowledgeBaseSuggestion> const& suggestions); ///< Signal for the reception of knowledge base article suggestions.
|
||||||
|
void repairStarted(); ///< Signal for the 'repairStarted' gRPC stream event.
|
||||||
|
void allUsersLoaded(); ///< Signal for the 'allUsersLoaded' gRPC stream event
|
||||||
|
|
||||||
// This signal is emitted when an exception is intercepted is calls triggered by QML. QML engine would intercept the exception otherwise.
|
// This signal is emitted when an exception is intercepted is calls triggered by QML. QML engine would intercept the exception otherwise.
|
||||||
void fatalError(bridgepp::Exception const& e) const; ///< Signal emitted when an fatal error occurs.
|
void fatalError(bridgepp::Exception const& e) const; ///< Signal emitted when an fatal error occurs.
|
||||||
|
|||||||
@ -117,6 +117,7 @@
|
|||||||
<file>qml/Resources/Help/WhyProfileWarning.html</file>
|
<file>qml/Resources/Help/WhyProfileWarning.html</file>
|
||||||
<file>qml/SettingsItem.qml</file>
|
<file>qml/SettingsItem.qml</file>
|
||||||
<file>qml/SettingsView.qml</file>
|
<file>qml/SettingsView.qml</file>
|
||||||
|
<file>qml/SetupWizard/ClientConfigCertInstall.qml</file>
|
||||||
<file>qml/SetupWizard/ClientListItem.qml</file>
|
<file>qml/SetupWizard/ClientListItem.qml</file>
|
||||||
<file>qml/SetupWizard/LeftPane.qml</file>
|
<file>qml/SetupWizard/LeftPane.qml</file>
|
||||||
<file>qml/SetupWizard/ClientConfigAppleMail.qml</file>
|
<file>qml/SetupWizard/ClientConfigAppleMail.qml</file>
|
||||||
|
|||||||
@ -182,6 +182,7 @@ TrayIcon::TrayIcon()
|
|||||||
, notificationErrorIcon_(loadIconFromSVG(":/qml/icons/ic-alert.svg")) {
|
, notificationErrorIcon_(loadIconFromSVG(":/qml/icons/ic-alert.svg")) {
|
||||||
this->generateDotIcons();
|
this->generateDotIcons();
|
||||||
this->setContextMenu(menu_.get());
|
this->setContextMenu(menu_.get());
|
||||||
|
this->setToolTip(PROJECT_FULL_NAME);
|
||||||
|
|
||||||
connect(menu_.get(), &QMenu::aboutToShow, this, &TrayIcon::onMenuAboutToShow);
|
connect(menu_.get(), &QMenu::aboutToShow, this, &TrayIcon::onMenuAboutToShow);
|
||||||
connect(this, &TrayIcon::selectUser, &app().backend(), [](QString const& userID, bool forceShowWindow) {
|
connect(this, &TrayIcon::selectUser, &app().backend(), [](QString const& userID, bool forceShowWindow) {
|
||||||
|
|||||||
@ -102,6 +102,7 @@ git submodule update --init --recursive $vcpkgRoot
|
|||||||
-S . -B $buildDir
|
-S . -B $buildDir
|
||||||
|
|
||||||
check_exit "CMake failed"
|
check_exit "CMake failed"
|
||||||
|
|
||||||
. $cmakeExe --build $buildDir --config "$buildConfig"
|
. $cmakeExe --build $buildDir --config "$buildConfig"
|
||||||
check_exit "Build failed"
|
check_exit "Build failed"
|
||||||
|
|
||||||
@ -109,7 +110,7 @@ if ($($args.count) -gt 0 )
|
|||||||
{
|
{
|
||||||
if ($args[0] = "install")
|
if ($args[0] = "install")
|
||||||
{
|
{
|
||||||
. $cmakeExe --install $buildDir
|
. $cmakeExe --install "$buildDir" -v
|
||||||
check_exit "Install failed"
|
check_exit "Install failed"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,7 +15,6 @@
|
|||||||
// 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/>.
|
||||||
|
|
||||||
|
|
||||||
#include "BridgeApp.h"
|
#include "BridgeApp.h"
|
||||||
#include "BuildConfig.h"
|
#include "BuildConfig.h"
|
||||||
#include "CommandLine.h"
|
#include "CommandLine.h"
|
||||||
@ -30,13 +29,12 @@
|
|||||||
#include <bridgepp/Log/LogUtils.h>
|
#include <bridgepp/Log/LogUtils.h>
|
||||||
#include <bridgepp/ProcessMonitor.h>
|
#include <bridgepp/ProcessMonitor.h>
|
||||||
|
|
||||||
|
#include "bridgepp/CLI/CLIUtils.h"
|
||||||
|
|
||||||
#ifdef Q_OS_MACOS
|
#ifdef Q_OS_MACOS
|
||||||
|
|
||||||
|
|
||||||
#include "MacOS/SecondInstance.h"
|
#include "MacOS/SecondInstance.h"
|
||||||
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
using namespace bridgepp;
|
using namespace bridgepp;
|
||||||
@ -50,17 +48,14 @@ QString const exeSuffix = ".exe";
|
|||||||
QString const exeSuffix;
|
QString const exeSuffix;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
||||||
QString const bridgeLock = "bridge-v3.lock"; ///< The file name used for the bridge-gui lock file.
|
QString const bridgeLock = "bridge-v3.lock"; ///< The file name used for the bridge-gui lock file.
|
||||||
QString const bridgeGUILock = "bridge-v3-gui.lock"; ///< The file name used for the bridge-gui lock file.
|
QString const bridgeGUILock = "bridge-v3-gui.lock"; ///< The file name used for the bridge-gui lock file.
|
||||||
QString const exeName = "bridge" + exeSuffix; ///< The bridge executable file name.*
|
QString const exeName = "bridge" + exeSuffix; ///< The bridge executable file name.*
|
||||||
qint64 const grpcServiceConfigWaitDelayMs = 180000; ///< The wait delay for the gRPC config file in milliseconds.
|
qint64 constexpr grpcServiceConfigWaitDelayMs = 180000; ///< The wait delay for the gRPC config file in milliseconds.
|
||||||
QString const waitFlag = "--wait"; ///< The wait command-line flag.
|
QString const waitFlag = "--wait"; ///< The wait command-line flag.
|
||||||
|
|
||||||
|
|
||||||
} // anonymous namespace
|
} // anonymous namespace
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \return The path of the bridge executable.
|
/// \return The path of the bridge executable.
|
||||||
/// \return A null string if the executable could not be located.
|
/// \return A null string if the executable could not be located.
|
||||||
@ -70,7 +65,6 @@ QString locateBridgeExe() {
|
|||||||
return (fileInfo.exists() && fileInfo.isFile() && fileInfo.isExecutable()) ? fileInfo.absoluteFilePath() : QString();
|
return (fileInfo.exists() && fileInfo.isFile() && fileInfo.isExecutable()) ? fileInfo.absoluteFilePath() : QString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// // initialize the Qt application.
|
/// // initialize the Qt application.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
@ -97,8 +91,6 @@ void initQtApplication() {
|
|||||||
#endif // #ifdef Q_OS_MACOS
|
#endif // #ifdef Q_OS_MACOS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \param[in] engine The QML component.
|
/// \param[in] engine The QML component.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
@ -118,13 +110,12 @@ QQmlComponent *createRootQmlComponent(QQmlApplicationEngine &engine) {
|
|||||||
rootComponent->loadUrl(QUrl(qrcQmlDir + "/Bridge.qml"));
|
rootComponent->loadUrl(QUrl(qrcQmlDir + "/Bridge.qml"));
|
||||||
if (rootComponent->status() != QQmlComponent::Status::Ready) {
|
if (rootComponent->status() != QQmlComponent::Status::Ready) {
|
||||||
QString const &err = rootComponent->errorString();
|
QString const &err = rootComponent->errorString();
|
||||||
app().log().error(err);
|
app().log().error(err);
|
||||||
throw Exception("Could not load QML component", err);
|
throw Exception("Could not load QML component", err);
|
||||||
}
|
}
|
||||||
return rootComponent;
|
return rootComponent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \param[in] lock The lock file to be checked.
|
/// \param[in] lock The lock file to be checked.
|
||||||
/// \return True if the lock can be taken, false otherwise.
|
/// \return True if the lock can be taken, false otherwise.
|
||||||
@ -155,7 +146,6 @@ bool checkSingleInstance(QLockFile &lock) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \return QUrl to reach the bridge API.
|
/// \return QUrl to reach the bridge API.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
@ -184,7 +174,6 @@ QUrl getApiUrl() {
|
|||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \brief Check if bridge is running.
|
/// \brief Check if bridge is running.
|
||||||
///
|
///
|
||||||
@ -199,7 +188,6 @@ bool isBridgeRunning() {
|
|||||||
return (!lockFile.tryLock()) && (lockFile.error() == QLockFile::LockFailedError);
|
return (!lockFile.tryLock()) && (lockFile.error() == QLockFile::LockFailedError);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \brief Use api to bring focus on existing bridge instance.
|
/// \brief Use api to bring focus on existing bridge instance.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
@ -213,8 +201,7 @@ void focusOtherInstance() {
|
|||||||
if (!sc.load(path)) {
|
if (!sc.load(path)) {
|
||||||
throw Exception("The gRPC focus service configuration file is invalid.");
|
throw Exception("The gRPC focus service configuration file is invalid.");
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
throw Exception("Server did not provide gRPC Focus service configuration.");
|
throw Exception("Server did not provide gRPC Focus service configuration.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -225,20 +212,18 @@ void focusOtherInstance() {
|
|||||||
if (!client.raise("focusOtherInstance").ok()) {
|
if (!client.raise("focusOtherInstance").ok()) {
|
||||||
throw Exception(QString("The raise call to the bridge focus service failed."));
|
throw Exception(QString("The raise call to the bridge focus service failed."));
|
||||||
}
|
}
|
||||||
}
|
} catch (Exception const &e) {
|
||||||
catch (Exception const &e) {
|
|
||||||
app().log().error(e.qwhat());
|
app().log().error(e.qwhat());
|
||||||
auto uuid = reportSentryException("Exception occurred during focusOtherInstance()", e);
|
auto uuid = reportSentryException("Exception occurred during focusOtherInstance()", e);
|
||||||
app().log().fatal(QString("reportID: %1 Captured exception: %2").arg(QByteArray(uuid.bytes, 16).toHex(), e.qwhat()));
|
app().log().fatal(QString("reportID: %1 Captured exception: %2").arg(QByteArray(uuid.bytes, 16).toHex(), e.qwhat()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \param [in] args list of arguments to pass to bridge.
|
/// \param [in] args list of arguments to pass to bridge.
|
||||||
/// \return bridge executable path
|
/// \return bridge executable path
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
const QString launchBridge(QStringList const &args) {
|
QString launchBridge(QStringList const &args) {
|
||||||
UPOverseer &overseer = app().bridgeOverseer();
|
UPOverseer &overseer = app().bridgeOverseer();
|
||||||
overseer.reset();
|
overseer.reset();
|
||||||
|
|
||||||
@ -251,26 +236,38 @@ const QString launchBridge(QStringList const &args) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
qint64 const pid = qApp->applicationPid();
|
qint64 const pid = qApp->applicationPid();
|
||||||
QStringList const params = QStringList { "--grpc", "--parent-pid", QString::number(pid) } + args;
|
QStringList const params = QStringList{"--grpc", "--parent-pid", QString::number(pid)} + args;
|
||||||
app().log().info(QString("Launching bridge process with command \"%1\" %2").arg(bridgeExePath, params.join(" ")));
|
app().log().info(QString("Launching bridge process with command \"%1\" %2").arg(bridgeExePath, params.join(" ")));
|
||||||
overseer = std::make_unique<Overseer>(new ProcessMonitor(bridgeExePath, params, nullptr), nullptr);
|
overseer = std::make_unique<Overseer>(new ProcessMonitor(bridgeExePath, params, nullptr), nullptr);
|
||||||
overseer->startWorker(true);
|
overseer->startWorker(true);
|
||||||
return bridgeExePath;
|
return bridgeExePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
//
|
//
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
void closeBridgeApp() {
|
void closeBridgeApp() {
|
||||||
app().grpc().quit(); // this will cause the grpc service and the bridge app to close.
|
app().grpc().quit(); // this will cause the grpc service and the bridge app to close.
|
||||||
|
|
||||||
UPOverseer &overseer = app().bridgeOverseer();
|
UPOverseer const &overseer = app().bridgeOverseer();
|
||||||
if (overseer) { // A null overseer means the app was run in 'attach' mode. We're not monitoring it.
|
if (overseer) {
|
||||||
|
// A null overseer means the app was run in 'attach' mode. We're not monitoring it.
|
||||||
|
// ReSharper disable once CppExpressionWithoutSideEffects
|
||||||
overseer->wait(Overseer::maxTerminationWaitTimeMs);
|
overseer->wait(Overseer::maxTerminationWaitTimeMs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \param[in] argv The command-line argments, including the application name at index 0.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
void logCommandLineInvocation(QStringList argv) {
|
||||||
|
Log &log = app().log();
|
||||||
|
if (argv.isEmpty()) {
|
||||||
|
log.error("The command line is empty");
|
||||||
|
}
|
||||||
|
log.info("bridge-gui executable: " + argv.front());
|
||||||
|
log.info("Command-line invocation: " + (argv.size() > 1 ? argv.last(argv.size() - 1).join(" ") : "<none>"));
|
||||||
|
}
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \param[in] argc The number of command-line arguments.
|
/// \param[in] argc The number of command-line arguments.
|
||||||
@ -289,12 +286,11 @@ int main(int argc, char *argv[]) {
|
|||||||
auto sentryCloser = qScopeGuard([] { sentry_close(); });
|
auto sentryCloser = qScopeGuard([] { sentry_close(); });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
QString const& configDir = bridgepp::userConfigDir();
|
QString const &configDir = bridgepp::userConfigDir();
|
||||||
|
|
||||||
|
|
||||||
initQtApplication();
|
initQtApplication();
|
||||||
|
QStringList const argvList = cliArgsToStringList(argc, argv);
|
||||||
CommandLineOptions const cliOptions = parseCommandLine(argc, argv);
|
CommandLineOptions const cliOptions = parseCommandLine(argvList);
|
||||||
Log &log = initLog();
|
Log &log = initLog();
|
||||||
log.setLevel(cliOptions.logLevel);
|
log.setLevel(cliOptions.logLevel);
|
||||||
|
|
||||||
@ -309,6 +305,8 @@ int main(int argc, char *argv[]) {
|
|||||||
setDockIconVisibleState(!cliOptions.noWindow);
|
setDockIconVisibleState(!cliOptions.noWindow);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
logCommandLineInvocation(argvList);
|
||||||
|
|
||||||
// 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.
|
||||||
@ -348,7 +346,6 @@ int main(int argc, char *argv[]) {
|
|||||||
QQuickWindow::setSceneGraphBackend((app().settings().useSoftwareRenderer() || cliOptions.useSoftwareRenderer) ? "software" : "rhi");
|
QQuickWindow::setSceneGraphBackend((app().settings().useSoftwareRenderer() || cliOptions.useSoftwareRenderer) ? "software" : "rhi");
|
||||||
log.info(QString("Qt Quick renderer: %1").arg(QQuickWindow::sceneGraphBackend()));
|
log.info(QString("Qt Quick renderer: %1").arg(QQuickWindow::sceneGraphBackend()));
|
||||||
|
|
||||||
|
|
||||||
QQmlApplicationEngine engine;
|
QQmlApplicationEngine engine;
|
||||||
std::unique_ptr<QQmlComponent> rootComponent(createRootQmlComponent(engine));
|
std::unique_ptr<QQmlComponent> rootComponent(createRootQmlComponent(engine));
|
||||||
std::unique_ptr<QObject> rootObject(rootComponent->create(engine.rootContext()));
|
std::unique_ptr<QObject> rootObject(rootComponent->create(engine.rootContext()));
|
||||||
@ -374,7 +371,7 @@ int main(int argc, char *argv[]) {
|
|||||||
app().log().debug(QString("Monitoring Bridge PID : %1").arg(status.pid));
|
app().log().debug(QString("Monitoring Bridge PID : %1").arg(status.pid));
|
||||||
|
|
||||||
connection = QObject::connect(bridgeMonitor, &ProcessMonitor::processExited, [&](int returnCode) {
|
connection = QObject::connect(bridgeMonitor, &ProcessMonitor::processExited, [&](int returnCode) {
|
||||||
bridgeExited = true;// clazy:exclude=lambda-in-connect
|
bridgeExited = true; // clazy:exclude=lambda-in-connect
|
||||||
qGuiApp->exit(returnCode);
|
qGuiApp->exit(returnCode);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -383,7 +380,7 @@ int main(int argc, char *argv[]) {
|
|||||||
int result = 0;
|
int result = 0;
|
||||||
if (!startError) {
|
if (!startError) {
|
||||||
// we succeeded in launching bridge, so we can be set as mainExecutable.
|
// we succeeded in launching bridge, so we can be set as mainExecutable.
|
||||||
QString mainexec = QString::fromLocal8Bit(argv[0]);
|
QString const mainexec = argvList[0];
|
||||||
app().grpc().setMainExecutable(mainexec);
|
app().grpc().setMainExecutable(mainexec);
|
||||||
QStringList args = cliOptions.bridgeGuiArgs;
|
QStringList args = cliOptions.bridgeGuiArgs;
|
||||||
args.append(waitFlag);
|
args.append(waitFlag);
|
||||||
@ -412,8 +409,7 @@ int main(int argc, char *argv[]) {
|
|||||||
// release the lock file
|
// release the lock file
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
return result;
|
return result;
|
||||||
}
|
} catch (Exception const &e) {
|
||||||
catch (Exception const &e) {
|
|
||||||
sentry_uuid_s const uuid = reportSentryException("Exception occurred during main", e);
|
sentry_uuid_s const uuid = reportSentryException("Exception occurred during main", e);
|
||||||
QString message = e.qwhat();
|
QString message = e.qwhat();
|
||||||
if (e.showSupportLink()) {
|
if (e.showSupportLink()) {
|
||||||
|
|||||||
@ -21,6 +21,8 @@ SettingsView {
|
|||||||
|
|
||||||
property bool _isAdvancedShown: false
|
property bool _isAdvancedShown: false
|
||||||
property var notifications
|
property var notifications
|
||||||
|
property var allUsersLoaded: false
|
||||||
|
property var hasInternetConnection: true
|
||||||
|
|
||||||
fillHeight: false
|
fillHeight: false
|
||||||
|
|
||||||
@ -219,6 +221,37 @@ SettingsView {
|
|||||||
Backend.exportTLSCertificates();
|
Backend.exportTLSCertificates();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
SettingsItem {
|
||||||
|
id: repair
|
||||||
|
Layout.fillWidth: true
|
||||||
|
actionText: qsTr("Repair")
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
description: qsTr("Reload all accounts, cached data, and download all emails again. Email clients stay connected to Bridge.")
|
||||||
|
text: qsTr("Repair Bridge")
|
||||||
|
type: SettingsItem.Button
|
||||||
|
visible: root._isAdvancedShown
|
||||||
|
enabled: root.allUsersLoaded && Backend.users.count && root.hasInternetConnection
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
root.notifications.askRepairBridge();
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
function onInternetOff() {
|
||||||
|
root.hasInternetConnection = false;
|
||||||
|
repair.description = qsTr("This feature requires internet access to the Proton servers.")
|
||||||
|
|
||||||
|
}
|
||||||
|
function onInternetOn() {
|
||||||
|
root.hasInternetConnection = true;
|
||||||
|
repair.description = qsTr("Reload all accounts, cached data, and download all emails again. Email clients stay connected to Bridge.")
|
||||||
|
}
|
||||||
|
function onAllUsersLoaded() {
|
||||||
|
root.allUsersLoaded = true;
|
||||||
|
}
|
||||||
|
target: Backend
|
||||||
|
}
|
||||||
|
}
|
||||||
SettingsItem {
|
SettingsItem {
|
||||||
id: reset
|
id: reset
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|||||||
@ -105,4 +105,8 @@ Item {
|
|||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notification: root.notifications.genericQuestion
|
notification: root.notifications.genericQuestion
|
||||||
}
|
}
|
||||||
|
NotificationDialog {
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
notification: root.notifications.repairBridge
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,8 @@
|
|||||||
import QtQml
|
import QtQml
|
||||||
import Qt.labs.platform
|
import Qt.labs.platform
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import QtQuick
|
||||||
import "../"
|
import "../"
|
||||||
|
|
||||||
QtObject {
|
QtObject {
|
||||||
@ -60,7 +62,7 @@ QtObject {
|
|||||||
target: Backend
|
target: Backend
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
property var all: [root.noInternet, root.imapPortStartupError, root.smtpPortStartupError, root.imapPortChangeError, root.smtpPortChangeError, root.imapConnectionModeChangeError, root.smtpConnectionModeChangeError, root.updateManualReady, root.updateManualRestartNeeded, root.updateManualError, root.updateForce, root.updateForceError, root.updateSilentRestartNeeded, root.updateSilentError, root.updateIsLatestVersion, root.loginConnectionError, root.onlyPaidUsers, root.alreadyLoggedIn, root.enableBeta, root.bugReportSendSuccess, root.bugReportSendError, root.bugReportSendFallback, root.cacheCantMove, root.cacheLocationChangeSuccess, root.enableSplitMode, root.resetBridge, root.changeAllMailVisibility, root.deleteAccount, root.noKeychain, root.rebuildKeychain, root.addressChanged, root.apiCertIssue, root.userBadEvent, root.imapLoginWhileSignedOut, root.genericError, root.genericQuestion]
|
property var all: [root.noInternet, root.imapPortStartupError, root.smtpPortStartupError, root.imapPortChangeError, root.smtpPortChangeError, root.imapConnectionModeChangeError, root.smtpConnectionModeChangeError, root.updateManualReady, root.updateManualRestartNeeded, root.updateManualError, root.updateForce, root.updateForceError, root.updateSilentRestartNeeded, root.updateSilentError, root.updateIsLatestVersion, root.loginConnectionError, root.onlyPaidUsers, root.alreadyLoggedIn, root.enableBeta, root.bugReportSendSuccess, root.bugReportSendError, root.bugReportSendFallback, root.cacheCantMove, root.cacheLocationChangeSuccess, root.enableSplitMode, root.resetBridge, root.changeAllMailVisibility, root.deleteAccount, root.noKeychain, root.rebuildKeychain, root.addressChanged, root.apiCertIssue, root.userBadEvent, root.imapLoginWhileSignedOut, root.genericError, root.genericQuestion, root.hvErrorEvent, root.repairBridge]
|
||||||
property Notification alreadyLoggedIn: Notification {
|
property Notification alreadyLoggedIn: Notification {
|
||||||
brief: qsTr("Already signed in")
|
brief: qsTr("Already signed in")
|
||||||
description: qsTr("This account is already signed in.")
|
description: qsTr("This account is already signed in.")
|
||||||
@ -1130,6 +1132,73 @@ QtObject {
|
|||||||
target: Backend
|
target: Backend
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
property Notification hvErrorEvent: Notification {
|
||||||
|
group: Notifications.Group.Configuration
|
||||||
|
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||||
|
type: Notification.NotificationType.Danger
|
||||||
|
|
||||||
|
action: Action {
|
||||||
|
text: qsTr("OK")
|
||||||
|
onTriggered: {
|
||||||
|
root.hvErrorEvent.active = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
function onLoginHvError(errorMsg) {
|
||||||
|
root.hvErrorEvent.active = true;
|
||||||
|
root.hvErrorEvent.description = errorMsg;
|
||||||
|
}
|
||||||
|
target: Backend
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
property Notification repairBridge: Notification {
|
||||||
|
brief: title
|
||||||
|
description: qsTr("This action will reload all accounts, cached data, and re-download emails. Messages may temporarily disappear but will reappear progressively. Email clients stay connected to Bridge.")
|
||||||
|
group: Notifications.Group.Configuration | Notifications.Group.Dialogs
|
||||||
|
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||||
|
title: qsTr("Repair Bridge?")
|
||||||
|
type: Notification.NotificationType.Danger
|
||||||
|
|
||||||
|
action: [
|
||||||
|
Action {
|
||||||
|
id: repairBridge_cancel
|
||||||
|
text: qsTr("Cancel")
|
||||||
|
onTriggered: {
|
||||||
|
root.repairBridge.active = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Action {
|
||||||
|
id: repairBridge_repair
|
||||||
|
text: qsTr("Repair")
|
||||||
|
onTriggered: {
|
||||||
|
repairBridge_repair.loading = true;
|
||||||
|
repairBridge_repair.enabled = false;
|
||||||
|
repairBridge_cancel.enabled = false;
|
||||||
|
Backend.triggerRepair();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
function onAskRepairBridge() {
|
||||||
|
root.repairBridge.active = true;
|
||||||
|
}
|
||||||
|
target: root
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
function onRepairStarted() {
|
||||||
|
root.repairBridge.active = false;
|
||||||
|
repairBridge_repair.loading = false;
|
||||||
|
repairBridge_repair.enabled = true;
|
||||||
|
repairBridge_cancel.enabled = true;
|
||||||
|
}
|
||||||
|
target: Backend
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
signal askChangeAllMailVisibility(var isVisibleNow)
|
signal askChangeAllMailVisibility(var isVisibleNow)
|
||||||
signal askDeleteAccount(var user)
|
signal askDeleteAccount(var user)
|
||||||
@ -1137,4 +1206,5 @@ QtObject {
|
|||||||
signal askEnableSplitMode(var user)
|
signal askEnableSplitMode(var user)
|
||||||
signal askQuestion(var title, var description, var option1, var option2, var action1, var action2)
|
signal askQuestion(var title, var description, var option1, var option2, var action1, var action2)
|
||||||
signal askResetBridge
|
signal askResetBridge
|
||||||
|
signal askRepairBridge
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,256 +17,77 @@ import QtQuick.Controls
|
|||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
enum Screen {
|
|
||||||
CertificateInstall,
|
|
||||||
ProfileInstall
|
|
||||||
}
|
|
||||||
|
|
||||||
property var wizard
|
property var wizard
|
||||||
|
|
||||||
signal appleMailAutoconfigCertificateInstallPageShown
|
property bool profilePaneLaunched: false
|
||||||
signal appleMailAutoconfigProfileInstallPageShow
|
|
||||||
|
|
||||||
function showAutoconfig() {
|
function reset() {
|
||||||
if (Backend.isTLSCertificateInstalled()) {
|
profilePaneLaunched = false;
|
||||||
showProfileInstall();
|
|
||||||
} else {
|
|
||||||
showCertificateInstall();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function showCertificateInstall() {
|
|
||||||
certificateInstall.reset();
|
|
||||||
stack.currentIndex = ClientConfigAppleMail.Screen.CertificateInstall;
|
|
||||||
appleMailAutoconfigCertificateInstallPageShown();
|
|
||||||
}
|
|
||||||
function showProfileInstall() {
|
|
||||||
profileInstall.reset();
|
|
||||||
stack.currentIndex = ClientConfigAppleMail.Screen.ProfileInstall;
|
|
||||||
appleMailAutoconfigProfileInstallPageShow();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
StackLayout {
|
ColumnLayout {
|
||||||
id: stack
|
anchors.left: parent.left
|
||||||
anchors.fill: parent
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: ProtonStyle.wizard_spacing_large
|
||||||
|
|
||||||
// stack index 0
|
ColumnLayout {
|
||||||
Item {
|
|
||||||
id: certificateInstall
|
|
||||||
|
|
||||||
property string errorString: ""
|
|
||||||
property bool showBugReportLink: false
|
|
||||||
property bool waitingForCert: false
|
|
||||||
|
|
||||||
function clearError() {
|
|
||||||
errorString = "";
|
|
||||||
showBugReportLink = false;
|
|
||||||
}
|
|
||||||
function reset() {
|
|
||||||
waitingForCert = false;
|
|
||||||
clearError();
|
|
||||||
}
|
|
||||||
|
|
||||||
Layout.fillHeight: true
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
spacing: ProtonStyle.wizard_spacing_medium
|
||||||
|
|
||||||
ColumnLayout {
|
Label {
|
||||||
anchors.left: parent.left
|
Layout.alignment: Qt.AlignHCenter
|
||||||
anchors.right: parent.right
|
Layout.fillWidth: true
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
colorScheme: wizard.colorScheme
|
||||||
spacing: ProtonStyle.wizard_spacing_large
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
text: qsTr("Install the profile")
|
||||||
Connections {
|
type: Label.LabelType.Title
|
||||||
function onCertificateInstallCanceled() {
|
wrapMode: Text.WordWrap
|
||||||
certificateInstall.waitingForCert = false;
|
}
|
||||||
certificateInstall.errorString = qsTr("Apple Mail cannot be configured if you do not install the certificate. Please retry.");
|
Label {
|
||||||
certificateInstall.showBugReportLink = false;
|
Layout.alignment: Qt.AlignHCenter
|
||||||
}
|
Layout.fillWidth: true
|
||||||
function onCertificateInstallFailed() {
|
color: colorScheme.text_weak
|
||||||
certificateInstall.waitingForCert = false;
|
colorScheme: wizard.colorScheme
|
||||||
certificateInstall.errorString = qsTr("An error occurred while installing the certificate.");
|
horizontalAlignment: Text.AlignHCenter
|
||||||
certificateInstall.showBugReportLink = true;
|
text: qsTr("A system pop-up will appear. Double click on the entry with your email, and click ’Install’ in the dialog that appears.")
|
||||||
}
|
type: Label.LabelType.Body
|
||||||
function onCertificateInstallSuccess() {
|
wrapMode: Text.WordWrap
|
||||||
certificateInstall.reset();
|
|
||||||
root.showAutoconfig();
|
|
||||||
}
|
|
||||||
|
|
||||||
target: Backend
|
|
||||||
}
|
|
||||||
ColumnLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: ProtonStyle.wizard_spacing_medium
|
|
||||||
|
|
||||||
Label {
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
Layout.fillWidth: true
|
|
||||||
colorScheme: wizard.colorScheme
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
text: qsTr("Install the bridge certificate")
|
|
||||||
type: Label.LabelType.Title
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
Label {
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
Layout.fillWidth: true
|
|
||||||
color: colorScheme.text_weak
|
|
||||||
colorScheme: wizard.colorScheme
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
text: qsTr("After clicking on the button below, a system pop-up will ask you for your credentials, please enter your macOS user credentials (not your Proton account’s) and validate.")
|
|
||||||
type: Label.LabelType.Body
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Image {
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
height: 182
|
|
||||||
opacity: certificateInstall.waitingForCert ? 0.3 : 1.0
|
|
||||||
source: "/qml/icons/img-macos-cert-screenshot.png"
|
|
||||||
width: 140
|
|
||||||
}
|
|
||||||
ColumnLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: ProtonStyle.wizard_spacing_medium
|
|
||||||
|
|
||||||
Button {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
colorScheme: wizard.colorScheme
|
|
||||||
enabled: !certificateInstall.waitingForCert
|
|
||||||
loading: certificateInstall.waitingForCert
|
|
||||||
text: qsTr("Install the certificate")
|
|
||||||
|
|
||||||
onClicked: {
|
|
||||||
certificateInstall.clearError();
|
|
||||||
certificateInstall.waitingForCert = true;
|
|
||||||
Backend.installTLSCertificate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Button {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
colorScheme: wizard.colorScheme
|
|
||||||
enabled: !certificateInstall.waitingForCert
|
|
||||||
secondary: true
|
|
||||||
text: qsTr("Cancel")
|
|
||||||
|
|
||||||
onClicked: {
|
|
||||||
wizard.closeWizard();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ColumnLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: ProtonStyle.wizard_spacing_small
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: ProtonStyle.wizard_spacing_extra_small
|
|
||||||
|
|
||||||
ColorImage {
|
|
||||||
color: wizard.colorScheme.signal_danger
|
|
||||||
height: errorLabel.lineHeight
|
|
||||||
source: "/qml/icons/ic-exclamation-circle-filled.svg"
|
|
||||||
sourceSize.height: errorLabel.lineHeight
|
|
||||||
visible: certificateInstall.errorString.length > 0
|
|
||||||
}
|
|
||||||
Label {
|
|
||||||
id: errorLabel
|
|
||||||
Layout.fillWidth: true
|
|
||||||
color: wizard.colorScheme.signal_danger
|
|
||||||
colorScheme: wizard.colorScheme
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
text: certificateInstall.errorString
|
|
||||||
type: Label.LabelType.Body_semibold
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LinkLabel {
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
callback: wizard.showBugReport
|
|
||||||
colorScheme: wizard.colorScheme
|
|
||||||
link: "#"
|
|
||||||
text: qsTr("Report the problem")
|
|
||||||
visible: certificateInstall.showBugReportLink
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// stack index 1
|
Image {
|
||||||
Item {
|
Layout.alignment: Qt.AlignHCenter
|
||||||
id: profileInstall
|
height: 102
|
||||||
|
source: "/qml/icons/img-macos-profile-screenshot.png"
|
||||||
property bool profilePaneLaunched: false
|
width: 364
|
||||||
|
}
|
||||||
function reset() {
|
ColumnLayout {
|
||||||
profilePaneLaunched = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Layout.fillHeight: true
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
spacing: ProtonStyle.wizard_spacing_medium
|
||||||
|
|
||||||
ColumnLayout {
|
Button {
|
||||||
anchors.left: parent.left
|
Layout.fillWidth: true
|
||||||
anchors.right: parent.right
|
colorScheme: wizard.colorScheme
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
text: profilePaneLaunched ? qsTr("I have installed the profile") : qsTr("Install the profile")
|
||||||
spacing: ProtonStyle.wizard_spacing_large
|
|
||||||
|
|
||||||
ColumnLayout {
|
onClicked: {
|
||||||
Layout.fillWidth: true
|
if (profilePaneLaunched) {
|
||||||
spacing: ProtonStyle.wizard_spacing_medium
|
wizard.showClientConfigEnd();
|
||||||
|
} else {
|
||||||
Label {
|
wizard.user.configureAppleMail(wizard.address);
|
||||||
Layout.alignment: Qt.AlignHCenter
|
profilePaneLaunched = true;
|
||||||
Layout.fillWidth: true
|
|
||||||
colorScheme: wizard.colorScheme
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
text: qsTr("Install the profile")
|
|
||||||
type: Label.LabelType.Title
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
Label {
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
Layout.fillWidth: true
|
|
||||||
color: colorScheme.text_weak
|
|
||||||
colorScheme: wizard.colorScheme
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
text: qsTr("A system pop-up will appear. Double click on the entry with your email, and click ’Install’ in the dialog that appears.")
|
|
||||||
type: Label.LabelType.Body
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Image {
|
}
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Button {
|
||||||
height: 102
|
Layout.fillWidth: true
|
||||||
source: "/qml/icons/img-macos-profile-screenshot.png"
|
colorScheme: wizard.colorScheme
|
||||||
width: 364
|
secondary: true
|
||||||
}
|
text: qsTr("Cancel")
|
||||||
ColumnLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: ProtonStyle.wizard_spacing_medium
|
|
||||||
|
|
||||||
Button {
|
onClicked: {
|
||||||
Layout.fillWidth: true
|
wizard.closeWizard();
|
||||||
colorScheme: wizard.colorScheme
|
|
||||||
text: profileInstall.profilePaneLaunched ? qsTr("I have installed the profile") : qsTr("Install the profile")
|
|
||||||
|
|
||||||
onClicked: {
|
|
||||||
if (profileInstall.profilePaneLaunched) {
|
|
||||||
wizard.showClientConfigEnd();
|
|
||||||
} else {
|
|
||||||
wizard.user.configureAppleMail(wizard.address);
|
|
||||||
profileInstall.profilePaneLaunched = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Button {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
colorScheme: wizard.colorScheme
|
|
||||||
secondary: true
|
|
||||||
text: qsTr("Cancel")
|
|
||||||
|
|
||||||
onClicked: {
|
|
||||||
wizard.closeWizard();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,153 @@
|
|||||||
|
// Copyright (c) 2024 Proton AG
|
||||||
|
// This file is part of Proton Mail Bridge.
|
||||||
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import QtQml
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import QtQuick.Controls
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string errorString: ""
|
||||||
|
property bool showBugReportLink: false
|
||||||
|
property bool waitingForCert: false
|
||||||
|
property var wizard
|
||||||
|
|
||||||
|
function clearError() {
|
||||||
|
errorString = "";
|
||||||
|
showBugReportLink = false;
|
||||||
|
}
|
||||||
|
function reset() {
|
||||||
|
waitingForCert = false;
|
||||||
|
clearError();
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: ProtonStyle.wizard_spacing_large
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
function onCertificateInstallCanceled() {
|
||||||
|
root.waitingForCert = false;
|
||||||
|
root.errorString = qsTr("%1 cannot be configured if you do not install the certificate. Please retry.").arg(wizard.clientName());
|
||||||
|
root.showBugReportLink = false;
|
||||||
|
}
|
||||||
|
function onCertificateInstallFailed() {
|
||||||
|
root.waitingForCert = false;
|
||||||
|
root.errorString = qsTr("An error occurred while installing the certificate.");
|
||||||
|
root.showBugReportLink = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
target: Backend
|
||||||
|
}
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: ProtonStyle.wizard_spacing_medium
|
||||||
|
|
||||||
|
Label {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
Layout.fillWidth: true
|
||||||
|
colorScheme: wizard.colorScheme
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
text: qsTr("Install the bridge certificate")
|
||||||
|
type: Label.LabelType.Title
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
Layout.fillWidth: true
|
||||||
|
color: colorScheme.text_weak
|
||||||
|
colorScheme: wizard.colorScheme
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
text: qsTr("After clicking on the button below, a system pop-up will ask you for your credentials, please enter your macOS user credentials (not your Proton account’s) and validate.")
|
||||||
|
type: Label.LabelType.Body
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Image {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
height: 182
|
||||||
|
opacity: root.waitingForCert ? 0.3 : 1.0
|
||||||
|
source: "/qml/icons/img-macos-cert-screenshot.png"
|
||||||
|
width: 140
|
||||||
|
}
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: ProtonStyle.wizard_spacing_medium
|
||||||
|
|
||||||
|
Button {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
colorScheme: wizard.colorScheme
|
||||||
|
enabled: !root.waitingForCert
|
||||||
|
loading: root.waitingForCert
|
||||||
|
text: qsTr("Install the certificate")
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
root.clearError();
|
||||||
|
root.waitingForCert = true;
|
||||||
|
Backend.installTLSCertificate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
colorScheme: wizard.colorScheme
|
||||||
|
enabled: !root.waitingForCert
|
||||||
|
secondary: true
|
||||||
|
text: qsTr("Cancel")
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
wizard.closeWizard();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: ProtonStyle.wizard_spacing_small
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: ProtonStyle.wizard_spacing_extra_small
|
||||||
|
|
||||||
|
ColorImage {
|
||||||
|
color: wizard.colorScheme.signal_danger
|
||||||
|
height: errorLabel.lineHeight
|
||||||
|
source: "/qml/icons/ic-exclamation-circle-filled.svg"
|
||||||
|
sourceSize.height: errorLabel.lineHeight
|
||||||
|
visible: root.errorString.length > 0
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
id: errorLabel
|
||||||
|
Layout.fillWidth: true
|
||||||
|
color: wizard.colorScheme.signal_danger
|
||||||
|
colorScheme: wizard.colorScheme
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
text: root.errorString
|
||||||
|
type: Label.LabelType.Body_semibold
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LinkLabel {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
callback: wizard.showBugReport
|
||||||
|
colorScheme: wizard.colorScheme
|
||||||
|
link: "#"
|
||||||
|
text: qsTr("Report the problem")
|
||||||
|
visible: root.showBugReportLink
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -47,6 +47,10 @@ Item {
|
|||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
wizard.client = SetupWizard.Client.AppleMail;
|
wizard.client = SetupWizard.Client.AppleMail;
|
||||||
|
if (!Backend.isTLSCertificateInstalled()) {
|
||||||
|
wizard.showCertInstall();
|
||||||
|
return
|
||||||
|
}
|
||||||
wizard.showAppleMailAutoConfig();
|
wizard.showAppleMailAutoConfig();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -59,6 +63,10 @@ Item {
|
|||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
wizard.client = SetupWizard.Client.MicrosoftOutlook;
|
wizard.client = SetupWizard.Client.MicrosoftOutlook;
|
||||||
|
if (root.onMacOS && !Backend.isTLSCertificateInstalled()) {
|
||||||
|
wizard.showCertInstall();
|
||||||
|
return
|
||||||
|
}
|
||||||
wizard.showClientParams();
|
wizard.showClientParams();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,13 +34,23 @@ Item {
|
|||||||
|
|
||||||
signal startSetup()
|
signal startSetup()
|
||||||
|
|
||||||
function showAppleMailAutoconfigCertificateInstall() {
|
function showCertificateInstall() {
|
||||||
showAppleMailAutoconfigCommon();
|
showClientConfigCommon();
|
||||||
descriptionLabel.text = qsTr("Apple Mail configuration is mostly automated, but in order to work, Bridge needs to install a certificate in your keychain.");
|
if (wizard.client === SetupWizard.Client.AppleMail) {
|
||||||
linkLabel1.setCallback(function() { Backend.openExternalLink("https://proton.me/support/apple-mail-certificate"); }, qsTr("Why is this certificate needed?"), true);
|
descriptionLabel.text = qsTr("Apple Mail configuration is mostly automated, but in order to work, Bridge needs to install a certificate in your keychain.");
|
||||||
|
linkLabel1.setCallback(function () {
|
||||||
|
Backend.openExternalLink("https://proton.me/support/apple-mail-certificate");
|
||||||
|
}, qsTr("Why is this certificate needed?"), true);
|
||||||
|
} else {
|
||||||
|
descriptionLabel.text = qsTr("In order for Outlook to work, Bridge needs to install a certificate in your keychain.");
|
||||||
|
linkLabel1.setCallback(function () {
|
||||||
|
Backend.openExternalLink("https://proton.me/support/apple-mail-certificate");
|
||||||
|
}, qsTr("Why is this certificate needed?"), true);
|
||||||
|
}
|
||||||
linkLabel2.clear();
|
linkLabel2.clear();
|
||||||
}
|
}
|
||||||
function showAppleMailAutoconfigCommon() {
|
|
||||||
|
function showClientConfigCommon() {
|
||||||
titleLabel.text = "";
|
titleLabel.text = "";
|
||||||
linkLabel1.clear();
|
linkLabel1.clear();
|
||||||
linkLabel2.clear();
|
linkLabel2.clear();
|
||||||
@ -49,7 +59,7 @@ Item {
|
|||||||
iconWidth = 80;
|
iconWidth = 80;
|
||||||
}
|
}
|
||||||
function showAppleMailAutoconfigProfileInstall() {
|
function showAppleMailAutoconfigProfileInstall() {
|
||||||
showAppleMailAutoconfigCommon();
|
showClientConfigCommon();
|
||||||
descriptionLabel.text = qsTr("The final step before you can start using Apple Mail is to install the Bridge server profile in the system preferences.\n\nAdding a server profile is necessary to ensure that your Mac can receive and send Proton Mails.");
|
descriptionLabel.text = qsTr("The final step before you can start using Apple Mail is to install the Bridge server profile in the system preferences.\n\nAdding a server profile is necessary to ensure that your Mac can receive and send Proton Mails.");
|
||||||
linkLabel1.setCallback(function() { Backend.openExternalLink("https://proton.me/support/macos-certificate-warning"); }, qsTr("Why is there a yellow warning sign?"), true);
|
linkLabel1.setCallback(function() { Backend.openExternalLink("https://proton.me/support/macos-certificate-warning"); }, qsTr("Why is there a yellow warning sign?"), true);
|
||||||
linkLabel2.setCallback(wizard.showClientParams, qsTr("Configure Apple Mail manually"), false);
|
linkLabel2.setCallback(wizard.showClientParams, qsTr("Configure Apple Mail manually"), false);
|
||||||
|
|||||||
@ -20,12 +20,14 @@ FocusScope {
|
|||||||
enum RootStack {
|
enum RootStack {
|
||||||
Login,
|
Login,
|
||||||
TOTP,
|
TOTP,
|
||||||
MailboxPassword
|
MailboxPassword,
|
||||||
|
HV
|
||||||
}
|
}
|
||||||
|
|
||||||
property alias currentIndex: stackLayout.currentIndex
|
property alias currentIndex: stackLayout.currentIndex
|
||||||
property alias username: usernameTextField.text
|
property alias username: usernameTextField.text
|
||||||
property var wizard
|
property var wizard
|
||||||
|
property string hvLinkUrl: ""
|
||||||
|
|
||||||
signal loginAbort(string username, bool wasSignedOut)
|
signal loginAbort(string username, bool wasSignedOut)
|
||||||
|
|
||||||
@ -47,6 +49,14 @@ FocusScope {
|
|||||||
passwordTextField.hidePassword();
|
passwordTextField.hidePassword();
|
||||||
secondPasswordTextField.hidePassword();
|
secondPasswordTextField.hidePassword();
|
||||||
}
|
}
|
||||||
|
function resetViaHv() {
|
||||||
|
usernameTextField.enabled = false;
|
||||||
|
passwordTextField.enabled = false;
|
||||||
|
signInButton.loading = true;
|
||||||
|
secondPasswordButton.loading = false;
|
||||||
|
secondPasswordTextField.enabled = true;
|
||||||
|
totpLayout.reset();
|
||||||
|
}
|
||||||
|
|
||||||
StackLayout {
|
StackLayout {
|
||||||
id: stackLayout
|
id: stackLayout
|
||||||
@ -124,6 +134,18 @@ FocusScope {
|
|||||||
else
|
else
|
||||||
errorLabel.text = qsTr("Incorrect login credentials");
|
errorLabel.text = qsTr("Incorrect login credentials");
|
||||||
}
|
}
|
||||||
|
function onLoginHvRequested(hvUrl) {
|
||||||
|
console.assert(stackLayout.currentIndex === Login.RootStack.Login || stackLayout.currentIndex === Login.RootStack.MailboxPassword, "Unexpected loginHvRequested");
|
||||||
|
stackLayout.currentIndex = Login.RootStack.HV;
|
||||||
|
hvUsernameLabel.text = usernameTextField.text;
|
||||||
|
hvLinkUrl = hvUrl;
|
||||||
|
}
|
||||||
|
function onLoginHvError(_) {
|
||||||
|
console.assert(stackLayout.currentIndex === Login.RootStack.Login || stackLayout.currentIndex === Login.RootStack.MailboxPassword, "Unexpected onLoginHvInvalidTokenError");
|
||||||
|
stackLayout.currentIndex = Login.RootStack.Login;
|
||||||
|
root.resetViaHv();
|
||||||
|
root.reset()
|
||||||
|
}
|
||||||
|
|
||||||
target: Backend
|
target: Backend
|
||||||
}
|
}
|
||||||
@ -475,5 +497,112 @@ FocusScope {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Item {
|
||||||
|
id: hvLayout
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: ProtonStyle.wizard_spacing_extra_large
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: ProtonStyle.wizard_spacing_medium
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: ProtonStyle.wizard_spacing_small
|
||||||
|
|
||||||
|
Label {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
Layout.fillWidth: true
|
||||||
|
colorScheme: wizard.colorScheme
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
text: qsTr("Human verification")
|
||||||
|
type: Label.LabelType.Title
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
id: hvUsernameLabel
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
Layout.fillWidth: true
|
||||||
|
color: wizard.colorScheme.text_weak
|
||||||
|
colorScheme: wizard.colorScheme
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
type: Label.LabelType.Body
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
Layout.fillWidth: true
|
||||||
|
colorScheme: wizard.colorScheme
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
text: qsTr("Please open the following link in your favourite web browser to verify you are human.")
|
||||||
|
type: Label.LabelType.Body
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Label {
|
||||||
|
id: hvRequestedUrlText
|
||||||
|
type: Label.LabelType.Lead
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
Layout.fillWidth: true
|
||||||
|
colorScheme: wizard.colorScheme
|
||||||
|
horizontalAlignment: Text.AlignLeft
|
||||||
|
text: "<a href='" + hvLinkUrl + "'>" + hvLinkUrl.replace("&", "&")+ "</a>"
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
Qt.openUrlExternally(hvLinkUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: ProtonStyle.wizard_spacing_medium
|
||||||
|
|
||||||
|
Button {
|
||||||
|
id: hVContinueButton
|
||||||
|
Layout.fillWidth: true
|
||||||
|
colorScheme: wizard.colorScheme
|
||||||
|
text: qsTr("Continue")
|
||||||
|
|
||||||
|
function checkAndSignInHv() {
|
||||||
|
console.assert(stackLayout.currentIndex === Login.RootStack.HV || stackLayout.currentIndex === Login.RootStack.MailboxPassword, "Unexpected checkInAndSignInHv")
|
||||||
|
stackLayout.currentIndex = Login.RootStack.Login
|
||||||
|
usernameTextField.validate();
|
||||||
|
passwordTextField.validate();
|
||||||
|
if (usernameTextField.error || passwordTextField.error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
root.resetViaHv();
|
||||||
|
Backend.loginHv(usernameTextField.text, Qt.btoa(passwordTextField.text));
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
checkAndSignInHv()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
colorScheme: wizard.colorScheme
|
||||||
|
secondary: true
|
||||||
|
secondaryIsOpaque: true
|
||||||
|
text: qsTr("Cancel")
|
||||||
|
onClicked: {
|
||||||
|
root.abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,6 +27,7 @@ Item {
|
|||||||
Onboarding,
|
Onboarding,
|
||||||
Login,
|
Login,
|
||||||
ClientConfigSelector,
|
ClientConfigSelector,
|
||||||
|
ClientConfigCertInstall,
|
||||||
ClientConfigAppleMail
|
ClientConfigAppleMail
|
||||||
}
|
}
|
||||||
enum RootStack {
|
enum RootStack {
|
||||||
@ -95,8 +96,9 @@ Item {
|
|||||||
function showAppleMailAutoConfig() {
|
function showAppleMailAutoConfig() {
|
||||||
backAction = _showClientConfig;
|
backAction = _showClientConfig;
|
||||||
rootStackLayout.currentIndex = SetupWizard.RootStack.TwoPanesView;
|
rootStackLayout.currentIndex = SetupWizard.RootStack.TwoPanesView;
|
||||||
|
clientConfigAppleMail.reset()
|
||||||
rightContent.currentIndex = SetupWizard.ContentStack.ClientConfigAppleMail;
|
rightContent.currentIndex = SetupWizard.ContentStack.ClientConfigAppleMail;
|
||||||
clientConfigAppleMail.showAutoconfig(); // This will trigger signals that will display the appropriate left content.
|
leftContent.showAppleMailAutoconfigProfileInstall();
|
||||||
}
|
}
|
||||||
function showBugReport() {
|
function showBugReport() {
|
||||||
closeWizard();
|
closeWizard();
|
||||||
@ -118,6 +120,15 @@ Item {
|
|||||||
backAction = _showClientConfig;
|
backAction = _showClientConfig;
|
||||||
rootStackLayout.currentIndex = SetupWizard.RootStack.ClientConfigParameters;
|
rootStackLayout.currentIndex = SetupWizard.RootStack.ClientConfigParameters;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showCertInstall() {
|
||||||
|
backAction = _showClientConfig;
|
||||||
|
clientConfigCertInstall.reset();
|
||||||
|
rootStackLayout.currentIndex = SetupWizard.RootStack.TwoPanesView;
|
||||||
|
leftContent.showCertificateInstall()
|
||||||
|
rightContent.currentIndex = SetupWizard.ContentStack.ClientConfigCertInstall;
|
||||||
|
}
|
||||||
|
|
||||||
function showLogin(username = "") {
|
function showLogin(username = "") {
|
||||||
backAction = null;
|
backAction = null;
|
||||||
rootStackLayout.currentIndex = SetupWizard.RootStack.TwoPanesView;
|
rootStackLayout.currentIndex = SetupWizard.RootStack.TwoPanesView;
|
||||||
@ -146,7 +157,13 @@ Item {
|
|||||||
let address = user ? user.addresses[0] : "";
|
let address = user ? user.addresses[0] : "";
|
||||||
showClientConfig(user, address, true);
|
showClientConfig(user, address, true);
|
||||||
}
|
}
|
||||||
|
function onCertificateInstallSuccess() {
|
||||||
|
if (client === SetupWizard.Client.MicrosoftOutlook) {
|
||||||
|
showClientParams()
|
||||||
|
} else {
|
||||||
|
showAppleMailAutoConfig()
|
||||||
|
}
|
||||||
|
}
|
||||||
target: Backend
|
target: Backend
|
||||||
}
|
}
|
||||||
StackLayout {
|
StackLayout {
|
||||||
@ -176,17 +193,6 @@ Item {
|
|||||||
width: ProtonStyle.wizard_pane_width
|
width: ProtonStyle.wizard_pane_width
|
||||||
wizard: root
|
wizard: root
|
||||||
|
|
||||||
Connections {
|
|
||||||
function onAppleMailAutoconfigCertificateInstallPageShown() {
|
|
||||||
leftContent.showAppleMailAutoconfigCertificateInstall();
|
|
||||||
}
|
|
||||||
function onAppleMailAutoconfigProfileInstallPageShow() {
|
|
||||||
leftContent.showAppleMailAutoconfigProfileInstall();
|
|
||||||
}
|
|
||||||
|
|
||||||
target: clientConfigAppleMail
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
function onLogin2FARequested() {
|
function onLogin2FARequested() {
|
||||||
leftContent.showLogin2FA();
|
leftContent.showLogin2FA();
|
||||||
@ -247,7 +253,14 @@ Item {
|
|||||||
id: clientConfigSelector
|
id: clientConfigSelector
|
||||||
wizard: root
|
wizard: root
|
||||||
}
|
}
|
||||||
|
|
||||||
// rightContent stack index 3
|
// rightContent stack index 3
|
||||||
|
ClientConfigCertInstall {
|
||||||
|
id: clientConfigCertInstall
|
||||||
|
wizard: root
|
||||||
|
}
|
||||||
|
|
||||||
|
// rightContent stack index 4
|
||||||
ClientConfigAppleMail {
|
ClientConfigAppleMail {
|
||||||
id: clientConfigAppleMail
|
id: clientConfigAppleMail
|
||||||
wizard: root
|
wizard: root
|
||||||
|
|||||||
@ -17,22 +17,22 @@
|
|||||||
|
|
||||||
|
|
||||||
#include <bridgepp/CLI/CLIUtils.h>
|
#include <bridgepp/CLI/CLIUtils.h>
|
||||||
|
#include <bridgepp/SessionID/SessionID.h>
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
|
||||||
using namespace bridgepp;
|
using namespace bridgepp;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
//
|
//
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
TEST(CLI, stripStringParameterFromCommandLine) {
|
TEST(CLI, stripStringParameterFromCommandLine) {
|
||||||
struct Test {
|
struct TestData {
|
||||||
QStringList input;
|
QStringList input;
|
||||||
QStringList expectedOutput;
|
QStringList expectedOutput;
|
||||||
};
|
};
|
||||||
QList<Test> const tests = {
|
QList<TestData> const tests = {
|
||||||
{{}, {}},
|
{{}, {}},
|
||||||
{{ "--a", "-b", "--C" }, { "--a", "-b", "--C" } },
|
{{ "--a", "-b", "--C" }, { "--a", "-b", "--C" } },
|
||||||
{{ "--string", "value" }, {} },
|
{{ "--string", "value" }, {} },
|
||||||
@ -44,7 +44,45 @@ TEST(CLI, stripStringParameterFromCommandLine) {
|
|||||||
{{ "--string", "--string", "value", "-b", "--string"}, { "value", "-b" } },
|
{{ "--string", "--string", "value", "-b", "--string"}, { "value", "-b" } },
|
||||||
};
|
};
|
||||||
|
|
||||||
for (Test const& test: tests) {
|
for (TestData const& test: tests) {
|
||||||
EXPECT_EQ(stripStringParameterFromCommandLine("--string", test.input), test.expectedOutput);
|
EXPECT_EQ(stripStringParameterFromCommandLine("--string", test.input), test.expectedOutput);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST(CLI, parseGoCLIStringArgument) {
|
||||||
|
struct TestData {
|
||||||
|
QStringList args;
|
||||||
|
QStringList params;
|
||||||
|
QStringList expectedOutput;
|
||||||
|
};
|
||||||
|
|
||||||
|
QList<TestData> const tests = {
|
||||||
|
{ {}, {}, {} },
|
||||||
|
{ {"-param"}, {"param"}, {} },
|
||||||
|
{ {"--param", "1"}, {"param"}, { "1" } },
|
||||||
|
{ {"--param", "1","p", "-p", "2", "-flag", "-param=3", "--p=4"}, {"param", "p"}, { "1", "2", "3", "4" } },
|
||||||
|
{ {"--param", "--param", "1"}, {"param"}, { "--param" } },
|
||||||
|
};
|
||||||
|
|
||||||
|
for (TestData const& test: tests) {
|
||||||
|
EXPECT_EQ(parseGoCLIStringArgument(test.args, test.params), test.expectedOutput);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(CLI, cliArgsToStringList) {
|
||||||
|
int constexpr argc = 3;
|
||||||
|
char *argv[] = { const_cast<char *>("1"), const_cast<char *>("2"), const_cast<char *>("3") };
|
||||||
|
QStringList const strList { "1", "2", "3" };
|
||||||
|
EXPECT_EQ(cliArgsToStringList(argc,argv), strList);
|
||||||
|
EXPECT_EQ(cliArgsToStringList(0, nullptr), QStringList {});
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(CLI, mostRecentSessionID) {
|
||||||
|
QStringList const sessionIDs { "20220411_155931148", "20230411_155931148", "20240411_155931148" };
|
||||||
|
EXPECT_EQ(mostRecentSessionID({ hyphenatedSessionIDFlag, sessionIDs[0] }), sessionIDs[0]);
|
||||||
|
EXPECT_EQ(mostRecentSessionID({ hyphenatedSessionIDFlag, sessionIDs[1], hyphenatedSessionIDFlag, sessionIDs[2] }), sessionIDs[2]);
|
||||||
|
EXPECT_EQ(mostRecentSessionID({ hyphenatedSessionIDFlag, sessionIDs[2], hyphenatedSessionIDFlag, sessionIDs[1] }), sessionIDs[2]);
|
||||||
|
EXPECT_EQ(mostRecentSessionID({ hyphenatedSessionIDFlag, sessionIDs[1], hyphenatedSessionIDFlag, sessionIDs[2], hyphenatedSessionIDFlag,
|
||||||
|
sessionIDs[0] }), sessionIDs[2]);
|
||||||
|
}
|
||||||
|
|||||||
@ -16,7 +16,7 @@
|
|||||||
// 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/>.
|
||||||
|
|
||||||
#include "CLIUtils.h"
|
#include "CLIUtils.h"
|
||||||
|
#include "../SessionID/SessionID.h"
|
||||||
|
|
||||||
namespace bridgepp {
|
namespace bridgepp {
|
||||||
|
|
||||||
@ -42,4 +42,67 @@ QStringList stripStringParameterFromCommandLine(QString const ¶mName, QStrin
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// The flags may be present more than once in the args. All values are returned in order of appearance.
|
||||||
|
///
|
||||||
|
/// \param[in] args The arguments
|
||||||
|
/// \param[in] paramNames the list of names for the parameter, without any prefix hypen.
|
||||||
|
/// \return The values found for the flag.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
QStringList parseGoCLIStringArgument(QStringList const &args, QStringList const& paramNames) {
|
||||||
|
// go cli package is pretty permissive when it comes to parsing arguments. For each name 'param', all the following seems to be accepted:
|
||||||
|
// -param value
|
||||||
|
// --param value
|
||||||
|
// -param=value
|
||||||
|
// --param=value
|
||||||
|
|
||||||
|
QStringList result;
|
||||||
|
qsizetype const argCount = args.count();
|
||||||
|
for (qsizetype i = 0; i < args.size(); ++i) {
|
||||||
|
for (QString const ¶mName: paramNames) {
|
||||||
|
if ((i < argCount - 1) && ((args[i] == "-" + paramName) || (args[i] == "--" + paramName))) {
|
||||||
|
result.append(args[i + 1]);
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (QRegularExpressionMatch match = QRegularExpression(QString("^-{1,2}%1=(.+)$").arg(paramName)).match(args[i]); match.hasMatch()) {
|
||||||
|
result.append(match.captured(1));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \param[in] argc The number of command-line arguments.
|
||||||
|
/// \param[in] argv The list of command-line arguments.
|
||||||
|
/// \return A QStringList representing the arguments list.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
QStringList cliArgsToStringList(int argc, char **argv) {
|
||||||
|
QStringList result;
|
||||||
|
result.reserve(argc);
|
||||||
|
for (qsizetype i = 0; i < argc; ++i) {
|
||||||
|
result.append(QString::fromLocal8Bit(argv[i]));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \param[in] args The command-line arguments.
|
||||||
|
/// \return The most recent sessionID in the list. If the list is empty, a new sessionID is created.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
QString mostRecentSessionID(QStringList const& args) {
|
||||||
|
QStringList const sessionIDs = parseGoCLIStringArgument(args, {sessionIDFlag});
|
||||||
|
if (sessionIDs.isEmpty()) {
|
||||||
|
return newSessionID();
|
||||||
|
}
|
||||||
|
|
||||||
|
return *std::max_element(sessionIDs.constBegin(), sessionIDs.constEnd(), [](QString const &lhs, QString const &rhs) -> bool {
|
||||||
|
return sessionIDToDateTime(lhs) < sessionIDToDateTime(rhs);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
} // namespace bridgepp
|
} // namespace bridgepp
|
||||||
|
|||||||
@ -15,18 +15,16 @@
|
|||||||
// 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/>.
|
||||||
|
|
||||||
|
|
||||||
#ifndef BRIDGEPP_CLI_UTILS_H
|
#ifndef BRIDGEPP_CLI_UTILS_H
|
||||||
#define BRIDGEPP_CLI_UTILS_H
|
#define BRIDGEPP_CLI_UTILS_H
|
||||||
|
|
||||||
|
|
||||||
namespace bridgepp {
|
namespace bridgepp {
|
||||||
|
|
||||||
|
|
||||||
QStringList stripStringParameterFromCommandLine(QString const ¶mName, QStringList const &commandLineParams); ///< Remove a string parameter from a list of command-line parameters.
|
QStringList stripStringParameterFromCommandLine(QString const ¶mName, QStringList const &commandLineParams); ///< Remove a string parameter from a list of command-line parameters.
|
||||||
|
QStringList parseGoCLIStringArgument(QStringList const &args, QStringList const ¶mNames); ///< Parse a command-line string argument as expected by go's CLI package.
|
||||||
|
QStringList cliArgsToStringList(int argc, char **argv); ///< Converts C-style command-line arguments to a string list.
|
||||||
|
QString mostRecentSessionID(QStringList const& args); ///< Returns the most recent sessionID parsed in command-line arguments.
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#endif // BRIDGEPP_CLI_UTILS_H
|
#endif // BRIDGEPP_CLI_UTILS_H
|
||||||
|
|||||||
@ -302,6 +302,18 @@ SPStreamEvent newLoginTfaRequestedEvent(QString const &username) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \return The event.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
SPStreamEvent newLoginHvRequestedEvent() {
|
||||||
|
auto event = new ::grpc::LoginHvRequestedEvent;
|
||||||
|
event->set_hvurl("https://verify.proton.me/?methods=captcha&token=SOME_RANDOM_TOKEN");
|
||||||
|
auto loginEvent = new grpc::LoginEvent;
|
||||||
|
loginEvent->set_allocated_hvrequested(event);
|
||||||
|
return wrapLoginEvent(loginEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \param[in] username The username.
|
/// \param[in] username The username.
|
||||||
/// \return The event.
|
/// \return The event.
|
||||||
|
|||||||
@ -48,6 +48,7 @@ SPStreamEvent newLoginTfaRequestedEvent(QString const &username); ///< Create a
|
|||||||
SPStreamEvent newLoginTwoPasswordsRequestedEvent(QString const &username); ///< Create a new LoginTwoPasswordsRequestedEvent event.
|
SPStreamEvent newLoginTwoPasswordsRequestedEvent(QString const &username); ///< Create a new LoginTwoPasswordsRequestedEvent event.
|
||||||
SPStreamEvent newLoginFinishedEvent(QString const &userID, bool wasSignedOut); ///< Create a new LoginFinishedEvent event.
|
SPStreamEvent newLoginFinishedEvent(QString const &userID, bool wasSignedOut); ///< Create a new LoginFinishedEvent event.
|
||||||
SPStreamEvent newLoginAlreadyLoggedInEvent(QString const &userID); ///< Create a new LoginAlreadyLoggedInEvent event.
|
SPStreamEvent newLoginAlreadyLoggedInEvent(QString const &userID); ///< Create a new LoginAlreadyLoggedInEvent event.
|
||||||
|
SPStreamEvent newLoginHvRequestedEvent(); ///< Create a new LoginHvRequestedEvent
|
||||||
|
|
||||||
// Update related events
|
// Update related events
|
||||||
SPStreamEvent newUpdateErrorEvent(grpc::UpdateErrorType errorType); ///< Create a new UpdateErrorEvent event.
|
SPStreamEvent newUpdateErrorEvent(grpc::UpdateErrorType errorType); ///< Create a new UpdateErrorEvent event.
|
||||||
|
|||||||
@ -23,7 +23,6 @@
|
|||||||
#include "../ProcessMonitor.h"
|
#include "../ProcessMonitor.h"
|
||||||
#include "../Log/LogUtils.h"
|
#include "../Log/LogUtils.h"
|
||||||
|
|
||||||
|
|
||||||
using namespace google::protobuf;
|
using namespace google::protobuf;
|
||||||
using namespace grpc;
|
using namespace grpc;
|
||||||
|
|
||||||
@ -607,6 +606,20 @@ grpc::Status GRPCClient::login(QString const &username, QString const &password)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \param[in] username The username.
|
||||||
|
/// \param[in] password The password.
|
||||||
|
/// \return the status for the gRPC call.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
grpc::Status GRPCClient::loginHv(QString const &username, QString const &password) {
|
||||||
|
LoginRequest request;
|
||||||
|
request.set_username(username.toStdString());
|
||||||
|
request.set_password(password.toStdString());
|
||||||
|
request.set_usehvdetails(true);
|
||||||
|
return this->logGRPCCallStatus(stub_->Login(this->clientContext().get(), request, &empty), __FUNCTION__);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \param[in] username The username.
|
/// \param[in] username The username.
|
||||||
/// \param[in] code The The 2FA code.
|
/// \param[in] code The The 2FA code.
|
||||||
@ -1185,6 +1198,14 @@ void GRPCClient::processAppEvent(AppEvent const &event) {
|
|||||||
emit knowledgeBasSuggestionsReceived(suggestions);
|
emit knowledgeBasSuggestionsReceived(suggestions);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case AppEvent::kRepairStarted:
|
||||||
|
this->logTrace("App event received: RepairStarted.");
|
||||||
|
emit repairStarted();
|
||||||
|
break;
|
||||||
|
case AppEvent::kAllUsersLoaded:
|
||||||
|
this->logTrace("App event received: AllUsersLoaded");
|
||||||
|
emit allUsersLoaded();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
this->logError("Unknown App event received.");
|
this->logError("Unknown App event received.");
|
||||||
}
|
}
|
||||||
@ -1221,6 +1242,9 @@ void GRPCClient::processLoginEvent(LoginEvent const &event) {
|
|||||||
case TWO_PASSWORDS_ABORT:
|
case TWO_PASSWORDS_ABORT:
|
||||||
emit login2PasswordErrorAbort(QString::fromStdString(error.message()));
|
emit login2PasswordErrorAbort(QString::fromStdString(error.message()));
|
||||||
break;
|
break;
|
||||||
|
case HV_ERROR:
|
||||||
|
emit loginHvError(QString::fromStdString(error.message()));
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
this->logError("Unknown login error event received.");
|
this->logError("Unknown login error event received.");
|
||||||
break;
|
break;
|
||||||
@ -1245,6 +1269,10 @@ void GRPCClient::processLoginEvent(LoginEvent const &event) {
|
|||||||
this->logTrace("Login event received: AlreadyLoggedIn.");
|
this->logTrace("Login event received: AlreadyLoggedIn.");
|
||||||
emit loginAlreadyLoggedIn(QString::fromStdString(event.finished().userid()));
|
emit loginAlreadyLoggedIn(QString::fromStdString(event.finished().userid()));
|
||||||
break;
|
break;
|
||||||
|
case LoginEvent::kHvRequested:
|
||||||
|
this->logTrace("Login event Received: HvRequested");
|
||||||
|
emit loginHvRequested(QString::fromStdString(event.hvrequested().hvurl()));
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
this->logError("Unknown Login event received.");
|
this->logError("Unknown Login event received.");
|
||||||
break;
|
break;
|
||||||
@ -1560,5 +1588,12 @@ grpc::Status GRPCClient::externalLinkClicked(QString const &link) {
|
|||||||
return this->logGRPCCallStatus(stub_->ExternalLinkClicked(this->clientContext().get(), s, &empty), __FUNCTION__);
|
return this->logGRPCCallStatus(stub_->ExternalLinkClicked(this->clientContext().get(), s, &empty), __FUNCTION__);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
//
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
grpc::Status GRPCClient::triggerRepair() {
|
||||||
|
return this->logGRPCCallStatus(stub_->TriggerRepair(this->clientContext().get(), empty, &empty), __FUNCTION__ );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
} // namespace bridgepp
|
} // namespace bridgepp
|
||||||
|
|||||||
@ -108,6 +108,7 @@ public: // member functions.
|
|||||||
grpc::Status landingPageLink(QUrl &outUrl); ///< Performs the 'landingPageLink' call.
|
grpc::Status landingPageLink(QUrl &outUrl); ///< Performs the 'landingPageLink' call.
|
||||||
grpc::Status hostname(QString &outHostname); ///< Performs the 'Hostname' call.
|
grpc::Status hostname(QString &outHostname); ///< Performs the 'Hostname' call.
|
||||||
grpc::Status requestKnowledgeBaseSuggestions(QString const &input); ///< Performs the 'RequestKnowledgeBaseSuggestions' call.
|
grpc::Status requestKnowledgeBaseSuggestions(QString const &input); ///< Performs the 'RequestKnowledgeBaseSuggestions' call.
|
||||||
|
grpc::Status triggerRepair(); ///< Performs the triggerRepair gRPC call.
|
||||||
|
|
||||||
signals: // app related signals
|
signals: // app related signals
|
||||||
void internetStatus(bool isOn);
|
void internetStatus(bool isOn);
|
||||||
@ -122,6 +123,8 @@ signals: // app related signals
|
|||||||
void certificateInstallFailed();
|
void certificateInstallFailed();
|
||||||
void showMainWindow();
|
void showMainWindow();
|
||||||
void knowledgeBasSuggestionsReceived(QList<KnowledgeBaseSuggestion> const& suggestions);
|
void knowledgeBasSuggestionsReceived(QList<KnowledgeBaseSuggestion> const& suggestions);
|
||||||
|
void repairStarted();
|
||||||
|
void allUsersLoaded();
|
||||||
|
|
||||||
|
|
||||||
public: // cache related calls
|
public: // cache related calls
|
||||||
@ -155,6 +158,7 @@ public: // login related calls
|
|||||||
grpc::Status login2FA(QString const &username, QString const &code); ///< Performs the 'login2FA' call.
|
grpc::Status login2FA(QString const &username, QString const &code); ///< Performs the 'login2FA' call.
|
||||||
grpc::Status login2Passwords(QString const &username, QString const &password); ///< Performs the 'login2Passwords' call.
|
grpc::Status login2Passwords(QString const &username, QString const &password); ///< Performs the 'login2Passwords' call.
|
||||||
grpc::Status loginAbort(QString const &username); ///< Performs the 'loginAbort' call.
|
grpc::Status loginAbort(QString const &username); ///< Performs the 'loginAbort' call.
|
||||||
|
grpc::Status loginHv(QString const &username, QString const &password); ///< Performs the 'login' call with additional useHv flag
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void loginUsernamePasswordError(QString const &errMsg);
|
void loginUsernamePasswordError(QString const &errMsg);
|
||||||
@ -168,6 +172,8 @@ signals:
|
|||||||
void login2PasswordErrorAbort(QString const &errMsg);
|
void login2PasswordErrorAbort(QString const &errMsg);
|
||||||
void loginFinished(QString const &userID, bool wasSignedOut);
|
void loginFinished(QString const &userID, bool wasSignedOut);
|
||||||
void loginAlreadyLoggedIn(QString const &userID);
|
void loginAlreadyLoggedIn(QString const &userID);
|
||||||
|
void loginHvRequested(QString const &hvUrl);
|
||||||
|
void loginHvError(QString const &errMsg);
|
||||||
|
|
||||||
public: // Update related calls
|
public: // Update related calls
|
||||||
grpc::Status checkUpdate();
|
grpc::Status checkUpdate();
|
||||||
|
|||||||
@ -32,6 +32,10 @@ QString const dateTimeFormat = "yyyyMMdd_hhmmsszzz"; ///< The format string for
|
|||||||
namespace bridgepp {
|
namespace bridgepp {
|
||||||
|
|
||||||
|
|
||||||
|
QString const sessionIDFlag = "session-id";
|
||||||
|
QString const hyphenatedSessionIDFlag = "--" + sessionIDFlag;
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \return a new session ID based on the current local date/time
|
/// \return a new session ID based on the current local date/time
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
|
|||||||
@ -23,6 +23,10 @@
|
|||||||
namespace bridgepp {
|
namespace bridgepp {
|
||||||
|
|
||||||
|
|
||||||
|
extern QString const sessionIDFlag; ///< The sessionID command-line flag (without hyphens)
|
||||||
|
extern QString const hyphenatedSessionIDFlag; ///< The sessionID command-line flag (with two hyphens)
|
||||||
|
|
||||||
|
|
||||||
QString newSessionID(); ///< Create a new sessions
|
QString newSessionID(); ///< Create a new sessions
|
||||||
QDateTime sessionIDToDateTime(QString const &sessionID); ///< Parse the date/time from a sessionID.
|
QDateTime sessionIDToDateTime(QString const &sessionID); ///< Parse the date/time from a sessionID.
|
||||||
|
|
||||||
|
|||||||
@ -19,12 +19,14 @@ package cli
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/certs"
|
"github.com/ProtonMail/proton-bridge/v3/internal/certs"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/hv"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||||
"github.com/abiosoft/ishell"
|
"github.com/abiosoft/ishell"
|
||||||
)
|
)
|
||||||
@ -116,6 +118,13 @@ func (f *frontendCLI) showAccountAddressInfo(user bridge.UserInfo, address strin
|
|||||||
f.Println("")
|
f.Println("")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *frontendCLI) promptHvURL(details *proton.APIHVDetails) {
|
||||||
|
hvURL := hv.FormatHvURL(details)
|
||||||
|
fmt.Print("\nHuman Verification requested. Please open the URL below in a browser and press ENTER when the challenge has been completed.\n\n", hvURL+"\n")
|
||||||
|
f.ReadLine()
|
||||||
|
fmt.Println("Authenticating ...")
|
||||||
|
}
|
||||||
|
|
||||||
func (f *frontendCLI) loginAccount(c *ishell.Context) {
|
func (f *frontendCLI) loginAccount(c *ishell.Context) {
|
||||||
f.ShowPrompt(false)
|
f.ShowPrompt(false)
|
||||||
defer f.ShowPrompt(true)
|
defer f.ShowPrompt(true)
|
||||||
@ -144,7 +153,19 @@ func (f *frontendCLI) loginAccount(c *ishell.Context) {
|
|||||||
|
|
||||||
f.Println("Authenticating ... ")
|
f.Println("Authenticating ... ")
|
||||||
|
|
||||||
client, auth, err := f.bridge.LoginAuth(context.Background(), loginName, []byte(password))
|
client, auth, err := f.bridge.LoginAuth(context.Background(), loginName, []byte(password), nil)
|
||||||
|
|
||||||
|
var hvDetails *proton.APIHVDetails
|
||||||
|
hvDetails, hvErr := hv.VerifyAndExtractHvRequest(err)
|
||||||
|
if hvErr != nil || hvDetails != nil {
|
||||||
|
if hvErr != nil {
|
||||||
|
f.printAndLogError("Cannot login", hvErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f.promptHvURL(hvDetails)
|
||||||
|
client, auth, err = f.bridge.LoginAuth(context.Background(), loginName, []byte(password), hvDetails)
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
f.printAndLogError("Cannot login: ", err)
|
f.printAndLogError("Cannot login: ", err)
|
||||||
return
|
return
|
||||||
@ -175,7 +196,55 @@ func (f *frontendCLI) loginAccount(c *ishell.Context) {
|
|||||||
keyPass = []byte(password)
|
keyPass = []byte(password)
|
||||||
}
|
}
|
||||||
|
|
||||||
userID, err := f.bridge.LoginUser(context.Background(), client, auth, keyPass)
|
userID, err := f.bridge.LoginUser(context.Background(), client, auth, keyPass, hvDetails)
|
||||||
|
|
||||||
|
hvDetails, hvErr = hv.VerifyAndExtractHvRequest(err)
|
||||||
|
if hvDetails != nil || hvErr != nil {
|
||||||
|
if hvErr != nil {
|
||||||
|
f.printAndLogError("Cannot login: ", hvErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f.loginAccountHv(c, loginName, password, keyPass, hvDetails)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
f.processAPIError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := f.bridge.GetUserInfo(userID)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.Printf("Account %s was added successfully.\n", bold(user.Username))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *frontendCLI) loginAccountHv(c *ishell.Context, loginName string, password string, keyPass []byte, hvDetails *proton.APIHVDetails) {
|
||||||
|
f.promptHvURL(hvDetails)
|
||||||
|
client, auth, err := f.bridge.LoginAuth(context.Background(), loginName, []byte(password), hvDetails)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
f.printAndLogError("Cannot login: ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if auth.TwoFA.Enabled&proton.HasTOTP != 0 {
|
||||||
|
code := f.readStringInAttempts("Two factor code", c.ReadLine, isNotEmpty)
|
||||||
|
if code == "" {
|
||||||
|
f.printAndLogError("Cannot login: need two factor code")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.Auth2FA(context.Background(), proton.Auth2FAReq{TwoFactorCode: code}); err != nil {
|
||||||
|
f.printAndLogError("Cannot login: ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, err := f.bridge.LoginUser(context.Background(), client, auth, keyPass, hvDetails)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
f.processAPIError(err)
|
f.processAPIError(err)
|
||||||
return
|
return
|
||||||
|
|||||||
@ -306,6 +306,12 @@ func New(
|
|||||||
Aliases: []string{"del", "rm", "remove"},
|
Aliases: []string{"del", "rm", "remove"},
|
||||||
Completer: fe.completeUsernames,
|
Completer: fe.completeUsernames,
|
||||||
})
|
})
|
||||||
|
fe.AddCmd(&ishell.Cmd{
|
||||||
|
Name: "repair",
|
||||||
|
Help: "reload all accounts and cached data, re-download emails. Email clients remain connected. Logged out users will be repaired on next login. (aliases: rep)",
|
||||||
|
Func: fe.repair,
|
||||||
|
Aliases: []string{"rep"},
|
||||||
|
})
|
||||||
|
|
||||||
badEventCmd := &ishell.Cmd{
|
badEventCmd := &ishell.Cmd{
|
||||||
Name: "bad-event",
|
Name: "bad-event",
|
||||||
|
|||||||
@ -359,3 +359,13 @@ func (f *frontendCLI) isFile(location string) bool {
|
|||||||
|
|
||||||
return !stat.IsDir()
|
return !stat.IsDir()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *frontendCLI) repair(_ *ishell.Context) {
|
||||||
|
if f.bridge.HasAPIConnection() {
|
||||||
|
if f.yesNoQuestion("Are you sure you want to initialize a repair, this may take a while") {
|
||||||
|
f.bridge.Repair()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
f.Println("Bridge cannot connect to the Proton servers. A connection is required to utilize this feature.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -111,6 +111,9 @@ service Bridge {
|
|||||||
// Server -> Client event stream
|
// Server -> Client event stream
|
||||||
rpc RunEventStream(EventStreamRequest) returns (stream StreamEvent); // Keep streaming until StopEventStream is called.
|
rpc RunEventStream(EventStreamRequest) returns (stream StreamEvent); // Keep streaming until StopEventStream is called.
|
||||||
rpc StopEventStream(google.protobuf.Empty) returns (google.protobuf.Empty);
|
rpc StopEventStream(google.protobuf.Empty) returns (google.protobuf.Empty);
|
||||||
|
|
||||||
|
// Repair
|
||||||
|
rpc TriggerRepair(google.protobuf.Empty) returns (google.protobuf.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
//**********************************************************************************************************************
|
//**********************************************************************************************************************
|
||||||
@ -168,6 +171,7 @@ message ReportBugRequest {
|
|||||||
message LoginRequest {
|
message LoginRequest {
|
||||||
string username = 1;
|
string username = 1;
|
||||||
bytes password = 2;
|
bytes password = 2;
|
||||||
|
optional bool useHvDetails = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message LoginAbortRequest {
|
message LoginAbortRequest {
|
||||||
@ -271,6 +275,8 @@ message AppEvent {
|
|||||||
CertificateInstallCanceledEvent certificateInstallCanceled = 10;
|
CertificateInstallCanceledEvent certificateInstallCanceled = 10;
|
||||||
CertificateInstallFailedEvent certificateInstallFailed = 11;
|
CertificateInstallFailedEvent certificateInstallFailed = 11;
|
||||||
KnowledgeBaseSuggestionsEvent knowledgeBaseSuggestions = 12;
|
KnowledgeBaseSuggestionsEvent knowledgeBaseSuggestions = 12;
|
||||||
|
RepairStartedEvent repairStarted = 13;
|
||||||
|
AllUsersLoadedEvent allUsersLoaded = 14;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -288,6 +294,8 @@ message ReportBugFallbackEvent {}
|
|||||||
message CertificateInstallSuccessEvent {}
|
message CertificateInstallSuccessEvent {}
|
||||||
message CertificateInstallCanceledEvent {}
|
message CertificateInstallCanceledEvent {}
|
||||||
message CertificateInstallFailedEvent {}
|
message CertificateInstallFailedEvent {}
|
||||||
|
message RepairStartedEvent {}
|
||||||
|
message AllUsersLoadedEvent {}
|
||||||
|
|
||||||
message KnowledgeBaseSuggestion {
|
message KnowledgeBaseSuggestion {
|
||||||
string url = 1;
|
string url = 1;
|
||||||
@ -308,6 +316,7 @@ message LoginEvent {
|
|||||||
LoginTwoPasswordsRequestedEvent twoPasswordRequested = 3;
|
LoginTwoPasswordsRequestedEvent twoPasswordRequested = 3;
|
||||||
LoginFinishedEvent finished = 4;
|
LoginFinishedEvent finished = 4;
|
||||||
LoginFinishedEvent alreadyLoggedIn = 5;
|
LoginFinishedEvent alreadyLoggedIn = 5;
|
||||||
|
LoginHvRequestedEvent hvRequested = 6;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -319,6 +328,7 @@ enum LoginErrorType {
|
|||||||
TFA_ABORT = 4;
|
TFA_ABORT = 4;
|
||||||
TWO_PASSWORDS_ERROR = 5;
|
TWO_PASSWORDS_ERROR = 5;
|
||||||
TWO_PASSWORDS_ABORT = 6;
|
TWO_PASSWORDS_ABORT = 6;
|
||||||
|
HV_ERROR = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
message LoginErrorEvent {
|
message LoginErrorEvent {
|
||||||
@ -339,6 +349,10 @@ message LoginFinishedEvent {
|
|||||||
bool wasSignedOut = 2;
|
bool wasSignedOut = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message LoginHvRequestedEvent {
|
||||||
|
string hvUrl = 1;
|
||||||
|
}
|
||||||
|
|
||||||
//**********************************************************
|
//**********************************************************
|
||||||
// Update related events
|
// Update related events
|
||||||
//**********************************************************
|
//**********************************************************
|
||||||
|
|||||||
@ -101,6 +101,7 @@ const (
|
|||||||
Bridge_ExportTLSCertificates_FullMethodName = "/grpc.Bridge/ExportTLSCertificates"
|
Bridge_ExportTLSCertificates_FullMethodName = "/grpc.Bridge/ExportTLSCertificates"
|
||||||
Bridge_RunEventStream_FullMethodName = "/grpc.Bridge/RunEventStream"
|
Bridge_RunEventStream_FullMethodName = "/grpc.Bridge/RunEventStream"
|
||||||
Bridge_StopEventStream_FullMethodName = "/grpc.Bridge/StopEventStream"
|
Bridge_StopEventStream_FullMethodName = "/grpc.Bridge/StopEventStream"
|
||||||
|
Bridge_TriggerRepair_FullMethodName = "/grpc.Bridge/TriggerRepair"
|
||||||
)
|
)
|
||||||
|
|
||||||
// BridgeClient is the client API for Bridge service.
|
// BridgeClient is the client API for Bridge service.
|
||||||
@ -180,6 +181,8 @@ type BridgeClient interface {
|
|||||||
// Server -> Client event stream
|
// Server -> Client event stream
|
||||||
RunEventStream(ctx context.Context, in *EventStreamRequest, opts ...grpc.CallOption) (Bridge_RunEventStreamClient, error)
|
RunEventStream(ctx context.Context, in *EventStreamRequest, opts ...grpc.CallOption) (Bridge_RunEventStreamClient, error)
|
||||||
StopEventStream(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
StopEventStream(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||||
|
// Repair
|
||||||
|
TriggerRepair(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type bridgeClient struct {
|
type bridgeClient struct {
|
||||||
@ -780,6 +783,15 @@ func (c *bridgeClient) StopEventStream(ctx context.Context, in *emptypb.Empty, o
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *bridgeClient) TriggerRepair(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||||
|
out := new(emptypb.Empty)
|
||||||
|
err := c.cc.Invoke(ctx, Bridge_TriggerRepair_FullMethodName, in, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
// BridgeServer is the server API for Bridge service.
|
// BridgeServer is the server API for Bridge service.
|
||||||
// All implementations must embed UnimplementedBridgeServer
|
// All implementations must embed UnimplementedBridgeServer
|
||||||
// for forward compatibility
|
// for forward compatibility
|
||||||
@ -857,6 +869,8 @@ type BridgeServer interface {
|
|||||||
// Server -> Client event stream
|
// Server -> Client event stream
|
||||||
RunEventStream(*EventStreamRequest, Bridge_RunEventStreamServer) error
|
RunEventStream(*EventStreamRequest, Bridge_RunEventStreamServer) error
|
||||||
StopEventStream(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
|
StopEventStream(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
|
||||||
|
// Repair
|
||||||
|
TriggerRepair(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
|
||||||
mustEmbedUnimplementedBridgeServer()
|
mustEmbedUnimplementedBridgeServer()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1053,6 +1067,9 @@ func (UnimplementedBridgeServer) RunEventStream(*EventStreamRequest, Bridge_RunE
|
|||||||
func (UnimplementedBridgeServer) StopEventStream(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
|
func (UnimplementedBridgeServer) StopEventStream(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method StopEventStream not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method StopEventStream not implemented")
|
||||||
}
|
}
|
||||||
|
func (UnimplementedBridgeServer) TriggerRepair(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method TriggerRepair not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedBridgeServer) mustEmbedUnimplementedBridgeServer() {}
|
func (UnimplementedBridgeServer) mustEmbedUnimplementedBridgeServer() {}
|
||||||
|
|
||||||
// UnsafeBridgeServer may be embedded to opt out of forward compatibility for this service.
|
// UnsafeBridgeServer may be embedded to opt out of forward compatibility for this service.
|
||||||
@ -2203,6 +2220,24 @@ func _Bridge_StopEventStream_Handler(srv interface{}, ctx context.Context, dec f
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _Bridge_TriggerRepair_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(emptypb.Empty)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(BridgeServer).TriggerRepair(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: Bridge_TriggerRepair_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(BridgeServer).TriggerRepair(ctx, req.(*emptypb.Empty))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
// Bridge_ServiceDesc is the grpc.ServiceDesc for Bridge service.
|
// Bridge_ServiceDesc is the grpc.ServiceDesc for Bridge service.
|
||||||
// It's only intended for direct use with grpc.RegisterService,
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
// and not to be introspected or modified (even as a copy)
|
// and not to be introspected or modified (even as a copy)
|
||||||
@ -2458,6 +2493,10 @@ var Bridge_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "StopEventStream",
|
MethodName: "StopEventStream",
|
||||||
Handler: _Bridge_StopEventStream_Handler,
|
Handler: _Bridge_StopEventStream_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "TriggerRepair",
|
||||||
|
Handler: _Bridge_TriggerRepair_Handler,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Streams: []grpc.StreamDesc{
|
Streams: []grpc.StreamDesc{
|
||||||
{
|
{
|
||||||
|
|||||||
@ -100,6 +100,10 @@ func NewLoginAlreadyLoggedInEvent(userID string) *StreamEvent {
|
|||||||
return loginEvent(&LoginEvent{Event: &LoginEvent_AlreadyLoggedIn{AlreadyLoggedIn: &LoginFinishedEvent{UserID: userID}}})
|
return loginEvent(&LoginEvent{Event: &LoginEvent_AlreadyLoggedIn{AlreadyLoggedIn: &LoginFinishedEvent{UserID: userID}}})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewLoginHvRequestedEvent(hvChallengeURL string) *StreamEvent {
|
||||||
|
return loginEvent(&LoginEvent{Event: &LoginEvent_HvRequested{HvRequested: &LoginHvRequestedEvent{HvUrl: hvChallengeURL}}})
|
||||||
|
}
|
||||||
|
|
||||||
func NewUpdateErrorEvent(errorType UpdateErrorType) *StreamEvent {
|
func NewUpdateErrorEvent(errorType UpdateErrorType) *StreamEvent {
|
||||||
return updateEvent(&UpdateEvent{Event: &UpdateEvent_Error{Error: &UpdateErrorEvent{Type: errorType}}})
|
return updateEvent(&UpdateEvent{Event: &UpdateEvent_Error{Error: &UpdateErrorEvent{Type: errorType}}})
|
||||||
}
|
}
|
||||||
@ -237,6 +241,14 @@ func NewGenericErrorEvent(errorCode ErrorCode) *StreamEvent {
|
|||||||
return genericErrorEvent(&GenericErrorEvent{Code: errorCode})
|
return genericErrorEvent(&GenericErrorEvent{Code: errorCode})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewRepairStartedEvent() *StreamEvent {
|
||||||
|
return appEvent(&AppEvent{Event: &AppEvent_RepairStarted{RepairStarted: &RepairStartedEvent{}}})
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAllUsersLoadedEvent() *StreamEvent {
|
||||||
|
return appEvent(&AppEvent{Event: &AppEvent_AllUsersLoaded{AllUsersLoaded: &AllUsersLoadedEvent{}}})
|
||||||
|
}
|
||||||
|
|
||||||
// Event category factory functions.
|
// Event category factory functions.
|
||||||
|
|
||||||
func appEvent(appEvent *AppEvent) *StreamEvent {
|
func appEvent(appEvent *AppEvent) *StreamEvent {
|
||||||
|
|||||||
@ -38,6 +38,7 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/certs"
|
"github.com/ProtonMail/proton-bridge/v3/internal/certs"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/hv"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/service"
|
"github.com/ProtonMail/proton-bridge/v3/internal/service"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
||||||
@ -95,6 +96,9 @@ type Service struct { // nolint:structcheck
|
|||||||
parentPID int
|
parentPID int
|
||||||
parentPIDDoneCh chan struct{}
|
parentPIDDoneCh chan struct{}
|
||||||
showOnStartup bool
|
showOnStartup bool
|
||||||
|
|
||||||
|
hvDetails *proton.APIHVDetails
|
||||||
|
useHvDetails bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewService returns a new instance of the service.
|
// NewService returns a new instance of the service.
|
||||||
@ -397,6 +401,9 @@ func (s *Service) watchEvents() {
|
|||||||
|
|
||||||
case events.TLSIssue:
|
case events.TLSIssue:
|
||||||
_ = s.SendEvent(NewMailApiCertIssue())
|
_ = s.SendEvent(NewMailApiCertIssue())
|
||||||
|
|
||||||
|
case events.AllUsersLoaded:
|
||||||
|
_ = s.SendEvent(NewAllUsersLoadedEvent())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -412,6 +419,7 @@ func (s *Service) loginClean() {
|
|||||||
s.password[i] = '\x00'
|
s.password[i] = '\x00'
|
||||||
}
|
}
|
||||||
s.password = s.password[0:0]
|
s.password = s.password[0:0]
|
||||||
|
s.useHvDetails = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) finishLogin() {
|
func (s *Service) finishLogin() {
|
||||||
@ -424,6 +432,11 @@ func (s *Service) finishLogin() {
|
|||||||
|
|
||||||
wasSignedOut := s.bridge.HasUser(s.auth.UserID)
|
wasSignedOut := s.bridge.HasUser(s.auth.UserID)
|
||||||
|
|
||||||
|
var hvDetails *proton.APIHVDetails
|
||||||
|
if s.useHvDetails {
|
||||||
|
hvDetails = s.hvDetails
|
||||||
|
}
|
||||||
|
|
||||||
if len(s.password) == 0 || s.auth.UID == "" || s.authClient == nil {
|
if len(s.password) == 0 || s.auth.UID == "" || s.authClient == nil {
|
||||||
s.log.
|
s.log.
|
||||||
WithField("hasPass", len(s.password) != 0).
|
WithField("hasPass", len(s.password) != 0).
|
||||||
@ -439,8 +452,20 @@ func (s *Service) finishLogin() {
|
|||||||
defer done()
|
defer done()
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
userID, err := s.bridge.LoginUser(ctx, s.authClient, s.auth, s.password)
|
userID, err := s.bridge.LoginUser(ctx, s.authClient, s.auth, s.password, hvDetails)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if hv.IsHvRequest(err) {
|
||||||
|
s.handleHvRequest(err)
|
||||||
|
performCleanup = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if apiErr := new(proton.APIError); errors.As(err, &apiErr) && apiErr.Code == proton.HumanValidationInvalidToken {
|
||||||
|
s.hvDetails = nil
|
||||||
|
_ = s.SendEvent(NewLoginError(LoginErrorType_HV_ERROR, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
s.log.WithError(err).Errorf("Finish login failed")
|
s.log.WithError(err).Errorf("Finish login failed")
|
||||||
s.twoPasswordAttemptCount++
|
s.twoPasswordAttemptCount++
|
||||||
errType := LoginErrorType_TWO_PASSWORDS_ABORT
|
errType := LoginErrorType_TWO_PASSWORDS_ABORT
|
||||||
@ -614,6 +639,18 @@ func (s *Service) monitorParentPID() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) handleHvRequest(err error) {
|
||||||
|
hvDet, hvErr := hv.VerifyAndExtractHvRequest(err)
|
||||||
|
if hvErr != nil {
|
||||||
|
_ = s.SendEvent(NewLoginError(LoginErrorType_HV_ERROR, hvErr.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.hvDetails = hvDet
|
||||||
|
hvChallengeURL := hv.FormatHvURL(hvDet)
|
||||||
|
_ = s.SendEvent(NewLoginHvRequestedEvent(hvChallengeURL))
|
||||||
|
}
|
||||||
|
|
||||||
// computeFileSocketPath Return an available path for a socket file in the temp folder.
|
// computeFileSocketPath Return an available path for a socket file in the temp folder.
|
||||||
func computeFileSocketPath() (string, error) {
|
func computeFileSocketPath() (string, error) {
|
||||||
tempPath := os.TempDir()
|
tempPath := os.TempDir()
|
||||||
|
|||||||
@ -30,6 +30,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (s *Service) IsTLSCertificateInstalled(context.Context, *emptypb.Empty) (*wrapperspb.BoolValue, error) {
|
func (s *Service) IsTLSCertificateInstalled(context.Context, *emptypb.Empty) (*wrapperspb.BoolValue, error) {
|
||||||
|
defer async.HandlePanic(s.panicHandler)
|
||||||
|
|
||||||
s.log.Info("IsTLSCertificateInstalled")
|
s.log.Info("IsTLSCertificateInstalled")
|
||||||
|
|
||||||
cert, _ := s.bridge.GetBridgeTLSCert()
|
cert, _ := s.bridge.GetBridgeTLSCert()
|
||||||
|
|||||||
@ -45,6 +45,7 @@ import (
|
|||||||
|
|
||||||
// CheckTokens implements the CheckToken gRPC service call.
|
// CheckTokens implements the CheckToken gRPC service call.
|
||||||
func (s *Service) CheckTokens(_ context.Context, clientConfigPath *wrapperspb.StringValue) (*wrapperspb.StringValue, error) {
|
func (s *Service) CheckTokens(_ context.Context, clientConfigPath *wrapperspb.StringValue) (*wrapperspb.StringValue, error) {
|
||||||
|
defer async.HandlePanic(s.panicHandler)
|
||||||
s.log.Debug("CheckTokens")
|
s.log.Debug("CheckTokens")
|
||||||
|
|
||||||
path := clientConfigPath.Value
|
path := clientConfigPath.Value
|
||||||
@ -63,6 +64,7 @@ func (s *Service) CheckTokens(_ context.Context, clientConfigPath *wrapperspb.St
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) AddLogEntry(_ context.Context, request *AddLogEntryRequest) (*emptypb.Empty, error) {
|
func (s *Service) AddLogEntry(_ context.Context, request *AddLogEntryRequest) (*emptypb.Empty, error) {
|
||||||
|
defer async.HandlePanic(s.panicHandler)
|
||||||
entry := s.log
|
entry := s.log
|
||||||
|
|
||||||
if len(request.Package) > 0 {
|
if len(request.Package) > 0 {
|
||||||
@ -91,6 +93,7 @@ func (s *Service) AddLogEntry(_ context.Context, request *AddLogEntryRequest) (*
|
|||||||
|
|
||||||
// GuiReady implement the GuiReady gRPC service call.
|
// GuiReady implement the GuiReady gRPC service call.
|
||||||
func (s *Service) GuiReady(_ context.Context, _ *emptypb.Empty) (*GuiReadyResponse, error) {
|
func (s *Service) GuiReady(_ context.Context, _ *emptypb.Empty) (*GuiReadyResponse, error) {
|
||||||
|
defer async.HandlePanic(s.panicHandler)
|
||||||
s.log.Debug("GuiReady")
|
s.log.Debug("GuiReady")
|
||||||
|
|
||||||
s.initializationDone.Do(s.initializing.Done)
|
s.initializationDone.Do(s.initializing.Done)
|
||||||
@ -105,6 +108,7 @@ func (s *Service) GuiReady(_ context.Context, _ *emptypb.Empty) (*GuiReadyRespon
|
|||||||
|
|
||||||
// Quit implement the Quit gRPC service call.
|
// Quit implement the Quit gRPC service call.
|
||||||
func (s *Service) Quit(_ context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
|
func (s *Service) Quit(_ context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
|
||||||
|
defer async.HandlePanic(s.panicHandler)
|
||||||
s.log.Debug("Quit")
|
s.log.Debug("Quit")
|
||||||
return &emptypb.Empty{}, s.quit()
|
return &emptypb.Empty{}, s.quit()
|
||||||
}
|
}
|
||||||
@ -134,6 +138,7 @@ func (s *Service) quit() error {
|
|||||||
|
|
||||||
// Restart implement the Restart gRPC service call.
|
// Restart implement the Restart gRPC service call.
|
||||||
func (s *Service) Restart(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) {
|
func (s *Service) Restart(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) {
|
||||||
|
defer async.HandlePanic(s.panicHandler)
|
||||||
s.log.Debug("Restart")
|
s.log.Debug("Restart")
|
||||||
|
|
||||||
s.restarter.Set(true, false)
|
s.restarter.Set(true, false)
|
||||||
@ -141,12 +146,14 @@ func (s *Service) Restart(ctx context.Context, empty *emptypb.Empty) (*emptypb.E
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) ShowOnStartup(_ context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) {
|
func (s *Service) ShowOnStartup(_ context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) {
|
||||||
|
defer async.HandlePanic(s.panicHandler)
|
||||||
s.log.Debug("ShowOnStartup")
|
s.log.Debug("ShowOnStartup")
|
||||||
|
|
||||||
return wrapperspb.Bool(s.showOnStartup), nil
|
return wrapperspb.Bool(s.showOnStartup), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) SetIsAutostartOn(_ context.Context, isOn *wrapperspb.BoolValue) (*emptypb.Empty, error) {
|
func (s *Service) SetIsAutostartOn(_ context.Context, isOn *wrapperspb.BoolValue) (*emptypb.Empty, error) {
|
||||||
|
defer async.HandlePanic(s.panicHandler)
|
||||||
s.log.WithField("show", isOn.Value).Debug("SetIsAutostartOn")
|
s.log.WithField("show", isOn.Value).Debug("SetIsAutostartOn")
|
||||||
|
|
||||||
defer func() { _ = s.SendEvent(NewToggleAutostartFinishedEvent()) }()
|
defer func() { _ = s.SendEvent(NewToggleAutostartFinishedEvent()) }()
|
||||||
@ -167,12 +174,14 @@ func (s *Service) SetIsAutostartOn(_ context.Context, isOn *wrapperspb.BoolValue
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) IsAutostartOn(_ context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) {
|
func (s *Service) IsAutostartOn(_ context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) {
|
||||||
|
defer async.HandlePanic(s.panicHandler)
|
||||||
s.log.Debug("IsAutostartOn")
|
s.log.Debug("IsAutostartOn")
|
||||||
|
|
||||||
return wrapperspb.Bool(s.bridge.GetAutostart()), nil
|
return wrapperspb.Bool(s.bridge.GetAutostart()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) SetIsBetaEnabled(_ context.Context, isEnabled *wrapperspb.BoolValue) (*emptypb.Empty, error) {
|
func (s *Service) SetIsBetaEnabled(_ context.Context, isEnabled *wrapperspb.BoolValue) (*emptypb.Empty, error) {
|
||||||
|
defer async.HandlePanic(s.panicHandler)
|
||||||
s.log.WithField("isEnabled", isEnabled.Value).Debug("SetIsBetaEnabled")
|
s.log.WithField("isEnabled", isEnabled.Value).Debug("SetIsBetaEnabled")
|
||||||
|
|
||||||
channel := updater.StableChannel
|
channel := updater.StableChannel
|
||||||
@ -189,12 +198,14 @@ func (s *Service) SetIsBetaEnabled(_ context.Context, isEnabled *wrapperspb.Bool
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) IsBetaEnabled(_ context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) {
|
func (s *Service) IsBetaEnabled(_ context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) {
|
||||||
|
defer async.HandlePanic(s.panicHandler)
|
||||||
s.log.Debug("IsBetaEnabled")
|
s.log.Debug("IsBetaEnabled")
|
||||||
|
|
||||||
return wrapperspb.Bool(s.bridge.GetUpdateChannel() == updater.EarlyChannel), nil
|
return wrapperspb.Bool(s.bridge.GetUpdateChannel() == updater.EarlyChannel), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) SetIsAllMailVisible(_ context.Context, isVisible *wrapperspb.BoolValue) (*emptypb.Empty, error) {
|
func (s *Service) SetIsAllMailVisible(_ context.Context, isVisible *wrapperspb.BoolValue) (*emptypb.Empty, error) {
|
||||||
|
defer async.HandlePanic(s.panicHandler)
|
||||||
s.log.WithField("isVisible", isVisible.Value).Debug("SetIsAllMailVisible")
|
s.log.WithField("isVisible", isVisible.Value).Debug("SetIsAllMailVisible")
|
||||||
|
|
||||||
if err := s.bridge.SetShowAllMail(isVisible.Value); err != nil {
|
if err := s.bridge.SetShowAllMail(isVisible.Value); err != nil {
|
||||||
@ -206,12 +217,14 @@ func (s *Service) SetIsAllMailVisible(_ context.Context, isVisible *wrapperspb.B
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) IsAllMailVisible(_ context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) {
|
func (s *Service) IsAllMailVisible(_ context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) {
|
||||||
|
defer async.HandlePanic(s.panicHandler)
|
||||||
s.log.Debug("IsAllMailVisible")
|
s.log.Debug("IsAllMailVisible")
|
||||||
|
|
||||||
return wrapperspb.Bool(s.bridge.GetShowAllMail()), nil
|
return wrapperspb.Bool(s.bridge.GetShowAllMail()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) SetIsTelemetryDisabled(_ context.Context, isDisabled *wrapperspb.BoolValue) (*emptypb.Empty, error) {
|
func (s *Service) SetIsTelemetryDisabled(_ context.Context, isDisabled *wrapperspb.BoolValue) (*emptypb.Empty, error) {
|
||||||
|
defer async.HandlePanic(s.panicHandler)
|
||||||
s.log.WithField("isEnabled", isDisabled.Value).Debug("SetIsTelemetryDisabled")
|
s.log.WithField("isEnabled", isDisabled.Value).Debug("SetIsTelemetryDisabled")
|
||||||
|
|
||||||
if err := s.bridge.SetTelemetryDisabled(isDisabled.Value); err != nil {
|
if err := s.bridge.SetTelemetryDisabled(isDisabled.Value); err != nil {
|
||||||
@ -223,12 +236,14 @@ func (s *Service) SetIsTelemetryDisabled(_ context.Context, isDisabled *wrappers
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) IsTelemetryDisabled(_ context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) {
|
func (s *Service) IsTelemetryDisabled(_ context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) {
|
||||||
|
defer async.HandlePanic(s.panicHandler)
|
||||||
s.log.Debug("IsTelemetryDisabled")
|
s.log.Debug("IsTelemetryDisabled")
|
||||||
|
|
||||||
return wrapperspb.Bool(s.bridge.GetTelemetryDisabled()), nil
|
return wrapperspb.Bool(s.bridge.GetTelemetryDisabled()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) GoOs(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
|
func (s *Service) GoOs(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
|
||||||
|
defer async.HandlePanic(s.panicHandler)
|
||||||
s.log.Debug("GoOs") // TO-DO We can probably get rid of this and use QSysInfo::product name
|
s.log.Debug("GoOs") // TO-DO We can probably get rid of this and use QSysInfo::product name
|
||||||
|
|
||||||
return wrapperspb.String(runtime.GOOS), nil
|
return wrapperspb.String(runtime.GOOS), nil
|
||||||
@ -246,12 +261,14 @@ func (s *Service) TriggerReset(_ context.Context, _ *emptypb.Empty) (*emptypb.Em
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Version(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
|
func (s *Service) Version(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
|
||||||
|
defer async.HandlePanic(s.panicHandler)
|
||||||
s.log.Debug("Version")
|
s.log.Debug("Version")
|
||||||
|
|
||||||
return wrapperspb.String(s.bridge.GetCurrentVersion().Original()), nil
|
return wrapperspb.String(s.bridge.GetCurrentVersion().Original()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) LogsPath(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
|
func (s *Service) LogsPath(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
|
||||||
|
defer async.HandlePanic(s.panicHandler)
|
||||||
s.log.Debug("LogsPath")
|
s.log.Debug("LogsPath")
|
||||||
|
|
||||||
path, err := s.bridge.GetLogsPath()
|
path, err := s.bridge.GetLogsPath()
|
||||||
@ -263,30 +280,40 @@ func (s *Service) LogsPath(_ context.Context, _ *emptypb.Empty) (*wrapperspb.Str
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) LicensePath(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
|
func (s *Service) LicensePath(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
|
||||||
|
defer async.HandlePanic(s.panicHandler)
|
||||||
s.log.Debug("LicensePath")
|
s.log.Debug("LicensePath")
|
||||||
|
|
||||||
return wrapperspb.String(s.bridge.GetLicenseFilePath()), nil
|
return wrapperspb.String(s.bridge.GetLicenseFilePath()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) DependencyLicensesLink(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
|
func (s *Service) DependencyLicensesLink(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
|
||||||
|
defer async.HandlePanic(s.panicHandler)
|
||||||
return wrapperspb.String(s.bridge.GetDependencyLicensesLink()), nil
|
return wrapperspb.String(s.bridge.GetDependencyLicensesLink()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) ReleaseNotesPageLink(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
|
func (s *Service) ReleaseNotesPageLink(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
|
||||||
s.latestLock.RLock()
|
s.latestLock.RLock()
|
||||||
defer s.latestLock.RUnlock()
|
defer func() {
|
||||||
|
async.HandlePanic(s.panicHandler)
|
||||||
|
s.latestLock.RUnlock()
|
||||||
|
}()
|
||||||
|
|
||||||
return wrapperspb.String(s.latest.ReleaseNotesPage), nil
|
return wrapperspb.String(s.latest.ReleaseNotesPage), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) LandingPageLink(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
|
func (s *Service) LandingPageLink(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
|
||||||
s.latestLock.RLock()
|
s.latestLock.RLock()
|
||||||
defer s.latestLock.RUnlock()
|
defer func() {
|
||||||
|
async.HandlePanic(s.panicHandler)
|
||||||
|
s.latestLock.RUnlock()
|
||||||
|
}()
|
||||||
|
|
||||||
return wrapperspb.String(s.latest.LandingPage), nil
|
return wrapperspb.String(s.latest.LandingPage), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) SetColorSchemeName(_ context.Context, name *wrapperspb.StringValue) (*emptypb.Empty, error) {
|
func (s *Service) SetColorSchemeName(_ context.Context, name *wrapperspb.StringValue) (*emptypb.Empty, error) {
|
||||||
|
defer async.HandlePanic(s.panicHandler)
|
||||||
|
|
||||||
s.log.WithField("ColorSchemeName", name.Value).Debug("SetColorSchemeName")
|
s.log.WithField("ColorSchemeName", name.Value).Debug("SetColorSchemeName")
|
||||||
|
|
||||||
if !theme.IsAvailable(theme.Theme(name.Value)) {
|
if !theme.IsAvailable(theme.Theme(name.Value)) {
|
||||||
@ -303,6 +330,8 @@ func (s *Service) SetColorSchemeName(_ context.Context, name *wrapperspb.StringV
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) ColorSchemeName(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
|
func (s *Service) ColorSchemeName(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
|
||||||
|
defer async.HandlePanic(s.panicHandler)
|
||||||
|
|
||||||
s.log.Debug("ColorSchemeName")
|
s.log.Debug("ColorSchemeName")
|
||||||
|
|
||||||
current := s.bridge.GetColorScheme()
|
current := s.bridge.GetColorScheme()
|
||||||
@ -318,6 +347,8 @@ func (s *Service) ColorSchemeName(_ context.Context, _ *emptypb.Empty) (*wrapper
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) CurrentEmailClient(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
|
func (s *Service) CurrentEmailClient(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
|
||||||
|
defer async.HandlePanic(s.panicHandler)
|
||||||
|
|
||||||
s.log.Debug("CurrentEmailClient")
|
s.log.Debug("CurrentEmailClient")
|
||||||
|
|
||||||
return wrapperspb.String(s.bridge.GetCurrentUserAgent()), nil
|
return wrapperspb.String(s.bridge.GetCurrentUserAgent()), nil
|
||||||
@ -361,6 +392,8 @@ func (s *Service) ReportBug(_ context.Context, report *ReportBugRequest) (*empty
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) ForceLauncher(_ context.Context, launcher *wrapperspb.StringValue) (*emptypb.Empty, error) {
|
func (s *Service) ForceLauncher(_ context.Context, launcher *wrapperspb.StringValue) (*emptypb.Empty, error) {
|
||||||
|
defer async.HandlePanic(s.panicHandler)
|
||||||
|
|
||||||
s.log.WithField("launcher", launcher.Value).Debug("ForceLauncher")
|
s.log.WithField("launcher", launcher.Value).Debug("ForceLauncher")
|
||||||
|
|
||||||
s.restarter.Override(launcher.Value)
|
s.restarter.Override(launcher.Value)
|
||||||
@ -369,6 +402,8 @@ func (s *Service) ForceLauncher(_ context.Context, launcher *wrapperspb.StringVa
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) SetMainExecutable(_ context.Context, exe *wrapperspb.StringValue) (*emptypb.Empty, error) {
|
func (s *Service) SetMainExecutable(_ context.Context, exe *wrapperspb.StringValue) (*emptypb.Empty, error) {
|
||||||
|
defer async.HandlePanic(s.panicHandler)
|
||||||
|
|
||||||
s.log.WithField("executable", exe.Value).Debug("SetMainExecutable")
|
s.log.WithField("executable", exe.Value).Debug("SetMainExecutable")
|
||||||
|
|
||||||
s.restarter.AddFlags("--wait", exe.Value)
|
s.restarter.AddFlags("--wait", exe.Value)
|
||||||
@ -396,6 +431,14 @@ func (s *Service) RequestKnowledgeBaseSuggestions(_ context.Context, userInput *
|
|||||||
func (s *Service) Login(_ context.Context, login *LoginRequest) (*emptypb.Empty, error) {
|
func (s *Service) Login(_ context.Context, login *LoginRequest) (*emptypb.Empty, error) {
|
||||||
s.log.WithField("username", login.Username).Debug("Login")
|
s.log.WithField("username", login.Username).Debug("Login")
|
||||||
|
|
||||||
|
var hvDetails *proton.APIHVDetails
|
||||||
|
if login.UseHvDetails != nil && *login.UseHvDetails {
|
||||||
|
hvDetails = s.hvDetails
|
||||||
|
s.useHvDetails = true
|
||||||
|
} else {
|
||||||
|
s.useHvDetails = false
|
||||||
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer async.HandlePanic(s.panicHandler)
|
defer async.HandlePanic(s.panicHandler)
|
||||||
|
|
||||||
@ -407,7 +450,7 @@ func (s *Service) Login(_ context.Context, login *LoginRequest) (*emptypb.Empty,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
client, auth, err := s.bridge.LoginAuth(context.Background(), login.Username, password)
|
client, auth, err := s.bridge.LoginAuth(context.Background(), login.Username, password, hvDetails)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
defer s.loginClean()
|
defer s.loginClean()
|
||||||
|
|
||||||
@ -421,6 +464,13 @@ func (s *Service) Login(_ context.Context, login *LoginRequest) (*emptypb.Empty,
|
|||||||
case proton.PaidPlanRequired:
|
case proton.PaidPlanRequired:
|
||||||
_ = s.SendEvent(NewLoginError(LoginErrorType_FREE_USER, ""))
|
_ = s.SendEvent(NewLoginError(LoginErrorType_FREE_USER, ""))
|
||||||
|
|
||||||
|
case proton.HumanVerificationRequired:
|
||||||
|
s.handleHvRequest(apiErr)
|
||||||
|
|
||||||
|
case proton.HumanValidationInvalidToken:
|
||||||
|
s.hvDetails = nil
|
||||||
|
_ = s.SendEvent(NewLoginError(LoginErrorType_HV_ERROR, err.Error()))
|
||||||
|
|
||||||
default:
|
default:
|
||||||
_ = s.SendEvent(NewLoginError(LoginErrorType_USERNAME_PASSWORD_ERROR, err.Error()))
|
_ = s.SendEvent(NewLoginError(LoginErrorType_USERNAME_PASSWORD_ERROR, err.Error()))
|
||||||
}
|
}
|
||||||
@ -522,7 +572,6 @@ func (s *Service) LoginAbort(_ context.Context, loginAbort *LoginAbortRequest) (
|
|||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer async.HandlePanic(s.panicHandler)
|
defer async.HandlePanic(s.panicHandler)
|
||||||
|
|
||||||
s.loginAbort()
|
s.loginAbort()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@ -576,6 +625,8 @@ func (s *Service) InstallUpdate(_ context.Context, _ *emptypb.Empty) (*emptypb.E
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) SetIsAutomaticUpdateOn(_ context.Context, isOn *wrapperspb.BoolValue) (*emptypb.Empty, error) {
|
func (s *Service) SetIsAutomaticUpdateOn(_ context.Context, isOn *wrapperspb.BoolValue) (*emptypb.Empty, error) {
|
||||||
|
defer async.HandlePanic(s.panicHandler)
|
||||||
|
|
||||||
s.log.WithField("isOn", isOn.Value).Debug("SetIsAutomaticUpdateOn")
|
s.log.WithField("isOn", isOn.Value).Debug("SetIsAutomaticUpdateOn")
|
||||||
|
|
||||||
if currentlyOn := s.bridge.GetAutoUpdate(); currentlyOn == isOn.Value {
|
if currentlyOn := s.bridge.GetAutoUpdate(); currentlyOn == isOn.Value {
|
||||||
@ -591,12 +642,16 @@ func (s *Service) SetIsAutomaticUpdateOn(_ context.Context, isOn *wrapperspb.Boo
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) IsAutomaticUpdateOn(_ context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) {
|
func (s *Service) IsAutomaticUpdateOn(_ context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) {
|
||||||
|
defer async.HandlePanic(s.panicHandler)
|
||||||
|
|
||||||
s.log.Debug("IsAutomaticUpdateOn")
|
s.log.Debug("IsAutomaticUpdateOn")
|
||||||
|
|
||||||
return wrapperspb.Bool(s.bridge.GetAutoUpdate()), nil
|
return wrapperspb.Bool(s.bridge.GetAutoUpdate()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) DiskCachePath(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
|
func (s *Service) DiskCachePath(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
|
||||||
|
defer async.HandlePanic(s.panicHandler)
|
||||||
|
|
||||||
s.log.Debug("DiskCachePath")
|
s.log.Debug("DiskCachePath")
|
||||||
|
|
||||||
return wrapperspb.String(s.bridge.GetGluonCacheDir()), nil
|
return wrapperspb.String(s.bridge.GetGluonCacheDir()), nil
|
||||||
@ -634,6 +689,8 @@ func (s *Service) SetDiskCachePath(_ context.Context, newPath *wrapperspb.String
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) SetIsDoHEnabled(_ context.Context, isEnabled *wrapperspb.BoolValue) (*emptypb.Empty, error) {
|
func (s *Service) SetIsDoHEnabled(_ context.Context, isEnabled *wrapperspb.BoolValue) (*emptypb.Empty, error) {
|
||||||
|
defer async.HandlePanic(s.panicHandler)
|
||||||
|
|
||||||
s.log.WithField("isEnabled", isEnabled.Value).Debug("SetIsDohEnabled")
|
s.log.WithField("isEnabled", isEnabled.Value).Debug("SetIsDohEnabled")
|
||||||
|
|
||||||
if err := s.bridge.SetProxyAllowed(isEnabled.Value); err != nil {
|
if err := s.bridge.SetProxyAllowed(isEnabled.Value); err != nil {
|
||||||
@ -645,12 +702,16 @@ func (s *Service) SetIsDoHEnabled(_ context.Context, isEnabled *wrapperspb.BoolV
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) IsDoHEnabled(_ context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) {
|
func (s *Service) IsDoHEnabled(_ context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) {
|
||||||
|
defer async.HandlePanic(s.panicHandler)
|
||||||
|
|
||||||
s.log.Debug("IsDohEnabled")
|
s.log.Debug("IsDohEnabled")
|
||||||
|
|
||||||
return wrapperspb.Bool(s.bridge.GetProxyAllowed()), nil
|
return wrapperspb.Bool(s.bridge.GetProxyAllowed()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) MailServerSettings(_ context.Context, _ *emptypb.Empty) (*ImapSmtpSettings, error) {
|
func (s *Service) MailServerSettings(_ context.Context, _ *emptypb.Empty) (*ImapSmtpSettings, error) {
|
||||||
|
defer async.HandlePanic(s.panicHandler)
|
||||||
|
|
||||||
s.log.Debug("ConnectionMode")
|
s.log.Debug("ConnectionMode")
|
||||||
|
|
||||||
return &ImapSmtpSettings{
|
return &ImapSmtpSettings{
|
||||||
@ -714,24 +775,32 @@ func (s *Service) SetMailServerSettings(_ context.Context, settings *ImapSmtpSet
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Hostname(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
|
func (s *Service) Hostname(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
|
||||||
|
defer async.HandlePanic(s.panicHandler)
|
||||||
|
|
||||||
s.log.Debug("Hostname")
|
s.log.Debug("Hostname")
|
||||||
|
|
||||||
return wrapperspb.String(constants.Host), nil
|
return wrapperspb.String(constants.Host), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) IsPortFree(_ context.Context, port *wrapperspb.Int32Value) (*wrapperspb.BoolValue, error) {
|
func (s *Service) IsPortFree(_ context.Context, port *wrapperspb.Int32Value) (*wrapperspb.BoolValue, error) {
|
||||||
|
defer async.HandlePanic(s.panicHandler)
|
||||||
|
|
||||||
s.log.Debug("IsPortFree")
|
s.log.Debug("IsPortFree")
|
||||||
|
|
||||||
return wrapperspb.Bool(ports.IsPortFree(int(port.Value))), nil
|
return wrapperspb.Bool(ports.IsPortFree(int(port.Value))), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) AvailableKeychains(_ context.Context, _ *emptypb.Empty) (*AvailableKeychainsResponse, error) {
|
func (s *Service) AvailableKeychains(_ context.Context, _ *emptypb.Empty) (*AvailableKeychainsResponse, error) {
|
||||||
|
defer async.HandlePanic(s.panicHandler)
|
||||||
|
|
||||||
s.log.Debug("AvailableKeychains")
|
s.log.Debug("AvailableKeychains")
|
||||||
|
|
||||||
return &AvailableKeychainsResponse{Keychains: s.bridge.GetHelpersNames()}, nil
|
return &AvailableKeychainsResponse{Keychains: s.bridge.GetHelpersNames()}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) SetCurrentKeychain(ctx context.Context, keychain *wrapperspb.StringValue) (*emptypb.Empty, error) {
|
func (s *Service) SetCurrentKeychain(ctx context.Context, keychain *wrapperspb.StringValue) (*emptypb.Empty, error) {
|
||||||
|
defer async.HandlePanic(s.panicHandler)
|
||||||
|
|
||||||
s.log.WithField("keychain", keychain.Value).Debug("SetCurrentKeyChain") // we do not check validity.
|
s.log.WithField("keychain", keychain.Value).Debug("SetCurrentKeyChain") // we do not check validity.
|
||||||
|
|
||||||
defer func() { _, _ = s.Restart(ctx, &emptypb.Empty{}) }()
|
defer func() { _, _ = s.Restart(ctx, &emptypb.Empty{}) }()
|
||||||
@ -756,6 +825,8 @@ func (s *Service) SetCurrentKeychain(ctx context.Context, keychain *wrapperspb.S
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) CurrentKeychain(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
|
func (s *Service) CurrentKeychain(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
|
||||||
|
defer async.HandlePanic(s.panicHandler)
|
||||||
|
|
||||||
s.log.Debug("CurrentKeychain")
|
s.log.Debug("CurrentKeychain")
|
||||||
|
|
||||||
helper, err := s.bridge.GetKeychainApp()
|
helper, err := s.bridge.GetKeychainApp()
|
||||||
@ -767,6 +838,20 @@ func (s *Service) CurrentKeychain(_ context.Context, _ *emptypb.Empty) (*wrapper
|
|||||||
return wrapperspb.String(helper), nil
|
return wrapperspb.String(helper), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) TriggerRepair(_ context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
|
||||||
|
s.log.Debug("TriggerRepair")
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
async.HandlePanic(s.panicHandler)
|
||||||
|
_ = s.SendEvent(NewRepairStartedEvent())
|
||||||
|
}()
|
||||||
|
|
||||||
|
s.bridge.Repair()
|
||||||
|
}()
|
||||||
|
|
||||||
|
return &emptypb.Empty{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func base64Decode(in []byte) ([]byte, error) {
|
func base64Decode(in []byte) ([]byte, error) {
|
||||||
out := make([]byte, base64.StdEncoding.DecodedLen(len(in)))
|
out := make([]byte, base64.StdEncoding.DecodedLen(len(in)))
|
||||||
|
|
||||||
|
|||||||
@ -20,21 +20,25 @@ package grpc
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/gluon/async"
|
||||||
"google.golang.org/protobuf/types/known/emptypb"
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
"google.golang.org/protobuf/types/known/wrapperspb"
|
"google.golang.org/protobuf/types/known/wrapperspb"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Service) ReportBugClicked(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
|
func (s *Service) ReportBugClicked(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
|
||||||
|
defer async.HandlePanic(s.panicHandler)
|
||||||
s.bridge.ReportBugClicked()
|
s.bridge.ReportBugClicked()
|
||||||
return &emptypb.Empty{}, nil
|
return &emptypb.Empty{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) AutoconfigClicked(_ context.Context, client *wrapperspb.StringValue) (*emptypb.Empty, error) {
|
func (s *Service) AutoconfigClicked(_ context.Context, client *wrapperspb.StringValue) (*emptypb.Empty, error) {
|
||||||
|
defer async.HandlePanic(s.panicHandler)
|
||||||
s.bridge.AutoconfigUsed(client.Value)
|
s.bridge.AutoconfigUsed(client.Value)
|
||||||
return &emptypb.Empty{}, nil
|
return &emptypb.Empty{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) ExternalLinkClicked(_ context.Context, article *wrapperspb.StringValue) (*emptypb.Empty, error) {
|
func (s *Service) ExternalLinkClicked(_ context.Context, article *wrapperspb.StringValue) (*emptypb.Empty, error) {
|
||||||
|
defer async.HandlePanic(s.panicHandler)
|
||||||
s.bridge.ExternalLinkClicked(article.Value)
|
s.bridge.ExternalLinkClicked(article.Value)
|
||||||
return &emptypb.Empty{}, nil
|
return &emptypb.Empty{}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,6 +29,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (s *Service) GetUserList(_ context.Context, _ *emptypb.Empty) (*UserListResponse, error) {
|
func (s *Service) GetUserList(_ context.Context, _ *emptypb.Empty) (*UserListResponse, error) {
|
||||||
|
defer async.HandlePanic(s.panicHandler)
|
||||||
s.log.Debug("GetUserList")
|
s.log.Debug("GetUserList")
|
||||||
|
|
||||||
userIDs := s.bridge.GetUserIDs()
|
userIDs := s.bridge.GetUserIDs()
|
||||||
@ -52,6 +53,7 @@ func (s *Service) GetUserList(_ context.Context, _ *emptypb.Empty) (*UserListRes
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) GetUser(_ context.Context, userID *wrapperspb.StringValue) (*User, error) {
|
func (s *Service) GetUser(_ context.Context, userID *wrapperspb.StringValue) (*User, error) {
|
||||||
|
defer async.HandlePanic(s.panicHandler)
|
||||||
s.log.WithField("userID", userID).Debug("GetUser")
|
s.log.WithField("userID", userID).Debug("GetUser")
|
||||||
|
|
||||||
user, err := s.bridge.GetUserInfo(userID.Value)
|
user, err := s.bridge.GetUserInfo(userID.Value)
|
||||||
@ -63,6 +65,7 @@ func (s *Service) GetUser(_ context.Context, userID *wrapperspb.StringValue) (*U
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) SetUserSplitMode(_ context.Context, splitMode *UserSplitModeRequest) (*emptypb.Empty, error) {
|
func (s *Service) SetUserSplitMode(_ context.Context, splitMode *UserSplitModeRequest) (*emptypb.Empty, error) {
|
||||||
|
defer async.HandlePanic(s.panicHandler)
|
||||||
s.log.WithField("UserID", splitMode.UserID).WithField("Active", splitMode.Active).Debug("SetUserSplitMode")
|
s.log.WithField("UserID", splitMode.UserID).WithField("Active", splitMode.Active).Debug("SetUserSplitMode")
|
||||||
|
|
||||||
user, err := s.bridge.GetUserInfo(splitMode.UserID)
|
user, err := s.bridge.GetUserInfo(splitMode.UserID)
|
||||||
@ -97,6 +100,7 @@ func (s *Service) SetUserSplitMode(_ context.Context, splitMode *UserSplitModeRe
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) SendBadEventUserFeedback(_ context.Context, feedback *UserBadEventFeedbackRequest) (*emptypb.Empty, error) {
|
func (s *Service) SendBadEventUserFeedback(_ context.Context, feedback *UserBadEventFeedbackRequest) (*emptypb.Empty, error) {
|
||||||
|
defer async.HandlePanic(s.panicHandler)
|
||||||
l := s.log.WithField("UserID", feedback.UserID).WithField("doResync", feedback.DoResync)
|
l := s.log.WithField("UserID", feedback.UserID).WithField("doResync", feedback.DoResync)
|
||||||
l.Debug("SendBadEventUserFeedback")
|
l.Debug("SendBadEventUserFeedback")
|
||||||
|
|
||||||
@ -115,6 +119,7 @@ func (s *Service) SendBadEventUserFeedback(_ context.Context, feedback *UserBadE
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) LogoutUser(_ context.Context, userID *wrapperspb.StringValue) (*emptypb.Empty, error) {
|
func (s *Service) LogoutUser(_ context.Context, userID *wrapperspb.StringValue) (*emptypb.Empty, error) {
|
||||||
|
defer async.HandlePanic(s.panicHandler)
|
||||||
s.log.WithField("UserID", userID.Value).Debug("LogoutUser")
|
s.log.WithField("UserID", userID.Value).Debug("LogoutUser")
|
||||||
|
|
||||||
if _, err := s.bridge.GetUserInfo(userID.Value); err != nil {
|
if _, err := s.bridge.GetUserInfo(userID.Value); err != nil {
|
||||||
@ -133,6 +138,7 @@ func (s *Service) LogoutUser(_ context.Context, userID *wrapperspb.StringValue)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) RemoveUser(_ context.Context, userID *wrapperspb.StringValue) (*emptypb.Empty, error) {
|
func (s *Service) RemoveUser(_ context.Context, userID *wrapperspb.StringValue) (*emptypb.Empty, error) {
|
||||||
|
defer async.HandlePanic(s.panicHandler)
|
||||||
s.log.WithField("UserID", userID.Value).Debug("RemoveUser")
|
s.log.WithField("UserID", userID.Value).Debug("RemoveUser")
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
@ -148,6 +154,7 @@ func (s *Service) RemoveUser(_ context.Context, userID *wrapperspb.StringValue)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) ConfigureUserAppleMail(ctx context.Context, request *ConfigureAppleMailRequest) (*emptypb.Empty, error) {
|
func (s *Service) ConfigureUserAppleMail(ctx context.Context, request *ConfigureAppleMailRequest) (*emptypb.Empty, error) {
|
||||||
|
defer async.HandlePanic(s.panicHandler)
|
||||||
s.log.WithField("UserID", request.UserID).WithField("Address", request.Address).Debug("ConfigureUserAppleMail")
|
s.log.WithField("UserID", request.UserID).WithField("Address", request.Address).Debug("ConfigureUserAppleMail")
|
||||||
|
|
||||||
sslWasEnabled := s.bridge.GetSMTPSSL()
|
sslWasEnabled := s.bridge.GetSMTPSSL()
|
||||||
|
|||||||
62
internal/hv/hv.go
Normal file
62
internal/hv/hv.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
// Copyright (c) 2024 Proton AG
|
||||||
|
// This file is part of Proton Mail Bridge.
|
||||||
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package hv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/go-proton-api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// VerifyAndExtractHvRequest expects an error request as input
|
||||||
|
// determines whether the given error is a Proton human verification request; if it isn't then it returns -> nil, nil (no details, no error)
|
||||||
|
// if it is a HV req. then it tries to parse the json data and verify that the captcha method is included; if either fails -> nil, err
|
||||||
|
// if the HV request was successfully decoded and the preconditions were met it returns the hv details -> hvDetails, nil.
|
||||||
|
func VerifyAndExtractHvRequest(err error) (*proton.APIHVDetails, error) {
|
||||||
|
if err == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var protonErr *proton.APIError
|
||||||
|
if errors.As(err, &protonErr) && protonErr.IsHVError() {
|
||||||
|
hvDetails, hvErr := protonErr.GetHVDetails()
|
||||||
|
if hvErr != nil {
|
||||||
|
return nil, fmt.Errorf("received HV request, but can't decode HV details")
|
||||||
|
}
|
||||||
|
return hvDetails, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsHvRequest(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var protonErr *proton.APIError
|
||||||
|
if errors.As(err, &protonErr) && protonErr.IsHVError() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func FormatHvURL(details *proton.APIHVDetails) string {
|
||||||
|
return fmt.Sprintf("https://verify.proton.me/?methods=%v&token=%v",
|
||||||
|
strings.Join(details.Methods, ","),
|
||||||
|
details.Token)
|
||||||
|
}
|
||||||
144
internal/hv/hv_test.go
Normal file
144
internal/hv/hv_test.go
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
// Copyright (c) 2024 Proton AG
|
||||||
|
//
|
||||||
|
// This file is part of Proton Mail Bridge.Bridge.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package hv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/go-proton-api"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestVerifyAndExtractHvRequest(t *testing.T) {
|
||||||
|
det1, _ := json.Marshal("test")
|
||||||
|
det2, _ := json.Marshal(proton.APIHVDetails{Methods: []string{"ownership-email"}, Token: "test"})
|
||||||
|
det3, _ := json.Marshal(proton.APIHVDetails{Methods: []string{"captcha"}, Token: "test"})
|
||||||
|
det4, _ := json.Marshal(proton.APIHVDetails{Methods: []string{"ownership-email", "test"}, Token: "test"})
|
||||||
|
det5, _ := json.Marshal(proton.APIHVDetails{Methods: []string{"captcha", "ownership-email"}, Token: "test"})
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
err error
|
||||||
|
hasHvDetails bool
|
||||||
|
hasErr bool
|
||||||
|
}{
|
||||||
|
{err: nil,
|
||||||
|
hasHvDetails: false,
|
||||||
|
hasErr: false},
|
||||||
|
{err: fmt.Errorf("test"),
|
||||||
|
hasHvDetails: false,
|
||||||
|
hasErr: false},
|
||||||
|
{err: new(proton.APIError),
|
||||||
|
hasHvDetails: false,
|
||||||
|
hasErr: false},
|
||||||
|
{err: &proton.APIError{Status: 429},
|
||||||
|
hasHvDetails: false,
|
||||||
|
hasErr: false},
|
||||||
|
{err: &proton.APIError{Status: 9001},
|
||||||
|
hasHvDetails: false,
|
||||||
|
hasErr: false},
|
||||||
|
{err: &proton.APIError{Code: 9001},
|
||||||
|
hasHvDetails: false,
|
||||||
|
hasErr: true},
|
||||||
|
{err: &proton.APIError{Code: 9001, Details: det1},
|
||||||
|
hasHvDetails: false,
|
||||||
|
hasErr: true},
|
||||||
|
{err: &proton.APIError{Code: 9001, Details: det2},
|
||||||
|
hasHvDetails: true,
|
||||||
|
hasErr: false},
|
||||||
|
{err: &proton.APIError{Code: 9001, Details: det3},
|
||||||
|
hasHvDetails: true,
|
||||||
|
hasErr: false},
|
||||||
|
{err: &proton.APIError{Code: 9001, Details: det4},
|
||||||
|
hasHvDetails: true,
|
||||||
|
hasErr: false},
|
||||||
|
{err: &proton.APIError{Code: 9001, Details: det5},
|
||||||
|
hasHvDetails: true,
|
||||||
|
hasErr: false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
hvDetails, err := VerifyAndExtractHvRequest(test.err)
|
||||||
|
hasHv := hvDetails != nil
|
||||||
|
hasErr := err != nil
|
||||||
|
require.True(t, hasHv == test.hasHvDetails && hasErr == test.hasErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsHvRequest(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
err error
|
||||||
|
result bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
err: nil,
|
||||||
|
result: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
err: fmt.Errorf("test"),
|
||||||
|
result: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
err: new(proton.APIError),
|
||||||
|
result: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
err: &proton.APIError{Status: 429},
|
||||||
|
result: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
err: &proton.APIError{Status: 9001},
|
||||||
|
result: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
err: &proton.APIError{Code: 9001},
|
||||||
|
result: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
isHvRequest := IsHvRequest(test.err)
|
||||||
|
require.Equal(t, test.result, isHvRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormatHvURL(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
details *proton.APIHVDetails
|
||||||
|
result string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
details: &proton.APIHVDetails{Methods: []string{"test"}, Token: "test"},
|
||||||
|
result: "https://verify.proton.me/?methods=test&token=test",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
details: &proton.APIHVDetails{Methods: []string{""}, Token: "test"},
|
||||||
|
result: "https://verify.proton.me/?methods=&token=test",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
details: &proton.APIHVDetails{Methods: []string{"test"}, Token: ""},
|
||||||
|
result: "https://verify.proton.me/?methods=test&token=",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, el := range tests {
|
||||||
|
result := FormatHvURL(el.details)
|
||||||
|
require.Equal(t, el.result, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -262,7 +262,8 @@
|
|||||||
"keywords": [
|
"keywords": [
|
||||||
"Outlook",
|
"Outlook",
|
||||||
"setup",
|
"setup",
|
||||||
"configuration"
|
"configuration",
|
||||||
|
"We encountered an error while adding account"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -421,5 +422,38 @@
|
|||||||
"Apple Mail",
|
"Apple Mail",
|
||||||
"macOS"
|
"macOS"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 37,
|
||||||
|
"url": "https://proton.me/support/protonmail-bridge-clients-macos-new-outlook#may-17",
|
||||||
|
"title": "Important notice regarding the New Outlook for Mac and issues you might face",
|
||||||
|
"keywords": [
|
||||||
|
"Receiving",
|
||||||
|
"Sending",
|
||||||
|
"Outlook",
|
||||||
|
"Configuration",
|
||||||
|
"Sync",
|
||||||
|
"New Outlook"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 38,
|
||||||
|
"url": "https://proton.me/support/proton-mail-bridge-new-outlook-for-windows-set-up-guide",
|
||||||
|
"title": "What is the Recovered Messages folder in Bridge (and your email client)?",
|
||||||
|
"keywords": [
|
||||||
|
"recovered messages",
|
||||||
|
"recovered messages folder"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 39,
|
||||||
|
"url": "https://proton.me/support/proton-mail-bridge-new-outlook-for-windows-set-up-guide",
|
||||||
|
"title": "Proton Mail Bridge New Outlook for Windows set up guide",
|
||||||
|
"keywords": [
|
||||||
|
"app password",
|
||||||
|
"INVALIDCREDENTIALS",
|
||||||
|
"TEMPORARILYUNAVAILABLE",
|
||||||
|
"New Outlook"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -23,12 +23,15 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ProtonMail/gluon/async"
|
"github.com/ProtonMail/gluon/async"
|
||||||
"github.com/ProtonMail/gluon/connector"
|
"github.com/ProtonMail/gluon/connector"
|
||||||
"github.com/ProtonMail/gluon/imap"
|
"github.com/ProtonMail/gluon/imap"
|
||||||
|
"github.com/ProtonMail/gluon/reporter"
|
||||||
|
"github.com/ProtonMail/gluon/rfc5322"
|
||||||
"github.com/ProtonMail/gluon/rfc822"
|
"github.com/ProtonMail/gluon/rfc822"
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||||
@ -54,6 +57,7 @@ type Connector struct {
|
|||||||
identityState sharedIdentity
|
identityState sharedIdentity
|
||||||
client APIClient
|
client APIClient
|
||||||
telemetry Telemetry
|
telemetry Telemetry
|
||||||
|
reporter reporter.Reporter
|
||||||
panicHandler async.PanicHandler
|
panicHandler async.PanicHandler
|
||||||
sendRecorder *sendrecorder.SendRecorder
|
sendRecorder *sendrecorder.SendRecorder
|
||||||
|
|
||||||
@ -75,6 +79,7 @@ func NewConnector(
|
|||||||
sendRecorder *sendrecorder.SendRecorder,
|
sendRecorder *sendrecorder.SendRecorder,
|
||||||
panicHandler async.PanicHandler,
|
panicHandler async.PanicHandler,
|
||||||
telemetry Telemetry,
|
telemetry Telemetry,
|
||||||
|
reporter reporter.Reporter,
|
||||||
showAllMail bool,
|
showAllMail bool,
|
||||||
syncState *SyncState,
|
syncState *SyncState,
|
||||||
) *Connector {
|
) *Connector {
|
||||||
@ -90,6 +95,7 @@ func NewConnector(
|
|||||||
|
|
||||||
client: apiClient,
|
client: apiClient,
|
||||||
telemetry: telemetry,
|
telemetry: telemetry,
|
||||||
|
reporter: reporter,
|
||||||
panicHandler: panicHandler,
|
panicHandler: panicHandler,
|
||||||
sendRecorder: sendRecorder,
|
sendRecorder: sendRecorder,
|
||||||
|
|
||||||
@ -279,7 +285,7 @@ func (s *Connector) CreateMessage(ctx context.Context, _ connector.IMAPStateWrit
|
|||||||
if messageID, ok, err := s.sendRecorder.HasEntryWait(ctx, hash, time.Now().Add(90*time.Second), toList); err != nil {
|
if messageID, ok, err := s.sendRecorder.HasEntryWait(ctx, hash, time.Now().Add(90*time.Second), toList); err != nil {
|
||||||
return imap.Message{}, nil, fmt.Errorf("failed to check send hash: %w", err)
|
return imap.Message{}, nil, fmt.Errorf("failed to check send hash: %w", err)
|
||||||
} else if ok {
|
} else if ok {
|
||||||
s.log.WithField("messageID", messageID).Warn("Message already sent")
|
s.log.WithField("messageID", messageID).Warn("Message already in sent mailbox")
|
||||||
|
|
||||||
// Query the server-side message.
|
// Query the server-side message.
|
||||||
full, err := s.client.GetFullMessage(ctx, messageID, usertypes.NewProtonAPIScheduler(s.panicHandler), proton.NewDefaultAttachmentAllocator())
|
full, err := s.client.GetFullMessage(ctx, messageID, usertypes.NewProtonAPIScheduler(s.panicHandler), proton.NewDefaultAttachmentAllocator())
|
||||||
@ -671,11 +677,21 @@ func (s *Connector) importMessage(
|
|||||||
) (imap.Message, []byte, error) {
|
) (imap.Message, []byte, error) {
|
||||||
var full proton.FullMessage
|
var full proton.FullMessage
|
||||||
|
|
||||||
|
// addr is primary for combined mode or active for split mode
|
||||||
addr, ok := s.identityState.GetAddress(s.addrID)
|
addr, ok := s.identityState.GetAddress(s.addrID)
|
||||||
if !ok {
|
if !ok {
|
||||||
return imap.Message{}, nil, fmt.Errorf("could not find address")
|
return imap.Message{}, nil, fmt.Errorf("could not find address")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p, err2 := parser.New(bytes.NewReader(literal))
|
||||||
|
if err2 != nil {
|
||||||
|
return imap.Message{}, nil, fmt.Errorf("failed to parse literal: %w", err2)
|
||||||
|
}
|
||||||
|
|
||||||
|
isDraft := slices.Contains(labelIDs, proton.DraftsLabel)
|
||||||
|
|
||||||
|
s.reportGODT3185(isDraft, addr.Email, p, s.addressMode == usertypes.AddressModeCombined)
|
||||||
|
|
||||||
if err := s.identityState.WithAddrKR(s.addrID, func(_, addrKR *crypto.KeyRing) error {
|
if err := s.identityState.WithAddrKR(s.addrID, func(_, addrKR *crypto.KeyRing) error {
|
||||||
primaryKey, errKey := addrKR.FirstKey()
|
primaryKey, errKey := addrKR.FirstKey()
|
||||||
if errKey != nil {
|
if errKey != nil {
|
||||||
@ -683,11 +699,8 @@ func (s *Connector) importMessage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var messageID string
|
var messageID string
|
||||||
p, err2 := parser.New(bytes.NewReader(literal))
|
|
||||||
if err2 != nil {
|
if isDraft {
|
||||||
return fmt.Errorf("failed to parse literal: %w", err2)
|
|
||||||
}
|
|
||||||
if slices.Contains(labelIDs, proton.DraftsLabel) {
|
|
||||||
msg, err := s.createDraftWithParser(ctx, p, primaryKey, addr)
|
msg, err := s.createDraftWithParser(ctx, p, primaryKey, addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create draft: %w", err)
|
return fmt.Errorf("failed to create draft: %w", err)
|
||||||
@ -850,3 +863,94 @@ func defaultMailboxPermanentFlags() imap.FlagSet {
|
|||||||
func defaultMailboxAttributes() imap.FlagSet {
|
func defaultMailboxAttributes() imap.FlagSet {
|
||||||
return imap.NewFlagSet()
|
return imap.NewFlagSet()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func stripPlusAlias(a string) string {
|
||||||
|
iPlus := strings.Index(a, "+")
|
||||||
|
iAt := strings.Index(a, "@")
|
||||||
|
if iPlus <= 0 || iAt <= 0 || iPlus >= iAt {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
return a[:iPlus] + a[iAt:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func equalAddresses(a, b string) bool {
|
||||||
|
return strings.EqualFold(stripPlusAlias(a), stripPlusAlias(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Connector) reportGODT3185(isDraft bool, defaultAddr string, p *parser.Parser, isCombinedMode bool) {
|
||||||
|
reportAction := "draft"
|
||||||
|
if !isDraft {
|
||||||
|
reportAction = "import"
|
||||||
|
}
|
||||||
|
|
||||||
|
reportMode := "combined"
|
||||||
|
if !isCombinedMode {
|
||||||
|
reportMode = "split"
|
||||||
|
}
|
||||||
|
|
||||||
|
senderAddr := ""
|
||||||
|
if p != nil && p.Root() != nil && p.Root().Header.Len() != 0 {
|
||||||
|
addrField := p.Root().Header.Get("From")
|
||||||
|
if addrField == "" {
|
||||||
|
addrField = p.Root().Header.Get("Sender")
|
||||||
|
}
|
||||||
|
if addrField != "" {
|
||||||
|
sender, err := rfc5322.ParseAddressList(addrField)
|
||||||
|
if err == nil && len(sender) > 0 {
|
||||||
|
senderAddr = sender[0].Address
|
||||||
|
} else {
|
||||||
|
s.log.WithError(err).Warn("Invalid sender address in reporter")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if equalAddresses(defaultAddr, senderAddr) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isDisabled := false
|
||||||
|
isUserAddress := false
|
||||||
|
for _, a := range s.identityState.GetAddresses() {
|
||||||
|
if !equalAddresses(a.Email, senderAddr) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
isUserAddress = true
|
||||||
|
isDisabled = !bool(a.Send) || (a.Status != proton.AddressStatusEnabled)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isUserAddress && senderAddr != "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reportResult := "using sender address"
|
||||||
|
|
||||||
|
if !isCombinedMode {
|
||||||
|
reportResult = "error address not match"
|
||||||
|
}
|
||||||
|
|
||||||
|
reportAddress := ""
|
||||||
|
if senderAddr == "" {
|
||||||
|
reportAddress = " invalid"
|
||||||
|
reportResult = "error import/draft"
|
||||||
|
}
|
||||||
|
|
||||||
|
if isDisabled {
|
||||||
|
reportAddress = " disabled"
|
||||||
|
if isDraft {
|
||||||
|
reportResult = "error draft"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
report := fmt.Sprintf(
|
||||||
|
"GODT-3185: %s with non-default%s address in %s mode: %s",
|
||||||
|
reportAction, reportAddress, reportMode, reportResult,
|
||||||
|
)
|
||||||
|
|
||||||
|
s.log.Warn(report)
|
||||||
|
if s.reporter != nil {
|
||||||
|
_ = s.reporter.ReportMessage(report)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -203,3 +203,35 @@ func TestFixGODT3003Labels_Noop(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.False(t, applied)
|
require.False(t, applied)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStripPlusAlias(t *testing.T) {
|
||||||
|
cases := map[string]string{
|
||||||
|
"one@three.com": "one@three.com",
|
||||||
|
"one+two@three.com": "one@three.com",
|
||||||
|
"one@three+two.com": "one@three+two.com",
|
||||||
|
"+one@three.com": "+one@three.com",
|
||||||
|
"@three.com": "@three.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
for given, want := range cases {
|
||||||
|
require.Equal(t, want, stripPlusAlias(given), "input was %q", given)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEqualAddresse(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
a, b string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{"one@three.com", "one@three.com", true},
|
||||||
|
{"one@three.com", "one+two@three.com", true},
|
||||||
|
{"OnE@thReE.com", "One@THree.com", true},
|
||||||
|
{"one@three.com", "two@three.com", false},
|
||||||
|
{"one+two@three.com", "two@three.com", false},
|
||||||
|
{"one@three.com", "one@three+two.com", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
require.Equal(t, c.want, equalAddresses(c.a, c.b), "input was %q and %q", c.a, c.b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -508,6 +508,7 @@ func (s *Service) buildConnectors() (map[string]*Connector, error) {
|
|||||||
s.sendRecorder,
|
s.sendRecorder,
|
||||||
s.panicHandler,
|
s.panicHandler,
|
||||||
s.telemetry,
|
s.telemetry,
|
||||||
|
s.reporter,
|
||||||
s.showAllMail,
|
s.showAllMail,
|
||||||
s.syncStateProvider,
|
s.syncStateProvider,
|
||||||
)
|
)
|
||||||
@ -525,6 +526,7 @@ func (s *Service) buildConnectors() (map[string]*Connector, error) {
|
|||||||
s.sendRecorder,
|
s.sendRecorder,
|
||||||
s.panicHandler,
|
s.panicHandler,
|
||||||
s.telemetry,
|
s.telemetry,
|
||||||
|
s.reporter,
|
||||||
s.showAllMail,
|
s.showAllMail,
|
||||||
s.syncStateProvider,
|
s.syncStateProvider,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -155,6 +155,7 @@ func addNewAddressSplitMode(ctx context.Context, s *Service, addrID string) erro
|
|||||||
s.sendRecorder,
|
s.sendRecorder,
|
||||||
s.panicHandler,
|
s.panicHandler,
|
||||||
s.telemetry,
|
s.telemetry,
|
||||||
|
s.reporter,
|
||||||
s.showAllMail,
|
s.showAllMail,
|
||||||
s.syncStateProvider,
|
s.syncStateProvider,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -46,6 +46,7 @@ func defaultMessageJobOpts() message.JobOptions {
|
|||||||
AddExternalID: true, // Whether to include ExternalID as X-Pm-External-Id.
|
AddExternalID: true, // Whether to include ExternalID as X-Pm-External-Id.
|
||||||
AddMessageDate: true, // Whether to include message time as X-Pm-Date.
|
AddMessageDate: true, // Whether to include message time as X-Pm-Date.
|
||||||
AddMessageIDReference: true, // Whether to include the MessageID in References.
|
AddMessageIDReference: true, // Whether to include the MessageID in References.
|
||||||
|
SanitizeMBOXHeaderLine: true, // Whether to ignore header line representing MBOX delimiter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -55,9 +55,8 @@ type Service struct {
|
|||||||
panicHandler async.PanicHandler
|
panicHandler async.PanicHandler
|
||||||
reporter reporter.Reporter
|
reporter reporter.Reporter
|
||||||
|
|
||||||
loadedUserCount int
|
log *logrus.Entry
|
||||||
log *logrus.Entry
|
tasks *async.Group
|
||||||
tasks *async.Group
|
|
||||||
|
|
||||||
uidValidityGenerator imap.UIDValidityGenerator
|
uidValidityGenerator imap.UIDValidityGenerator
|
||||||
telemetry Telemetry
|
telemetry Telemetry
|
||||||
@ -108,6 +107,16 @@ func (sm *Service) Init(ctx context.Context, group *async.Group, subscription ev
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if err := sm.serveIMAP(ctx); err != nil {
|
||||||
|
sm.log.WithError(err).Error("Failed to start IMAP server on bridge start")
|
||||||
|
sm.imapListener = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sm.serveSMTP(ctx); err != nil {
|
||||||
|
sm.log.WithError(err).Error("Failed to start SMTP server on bridge start")
|
||||||
|
sm.smtpListener = nil
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -255,30 +264,16 @@ func (sm *Service) run(ctx context.Context, subscription events.Subscription) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (sm *Service) handleLoadedUserCountChange(ctx context.Context) {
|
func (sm *Service) handleLoadedUserCountChange(ctx context.Context) {
|
||||||
sm.log.Infof("Validating Listener State %v", sm.loadedUserCount)
|
sm.log.Infof("Validating Listener State")
|
||||||
if sm.shouldStartServers() {
|
if sm.imapListener == nil {
|
||||||
if sm.imapListener == nil {
|
if err := sm.serveIMAP(ctx); err != nil {
|
||||||
if err := sm.serveIMAP(ctx); err != nil {
|
sm.log.WithError(err).Error("Failed to start IMAP server")
|
||||||
sm.log.WithError(err).Error("Failed to start IMAP server")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if sm.smtpListener == nil {
|
if sm.smtpListener == nil {
|
||||||
if err := sm.restartSMTP(ctx); err != nil {
|
if err := sm.restartSMTP(ctx); err != nil {
|
||||||
sm.log.WithError(err).Error("Failed to start SMTP server")
|
sm.log.WithError(err).Error("Failed to start SMTP server")
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if sm.imapListener != nil {
|
|
||||||
if err := sm.stopIMAPListener(ctx); err != nil {
|
|
||||||
sm.log.WithError(err).Error("Failed to stop IMAP server")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if sm.smtpListener != nil {
|
|
||||||
if err := sm.closeSMTPServer(ctx); err != nil {
|
|
||||||
sm.log.WithError(err).Error("Failed to stop SMTP server")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -307,12 +302,7 @@ func (sm *Service) handleAddIMAPUser(ctx context.Context,
|
|||||||
) error {
|
) error {
|
||||||
// Due to the many different error exits, performer user count change at this stage rather we split the incrementing
|
// Due to the many different error exits, performer user count change at this stage rather we split the incrementing
|
||||||
// of users from the logic.
|
// of users from the logic.
|
||||||
err := sm.handleAddIMAPUserImpl(ctx, connector, addrID, idProvider, syncStateProvider)
|
return sm.handleAddIMAPUserImpl(ctx, connector, addrID, idProvider, syncStateProvider)
|
||||||
if err == nil {
|
|
||||||
sm.loadedUserCount++
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sm *Service) handleAddIMAPUserImpl(ctx context.Context,
|
func (sm *Service) handleAddIMAPUserImpl(ctx context.Context,
|
||||||
@ -436,8 +426,6 @@ func (sm *Service) handleRemoveIMAPUser(ctx context.Context, withData bool, idPr
|
|||||||
return fmt.Errorf("failed to remove IMAP user ID: %w", err)
|
return fmt.Errorf("failed to remove IMAP user ID: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sm.loadedUserCount--
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -542,11 +530,7 @@ func (sm *Service) restartIMAP(ctx context.Context) error {
|
|||||||
sm.eventPublisher.PublishEvent(ctx, events.IMAPServerStopped{})
|
sm.eventPublisher.PublishEvent(ctx, events.IMAPServerStopped{})
|
||||||
}
|
}
|
||||||
|
|
||||||
if sm.shouldStartServers() {
|
return sm.serveIMAP(ctx)
|
||||||
return sm.serveIMAP(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sm *Service) restartSMTP(ctx context.Context) error {
|
func (sm *Service) restartSMTP(ctx context.Context) error {
|
||||||
@ -560,11 +544,7 @@ func (sm *Service) restartSMTP(ctx context.Context) error {
|
|||||||
|
|
||||||
sm.smtpServer = newSMTPServer(sm.smtpAccounts, sm.smtpSettings)
|
sm.smtpServer = newSMTPServer(sm.smtpAccounts, sm.smtpSettings)
|
||||||
|
|
||||||
if sm.shouldStartServers() {
|
return sm.serveSMTP(ctx)
|
||||||
return sm.serveSMTP(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sm *Service) serveSMTP(ctx context.Context) error {
|
func (sm *Service) serveSMTP(ctx context.Context) error {
|
||||||
@ -679,8 +659,6 @@ func (sm *Service) handleSetGluonDir(ctx context.Context, newGluonDir string) er
|
|||||||
return fmt.Errorf("failed to close IMAP: %w", err)
|
return fmt.Errorf("failed to close IMAP: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
sm.loadedUserCount = 0
|
|
||||||
|
|
||||||
if err := moveGluonCacheDir(sm.imapSettings, currentGluonDir, newGluonDir); err != nil {
|
if err := moveGluonCacheDir(sm.imapSettings, currentGluonDir, newGluonDir); err != nil {
|
||||||
sm.log.WithError(err).Error("failed to move GluonCacheDir")
|
sm.log.WithError(err).Error("failed to move GluonCacheDir")
|
||||||
|
|
||||||
@ -700,19 +678,13 @@ func (sm *Service) handleSetGluonDir(ctx context.Context, newGluonDir string) er
|
|||||||
|
|
||||||
sm.imapServer = imapServer
|
sm.imapServer = imapServer
|
||||||
|
|
||||||
if sm.shouldStartServers() {
|
if err := sm.serveIMAP(ctx); err != nil {
|
||||||
if err := sm.serveIMAP(ctx); err != nil {
|
return fmt.Errorf("failed to serve IMAP: %w", err)
|
||||||
return fmt.Errorf("failed to serve IMAP: %w", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sm *Service) shouldStartServers() bool {
|
|
||||||
return sm.loadedUserCount >= 1
|
|
||||||
}
|
|
||||||
|
|
||||||
type smRequestClose struct{}
|
type smRequestClose struct{}
|
||||||
|
|
||||||
type smRequestRestartIMAP struct{}
|
type smRequestRestartIMAP struct{}
|
||||||
|
|||||||
@ -27,14 +27,14 @@ var ErrInvalidReturnPath = errors.New("invalid return path")
|
|||||||
var ErrNoSuchUser = errors.New("no such user")
|
var ErrNoSuchUser = errors.New("no such user")
|
||||||
var ErrTooManyErrors = errors.New("too many failed requests, please try again later")
|
var ErrTooManyErrors = errors.New("too many failed requests, please try again later")
|
||||||
|
|
||||||
type ErrCanNotSendOnAddress struct {
|
type ErrCannotSendFromAddress struct {
|
||||||
address string
|
address string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewErrCanNotSendOnAddress(address string) *ErrCanNotSendOnAddress {
|
func NewErrCannotSendFromAddress(address string) *ErrCannotSendFromAddress {
|
||||||
return &ErrCanNotSendOnAddress{address: address}
|
return &ErrCannotSendFromAddress{address: address}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e ErrCanNotSendOnAddress) Error() string {
|
func (e ErrCannotSendFromAddress) Error() string {
|
||||||
return fmt.Sprintf("can't send on address: %v", e.address)
|
return fmt.Sprintf("cannot send from address: %v", e.address)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -103,8 +103,8 @@ func (s *Service) smtpSendMail(ctx context.Context, authID string, from string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !fromAddr.Send || fromAddr.Status != proton.AddressStatusEnabled {
|
if !fromAddr.Send || fromAddr.Status != proton.AddressStatusEnabled {
|
||||||
s.log.Errorf("Can't send emails on address: %v", fromAddr.Email)
|
s.log.Errorf("Cannot send emails from address: %v", fromAddr.Email)
|
||||||
return &ErrCanNotSendOnAddress{address: fromAddr.Email}
|
return &ErrCannotSendFromAddress{address: fromAddr.Email}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the user's mail settings.
|
// Load the user's mail settings.
|
||||||
|
|||||||
@ -65,8 +65,10 @@ func TestHeartbeat_default_heartbeat(t *testing.T) {
|
|||||||
func TestHeartbeat_already_sent_heartbeat(t *testing.T) {
|
func TestHeartbeat_already_sent_heartbeat(t *testing.T) {
|
||||||
withHeartbeat(t, 1143, 1025, "/tmp", "defaultKeychain", func(hb *telemetry.Heartbeat, mock *mocks.MockHeartbeatManager) {
|
withHeartbeat(t, 1143, 1025, "/tmp", "defaultKeychain", func(hb *telemetry.Heartbeat, mock *mocks.MockHeartbeatManager) {
|
||||||
mock.EXPECT().IsTelemetryAvailable(context.Background()).Return(true)
|
mock.EXPECT().IsTelemetryAvailable(context.Background()).Return(true)
|
||||||
mock.EXPECT().GetLastHeartbeatSent().Return(time.Now().Truncate(24 * time.Hour))
|
mock.EXPECT().GetLastHeartbeatSent().DoAndReturn(func() time.Time {
|
||||||
|
curTime := time.Now()
|
||||||
|
return time.Date(curTime.Year(), curTime.Month(), curTime.Day(), 0, 0, 0, 0, curTime.Location())
|
||||||
|
})
|
||||||
hb.TrySending(context.Background())
|
hb.TrySending(context.Background())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
43
internal/telemetry/repair.go
Normal file
43
internal/telemetry/repair.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
// Copyright (c) 2024 Proton AG
|
||||||
|
//
|
||||||
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package telemetry
|
||||||
|
|
||||||
|
type RepairData struct {
|
||||||
|
MeasurementGroup string
|
||||||
|
Event string
|
||||||
|
Values map[string]string
|
||||||
|
Dimensions map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRepairTriggerData() RepairData {
|
||||||
|
return RepairData{
|
||||||
|
MeasurementGroup: "bridge.any.repair",
|
||||||
|
Event: "repair_trigger",
|
||||||
|
Values: map[string]string{},
|
||||||
|
Dimensions: map[string]string{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRepairDeferredTriggerData() RepairData {
|
||||||
|
return RepairData{
|
||||||
|
MeasurementGroup: "bridge.any.repair",
|
||||||
|
Event: "repair_deferred_trigger",
|
||||||
|
Values: map[string]string{},
|
||||||
|
Dimensions: map[string]string{},
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -146,7 +146,7 @@ func mkdirAllClear(path string) error {
|
|||||||
func checksum(path string) (hash string) {
|
func checksum(path string) (hash string) {
|
||||||
file, err := os.Open(filepath.Clean(path))
|
file, err := os.Open(filepath.Clean(path))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.WithError(err).WithField("path", path).Error("Cannot open file for checksum")
|
logrus.WithError(err).WithField("path", path).Warn("Cannot open file for checksum")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer file.Close() //nolint:errcheck,gosec
|
defer file.Close() //nolint:errcheck,gosec
|
||||||
|
|||||||
65
internal/user/repair_telemetry.go
Normal file
65
internal/user/repair_telemetry.go
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
// Copyright (c) 2024 Proton AG
|
||||||
|
//
|
||||||
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/telemetry"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (user *User) SendRepairTrigger(ctx context.Context) {
|
||||||
|
if !user.IsTelemetryEnabled(ctx) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerData := telemetry.NewRepairTriggerData()
|
||||||
|
data, err := json.Marshal(triggerData)
|
||||||
|
if err != nil {
|
||||||
|
user.log.WithError(err).Error("Failed to parse repair trigger data.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := user.SendTelemetry(ctx, data); err != nil {
|
||||||
|
user.log.WithError(err).Error("Failed to send repair trigger event.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user.log.Info("Repair trigger event successfully sent.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) SendRepairDeferredTrigger(ctx context.Context) {
|
||||||
|
if !user.IsTelemetryEnabled(ctx) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
deferredTriggerData := telemetry.NewRepairDeferredTriggerData()
|
||||||
|
data, err := json.Marshal(deferredTriggerData)
|
||||||
|
if err != nil {
|
||||||
|
user.log.WithError(err).Error("Failed to parse deferred repair trigger data.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := user.SendTelemetry(ctx, data); err != nil {
|
||||||
|
user.log.WithError(err).Error("Failed to send deferred repair trigger event.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user.log.Info("Deferred repair trigger event successfully sent.")
|
||||||
|
}
|
||||||
@ -717,3 +717,28 @@ func (user *User) protonAddresses() []proton.Address {
|
|||||||
|
|
||||||
return addresses
|
return addresses
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (user *User) VerifyResyncAndExecute() {
|
||||||
|
user.log.Info("Checking whether logged in user should re-sync. UserID:", user.ID())
|
||||||
|
if user.vault.GetShouldResync() {
|
||||||
|
user.log.Info("User should re-sync, starting re-sync process. UserID:", user.ID())
|
||||||
|
|
||||||
|
if err := user.vault.SetShouldSync(false); err != nil {
|
||||||
|
user.log.WithError(err).Error("Failed to disable re-sync flag in user vault. UserID:", user.ID())
|
||||||
|
}
|
||||||
|
|
||||||
|
user.SendRepairDeferredTrigger(context.Background())
|
||||||
|
if err := user.resyncIMAP(); err != nil {
|
||||||
|
user.log.WithError(err).Error("Failed re-syncing IMAP for userID", user.ID())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) TriggerRepair() error {
|
||||||
|
user.SendRepairTrigger(context.Background())
|
||||||
|
return user.resyncIMAP()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) resyncIMAP() error {
|
||||||
|
return user.imapService.Resync(context.Background())
|
||||||
|
}
|
||||||
|
|||||||
@ -18,11 +18,13 @@
|
|||||||
package vault
|
package vault
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
@ -200,7 +202,14 @@ func (vault *Vault) SetTelemetryDisabled(telemetryDisabled bool) error {
|
|||||||
|
|
||||||
// GetLastVersion returns the last version of the bridge that was run.
|
// GetLastVersion returns the last version of the bridge that was run.
|
||||||
func (vault *Vault) GetLastVersion() *semver.Version {
|
func (vault *Vault) GetLastVersion() *semver.Version {
|
||||||
return semver.MustParse(vault.getSafe().Settings.LastVersion)
|
lastVersion := vault.getSafe().Settings.LastVersion
|
||||||
|
version, err := semver.NewVersion(lastVersion)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Error(fmt.Sprintf("Error encountered when trying to get last version from vault: %s", lastVersion))
|
||||||
|
version, _ = semver.NewVersion(constants.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
return version
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetLastVersion sets the last version of the bridge that was run.
|
// SetLastVersion sets the last version of the bridge that was run.
|
||||||
|
|||||||
@ -40,6 +40,8 @@ type UserData struct {
|
|||||||
|
|
||||||
// **WARNING**: This value can't be removed until we have vault migration support.
|
// **WARNING**: This value can't be removed until we have vault migration support.
|
||||||
UIDValidity map[string]imap.UID
|
UIDValidity map[string]imap.UID
|
||||||
|
|
||||||
|
ShouldResync bool // Whether user should re-sync on log-in (this is triggered by the `repair` button)
|
||||||
}
|
}
|
||||||
|
|
||||||
type AddressMode int
|
type AddressMode int
|
||||||
@ -88,5 +90,7 @@ func newDefaultUser(userID, username, primaryEmail, authUID, authRef string, key
|
|||||||
AuthUID: authUID,
|
AuthUID: authUID,
|
||||||
AuthRef: authRef,
|
AuthRef: authRef,
|
||||||
KeyPass: keyPass,
|
KeyPass: keyPass,
|
||||||
|
|
||||||
|
ShouldResync: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -232,3 +232,13 @@ func (user *User) Clear() error {
|
|||||||
func (user *User) Close() error {
|
func (user *User) Close() error {
|
||||||
return user.vault.detachUser(user.userID)
|
return user.vault.detachUser(user.userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (user *User) SetShouldSync(shouldResync bool) error {
|
||||||
|
return user.vault.modUser(user.userID, func(data *UserData) {
|
||||||
|
data.ShouldResync = shouldResync
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) GetShouldResync() bool {
|
||||||
|
return user.vault.getUser(user.userID).ShouldResync
|
||||||
|
}
|
||||||
|
|||||||
@ -61,6 +61,7 @@ func TestUser_New(t *testing.T) {
|
|||||||
// Check the user's initial sync status.
|
// Check the user's initial sync status.
|
||||||
require.False(t, user.SyncStatus().HasLabels)
|
require.False(t, user.SyncStatus().HasLabels)
|
||||||
require.False(t, user.SyncStatus().HasMessages)
|
require.False(t, user.SyncStatus().HasMessages)
|
||||||
|
require.False(t, user.GetShouldResync())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUser_Clear(t *testing.T) {
|
func TestUser_Clear(t *testing.T) {
|
||||||
@ -239,3 +240,34 @@ func TestUser_ForEach(t *testing.T) {
|
|||||||
// The store should have no users again.
|
// The store should have no users again.
|
||||||
require.Empty(t, s.GetUserIDs())
|
require.Empty(t, s.GetUserIDs())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUser_ShouldResync(t *testing.T) {
|
||||||
|
// Replace the token generator with a dummy one.
|
||||||
|
vault.RandomToken = func(size int) ([]byte, error) {
|
||||||
|
return []byte("token"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new test vault.
|
||||||
|
s := newVault(t)
|
||||||
|
|
||||||
|
// There should be no users in the store.
|
||||||
|
require.Empty(t, s.GetUserIDs())
|
||||||
|
|
||||||
|
// Create a new user.
|
||||||
|
user, err := s.AddUser("userID", "username", "username@pm.me", "authUID", "authRef", []byte("keyPass"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// The user should be listed in the store.
|
||||||
|
require.ElementsMatch(t, []string{"userID"}, s.GetUserIDs())
|
||||||
|
|
||||||
|
// The shouldResync field is supposed to be false for new users.
|
||||||
|
require.False(t, user.GetShouldResync())
|
||||||
|
|
||||||
|
// Set it to true
|
||||||
|
if err := user.SetShouldSync(true); err != nil {
|
||||||
|
t.Fatalf("Failed to set should-sync: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check whether it matches the correct value
|
||||||
|
require.True(t, user.GetShouldResync())
|
||||||
|
}
|
||||||
|
|||||||
@ -22,9 +22,11 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||||
"github.com/docker/docker-credential-helpers/credentials"
|
"github.com/docker/docker-credential-helpers/credentials"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
@ -216,11 +218,7 @@ func isUsable(helper credentials.Helper, err error) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
creds := &credentials.Credentials{
|
creds := getTestCredentials()
|
||||||
ServerURL: "bridge/check",
|
|
||||||
Username: "check",
|
|
||||||
Secret: "check",
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := retry(func() error {
|
if err := retry(func() error {
|
||||||
return helper.Add(creds)
|
return helper.Add(creds)
|
||||||
@ -242,6 +240,23 @@ func isUsable(helper credentials.Helper, err error) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getTestCredentials() *credentials.Credentials {
|
||||||
|
// On macOS, a handful of users experience failures of the test credentials.
|
||||||
|
if runtime.GOOS == "darwin" {
|
||||||
|
return &credentials.Credentials{
|
||||||
|
ServerURL: hostURL(constants.KeyChainName) + fmt.Sprintf("/check_%v", time.Now().UTC().UnixMicro()),
|
||||||
|
Username: "", // username is ignored on macOS, it's extracted from splitting the server URL
|
||||||
|
Secret: "check",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &credentials.Credentials{
|
||||||
|
ServerURL: "bridge/check",
|
||||||
|
Username: "check",
|
||||||
|
Secret: "check",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func retry(condition func() error) error {
|
func retry(condition func() error) error {
|
||||||
var maxRetry = 5
|
var maxRetry = 5
|
||||||
for r := 0; ; r++ {
|
for r := 0; ; r++ {
|
||||||
|
|||||||
@ -19,6 +19,7 @@ package message
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"fmt"
|
||||||
"mime"
|
"mime"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
"strings"
|
"strings"
|
||||||
@ -46,6 +47,12 @@ var (
|
|||||||
const InternalIDDomain = `protonmail.internalid`
|
const InternalIDDomain = `protonmail.internalid`
|
||||||
|
|
||||||
func BuildRFC822Into(kr *crypto.KeyRing, decrypted *DecryptedMessage, opts JobOptions, buf *bytes.Buffer) error {
|
func BuildRFC822Into(kr *crypto.KeyRing, decrypted *DecryptedMessage, opts JobOptions, buf *bytes.Buffer) error {
|
||||||
|
if opts.SanitizeMBOXHeaderLine {
|
||||||
|
if err := sanitizeMBOXHeaderLine(decrypted); err != nil {
|
||||||
|
return fmt.Errorf("failed to sanitize MBOX header: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case len(decrypted.Msg.Attachments) > 0:
|
case len(decrypted.Msg.Attachments) > 0:
|
||||||
return buildMultipartRFC822(decrypted, opts, buf)
|
return buildMultipartRFC822(decrypted, opts, buf)
|
||||||
@ -560,3 +567,80 @@ func (bw *boundary) gen() string {
|
|||||||
bw.val = algo.HashHexSHA256(bw.val)
|
bw.val = algo.HashHexSHA256(bw.val)
|
||||||
return bw.val
|
return bw.val
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mboxFrom() []byte {
|
||||||
|
return []byte("From ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func mboxGtFrom() []byte {
|
||||||
|
return []byte(">From ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitizeMBOXHeaderLine(decrypted *DecryptedMessage) error {
|
||||||
|
if decrypted == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if decrypted.Body.Len() == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
i := indexMBOXHeaderLine(decrypted)
|
||||||
|
for i >= 0 {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
// copy until mbox line
|
||||||
|
if i > 0 {
|
||||||
|
if _, err := buf.Write(decrypted.Body.Next(i)); err != nil {
|
||||||
|
return fmt.Errorf("cannot copy first lines: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// dump mbox line
|
||||||
|
eol := bytes.IndexRune(decrypted.Body.Bytes(), '\n')
|
||||||
|
if eol == 0 || eol == -1 {
|
||||||
|
return errors.New("cannot find end of mbox line")
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = decrypted.Body.Next(eol + 1)
|
||||||
|
|
||||||
|
// copy rest
|
||||||
|
if _, err := buf.Write(decrypted.Body.Bytes()); err != nil {
|
||||||
|
return fmt.Errorf("cannot rest of message: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
decrypted.Body = buf
|
||||||
|
i = indexMBOXHeaderLine(decrypted)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func indexMBOXHeaderLine(decrypted *DecryptedMessage) int {
|
||||||
|
b := decrypted.Body.Bytes()
|
||||||
|
|
||||||
|
headerEnd := bytes.Index(b, []byte("\n\n"))
|
||||||
|
if headerEnd < 0 {
|
||||||
|
headerEnd = bytes.Index(b, []byte("\r\n\r\n"))
|
||||||
|
}
|
||||||
|
if headerEnd < 0 {
|
||||||
|
headerEnd = len(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < headerEnd; i++ {
|
||||||
|
if i != 0 && b[i] != '\n' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
j := 0
|
||||||
|
if i != 0 {
|
||||||
|
j = i + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if bytes.HasPrefix(b[j:], mboxFrom()) || bytes.HasPrefix(b[j:], mboxGtFrom()) {
|
||||||
|
return j
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|||||||
@ -18,6 +18,7 @@
|
|||||||
package message
|
package message
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@ -1298,3 +1299,96 @@ func TestBuildComplexMIMEType(t *testing.T) {
|
|||||||
expectContentTypeParam(`name`, is(`Cat_August_2010-4.jpeg`)).
|
expectContentTypeParam(`name`, is(`Cat_August_2010-4.jpeg`)).
|
||||||
expectContentDispositionParam(`filename`, is(`Cat_August_2010-4.jpeg`))
|
expectContentDispositionParam(`filename`, is(`Cat_August_2010-4.jpeg`))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHasMBOXHeaderLine(t *testing.T) {
|
||||||
|
cases := map[string]struct {
|
||||||
|
index, indexCRLF int
|
||||||
|
}{
|
||||||
|
"From: ok\nTo: Ok": {-1, -1},
|
||||||
|
"From: ok\nTo: Ok\n\nFrom - 123": {-1, -1},
|
||||||
|
"From: ok\nTo: Ok\n\n>From - 123": {-1, -1},
|
||||||
|
">From: ok\nTo: Ok": {-1, -1},
|
||||||
|
">From: ok\nTo: Ok\n\nFrom - 123": {-1, -1},
|
||||||
|
">From: ok\nTo: Ok\n\n>From - 123": {-1, -1},
|
||||||
|
|
||||||
|
"From - 123\nFrom: ok\nTo: Ok": {0, 0},
|
||||||
|
"From - 123\nFrom: ok\nTo: Ok\n\nFrom - 123": {0, 0},
|
||||||
|
"From - 123\nFrom: ok\nTo: Ok\n\n>From - 123": {0, 0},
|
||||||
|
|
||||||
|
"From: ok\nFrom - 123\nTo: Ok": {9, 10},
|
||||||
|
"From: ok\nFrom - 123\nTo: Ok\n\nFrom - 123": {9, 10},
|
||||||
|
"From: ok\nFrom - 123\nTo: Ok\n\n>From - 123": {9, 10},
|
||||||
|
|
||||||
|
">From - 123\nFrom: ok\nTo: Ok": {0, 0},
|
||||||
|
">From - 123\nFrom: ok\nTo: Ok\n\nFrom - 123": {0, 0},
|
||||||
|
">From - 123\nFrom: ok\nTo: Ok\n\n>From - 123": {0, 0},
|
||||||
|
|
||||||
|
"From: ok\n>From - 123\nTo: Ok": {9, 10},
|
||||||
|
"From: ok\n>From - 123\nTo: Ok\n\nFrom - 123": {9, 10},
|
||||||
|
"From: ok\n>From - 123\nTo: Ok\n\n>From - 123": {9, 10},
|
||||||
|
}
|
||||||
|
|
||||||
|
test := func(t *testing.T, wantIndex int, given string, useCRLF bool) {
|
||||||
|
decrypted := &DecryptedMessage{}
|
||||||
|
|
||||||
|
if useCRLF {
|
||||||
|
decrypted.Body = *bytes.NewBufferString(strings.ReplaceAll(given, "\n", "\r\n"))
|
||||||
|
} else {
|
||||||
|
decrypted.Body = *bytes.NewBufferString(given)
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Equal(t, wantIndex, indexMBOXHeaderLine(decrypted))
|
||||||
|
}
|
||||||
|
|
||||||
|
for given, want := range cases {
|
||||||
|
t.Run("LF-"+given, func(t *testing.T) { test(t, want.index, given, false) })
|
||||||
|
t.Run("CRLF-"+given, func(t *testing.T) { test(t, want.indexCRLF, given, true) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSanitizeMBOXHeaderLine(t *testing.T) {
|
||||||
|
cases := map[string]string{
|
||||||
|
"From: ok\nTo: Ok": "From: ok\nTo: Ok",
|
||||||
|
"From: ok\nTo: Ok\n\nFrom - 123": "From: ok\nTo: Ok\n\nFrom - 123",
|
||||||
|
"From: ok\nTo: Ok\n\n>From - 123": "From: ok\nTo: Ok\n\n>From - 123",
|
||||||
|
|
||||||
|
">From: ok\nTo: Ok": ">From: ok\nTo: Ok",
|
||||||
|
">From: ok\nTo: Ok\n\nFrom - 123": ">From: ok\nTo: Ok\n\nFrom - 123",
|
||||||
|
">From: ok\nTo: Ok\n\n>From - 123": ">From: ok\nTo: Ok\n\n>From - 123",
|
||||||
|
|
||||||
|
"From - 123\nFrom: ok\nTo: Ok": "From: ok\nTo: Ok",
|
||||||
|
"From - 123\nFrom: ok\nTo: Ok\n\nFrom - 123": "From: ok\nTo: Ok\n\nFrom - 123",
|
||||||
|
"From - 123\nFrom: ok\nTo: Ok\n\n>From - 123": "From: ok\nTo: Ok\n\n>From - 123",
|
||||||
|
|
||||||
|
"From: ok\nFrom - 123\nTo: Ok": "From: ok\nTo: Ok",
|
||||||
|
"From: ok\nFrom - 123\nTo: Ok\n\nFrom - 123": "From: ok\nTo: Ok\n\nFrom - 123",
|
||||||
|
"From: ok\nFrom - 123\nTo: Ok\n\n>From - 123": "From: ok\nTo: Ok\n\n>From - 123",
|
||||||
|
|
||||||
|
">From - 123\nFrom: ok\nTo: Ok": "From: ok\nTo: Ok",
|
||||||
|
">From - 123\nFrom: ok\nTo: Ok\n\nFrom - 123": "From: ok\nTo: Ok\n\nFrom - 123",
|
||||||
|
">From - 123\nFrom: ok\nTo: Ok\n\n>From - 123": "From: ok\nTo: Ok\n\n>From - 123",
|
||||||
|
|
||||||
|
"From: ok\n>From - 123\nTo: Ok": "From: ok\nTo: Ok",
|
||||||
|
"From: ok\n>From - 123\nTo: Ok\n\nFrom - 123": "From: ok\nTo: Ok\n\nFrom - 123",
|
||||||
|
"From: ok\n>From - 123\nTo: Ok\n\n>From - 123": "From: ok\nTo: Ok\n\n>From - 123",
|
||||||
|
}
|
||||||
|
|
||||||
|
test := func(t *testing.T, given, want string, useCRLF bool) {
|
||||||
|
decrypted := &DecryptedMessage{}
|
||||||
|
|
||||||
|
if useCRLF {
|
||||||
|
decrypted.Body = *bytes.NewBufferString(strings.ReplaceAll(given, "\n", "\r\n"))
|
||||||
|
want = strings.ReplaceAll(want, "\n", "\r\n")
|
||||||
|
} else {
|
||||||
|
decrypted.Body = *bytes.NewBufferString(given)
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, sanitizeMBOXHeaderLine(decrypted))
|
||||||
|
require.Equal(t, []byte(want), decrypted.Body.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
for given, want := range cases {
|
||||||
|
t.Run("LF"+given, func(t *testing.T) { test(t, given, want, false) })
|
||||||
|
t.Run("CRLF"+given, func(t *testing.T) { test(t, given, want, true) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -24,4 +24,5 @@ type JobOptions struct {
|
|||||||
AddExternalID bool // Whether to include ExternalID as X-Pm-External-Id.
|
AddExternalID bool // Whether to include ExternalID as X-Pm-External-Id.
|
||||||
AddMessageDate bool // Whether to include message time as X-Pm-Date.
|
AddMessageDate bool // Whether to include message time as X-Pm-Date.
|
||||||
AddMessageIDReference bool // Whether to include the MessageID in References.
|
AddMessageIDReference bool // Whether to include the MessageID in References.
|
||||||
|
SanitizeMBOXHeaderLine bool // Whether to ignore header line representing MBOX delimiter
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,7 +45,7 @@ import (
|
|||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
var defaultVersion = semver.MustParse("3.0.6")
|
var defaultVersion = semver.MustParse("3.10.0")
|
||||||
|
|
||||||
type testUser struct {
|
type testUser struct {
|
||||||
name string // the test user name
|
name string // the test user name
|
||||||
|
|||||||
@ -205,3 +205,30 @@ func (s *scenario) theBodyInTheResponseToIs(method, path string, value *godog.Do
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *scenario) theMessageUsedKeyForSending(address string) error {
|
||||||
|
addrID := s.t.getUserByAddress(address).getAddrID(address)
|
||||||
|
|
||||||
|
call, err := s.t.getLastCallExcludingHTTPOverride("POST", "/mail/v4/messages")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var body, want map[string]any
|
||||||
|
|
||||||
|
if err := json.Unmarshal(call.ResponseBody, &body); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
want = map[string]any{
|
||||||
|
"Message": map[string]any{
|
||||||
|
"AddressID": addrID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if !IsSub(body, want) {
|
||||||
|
return fmt.Errorf("have body %v, want %v", body, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
42
tests/features/imap/addressmode.feature
Normal file
42
tests/features/imap/addressmode.feature
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
Feature: IMAP client authentication with address modes
|
||||||
|
Background:
|
||||||
|
Given there exists an account with username "[user:user]" and password "password"
|
||||||
|
And the account "[user:user]" has additional address "[alias:alias]@[domain]"
|
||||||
|
And it succeeds
|
||||||
|
|
||||||
|
Scenario: IMAP client can authenticate successfully with secondary address in combine mode
|
||||||
|
Given bridge starts
|
||||||
|
And the user logs in with username "[user:user]" and password "password"
|
||||||
|
And user "[user:user]" finishes syncing
|
||||||
|
Then user "[user:user]" connects and authenticates IMAP client "1" with address "[alias:alias]@[domain]"
|
||||||
|
|
||||||
|
Scenario: IMAP client can authenticate successfully with secondary address in split mode
|
||||||
|
Given bridge starts
|
||||||
|
And the user logs in with username "[user:user]" and password "password"
|
||||||
|
And the user sets the address mode of user "[user:user]" to "split"
|
||||||
|
And user "[user:user]" finishes syncing
|
||||||
|
Then user "[user:user]" connects and authenticates IMAP client "1" with address "[alias:alias]@[domain]"
|
||||||
|
|
||||||
|
# Need to find way to setup disabled address on black
|
||||||
|
@skip-black
|
||||||
|
Scenario: IMAP client cannot authenticate successfully with disabled alias in combine mode
|
||||||
|
Given the account "[user:user]" has additional disabled address "[alias:disabled]@[domain]"
|
||||||
|
And it succeeds
|
||||||
|
Given bridge starts
|
||||||
|
And the user logs in with username "[user:user]" and password "password"
|
||||||
|
And user "[user:user]" finishes syncing
|
||||||
|
# GODT-3307 it should succeed
|
||||||
|
When user "[user:user]" connects and can not authenticate IMAP client "1" with address "[alias:disabled]@[domain]"
|
||||||
|
|
||||||
|
# Need to find way to setup disabled address on black
|
||||||
|
@skip-black
|
||||||
|
Scenario: IMAP client cannot authenticate successfully with disabled alias in split mode
|
||||||
|
Given the account "[user:user]" has additional disabled address "[alias:disabled]@[domain]"
|
||||||
|
And it succeeds
|
||||||
|
Given bridge starts
|
||||||
|
And the user logs in with username "[user:user]" and password "password"
|
||||||
|
And the user sets the address mode of user "[user:user]" to "split"
|
||||||
|
And user "[user:user]" finishes syncing
|
||||||
|
# GODT-3307 it should succeed
|
||||||
|
When user "[user:user]" connects and can not authenticate IMAP client "1" with address "[alias:disabled]@[domain]"
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user