Compare commits

..

71 Commits

Author SHA1 Message Date
ed5adb18fb chore: Bastei Bridge 3.12.0 changelog. 2024-06-17 11:19:49 +02:00
85a91c5572 feat(BRIDGE-97): added repair button telemetry 2024-06-14 13:01:07 +00:00
56d4bfbb71 feat(BRIDGE-79): update to the KB suggestion list. 2024-06-13 10:05:23 +02:00
48a75b0dd7 chore: Bastei Bridge 3.12.0 changelog. 2024-06-06 10:10:36 +02:00
8688277ee6 ci: supress govulncheck vulns 2024-06-05 12:36:43 +00:00
63eb67760e fix(BRIDGE-90): disable repair button when bridge cannot connect to proton servers; bump GPA 2024-06-05 12:36:43 +00:00
cffab028b2 chore: cherry picked changelog from 3.11 release branch.
chore: Alcantara Bridge 3.11.0 changelog.

(cherry picked from commit 2569e83e51)

chore: Alcantara Bridge 3.11.0 changelog.

(cherry picked from commit b574ccb6ea)

chore: Alcantara Bridge 3.11.0 changelog.

(cherry picked from commit 82607efe1c)

chore: Alcantara Bridge 3.11.1 changelog.

(cherry picked from commit cd8db6fd1c)
2024-06-04 14:01:16 +00:00
8ea712b052 fix(BRIDGE-15): Apple Mail profile install page was not properly reset before showing.
(cherry picked from commit 961dc9435f)
2024-06-04 11:11:02 +02:00
ff0615167b feat(BRIDGE-75): Bridge repair button/feature implemented 2024-06-03 12:37:23 +00:00
e2b361b9a6 feat(BRIDGE-79): Add New Outlook for Mac KB disclaimer.
Update submitted by Laze Dimitrovski
2024-05-30 17:49:22 +02:00
1c6bbf1fae chore: enable GO-2024-2687 in govulncheck 2024-05-29 16:48:02 +02:00
e7713fa785 fix(BRIDGE-69): explicitly handle semver panic for last bridge version from vault 2024-05-22 10:54:38 +00:00
28ae54b5ca feat(BRIDGE-16): bump version Go 1.21.9 Qt 6.4.3. 2024-05-21 08:51:28 +02:00
00aff40160 fix(BRIDGE-70): hotfix for blocked smtp/imap port causing bridge to quit 2024-05-17 12:35:07 +02:00
ab289e6e01 fix(BRIDGE-29): bump gluon version 2024-05-14 15:41:10 +02:00
a28dc9f2f3 fix(BRIDGE-49): Configure gitleaks baseline and grype config 2024-05-02 10:59:43 +00:00
8a859082cd ci: added gitleaks and grype 2024-04-29 13:58:48 +02:00
1d972835ff fix(BRIDGE-21): missing panic handling 2024-04-26 13:24:02 +02:00
8469e0a661 fix(BRIDGE-17): broken telemetry heartbeat test 2024-04-25 11:16:13 +00:00
6ea970bf97 feat(BRIDGE-23): update gluon to go 1.21. 2024-04-23 14:52:31 +02:00
a05b90e803 feat(BRIDGE-22): update gpa to go 1.21. 2024-04-23 14:50:46 +02:00
239ad8b946 fix(BRIDGE-10): bumped gluon version 2024-04-23 12:42:08 +02:00
d9fdbb35bc fix(GODT-3185): logic mistake. 2024-04-22 07:26:18 +00:00
5769fb9466 ci: windows build missing revision 2024-04-19 10:39:47 +02:00
a4020cebd4 chore: do not use C++ 20 std::ranges. 2024-04-19 08:03:18 +02:00
7a8760e2ef fix(BRIDGE-19): warning instead of error on logs for checksum validation... 2024-04-17 12:59:36 +00:00
9552e72ba8 feat(BRIDGE-14): HV3 implementation - GUI & CLI; ownership verification & CAPTCHA are supported 2024-04-12 13:07:22 +00:00
c692c21b87 fix(BRIDGE-8): more robust command-line args parser in bridge-gui.
fix(BRIDGE-8): add command-line invocation to log.
2024-04-12 11:16:59 +00:00
bb15efa711 fix(BRIDGE-8): launcher replace session-id if provided instead of adding another one. 2024-04-12 11:16:59 +00:00
e94d3be12d chore: bump testing context bridge version 2024-04-12 11:55:53 +02:00
66569f71a0 fix(BRIDGE-7): add timestamp to test credentials for keychain on macOS. 2024-04-09 10:43:31 +02:00
9bfa79455e fix(BRIDGE-7): modify keychain test on macOS. 2024-04-08 14:35:36 +02:00
67e802e3a0 feat(BRIDGE-15): fix a stack layout index in comment. 2024-04-08 11:27:28 +02:00
8a5e2007f6 feat(BRIDGE-15): certificate install is now also done during Outlook setup on macOS. 2024-04-04 08:57:30 +02:00
5b92945626 chore: disable GO-2024-2687 in govulncheck 2024-04-04 07:58:16 +02:00
4a8a7ef093 fix(BRIDGE-4): logs not being created when invalid flag is passed 2024-03-21 16:32:12 +00:00
2cfda14b1a fix(BRIDGE-5): add tooltip to tray icon. 2024-03-20 14:55:40 +01:00
312993e08e feat(GODT-3253): windows cache and paths. 2024-03-15 11:28:52 +01:00
b1110b04c9 feat(GODT-3253): make paths. 2024-03-15 11:27:33 +01:00
d2bc60d9cb ci: debug 2024-03-15 11:27:29 +01:00
1d8f6c75c8 feat(GODT-3253): use new virtual machine for windows jobs. bump vcpkg to 2024.02.14 2024-03-15 11:23:46 +01:00
06daaf8d9f feat(GODT-3146): don't need to wait for IMAP in tests. 2024-03-14 11:57:55 +01:00
cb436fff63 feat(GODT-3146): remove unused 2024-03-13 14:31:53 +01:00
921a44f1a3 feat(GODT-3146): keep imap/smtp server always on. 2024-03-13 14:22:23 +01:00
d35af6b686 chore: added bridge-rollout to CI. 2024-03-13 09:25:40 +00:00
4cb938c57f chore: added bridge-rollout cli tool. 2024-03-13 09:25:40 +00:00
232e98d812 chore: Zaehringen Bridge 3.10.0 changelog. 2024-03-13 10:21:52 +01:00
6fadbde4a6 feat(GODT-3185): report cases which leads to wrong address key used 2024-03-13 07:49:25 +00:00
d2fbbc3e25 fix(GODT-3163): filter MBOX format delimiter. 2024-03-07 12:30:33 +00:00
1c7c342e19 ci(GODT-3304): ignore go vulncheck until go version bumped. 2024-03-07 13:00:16 +01:00
8e49c84a12 chore: changelog update. 2024-03-06 08:19:13 +01:00
754d80d097 feat(GODT-3193): assume text content type on attachments. 2024-03-01 15:25:37 +00:00
63e272e270 feat(GODT-3193): preserve attachment encoding. 2024-03-01 15:25:37 +00:00
54859a34b2 fix(GODT-3290): fix test failing because of leap day. 2024-03-01 10:56:24 +01:00
9b1feed68b feat(GODT-3214): encrypt only with primary key. 2024-02-28 13:42:09 +00:00
c9b6cc162b feat(GODT-3199): add package log field. 2024-02-27 13:07:37 +01:00
bf3c90b8e9 test(GODT-1602): rebased GPA changes. 2024-02-26 16:56:52 +01:00
8d63fb2301 feat(GODT-2662): enable cache on darwin tart. 2024-02-23 10:33:26 +01:00
7953306cc8 feat(GODT-2662): use tart runner for darwin jobs. 2024-02-23 10:00:47 +01:00
37352d44d2 test(GODT-1602): run integration tests against black 🖤 2024-02-19 10:43:35 +00:00
2a1aeb208d test(GODT-3257): quad9 provider test not working on CI. 2024-02-19 10:06:02 +01:00
94fbe260e4 test(GODT-3220): Fix linting issues by deleting a function
-Deleted a function that was no longer used

GODT-3220
2024-02-14 08:57:48 +01:00
6d4937222e test(GODT-3220): Rollback to a test scenario for logging in with an alias address
-Added test scenario for logging in with an alias address

GODT-3220
2024-02-13 10:56:23 +00:00
e33bad7bf1 test(GODT-3220): Add test scenario for sending an HTML msg with public key and multiple attachments to Internal
-Added test scenario for sending an HTML msg with public key and multiple attachments to Internal
- Verified the message on receipient's side

GODT-3220
2024-02-13 10:56:23 +00:00
70fdc91aff test(GODT-3220): Add test scenario for sending a message to multiple bcc accounts
- Added test scenario for sending a message to two bcc accounts
- Verified on recipients' side that the message is received

GODT-3220
2024-02-13 10:56:23 +00:00
bde8e45b37 test(GODT-3220): Add test scenarios for loging in with an alias address
-Added test scenarios for logging in with an alias address and logging in with an alias address that no longer exists

GODT-3220
2024-02-13 10:56:23 +00:00
6cb2d944d0 test(GODT-3220): Add test scenarios for logining in with alias address and loging in with an alias address
-Added a test scenario for logging in with an alias address
-Added a test scenario for logging in with alias address that no longer exists

GODT-3220
2024-02-13 10:56:23 +00:00
cf0f59afc0 test(GODT-3220): Add scenario cannot login with deleted alias 2024-02-13 10:56:23 +00:00
65d8fbbf31 test: keep deleted address in test suite 2024-02-13 10:56:23 +00:00
d919c0accf test(GODT-3220): Add step definition for logging in with alias address
GODT-3220
2024-02-13 10:56:23 +00:00
0ca07066db test(GODT-3220): Create function for getting the test user by address
GODT-3220
2024-02-13 10:56:23 +00:00
172 changed files with 5490 additions and 2297 deletions

View File

@ -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
View File

@ -0,0 +1,2 @@
# Check out for configuration details: https://github.com/anchore/grype?tab=readme-ov-file#configuration
fail-on-severity: "medium"

View File

@ -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)

View File

@ -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
@ -43,6 +106,12 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* GODT-3188: Happy new year. * GODT-3188: Happy new year.
## Xikou Bridge 3.8.2
### Fixed
* GODT-3235: Update bridge update key.
## Xikou Bridge 3.8.1 ## Xikou Bridge 3.8.1
### Added ### Added

View File

@ -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}"

View File

@ -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

View File

@ -2,23 +2,41 @@
--- ---
.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:
- .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-build, before_script] - !reference [.before-script-git-config, before_script]
cache: {} - mkdir -p .cache/bin
tags: - export PATH=$(pwd)/.cache/bin:$PATH
- macos-m1-bridge - export GOPATH="$CI_PROJECT_DIR/.cache"
variables:
BRIDGE_SYNC_FORCE_MINIMUM_SPEC: 1
VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.cache
cache:
key: darwin-go-and-vcpkg
paths:
- .cache
when: 'always'
.env-linux-build: .env-linux-build:
extends: extends:

View File

@ -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:

View File

@ -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
} }

View 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)
}
}

View File

@ -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,

View File

@ -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

18
go.mod
View File

@ -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.20240102132144-89b40fb6fe7e 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.20231130083229-e8aa47d7a366 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
View File

@ -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.20240102132144-89b40fb6fe7e h1:DR97ydcuS4/EjTTCkp7F9IRCi+ykD1UoAP7UBFtEcRA= github.com/ProtonMail/gluon v0.17.1-0.20240423123310-0266b0f75d41 h1:Lu2hKO4fcHeMcbZOon129iM1dAy0ERwZkJtuNQCLlOQ=
github.com/ProtonMail/gluon v0.17.1-0.20240102132144-89b40fb6fe7e/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.20231130083229-e8aa47d7a366 h1:W9P5GdDnuGkB3tbzKnXmUrTjIs6zk/K+4lpPTWzsoRE= 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.20231130083229-e8aa47d7a366/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=

View File

@ -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,

View File

@ -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

View File

@ -40,7 +40,7 @@ func defaultAPIOptions(
proton.WithAppVersion(constants.AppVersion(version.Original())), proton.WithAppVersion(constants.AppVersion(version.Original())),
proton.WithCookieJar(cookieJar), proton.WithCookieJar(cookieJar),
proton.WithTransport(transport), proton.WithTransport(transport),
proton.WithLogger(logrus.StandardLogger()), proton.WithLogger(logrus.WithField("pkg", "gpa/client")),
proton.WithPanicHandler(panicHandler), proton.WithPanicHandler(panicHandler),
} }
} }

View File

@ -132,6 +132,8 @@ type Bridge struct {
syncService *syncservice.Service syncService *syncservice.Service
} }
var logPkg = logrus.WithField("pkg", "bridge") //nolint:gochecknoglobals
// New creates a new bridge. // New creates a new bridge.
func New( func New(
locator Locator, // the locator to provide paths to store data locator Locator, // the locator to provide paths to store data
@ -322,7 +324,7 @@ func (bridge *Bridge) init(tlsReporter TLSReporter) error {
// Handle connection up/down events. // Handle connection up/down events.
bridge.api.AddStatusObserver(func(status proton.Status) { bridge.api.AddStatusObserver(func(status proton.Status) {
logrus.Info("API status changed: ", status) logPkg.Info("API status changed: ", status)
switch { switch {
case status == proton.StatusUp: case status == proton.StatusUp:
@ -337,7 +339,7 @@ func (bridge *Bridge) init(tlsReporter TLSReporter) error {
// If any call returns a bad version code, we need to update. // If any call returns a bad version code, we need to update.
bridge.api.AddErrorHandler(proton.AppVersionBadCode, func() { bridge.api.AddErrorHandler(proton.AppVersionBadCode, func() {
logrus.Warn("App version is bad") logPkg.Warn("App version is bad")
bridge.publish(events.UpdateForced{}) bridge.publish(events.UpdateForced{})
}) })
@ -350,7 +352,7 @@ func (bridge *Bridge) init(tlsReporter TLSReporter) error {
// Log all manager API requests (client requests are logged separately). // Log all manager API requests (client requests are logged separately).
bridge.api.AddPostRequestHook(func(_ *resty.Client, r *resty.Response) error { bridge.api.AddPostRequestHook(func(_ *resty.Client, r *resty.Response) error {
if _, ok := proton.ClientIDFromContext(r.Request.Context()); !ok { if _, ok := proton.ClientIDFromContext(r.Request.Context()); !ok {
logrus.Infof("[MANAGER] %v: %v %v", r.Status(), r.Request.Method, r.Request.URL) logrus.WithField("pkg", "gpa/manager").Infof("%v: %v %v", r.Status(), r.Request.Method, r.Request.URL)
} }
return nil return nil
@ -359,7 +361,7 @@ func (bridge *Bridge) init(tlsReporter TLSReporter) error {
// Publish a TLS issue event if a TLS issue is encountered. // Publish a TLS issue event if a TLS issue is encountered.
bridge.tasks.Once(func(ctx context.Context) { bridge.tasks.Once(func(ctx context.Context) {
async.RangeContext(ctx, tlsReporter.GetTLSIssueCh(), func(struct{}) { async.RangeContext(ctx, tlsReporter.GetTLSIssueCh(), func(struct{}) {
logrus.Warn("TLS issue encountered") logPkg.Warn("TLS issue encountered")
bridge.publish(events.TLSIssue{}) bridge.publish(events.TLSIssue{})
}) })
}) })
@ -367,7 +369,7 @@ func (bridge *Bridge) init(tlsReporter TLSReporter) error {
// Publish a raise event if the focus service is called. // Publish a raise event if the focus service is called.
bridge.tasks.Once(func(ctx context.Context) { bridge.tasks.Once(func(ctx context.Context) {
async.RangeContext(ctx, bridge.focusService.GetRaiseCh(), func(struct{}) { async.RangeContext(ctx, bridge.focusService.GetRaiseCh(), func(struct{}) {
logrus.Info("Focus service requested raise") logPkg.Info("Focus service requested raise")
bridge.publish(events.Raise{}) bridge.publish(events.Raise{})
}) })
}) })
@ -375,7 +377,7 @@ func (bridge *Bridge) init(tlsReporter TLSReporter) error {
// Handle any IMAP events that are forwarded to the bridge from gluon. // Handle any IMAP events that are forwarded to the bridge from gluon.
bridge.tasks.Once(func(ctx context.Context) { bridge.tasks.Once(func(ctx context.Context) {
async.RangeContext(ctx, bridge.imapEventCh, func(event imapEvents.Event) { async.RangeContext(ctx, bridge.imapEventCh, func(event imapEvents.Event) {
logrus.WithField("event", fmt.Sprintf("%T", event)).Debug("Received IMAP event") logPkg.WithField("event", fmt.Sprintf("%T", event)).Debug("Received IMAP event")
bridge.handleIMAPEvent(event) bridge.handleIMAPEvent(event)
}) })
}) })
@ -383,7 +385,7 @@ func (bridge *Bridge) init(tlsReporter TLSReporter) error {
// Attempt to load users from the vault when triggered. // Attempt to load users from the vault when triggered.
bridge.goLoad = bridge.tasks.Trigger(func(ctx context.Context) { bridge.goLoad = bridge.tasks.Trigger(func(ctx context.Context) {
if err := bridge.loadUsers(ctx); err != nil { if err := bridge.loadUsers(ctx); err != nil {
logrus.WithError(err).Error("Failed to load users") logPkg.WithError(err).Error("Failed to load users")
if netErr := new(proton.NetError); !errors.As(err, &netErr) { if netErr := new(proton.NetError); !errors.As(err, &netErr) {
sentry.ReportError(bridge.reporter, "Failed to load users", err) sentry.ReportError(bridge.reporter, "Failed to load users", err)
} }
@ -396,7 +398,7 @@ func (bridge *Bridge) init(tlsReporter TLSReporter) error {
// Check for updates when triggered. // Check for updates when triggered.
bridge.goUpdate = bridge.tasks.PeriodicOrTrigger(constants.UpdateCheckInterval, 0, func(ctx context.Context) { bridge.goUpdate = bridge.tasks.PeriodicOrTrigger(constants.UpdateCheckInterval, 0, func(ctx context.Context) {
logrus.Info("Checking for updates") logPkg.Info("Checking for updates")
version, err := bridge.updater.GetVersionInfo(ctx, bridge.api, bridge.vault.GetUpdateChannel()) version, err := bridge.updater.GetVersionInfo(ctx, bridge.api, bridge.vault.GetUpdateChannel())
if err != nil { if err != nil {
@ -434,7 +436,7 @@ func (bridge *Bridge) GetErrors() []error {
} }
func (bridge *Bridge) Close(ctx context.Context) { func (bridge *Bridge) Close(ctx context.Context) {
logrus.Info("Closing bridge") logPkg.Info("Closing bridge")
// Stop heart beat before closing users. // Stop heart beat before closing users.
bridge.heartbeat.stop() bridge.heartbeat.stop()
@ -448,7 +450,7 @@ func (bridge *Bridge) Close(ctx context.Context) {
// Close the servers // Close the servers
if err := bridge.serverManager.CloseServers(ctx); err != nil { if err := bridge.serverManager.CloseServers(ctx); err != nil {
logrus.WithError(err).Error("Failed to close servers") logPkg.WithError(err).Error("Failed to close servers")
} }
bridge.syncService.Close() bridge.syncService.Close()
@ -474,12 +476,12 @@ func (bridge *Bridge) publish(event events.Event) {
bridge.watchersLock.RLock() bridge.watchersLock.RLock()
defer bridge.watchersLock.RUnlock() defer bridge.watchersLock.RUnlock()
logrus.WithField("event", event).Debug("Publishing event") logPkg.WithField("event", event).Debug("Publishing event")
for _, watcher := range bridge.watchers { for _, watcher := range bridge.watchers {
if watcher.IsWatching(event) { if watcher.IsWatching(event) {
if ok := watcher.Send(event); !ok { if ok := watcher.Send(event); !ok {
logrus.WithField("event", event).Warn("Failed to send event to watcher") logPkg.WithField("event", event).Warn("Failed to send event to watcher")
} }
} }
} }
@ -512,13 +514,13 @@ func (bridge *Bridge) remWatcher(watcher *watcher.Watcher[events.Event]) {
} }
func (bridge *Bridge) onStatusUp(_ context.Context) { func (bridge *Bridge) onStatusUp(_ context.Context) {
logrus.Info("Handling API status up") logPkg.Info("Handling API status up")
bridge.goLoad() bridge.goLoad()
} }
func (bridge *Bridge) onStatusDown(ctx context.Context) { func (bridge *Bridge) onStatusDown(ctx context.Context) {
logrus.Info("Handling API status down") logPkg.Info("Handling API status down")
for backoff := time.Second; ; backoff = min(backoff*2, 30*time.Second) { for backoff := time.Second; ; backoff = min(backoff*2, 30*time.Second) {
select { select {
@ -526,10 +528,10 @@ func (bridge *Bridge) onStatusDown(ctx context.Context) {
return return
case <-time.After(backoff): case <-time.After(backoff):
logrus.Info("Pinging API") logPkg.Info("Pinging API")
if err := bridge.api.Ping(ctx); err != nil { if err := bridge.api.Ping(ctx); err != nil {
logrus.WithError(err).Warn("Ping failed, API is still unreachable") logPkg.WithError(err).Warn("Ping failed, API is still unreachable")
} else { } else {
return return
} }
@ -537,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 {
@ -556,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
}

View File

@ -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)))

View File

@ -34,7 +34,7 @@ import (
// ConfigureAppleMail configures Apple Mail for the given userID and address. // ConfigureAppleMail configures Apple Mail for the given userID and address.
// If configuring Apple Mail for Catalina or newer, it ensures Bridge is using SSL. // If configuring Apple Mail for Catalina or newer, it ensures Bridge is using SSL.
func (bridge *Bridge) ConfigureAppleMail(ctx context.Context, userID, address string) error { func (bridge *Bridge) ConfigureAppleMail(ctx context.Context, userID, address string) error {
logrus.WithFields(logrus.Fields{ logPkg.WithFields(logrus.Fields{
"userID": userID, "userID": userID,
"address": logging.Sensitive(address), "address": logging.Sensitive(address),
}).Info("Configuring Apple Mail") }).Info("Configuring Apple Mail")

View File

@ -65,7 +65,11 @@ func (bridge *Bridge) CheckClientState(ctx context.Context, checkFlags bool, pro
if progressCB != nil { if progressCB != nil {
progressCB(fmt.Sprintf("Checking state for user %v", usr.Name())) progressCB(fmt.Sprintf("Checking state for user %v", usr.Name()))
} }
log := logrus.WithField("user", usr.Name()).WithField("diag", "state-check") log := logrus.WithFields(logrus.Fields{
"pkg": "bridge/debug",
"user": usr.Name(),
"diag": "state-check",
})
log.Debug("Retrieving all server metadata") log.Debug("Retrieving all server metadata")
meta, err := usr.GetDiagnosticMetadata(ctx) meta, err := usr.GetDiagnosticMetadata(ctx)
if err != nil { if err != nil {
@ -280,7 +284,7 @@ func clientGetMessageIDs(client *goimapclient.Client, mailbox string) (map[strin
internalID, ok := header.GetChecked("X-Pm-Internal-Id") internalID, ok := header.GetChecked("X-Pm-Internal-Id")
if !ok { if !ok {
logrus.Errorf("Message %v does not have internal id", internalID) logrus.WithField("pkg", "bridge/debug").Errorf("Message %v does not have internal id", internalID)
continue continue
} }

View File

@ -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)

View File

@ -97,7 +97,7 @@ func (h *heartBeatState) start() {
h.taskStarted = true h.taskStarted = true
h.task.PeriodicOrTrigger(h.taskInterval, 0, func(ctx context.Context) { h.task.PeriodicOrTrigger(h.taskInterval, 0, func(ctx context.Context) {
logrus.Debug("Checking for heartbeat") logrus.WithField("pkg", "bridge/heartbeat").Debug("Checking for heartbeat")
h.TrySending(ctx) h.TrySending(ctx)
}) })
@ -135,7 +135,7 @@ func (bridge *Bridge) SendHeartbeat(ctx context.Context, heartbeat *telemetry.He
if err := bridge.reporter.ReportMessageWithContext("Cannot parse heartbeat data.", reporter.Context{ if err := bridge.reporter.ReportMessageWithContext("Cannot parse heartbeat data.", reporter.Context{
"error": err, "error": err,
}); err != nil { }); err != nil {
logrus.WithError(err).Error("Failed to parse heartbeat data.") logrus.WithField("pkg", "bridge/heartbeat").WithError(err).Error("Failed to parse heartbeat data.")
} }
return false return false
} }

View File

@ -35,10 +35,12 @@ func (bridge *Bridge) restartIMAP(ctx context.Context) error {
} }
func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) { func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) {
log := logrus.WithField("pkg", "bridge/event/imap")
switch event := event.(type) { switch event := event.(type) {
case imapEvents.UserAdded: case imapEvents.UserAdded:
for labelID, count := range event.Counts { for labelID, count := range event.Counts {
logrus.WithFields(logrus.Fields{ log.WithFields(logrus.Fields{
"gluonID": event.UserID, "gluonID": event.UserID,
"labelID": labelID, "labelID": labelID,
"count": count, "count": count,
@ -46,7 +48,7 @@ func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) {
} }
case imapEvents.IMAPID: case imapEvents.IMAPID:
logrus.WithFields(logrus.Fields{ log.WithFields(logrus.Fields{
"sessionID": event.SessionID, "sessionID": event.SessionID,
"name": event.IMAPID.Name, "name": event.IMAPID.Name,
"version": event.IMAPID.Version, "version": event.IMAPID.Version,
@ -57,7 +59,7 @@ func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) {
} }
case imapEvents.LoginFailed: case imapEvents.LoginFailed:
logrus.WithFields(logrus.Fields{ log.WithFields(logrus.Fields{
"sessionID": event.SessionID, "sessionID": event.SessionID,
"username": event.Username, "username": event.Username,
"pkg": "imap", "pkg": "imap",

View File

@ -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())
}) })
}) })

View File

@ -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)

View File

@ -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) require.NoError(t, err)
smtpClient.Close() //nolint:errcheck
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)
_, 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()

View File

@ -27,7 +27,6 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/services/userevents" "github.com/ProtonMail/proton-bridge/v3/internal/services/userevents"
"github.com/ProtonMail/proton-bridge/v3/internal/updater" "github.com/ProtonMail/proton-bridge/v3/internal/updater"
"github.com/ProtonMail/proton-bridge/v3/internal/vault" "github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/sirupsen/logrus"
) )
func (bridge *Bridge) GetKeychainApp() (string, error) { func (bridge *Bridge) GetKeychainApp() (string, error) {
@ -134,7 +133,7 @@ func (bridge *Bridge) SetGluonDir(ctx context.Context, newGluonDir string) error
bridge.usersLock.RLock() bridge.usersLock.RLock()
defer func() { defer func() {
logrus.Info("Restarting user event loops") logPkg.Info("Restarting user event loops")
for _, u := range bridge.users { for _, u := range bridge.users {
u.ResumeEventLoop() u.ResumeEventLoop()
} }
@ -149,20 +148,20 @@ func (bridge *Bridge) SetGluonDir(ctx context.Context, newGluonDir string) error
waiters := make([]waiter, 0, len(bridge.users)) waiters := make([]waiter, 0, len(bridge.users))
logrus.Info("Pausing user event loops for gluon dir change") logPkg.Info("Pausing user event loops for gluon dir change")
for id, u := range bridge.users { for id, u := range bridge.users {
waiters = append(waiters, waiter{w: u.PauseEventLoopWithWaiter(), id: id}) waiters = append(waiters, waiter{w: u.PauseEventLoopWithWaiter(), id: id})
} }
logrus.Info("Waiting on user event loop completion") logPkg.Info("Waiting on user event loop completion")
for _, waiter := range waiters { for _, waiter := range waiters {
if err := waiter.w.WaitPollFinished(ctx); err != nil { if err := waiter.w.WaitPollFinished(ctx); err != nil {
logrus.WithError(err).Errorf("Failed to wait on event loop pause for user %v", waiter.id) logPkg.WithError(err).Errorf("Failed to wait on event loop pause for user %v", waiter.id)
return fmt.Errorf("failed on event loop pause: %w", err) return fmt.Errorf("failed on event loop pause: %w", err)
} }
} }
logrus.Info("Changing gluon directory") logPkg.Info("Changing gluon directory")
return bridge.serverManager.SetGluonDir(ctx, newGluonDir) return bridge.serverManager.SetGluonDir(ctx, newGluonDir)
} }
@ -330,13 +329,13 @@ func (bridge *Bridge) FactoryReset(ctx context.Context) {
// Wipe the vault. // Wipe the vault.
gluonCacheDir, err := bridge.locator.ProvideGluonCachePath() gluonCacheDir, err := bridge.locator.ProvideGluonCachePath()
if err != nil { if err != nil {
logrus.WithError(err).Error("Failed to provide gluon dir") logPkg.WithError(err).Error("Failed to provide gluon dir")
} else if err := bridge.vault.Reset(gluonCacheDir); err != nil { } else if err := bridge.vault.Reset(gluonCacheDir); err != nil {
logrus.WithError(err).Error("Failed to reset vault") logPkg.WithError(err).Error("Failed to reset vault")
} }
// Lastly, delete all files except the vault. // Lastly, delete all files except the vault.
if err := bridge.locator.Clear(bridge.vault.Path()); err != nil { if err := bridge.locator.Clear(bridge.vault.Path()); err != nil {
logrus.WithError(err).Error("Failed to clear data paths") logPkg.WithError(err).Error("Failed to clear data paths")
} }
} }

View File

@ -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"
@ -38,6 +39,8 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
var logUser = logrus.WithField("pkg", "bridge/user") //nolint:gochecknoglobals
type UserState int type UserState int
const ( const (
@ -121,23 +124,28 @@ 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) {
logrus.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)
} }
if ok := safe.RLockRet(func() bool { return mapHas(bridge.users, auth.UserID) }, bridge.usersLock); ok { if ok := safe.RLockRet(func() bool { return mapHas(bridge.users, auth.UserID) }, bridge.usersLock); ok {
logrus.WithField("userID", auth.UserID).Warn("User already logged in") logUser.WithField("userID", auth.UserID).Warn("User already logged in")
if err := client.AuthDelete(ctx); err != nil { if err := client.AuthDelete(ctx); err != nil {
logrus.WithError(err).Warn("Failed to delete auth") logUser.WithError(err).Warn("Failed to delete auth")
} }
return nil, proton.Auth{}, ErrUserAlreadyLoggedIn return nil, proton.Auth{}, ErrUserAlreadyLoggedIn
@ -152,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) {
logrus.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)
}, },
) )
@ -165,7 +174,7 @@ func (bridge *Bridge) LoginUser(
// Failure to unlock will allow retries, so we do not delete auth. // Failure to unlock will allow retries, so we do not delete auth.
if !errors.Is(err, ErrFailedToUnlock) { if !errors.Is(err, ErrFailedToUnlock) {
if deleteErr := client.AuthDelete(ctx); deleteErr != nil { if deleteErr := client.AuthDelete(ctx); deleteErr != nil {
logrus.WithError(deleteErr).Error("Failed to delete auth") logUser.WithError(deleteErr).Error("Failed to delete auth")
} }
} }
return "", fmt.Errorf("failed to login user: %w", err) return "", fmt.Errorf("failed to login user: %w", err)
@ -188,15 +197,16 @@ func (bridge *Bridge) LoginFull(
getTOTP func() (string, error), getTOTP func() (string, error),
getKeyPass func() ([]byte, error), getKeyPass func() ([]byte, error),
) (string, error) { ) (string, error) {
logrus.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)
} }
if auth.TwoFA.Enabled&proton.HasTOTP != 0 { if auth.TwoFA.Enabled&proton.HasTOTP != 0 {
logrus.WithField("userID", auth.UserID).Info("Requesting TOTP") logUser.WithField("userID", auth.UserID).Info("Requesting TOTP")
totp, err := getTOTP() totp, err := getTOTP()
if err != nil { if err != nil {
@ -211,7 +221,7 @@ func (bridge *Bridge) LoginFull(
var keyPass []byte var keyPass []byte
if auth.PasswordMode == proton.TwoPasswordMode { if auth.PasswordMode == proton.TwoPasswordMode {
logrus.WithField("userID", auth.UserID).Info("Requesting mailbox password") logUser.WithField("userID", auth.UserID).Info("Requesting mailbox password")
userKeyPass, err := getKeyPass() userKeyPass, err := getKeyPass()
if err != nil { if err != nil {
@ -223,10 +233,10 @@ 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 {
logrus.WithError(err).Error("Failed to delete auth") logUser.WithError(err).Error("Failed to delete auth")
} }
return "", err return "", err
@ -237,7 +247,7 @@ func (bridge *Bridge) LoginFull(
// LogoutUser logs out the given user. // LogoutUser logs out the given user.
func (bridge *Bridge) LogoutUser(ctx context.Context, userID string) error { func (bridge *Bridge) LogoutUser(ctx context.Context, userID string) error {
logrus.WithField("userID", userID).Info("Logging out user") logUser.WithField("userID", userID).Info("Logging out user")
return safe.LockRet(func() error { return safe.LockRet(func() error {
user, ok := bridge.users[userID] user, ok := bridge.users[userID]
@ -257,7 +267,7 @@ func (bridge *Bridge) LogoutUser(ctx context.Context, userID string) error {
// DeleteUser deletes the given user. // DeleteUser deletes the given user.
func (bridge *Bridge) DeleteUser(ctx context.Context, userID string) error { func (bridge *Bridge) DeleteUser(ctx context.Context, userID string) error {
logrus.WithField("userID", userID).Info("Deleting user") logUser.WithField("userID", userID).Info("Deleting user")
syncConfigDir, err := bridge.locator.ProvideIMAPSyncConfigPath() syncConfigDir, err := bridge.locator.ProvideIMAPSyncConfigPath()
if err != nil { if err != nil {
@ -278,7 +288,7 @@ func (bridge *Bridge) DeleteUser(ctx context.Context, userID string) error {
} }
if err := bridge.vault.DeleteUser(userID); err != nil { if err := bridge.vault.DeleteUser(userID); err != nil {
logrus.WithError(err).Error("Failed to delete vault user") logUser.WithError(err).Error("Failed to delete vault user")
} }
bridge.publish(events.UserDeleted{ bridge.publish(events.UserDeleted{
@ -291,7 +301,7 @@ func (bridge *Bridge) DeleteUser(ctx context.Context, userID string) error {
// SetAddressMode sets the address mode for the given user. // SetAddressMode sets the address mode for the given user.
func (bridge *Bridge) SetAddressMode(ctx context.Context, userID string, mode vault.AddressMode) error { func (bridge *Bridge) SetAddressMode(ctx context.Context, userID string, mode vault.AddressMode) error {
logrus.WithField("userID", userID).WithField("mode", mode).Info("Setting address mode") logUser.WithField("userID", userID).WithField("mode", mode).Info("Setting address mode")
return safe.RLockRet(func() error { return safe.RLockRet(func() error {
user, ok := bridge.users[userID] user, ok := bridge.users[userID]
@ -327,7 +337,7 @@ func (bridge *Bridge) SetAddressMode(ctx context.Context, userID string, mode va
// SendBadEventUserFeedback passes the feedback to the given user. // SendBadEventUserFeedback passes the feedback to the given user.
func (bridge *Bridge) SendBadEventUserFeedback(_ context.Context, userID string, doResync bool) error { func (bridge *Bridge) SendBadEventUserFeedback(_ context.Context, userID string, doResync bool) error {
logrus.WithField("userID", userID).WithField("doResync", doResync).Info("Passing bad event feedback to user") logUser.WithField("userID", userID).WithField("doResync", doResync).Info("Passing bad event feedback to user")
return safe.RLockRet(func() error { return safe.RLockRet(func() error {
ctx := context.Background() ctx := context.Background()
@ -338,7 +348,7 @@ func (bridge *Bridge) SendBadEventUserFeedback(_ context.Context, userID string,
"Failed to handle event: feedback failed: no such user", "Failed to handle event: feedback failed: no such user",
reporter.Context{"user_id": userID}, reporter.Context{"user_id": userID},
); rerr != nil { ); rerr != nil {
logrus.WithError(rerr).Error("Failed to report feedback failure") logUser.WithError(rerr).Error("Failed to report feedback failure")
} }
return ErrNoSuchUser return ErrNoSuchUser
@ -349,7 +359,7 @@ func (bridge *Bridge) SendBadEventUserFeedback(_ context.Context, userID string,
"Failed to handle event: feedback resync", "Failed to handle event: feedback resync",
reporter.Context{"user_id": userID}, reporter.Context{"user_id": userID},
); rerr != nil { ); rerr != nil {
logrus.WithError(rerr).Error("Failed to report feedback failure") logUser.WithError(rerr).Error("Failed to report feedback failure")
} }
return user.BadEventFeedbackResync(ctx) return user.BadEventFeedbackResync(ctx)
@ -359,7 +369,7 @@ func (bridge *Bridge) SendBadEventUserFeedback(_ context.Context, userID string,
"Failed to handle event: feedback logout", "Failed to handle event: feedback logout",
reporter.Context{"user_id": userID}, reporter.Context{"user_id": userID},
); rerr != nil { ); rerr != nil {
logrus.WithError(rerr).Error("Failed to report feedback failure") logUser.WithError(rerr).Error("Failed to report feedback failure")
} }
bridge.logoutUser(ctx, user, true, false, false) bridge.logoutUser(ctx, user, true, false, false)
@ -372,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)
} }
@ -403,11 +413,11 @@ func (bridge *Bridge) loginUser(ctx context.Context, client *proton.Client, auth
// loadUsers tries to load each user in the vault that isn't already loaded. // loadUsers tries to load each user in the vault that isn't already loaded.
func (bridge *Bridge) loadUsers(ctx context.Context) error { func (bridge *Bridge) loadUsers(ctx context.Context) error {
logrus.WithField("count", len(bridge.vault.GetUserIDs())).Info("Loading users") logUser.WithField("count", len(bridge.vault.GetUserIDs())).Info("Loading users")
defer logrus.Info("Finished loading users") defer logUser.Info("Finished loading users")
return bridge.vault.ForUser(runtime.NumCPU(), func(user *vault.User) error { return bridge.vault.ForUser(runtime.NumCPU(), func(user *vault.User) error {
log := logrus.WithField("userID", user.UserID()) log := logUser.WithField("userID", user.UserID())
if user.AuthUID() == "" { if user.AuthUID() == "" {
log.Info("User is not connected (skipping)") log.Info("User is not connected (skipping)")
@ -451,7 +461,7 @@ func (bridge *Bridge) loadUser(ctx context.Context, user *vault.User) error {
if apiErr := new(proton.APIError); errors.As(err, &apiErr) && (apiErr.Code == proton.AuthRefreshTokenInvalid) { if apiErr := new(proton.APIError); errors.As(err, &apiErr) && (apiErr.Code == proton.AuthRefreshTokenInvalid) {
// The session cannot be refreshed, we sign out the user by clearing his auth secrets. // The session cannot be refreshed, we sign out the user by clearing his auth secrets.
if err := user.Clear(); err != nil { if err := user.Clear(); err != nil {
logrus.WithError(err).Warn("Failed to clear user secrets") logUser.WithError(err).Warn("Failed to clear user secrets")
} }
} }
@ -496,24 +506,24 @@ func (bridge *Bridge) addUser(
if err := bridge.addUserWithVault(ctx, client, apiUser, vaultUser, isNew); err != nil { if err := bridge.addUserWithVault(ctx, client, apiUser, vaultUser, isNew); err != nil {
if _, ok := err.(*resty.ResponseError); ok || isLogin { if _, ok := err.(*resty.ResponseError); ok || isLogin {
logrus.WithError(err).Error("Failed to add user, clearing its secrets from vault") logUser.WithError(err).Error("Failed to add user, clearing its secrets from vault")
if err := vaultUser.Clear(); err != nil { if err := vaultUser.Clear(); err != nil {
logrus.WithError(err).Error("Failed to clear user secrets") logUser.WithError(err).Error("Failed to clear user secrets")
} }
} else { } else {
logrus.WithError(err).Error("Failed to add user") logUser.WithError(err).Error("Failed to add user")
} }
if err := vaultUser.Close(); err != nil { if err := vaultUser.Close(); err != nil {
logrus.WithError(err).Error("Failed to close vault user") logUser.WithError(err).Error("Failed to close vault user")
} }
if isNew { if isNew {
logrus.Warn("Deleting newly added vault user") logUser.Warn("Deleting newly added vault user")
if err := bridge.vault.DeleteUser(apiUser.ID); err != nil { if err := bridge.vault.DeleteUser(apiUser.ID); err != nil {
logrus.WithError(err).Error("Failed to delete vault user") logUser.WithError(err).Error("Failed to delete vault user")
} }
} }
@ -567,7 +577,7 @@ func (bridge *Bridge) addUserWithVault(
// For example, if the user's addresses change, we need to update them in gluon. // For example, if the user's addresses change, we need to update them in gluon.
bridge.tasks.Once(func(ctx context.Context) { bridge.tasks.Once(func(ctx context.Context) {
async.RangeContext(ctx, user.GetEventCh(), func(event events.Event) { async.RangeContext(ctx, user.GetEventCh(), func(event events.Event) {
logrus.WithFields(logrus.Fields{ logUser.WithFields(logrus.Fields{
"userID": apiUser.ID, "userID": apiUser.ID,
"event": event, "event": event,
}).Debug("Received user event") }).Debug("Received user event")
@ -596,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
} }
@ -618,14 +630,14 @@ func (bridge *Bridge) logoutUser(ctx context.Context, user *user.User, withAPI,
user.SendConfigStatusAbort(ctx, withTelemetry) user.SendConfigStatusAbort(ctx, withTelemetry)
} }
logrus.WithFields(logrus.Fields{ logUser.WithFields(logrus.Fields{
"userID": user.ID(), "userID": user.ID(),
"withAPI": withAPI, "withAPI": withAPI,
"withData": withData, "withData": withData,
}).Debug("Logging out user") }).Debug("Logging out user")
if err := user.Logout(ctx, withAPI); err != nil { if err := user.Logout(ctx, withAPI); err != nil {
logrus.WithError(err).Error("Failed to logout user") logUser.WithError(err).Error("Failed to logout user")
} }
bridge.heartbeat.SetNbAccount(len(bridge.users)) bridge.heartbeat.SetNbAccount(len(bridge.users))

View File

@ -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)))

View File

@ -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)
} }
@ -58,7 +61,7 @@ func (bridge *Bridge) handleUserBadEvent(ctx context.Context, user *user.User, e
"error": event.Error, "error": event.Error,
"error_type": internal.ErrCauseType(event.Error), "error_type": internal.ErrCauseType(event.Error),
}); rerr != nil { }); rerr != nil {
logrus.WithError(rerr).Error("Failed to report failed event handling") logrus.WithField("pkg", "bridge/event").WithError(rerr).Error("Failed to report failed event handling")
} }
user.OnBadEvent(ctx) user.OnBadEvent(ctx)
@ -70,6 +73,6 @@ func (bridge *Bridge) handleUncategorizedErrorEvent(event events.UncategorizedEv
"error_type": internal.ErrCauseType(event.Error), "error_type": internal.ErrCauseType(event.Error),
"error": event.Error, "error": event.Error,
}); rerr != nil { }); rerr != nil {
logrus.WithError(rerr).Error("Failed to report failed event handling") logrus.WithField("pkg", "bridge/event").WithError(rerr).Error("Failed to report failed event handling")
} }
} }

View File

@ -95,6 +95,6 @@ func TestConfigurationProgress_fed_year_change(t *testing.T) {
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup) require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_progress", req.Event) require.Equal(t, "bridge_config_progress", req.Event)
require.Equal(t, 370, req.Values.NbDay) require.True(t, (req.Values.NbDay == 370) || (req.Values.NbDay == 371)) // leap year is accounted for in the simplest manner.
require.Equal(t, 2, req.Values.NbDaySinceLast) require.Equal(t, 2, req.Values.NbDaySinceLast)
} }

View File

@ -29,27 +29,34 @@ type TLSDialer interface {
DialTLSContext(ctx context.Context, network, address string) (conn net.Conn, err error) DialTLSContext(ctx context.Context, network, address string) (conn net.Conn, err error)
} }
// CreateTransportWithDialer creates an http.Transport that uses the given dialer to make TLS connections. func SetBasicTransportTimeouts(t *http.Transport) {
func CreateTransportWithDialer(dialer TLSDialer) *http.Transport { t.MaxIdleConns = 100
return &http.Transport{ t.MaxIdleConnsPerHost = 100
DialTLSContext: dialer.DialTLSContext, t.IdleConnTimeout = 5 * time.Minute
Proxy: http.ProxyFromEnvironment, t.ExpectContinueTimeout = 500 * time.Millisecond
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 5 * time.Minute,
ExpectContinueTimeout: 500 * time.Millisecond,
// GODT-126: this was initially 10s but logs from users showed a significant number // GODT-126: this was initially 10s but logs from users showed a significant number
// were hitting this timeout, possibly due to flaky wifi taking >10s to reconnect. // were hitting this timeout, possibly due to flaky wifi taking >10s to reconnect.
// Bumping to 30s for now to avoid this problem. // Bumping to 30s for now to avoid this problem.
ResponseHeaderTimeout: 30 * time.Second, t.ResponseHeaderTimeout = 30 * time.Second
// If we allow up to 30 seconds for response headers, it is reasonable to allow up // If we allow up to 30 seconds for response headers, it is reasonable to allow up
// to 30 seconds for the TLS handshake to take place. // to 30 seconds for the TLS handshake to take place.
TLSHandshakeTimeout: 30 * time.Second, t.TLSHandshakeTimeout = 30 * time.Second
} }
// CreateTransportWithDialer creates an http.Transport that uses the given dialer to make TLS connections.
func CreateTransportWithDialer(dialer TLSDialer) *http.Transport {
t := &http.Transport{
DialTLSContext: dialer.DialTLSContext,
Proxy: http.ProxyFromEnvironment,
}
SetBasicTransportTimeouts(t)
return t
} }
// BasicTLSDialer implements TLSDialer. // BasicTLSDialer implements TLSDialer.

View File

@ -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())

View File

@ -144,7 +144,8 @@ func TestProxyProvider_FindProxy_CanReachTimeout(t *testing.T) {
r.Error(t, err) r.Error(t, err)
} }
func TestProxyProvider_DoHLookup_Quad9(t *testing.T) { // DISABLED_TestProxyProvider_DoHLookup_Quad9 cannot run on CI, see GODT-3257.
func DISABLED_TestProxyProvider_DoHLookup_Quad9(t *testing.T) {
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{Quad9Provider, GoogleProvider}, async.NoopPanicHandler{}) p := newProxyProvider(NewBasicTLSDialer(""), "", []string{Quad9Provider, GoogleProvider}, async.NoopPanicHandler{})
records, err := p.dohLookup(context.Background(), proxyQuery, Quad9Provider) records, err := p.dohLookup(context.Background(), proxyQuery, Quad9Provider)

View File

@ -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)
}

View File

@ -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_ = "";
}

View File

@ -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.
}; };

View File

@ -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.
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************

View File

@ -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.

View File

@ -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">

View File

@ -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

View File

@ -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 &paramName: 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;
} }

View File

@ -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

View File

@ -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()

View File

@ -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();
)
}

View File

@ -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.
@ -280,6 +284,8 @@ signals: // Signals received from the Go backend, to be forwarded to QML
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 knowledge base 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.

View File

@ -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>

View 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) {

View File

@ -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"
} }
} }

View File

@ -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.
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
@ -124,7 +116,6 @@ QQmlComponent *createRootQmlComponent(QQmlApplicationEngine &engine) {
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();
@ -258,19 +243,31 @@ const QString launchBridge(QStringList const &args) {
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.
@ -291,10 +288,9 @@ int main(int argc, char *argv[]) {
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()));
@ -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()) {

View File

@ -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

View File

@ -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
}
} }

View File

@ -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
} }

View File

@ -17,192 +17,15 @@ import QtQuick.Controls
Item { Item {
id: root id: root
enum Screen {
CertificateInstall,
ProfileInstall
}
property var wizard property var wizard
signal appleMailAutoconfigCertificateInstallPageShown
signal appleMailAutoconfigProfileInstallPageShow
function showAutoconfig() {
if (Backend.isTLSCertificateInstalled()) {
showProfileInstall();
} else {
showCertificateInstall();
}
}
function showCertificateInstall() {
certificateInstall.reset();
stack.currentIndex = ClientConfigAppleMail.Screen.CertificateInstall;
appleMailAutoconfigCertificateInstallPageShown();
}
function showProfileInstall() {
profileInstall.reset();
stack.currentIndex = ClientConfigAppleMail.Screen.ProfileInstall;
appleMailAutoconfigProfileInstallPageShow();
}
StackLayout {
id: stack
anchors.fill: parent
// stack index 0
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
ColumnLayout {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: ProtonStyle.wizard_spacing_large
Connections {
function onCertificateInstallCanceled() {
certificateInstall.waitingForCert = false;
certificateInstall.errorString = qsTr("Apple Mail cannot be configured if you do not install the certificate. Please retry.");
certificateInstall.showBugReportLink = false;
}
function onCertificateInstallFailed() {
certificateInstall.waitingForCert = false;
certificateInstall.errorString = qsTr("An error occurred while installing the certificate.");
certificateInstall.showBugReportLink = true;
}
function onCertificateInstallSuccess() {
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 accounts) 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
Item {
id: profileInstall
property bool profilePaneLaunched: false property bool profilePaneLaunched: false
function reset() { function reset() {
profilePaneLaunched = false; profilePaneLaunched = false;
} }
Layout.fillHeight: true
Layout.fillWidth: true
ColumnLayout { ColumnLayout {
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
@ -246,14 +69,14 @@ Item {
Button { Button {
Layout.fillWidth: true Layout.fillWidth: true
colorScheme: wizard.colorScheme colorScheme: wizard.colorScheme
text: profileInstall.profilePaneLaunched ? qsTr("I have installed the profile") : qsTr("Install the profile") text: profilePaneLaunched ? qsTr("I have installed the profile") : qsTr("Install the profile")
onClicked: { onClicked: {
if (profileInstall.profilePaneLaunched) { if (profilePaneLaunched) {
wizard.showClientConfigEnd(); wizard.showClientConfigEnd();
} else { } else {
wizard.user.configureAppleMail(wizard.address); wizard.user.configureAppleMail(wizard.address);
profileInstall.profilePaneLaunched = true; profilePaneLaunched = true;
} }
} }
} }
@ -270,5 +93,3 @@ Item {
} }
} }
} }
}
}

View File

@ -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 accounts) 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
}
}
}
}
}

View File

@ -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();
} }
} }

View File

@ -34,13 +34,23 @@ Item {
signal startSetup() signal startSetup()
function showAppleMailAutoconfigCertificateInstall() { function showCertificateInstall() {
showAppleMailAutoconfigCommon(); showClientConfigCommon();
if (wizard.client === SetupWizard.Client.AppleMail) {
descriptionLabel.text = qsTr("Apple Mail configuration is mostly automated, but in order to work, Bridge needs to install a certificate in your keychain."); 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); 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);

View File

@ -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("&", "&amp;")+ "</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();
}
}
}
}
}
} }
} }

View File

@ -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

View File

@ -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]);
}

View File

@ -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 &paramName, 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 &paramName: 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

View File

@ -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 &paramName, QStringList const &commandLineParams); ///< Remove a string parameter from a list of command-line parameters. QStringList stripStringParameterFromCommandLine(QString const &paramName, QStringList const &commandLineParams); ///< Remove a string parameter from a list of command-line parameters.
QStringList parseGoCLIStringArgument(QStringList const &args, QStringList const &paramNames); ///< 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

View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -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();

View File

@ -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
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************

View File

@ -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.

View File

@ -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

View File

@ -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",

View File

@ -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

View File

@ -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
//********************************************************** //**********************************************************

View File

@ -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{
{ {

View File

@ -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 {

View File

@ -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()

View File

@ -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()

View File

@ -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)))

View File

@ -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
} }

View File

@ -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
View 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
View 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)
}
}

View File

@ -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"
]
} }
] ]

View File

@ -27,5 +27,5 @@ func NewIMAPLogger() *IMAPLogger {
} }
func (l *IMAPLogger) Write(p []byte) (n int, err error) { func (l *IMAPLogger) Write(p []byte) (n int, err error) {
return logrus.WithField("pkg", "IMAP").WriterLevel(logrus.TraceLevel).Write(p) return logrus.WithField("pkg", "log/IMAP").WriterLevel(logrus.TraceLevel).Write(p)
} }

View File

@ -27,7 +27,7 @@ type SMTPErrorLogger struct {
} }
func NewSMTPLogger() *SMTPErrorLogger { func NewSMTPLogger() *SMTPErrorLogger {
return &SMTPErrorLogger{l: logrus.WithField("pkg", "SMTP")} return &SMTPErrorLogger{l: logrus.WithField("pkg", "log/SMTP")}
} }
func (s *SMTPErrorLogger) Printf(format string, args ...interface{}) { func (s *SMTPErrorLogger) Printf(format string, args ...interface{}) {
@ -44,7 +44,7 @@ type SMTPDebugLogger struct {
} }
func NewSMTPDebugLogger() *SMTPDebugLogger { func NewSMTPDebugLogger() *SMTPDebugLogger {
return &SMTPDebugLogger{l: logrus.WithField("pkg", "SMTP")} return &SMTPDebugLogger{l: logrus.WithField("pkg", "log/SMTP")}
} }
func (l *SMTPDebugLogger) Write(p []byte) (n int, err error) { func (l *SMTPDebugLogger) Write(p []byte) (n int, err error) {

View File

@ -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,19 +677,31 @@ 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")
} }
if err := s.identityState.WithAddrKR(s.addrID, func(_, addrKR *crypto.KeyRing) error {
var messageID string
p, err2 := parser.New(bytes.NewReader(literal)) p, err2 := parser.New(bytes.NewReader(literal))
if err2 != nil { if err2 != nil {
return fmt.Errorf("failed to parse literal: %w", err2) return imap.Message{}, nil, fmt.Errorf("failed to parse literal: %w", err2)
} }
if slices.Contains(labelIDs, proton.DraftsLabel) {
msg, err := s.createDraftWithParser(ctx, p, addrKR, addr) 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 {
primaryKey, errKey := addrKR.FirstKey()
if errKey != nil {
return fmt.Errorf("failed to get primary key for import: %w", errKey)
}
var messageID string
if isDraft {
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)
} }
@ -699,7 +717,7 @@ func (s *Connector) importMessage(
} }
literal = buf.Bytes() literal = buf.Bytes()
} }
str, err := s.client.ImportMessages(ctx, addrKR, 1, 1, []proton.ImportReq{{ str, err := s.client.ImportMessages(ctx, primaryKey, 1, 1, []proton.ImportReq{{
Metadata: proton.ImportMetadata{ Metadata: proton.ImportMetadata{
AddressID: s.addrID, AddressID: s.addrID,
LabelIDs: labelIDs, LabelIDs: labelIDs,
@ -726,7 +744,7 @@ func (s *Connector) importMessage(
return fmt.Errorf("failed to fetch message: %w", err) return fmt.Errorf("failed to fetch message: %w", err)
} }
if literal, err = message.DecryptAndBuildRFC822(addrKR, full.Message, full.AttData, defaultMessageJobOpts()); err != nil { if literal, err = message.DecryptAndBuildRFC822(primaryKey, full.Message, full.AttData, defaultMessageJobOpts()); err != nil {
return fmt.Errorf("failed to build message: %w", err) return fmt.Errorf("failed to build message: %w", err)
} }
@ -845,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)
}
}

View File

@ -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)
}
}

View File

@ -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,
) )

View File

@ -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,
) )

View File

@ -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
} }
} }

View File

@ -39,6 +39,8 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
var logIMAP = logrus.WithField("pkg", "server/imap") //nolint:gochecknoglobals
type IMAPSettingsProvider interface { type IMAPSettingsProvider interface {
TLSConfig() *tls.Config TLSConfig() *tls.Config
LogClient() bool LogClient() bool
@ -79,7 +81,7 @@ func newIMAPServer(
gluonCacheDir = ApplyGluonCachePathSuffix(gluonCacheDir) gluonCacheDir = ApplyGluonCachePathSuffix(gluonCacheDir)
gluonConfigDir = ApplyGluonConfigPathSuffix(gluonConfigDir) gluonConfigDir = ApplyGluonConfigPathSuffix(gluonConfigDir)
logrus.WithFields(logrus.Fields{ logIMAP.WithFields(logrus.Fields{
"gluonStore": gluonCacheDir, "gluonStore": gluonCacheDir,
"gluonDB": gluonConfigDir, "gluonDB": gluonConfigDir,
"version": version, "version": version,
@ -88,10 +90,9 @@ func newIMAPServer(
}).Info("Creating IMAP server") }).Info("Creating IMAP server")
if logClient || logServer { if logClient || logServer {
log := logrus.WithField("protocol", "IMAP") logIMAP.Warning("================================================")
log.Warning("================================================") logIMAP.Warning("THIS LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA")
log.Warning("THIS LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA") logIMAP.Warning("================================================")
log.Warning("================================================")
} }
var imapClientLog io.Writer var imapClientLog io.Writer
@ -143,7 +144,7 @@ func newIMAPServer(
tasks.Once(func(ctx context.Context) { tasks.Once(func(ctx context.Context) {
async.RangeContext(ctx, imapServer.GetErrorCh(), func(err error) { async.RangeContext(ctx, imapServer.GetErrorCh(), func(err error) {
logrus.WithError(err).Error("IMAP server error") logIMAP.WithError(err).Error("IMAP server error")
}) })
}) })
@ -176,7 +177,7 @@ func (*storeBuilder) Delete(path, userID string) error {
} }
func moveGluonCacheDir(settings IMAPSettingsProvider, oldGluonDir, newGluonDir string) error { func moveGluonCacheDir(settings IMAPSettingsProvider, oldGluonDir, newGluonDir string) error {
logrus.Infof("gluon cache moving from %s to %s", oldGluonDir, newGluonDir) logIMAP.WithField("pkg", "service/imap").Infof("gluon cache moving from %s to %s", oldGluonDir, newGluonDir)
oldCacheDir := ApplyGluonCachePathSuffix(oldGluonDir) oldCacheDir := ApplyGluonCachePathSuffix(oldGluonDir)
if err := files.CopyDir(oldCacheDir, ApplyGluonCachePathSuffix(newGluonDir)); err != nil { if err := files.CopyDir(oldCacheDir, ApplyGluonCachePathSuffix(newGluonDir)); err != nil {
return fmt.Errorf("failed to copy gluon dir: %w", err) return fmt.Errorf("failed to copy gluon dir: %w", err)
@ -187,7 +188,7 @@ func moveGluonCacheDir(settings IMAPSettingsProvider, oldGluonDir, newGluonDir s
} }
if err := os.RemoveAll(oldCacheDir); err != nil { if err := os.RemoveAll(oldCacheDir); err != nil {
logrus.WithError(err).Error("failed to remove old gluon cache dir") logIMAP.WithError(err).Error("failed to remove old gluon cache dir")
} }
return nil return nil

View File

@ -55,7 +55,6 @@ 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
@ -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
} }
@ -190,16 +199,16 @@ func (sm *Service) run(ctx context.Context, subscription events.Subscription) {
case evt := <-eventSub.GetChannel(): case evt := <-eventSub.GetChannel():
switch evt.(type) { switch evt.(type) {
case events.ConnStatusDown: case events.ConnStatusDown:
logrus.Info("Server Manager, network down stopping listeners") sm.log.Info("Server Manager, network down stopping listeners")
if err := sm.closeSMTPServer(ctx); err != nil { if err := sm.closeSMTPServer(ctx); err != nil {
logrus.WithError(err).Error("Failed to close SMTP server") sm.log.WithError(err).Error("Failed to close SMTP server")
} }
if err := sm.stopIMAPListener(ctx); err != nil { if err := sm.stopIMAPListener(ctx); err != nil {
logrus.WithError(err) sm.log.WithError(err)
} }
case events.ConnStatusUp: case events.ConnStatusUp:
logrus.Info("Server Manager, network up starting listeners") sm.log.Info("Server Manager, network up starting listeners")
sm.handleLoadedUserCountChange(ctx) sm.handleLoadedUserCountChange(ctx)
} }
@ -241,12 +250,12 @@ func (sm *Service) run(ctx context.Context, subscription events.Subscription) {
request.Reply(ctx, nil, err) request.Reply(ctx, nil, err)
case *smRequestAddSMTPAccount: case *smRequestAddSMTPAccount:
logrus.WithField("user", r.account.UserID()).Debug("Adding SMTP Account") sm.log.WithField("user", r.account.UserID()).Debug("Adding SMTP Account")
sm.smtpAccounts.AddAccount(r.account) sm.smtpAccounts.AddAccount(r.account)
request.Reply(ctx, nil, nil) request.Reply(ctx, nil, nil)
case *smRequestRemoveSMTPAccount: case *smRequestRemoveSMTPAccount:
logrus.WithField("user", r.account.UserID()).Debug("Removing SMTP Account") sm.log.WithField("user", r.account.UserID()).Debug("Removing SMTP Account")
sm.smtpAccounts.RemoveAccount(r.account) sm.smtpAccounts.RemoveAccount(r.account)
request.Reply(ctx, nil, nil) request.Reply(ctx, nil, 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) {
logrus.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 {
logrus.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 {
logrus.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 {
logrus.WithError(err).Error("Failed to stop IMAP server")
}
}
if sm.smtpListener != nil {
if err := sm.closeSMTPServer(ctx); err != nil {
logrus.WithError(err).Error("Failed to stop SMTP server")
}
} }
} }
} }
@ -286,12 +281,12 @@ func (sm *Service) handleLoadedUserCountChange(ctx context.Context) {
func (sm *Service) handleClose(ctx context.Context) { func (sm *Service) handleClose(ctx context.Context) {
// Close the IMAP server. // Close the IMAP server.
if err := sm.closeIMAPServer(ctx); err != nil { if err := sm.closeIMAPServer(ctx); err != nil {
logrus.WithError(err).Error("Failed to close IMAP server") sm.log.WithError(err).Error("Failed to close IMAP server")
} }
// Close the SMTP server. // Close the SMTP server.
if err := sm.closeSMTPServer(ctx); err != nil { if err := sm.closeSMTPServer(ctx); err != nil {
logrus.WithError(err).Error("Failed to close SMTP server") sm.log.WithError(err).Error("Failed to close SMTP server")
} }
// Cancel and wait needs to be called here since the SMTP server does not have a way to exit // Cancel and wait needs to be called here since the SMTP server does not have a way to exit
@ -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,
@ -325,7 +315,7 @@ func (sm *Service) handleAddIMAPUserImpl(ctx context.Context,
return fmt.Errorf("no imap server instance running") return fmt.Errorf("no imap server instance running")
} }
log := logrus.WithFields(logrus.Fields{ log := sm.log.WithFields(logrus.Fields{
"addrID": addrID, "addrID": addrID,
}) })
log.Info("Adding user to imap server") log.Info("Adding user to imap server")
@ -341,7 +331,7 @@ func (sm *Service) handleAddIMAPUserImpl(ctx context.Context,
if isNew { if isNew {
// If the DB was newly created, clear the sync status; gluon's DB was not found. // If the DB was newly created, clear the sync status; gluon's DB was not found.
logrus.Warn("IMAP user DB was newly created, clearing sync status") sm.log.Warn("IMAP user DB was newly created, clearing sync status")
// Remove the user from IMAP so we can clear the sync status. // Remove the user from IMAP so we can clear the sync status.
if err := sm.imapServer.RemoveUser(ctx, gluonID, false); err != nil { if err := sm.imapServer.RemoveUser(ctx, gluonID, false); err != nil {
@ -415,7 +405,7 @@ func (sm *Service) handleRemoveIMAPUser(ctx context.Context, withData bool, idPr
return fmt.Errorf("no imap server instance running") return fmt.Errorf("no imap server instance running")
} }
logrus.WithFields(logrus.Fields{ sm.log.WithFields(logrus.Fields{
"withData": withData, "withData": withData,
"addresses": addrIDs, "addresses": addrIDs,
}).Debug("Removing IMAP user") }).Debug("Removing IMAP user")
@ -423,7 +413,7 @@ func (sm *Service) handleRemoveIMAPUser(ctx context.Context, withData bool, idPr
for _, addrID := range addrIDs { for _, addrID := range addrIDs {
gluonID, ok := idProvider.GetGluonID(addrID) gluonID, ok := idProvider.GetGluonID(addrID)
if !ok { if !ok {
logrus.Warnf("Could not find Gluon ID for addrID %v", addrID) sm.log.Warnf("Could not find Gluon ID for addrID %v", addrID)
continue continue
} }
@ -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
@ -480,7 +468,7 @@ func (sm *Service) closeSMTPServer(ctx context.Context) error {
// even after the server has been closed. So we close the listener ourselves to unblock it. // even after the server has been closed. So we close the listener ourselves to unblock it.
if sm.smtpListener != nil { if sm.smtpListener != nil {
logrus.Info("Closing SMTP Listener") sm.log.Info("Closing SMTP Listener")
if err := sm.smtpListener.Close(); err != nil { if err := sm.smtpListener.Close(); err != nil {
return fmt.Errorf("failed to close SMTP listener: %w", err) return fmt.Errorf("failed to close SMTP listener: %w", err)
} }
@ -489,9 +477,9 @@ func (sm *Service) closeSMTPServer(ctx context.Context) error {
} }
if sm.smtpServer != nil { if sm.smtpServer != nil {
logrus.Info("Closing SMTP server") sm.log.Info("Closing SMTP server")
if err := sm.smtpServer.Close(); err != nil { if err := sm.smtpServer.Close(); err != nil {
logrus.WithError(err).Debug("Failed to close SMTP server (expected -- we close the listener ourselves)") sm.log.WithError(err).Debug("Failed to close SMTP server (expected -- we close the listener ourselves)")
} }
sm.smtpServer = nil sm.smtpServer = nil
@ -504,7 +492,7 @@ func (sm *Service) closeSMTPServer(ctx context.Context) error {
func (sm *Service) closeIMAPServer(ctx context.Context) error { func (sm *Service) closeIMAPServer(ctx context.Context) error {
if sm.imapListener != nil { if sm.imapListener != nil {
logrus.Info("Closing IMAP Listener") sm.log.Info("Closing IMAP Listener")
if err := sm.imapListener.Close(); err != nil { if err := sm.imapListener.Close(); err != nil {
return fmt.Errorf("failed to close IMAP listener: %w", err) return fmt.Errorf("failed to close IMAP listener: %w", err)
@ -516,7 +504,7 @@ func (sm *Service) closeIMAPServer(ctx context.Context) error {
} }
if sm.imapServer != nil { if sm.imapServer != nil {
logrus.Info("Closing IMAP server") sm.log.Info("Closing IMAP server")
if err := sm.imapServer.Close(ctx); err != nil { if err := sm.imapServer.Close(ctx); err != nil {
return fmt.Errorf("failed to close IMAP server: %w", err) return fmt.Errorf("failed to close IMAP server: %w", err)
} }
@ -530,7 +518,7 @@ func (sm *Service) closeIMAPServer(ctx context.Context) error {
} }
func (sm *Service) restartIMAP(ctx context.Context) error { func (sm *Service) restartIMAP(ctx context.Context) error {
logrus.Info("Restarting IMAP server") sm.log.Info("Restarting IMAP server")
if sm.imapListener != nil { if sm.imapListener != nil {
if err := sm.imapListener.Close(); err != nil { if err := sm.imapListener.Close(); err != nil {
@ -542,15 +530,11 @@ 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 {
logrus.Info("Restarting SMTP server") sm.log.Info("Restarting SMTP server")
if err := sm.closeSMTPServer(ctx); err != nil { if err := sm.closeSMTPServer(ctx); err != nil {
return fmt.Errorf("failed to close SMTP: %w", err) return fmt.Errorf("failed to close SMTP: %w", err)
@ -560,16 +544,12 @@ 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 {
port, err := func() (int, error) { port, err := func() (int, error) {
logrus.WithFields(logrus.Fields{ sm.log.WithFields(logrus.Fields{
"port": sm.smtpSettings.Port(), "port": sm.smtpSettings.Port(),
"ssl": sm.smtpSettings.UseSSL(), "ssl": sm.smtpSettings.UseSSL(),
}).Info("Starting SMTP server") }).Info("Starting SMTP server")
@ -583,7 +563,7 @@ func (sm *Service) serveSMTP(ctx context.Context) error {
sm.tasks.Once(func(context.Context) { sm.tasks.Once(func(context.Context) {
if err := sm.smtpServer.Serve(smtpListener); err != nil { if err := sm.smtpServer.Serve(smtpListener); err != nil {
logrus.WithError(err).Info("SMTP server stopped") sm.log.WithError(err).Info("SMTP server stopped")
} }
}) })
@ -615,7 +595,7 @@ func (sm *Service) serveIMAP(ctx context.Context) error {
return 0, fmt.Errorf("no IMAP server instance running") return 0, fmt.Errorf("no IMAP server instance running")
} }
logrus.WithFields(logrus.Fields{ sm.log.WithFields(logrus.Fields{
"port": sm.imapSettings.Port(), "port": sm.imapSettings.Port(),
"ssl": sm.imapSettings.UseSSL(), "ssl": sm.imapSettings.UseSSL(),
}).Info("Starting IMAP server") }).Info("Starting IMAP server")
@ -654,7 +634,7 @@ func (sm *Service) serveIMAP(ctx context.Context) error {
} }
func (sm *Service) stopIMAPListener(ctx context.Context) error { func (sm *Service) stopIMAPListener(ctx context.Context) error {
logrus.Info("Stopping IMAP listener") sm.log.Info("Stopping IMAP listener")
if sm.imapListener != nil { if sm.imapListener != nil {
if err := sm.imapListener.Close(); err != nil { if err := sm.imapListener.Close(); err != nil {
return err return err
@ -679,10 +659,8 @@ 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 {
logrus.WithError(err).Error("failed to move GluonCacheDir") sm.log.WithError(err).Error("failed to move GluonCacheDir")
if err := sm.imapSettings.SetCacheDirectory(currentGluonDir); err != nil { if err := sm.imapSettings.SetCacheDirectory(currentGluonDir); err != nil {
return fmt.Errorf("failed to revert GluonCacheDir: %w", err) return fmt.Errorf("failed to revert GluonCacheDir: %w", err)
@ -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{}

View File

@ -29,6 +29,8 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
var logSMTP = logrus.WithField("pkg", "server/smtp") //nolint:gochecknoglobals
type SMTPSettingsProvider interface { type SMTPSettingsProvider interface {
TLSConfig() *tls.Config TLSConfig() *tls.Config
Log() bool Log() bool
@ -39,7 +41,7 @@ type SMTPSettingsProvider interface {
} }
func newSMTPServer(accounts *smtpservice.Accounts, settings SMTPSettingsProvider) *smtp.Server { func newSMTPServer(accounts *smtpservice.Accounts, settings SMTPSettingsProvider) *smtp.Server {
logrus.WithField("logSMTP", settings.Log()).Info("Creating SMTP server") logSMTP.WithField("logSMTP", settings.Log()).Info("Creating SMTP server")
smtpServer := smtp.NewServer(smtpservice.NewBackend(accounts, settings.Identifier())) smtpServer := smtp.NewServer(smtpservice.NewBackend(accounts, settings.Identifier()))
@ -57,10 +59,9 @@ func newSMTPServer(accounts *smtpservice.Accounts, settings SMTPSettingsProvider
}) })
if settings.Log() { if settings.Log() {
log := logrus.WithField("protocol", "SMTP") logSMTP.Warning("================================================")
log.Warning("================================================") logSMTP.Warning("THIS LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA")
log.Warning("THIS LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA") logSMTP.Warning("================================================")
log.Warning("================================================")
smtpServer.Debug = logging.NewSMTPDebugLogger() smtpServer.Debug = logging.NewSMTPDebugLogger()
} }

View File

@ -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)
} }

View File

@ -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.

View File

@ -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())
}) })
} }

View 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{},
}
}

View File

@ -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

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