forked from Silverfish/proton-bridge
Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dd9a819ea2 | |||
| 401e56224b | |||
| 1ee52f0f55 | |||
| 9efaf9184c | |||
| a8f270405f | |||
| 38606888fe | |||
| 1b22c32ef9 | |||
| 7a1c7e8743 | |||
| 9449177553 | |||
| bbcedc655a | |||
| 40c97ab19e | |||
| 50dd046b82 | |||
| 7d13c99710 | |||
| 6d7c21b2c9 | |||
| f7434109be | |||
| 414d74d06a | |||
| 110cdbf3ae | |||
| ec4ceb4552 | |||
| ef62704030 | |||
| eaba6b6363 | |||
| e1723fc24b | |||
| 2073513d5e | |||
| 36f7d9672f | |||
| ef183e0758 | |||
| 0d2a803711 | |||
| 06b5276981 | |||
| b2d61da41f | |||
| e51c81fc03 | |||
| 26897f06c4 | |||
| 5ca9a7db37 | |||
| b34f5d072f | |||
| eeb514cc81 | |||
| 650ad49ab0 | |||
| 0e5715c4e3 | |||
| b0f1c3d4c5 | |||
| ba935a6cce | |||
| 1370ff78c5 | |||
| 109c15410a |
271
.gitlab-ci.yml
271
.gitlab-ci.yml
@ -34,270 +34,9 @@ stages:
|
||||
- test
|
||||
- build
|
||||
|
||||
.rules-branch-and-MR-manual:
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH || $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
when: manual
|
||||
allow_failure: true
|
||||
- when: never
|
||||
include:
|
||||
- local: ci/rules.yml
|
||||
- local: ci/env.yml
|
||||
- local: ci/test.yml
|
||||
- local: ci/build.yml
|
||||
|
||||
.rules-branch-manual-MR-and-devel-always:
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == "devel" || $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
when: always
|
||||
allow_failure: false
|
||||
- if: $CI_COMMIT_BRANCH
|
||||
when: manual
|
||||
allow_failure: true
|
||||
- when: never
|
||||
|
||||
.rules-branch-manual-scheduled-and-test-branch-always:
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "schedule"
|
||||
when: always
|
||||
allow_failure: false
|
||||
- if: $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME=~ /^test/
|
||||
when: always
|
||||
allow_failure: false
|
||||
- if: $CI_COMMIT_BRANCH
|
||||
when: manual
|
||||
allow_failure: true
|
||||
- when: never
|
||||
|
||||
# ENV
|
||||
.env-windows:
|
||||
before_script:
|
||||
- export BRIDGE_SYNC_FORCE_MINIMUM_SPEC=1
|
||||
- export GOROOT=/c/Go1.20/
|
||||
- export PATH=$GOROOT/bin:$PATH
|
||||
- export GOARCH=amd64
|
||||
- export GOPATH=~/go1.20
|
||||
- export GO111MODULE=on
|
||||
- export PATH="${GOPATH}/bin:${PATH}"
|
||||
- export MSYSTEM=
|
||||
- export QT6DIR=/c/grrrQt/6.4.3/msvc2019_64
|
||||
- export PATH=$PATH:${QT6DIR}/bin
|
||||
- export PATH="/c/Program Files/Microsoft Visual Studio/2022/Community/Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/bin:$PATH"
|
||||
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
|
||||
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
|
||||
- git config --global safe.directory '*'
|
||||
- git status --porcelain
|
||||
cache: {}
|
||||
tags:
|
||||
- windows-bridge
|
||||
|
||||
.env-darwin:
|
||||
before_script:
|
||||
- export BRIDGE_SYNC_FORCE_MINIMUM_SPEC=1
|
||||
- export PATH=/usr/local/bin:$PATH
|
||||
- export PATH=/usr/local/opt/git/bin:$PATH
|
||||
- export PATH=/usr/local/opt/make/libexec/gnubin:$PATH
|
||||
- export PATH=/usr/local/opt/gnu-sed/libexec/gnubin:$PATH
|
||||
- export GOROOT=~/local/opt/go@1.20
|
||||
- export PATH="${GOROOT}/bin:$PATH"
|
||||
- export GOPATH=~/go1.20
|
||||
- export PATH="${GOPATH}/bin:$PATH"
|
||||
- export QT6DIR=/opt/Qt/6.4.3/macos
|
||||
- export PATH="${QT6DIR}/bin:$PATH"
|
||||
- uname -a
|
||||
cache: {}
|
||||
tags:
|
||||
- macos-m1-bridge
|
||||
|
||||
.env-linux-build:
|
||||
image: gitlab.protontech.ch:4567/go/bridge-internal:build-go1.20-qt6.4.3
|
||||
variables:
|
||||
VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.cache
|
||||
cache:
|
||||
key: linux-vcpkg
|
||||
paths:
|
||||
- .cache
|
||||
when: 'always'
|
||||
before_script:
|
||||
- mkdir -p .cache/bin
|
||||
- export BRIDGE_SYNC_FORCE_MINIMUM_SPEC=1
|
||||
- export PATH=$(pwd)/.cache/bin:$PATH
|
||||
- export GOPATH="$CI_PROJECT_DIR/.cache"
|
||||
- export PATH=$PATH:$QT6DIR/bin
|
||||
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
|
||||
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
|
||||
tags:
|
||||
- shared-large
|
||||
|
||||
# Stage: TEST
|
||||
|
||||
lint:
|
||||
stage: test
|
||||
extends:
|
||||
- .rules-branch-manual-MR-and-devel-always
|
||||
script:
|
||||
- make lint
|
||||
tags:
|
||||
- shared-medium
|
||||
|
||||
bug-report-preview:
|
||||
stage: test
|
||||
extends:
|
||||
- .rules-branch-and-MR-manual
|
||||
script:
|
||||
- make lint-bug-report-preview
|
||||
tags:
|
||||
- shared-medium
|
||||
|
||||
.script-test:
|
||||
stage: test
|
||||
extends:
|
||||
- .rules-branch-manual-MR-and-devel-always
|
||||
script:
|
||||
- make test
|
||||
artifacts:
|
||||
paths:
|
||||
- coverage/**
|
||||
|
||||
test-linux:
|
||||
extends:
|
||||
- .script-test
|
||||
tags:
|
||||
- shared-large
|
||||
|
||||
fuzz-linux:
|
||||
stage: test
|
||||
extends:
|
||||
- .rules-branch-manual-MR-and-devel-always
|
||||
script:
|
||||
- make fuzz
|
||||
tags:
|
||||
- shared-large
|
||||
|
||||
test-linux-race:
|
||||
extends:
|
||||
- test-linux
|
||||
- .rules-branch-and-MR-manual
|
||||
script:
|
||||
- make test-race
|
||||
|
||||
test-integration:
|
||||
extends:
|
||||
- test-linux
|
||||
script:
|
||||
- make test-integration
|
||||
|
||||
test-integration-race:
|
||||
extends:
|
||||
- test-integration
|
||||
- .rules-branch-and-MR-manual
|
||||
script:
|
||||
- make test-integration-race
|
||||
|
||||
test-integration-nightly:
|
||||
extends:
|
||||
- test-integration
|
||||
- .rules-branch-manual-scheduled-and-test-branch-always
|
||||
needs:
|
||||
- test-integration
|
||||
script:
|
||||
- make test-integration-nightly
|
||||
|
||||
test-windows:
|
||||
extends:
|
||||
- .env-windows
|
||||
- .script-test
|
||||
|
||||
test-darwin:
|
||||
extends:
|
||||
- .env-darwin
|
||||
- .script-test
|
||||
|
||||
test-coverage:
|
||||
stage: test
|
||||
extends:
|
||||
- .rules-branch-manual-scheduled-and-test-branch-always
|
||||
script:
|
||||
- ./utils/coverage.sh
|
||||
coverage: '/total:.*\(statements\).*\d+\.\d+%/'
|
||||
needs:
|
||||
- test-linux
|
||||
- test-windows
|
||||
- test-darwin
|
||||
- test-integration
|
||||
- test-integration-nightly
|
||||
tags:
|
||||
- shared-small
|
||||
artifacts:
|
||||
paths:
|
||||
- coverage*
|
||||
- coverage/**
|
||||
when: 'always'
|
||||
reports:
|
||||
coverage_report:
|
||||
coverage_format: cobertura
|
||||
path: coverage.xml
|
||||
|
||||
# Stage: BUILD
|
||||
|
||||
.script-build:
|
||||
stage: build
|
||||
needs: ["lint"]
|
||||
extends:
|
||||
- .rules-branch-and-MR-manual
|
||||
script:
|
||||
- make build
|
||||
- git diff && git diff-index --quiet HEAD
|
||||
- make vault-editor
|
||||
artifacts:
|
||||
expire_in: 1 day
|
||||
when: always
|
||||
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
|
||||
paths:
|
||||
- bridge_*.tgz
|
||||
- vault-editor
|
||||
|
||||
build-linux:
|
||||
extends:
|
||||
- .script-build
|
||||
- .env-linux-build
|
||||
|
||||
build-linux-qa:
|
||||
extends:
|
||||
- build-linux
|
||||
- .rules-branch-manual-MR-and-devel-always
|
||||
variables:
|
||||
BUILD_TAGS: "build_qa"
|
||||
|
||||
build-darwin:
|
||||
extends:
|
||||
- .script-build
|
||||
- .env-darwin
|
||||
|
||||
build-darwin-qa:
|
||||
extends:
|
||||
- build-darwin
|
||||
variables:
|
||||
BUILD_TAGS: "build_qa"
|
||||
|
||||
build-windows:
|
||||
extends:
|
||||
- .script-build
|
||||
- .env-windows
|
||||
|
||||
build-windows-qa:
|
||||
extends:
|
||||
- build-windows
|
||||
variables:
|
||||
BUILD_TAGS: "build_qa"
|
||||
|
||||
trigger-qa-installer:
|
||||
stage: build
|
||||
needs: ["lint"]
|
||||
extends:
|
||||
- .rules-branch-and-MR-manual
|
||||
variables:
|
||||
APP: bridge
|
||||
WORKFLOW: build-all
|
||||
SRC_TAG: $CI_COMMIT_BRANCH
|
||||
SRC_HASH: $CI_COMMIT_SHA
|
||||
trigger:
|
||||
project: "jcuth/bridge-release"
|
||||
branch: master
|
||||
|
||||
# TODO: PUT BACK ALL THE JOBS! JUST DID THIS FOR NOW TO GET CI WORKING AGAIN...
|
||||
|
||||
@ -50,6 +50,7 @@ Proton Mail Bridge includes the following 3rd party software:
|
||||
* [uuid](https://github.com/google/uuid) available under [license](https://github.com/google/uuid/blob/master/LICENSE)
|
||||
* [go-multierror](https://github.com/hashicorp/go-multierror) available under [license](https://github.com/hashicorp/go-multierror/blob/master/LICENSE)
|
||||
* [html2text](https://github.com/jaytaylor/html2text) available under [license](https://github.com/jaytaylor/html2text/blob/master/LICENSE)
|
||||
* [go-locale](https://github.com/jeandeaual/go-locale) available under [license](https://github.com/jeandeaual/go-locale/blob/master/LICENSE)
|
||||
* [go-keychain](https://github.com/keybase/go-keychain) available under [license](https://github.com/keybase/go-keychain/blob/master/LICENSE)
|
||||
* [dns](https://github.com/miekg/dns) available under [license](https://github.com/miekg/dns/blob/master/LICENSE)
|
||||
* [memory](https://github.com/pbnjay/memory) available under [license](https://github.com/pbnjay/memory/blob/master/LICENSE)
|
||||
|
||||
33
Changelog.md
33
Changelog.md
@ -3,6 +3,39 @@
|
||||
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
|
||||
|
||||
## Xikou Bridge 3.8.0
|
||||
|
||||
### Added
|
||||
* Test: Add test scenarios to add an /Answered flag to a replied message and revert.
|
||||
* GODT-3046: Added links to KB in error messages.
|
||||
* Test(GODT-3113): Inline HTML message and HTML attachment is getting altered.
|
||||
* Test(GODT-3124): Attempt to fix 401 during login.
|
||||
|
||||
### Changed
|
||||
* GODT-3134: Br tag triggers installer.
|
||||
* Added update events to bridge GUI tester.
|
||||
|
||||
### Fixed
|
||||
* GODT-3142: Pass br tag if available.
|
||||
* GODT-3151: Fix feature test with non modified HTML part.
|
||||
* GODT-3151: Only modify HTML Meta content if UTF-8 charset override is needed.
|
||||
* GODT-2851: Add empty text part if no text part when importing multipart.
|
||||
* GODT-3102: Distinguish Vault Decryption from Serialization Errors.
|
||||
* GODT-3124: Handling of sync child jobs.
|
||||
* GODT-3148: Bump go-sysinfo to get rid of linker warning on macOS Sonoma.
|
||||
* GODT-3124: Flaky tests.
|
||||
* GODT-3022: Handle multipart/related on fake server.
|
||||
* GODT-3133: Fix GetSystemLanguage.
|
||||
* GODT-3124: Race condition in sync task waiter.
|
||||
* GODT-3124: Race conditions reported by race check.
|
||||
* GODT-2797: Encode attached key name and use same pubkey name as web-app.
|
||||
* Fix case of IMAP login error.
|
||||
* GODT-3132: Do not allow sending on disabled accounts.
|
||||
* GODT-3046: fix typo spotted during KB article review.
|
||||
* GODT-3129: Bad Event during after address order change.
|
||||
* GODT-3117: Improve GetAllContacts and GetAllContactsEmail.
|
||||
|
||||
|
||||
## Wakato Bridge 3.7.1
|
||||
|
||||
### Added
|
||||
|
||||
2
Makefile
2
Makefile
@ -11,7 +11,7 @@ ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
|
||||
.PHONY: build build-gui build-nogui build-launcher versioner hasher
|
||||
|
||||
# Keep version hardcoded so app build works also without Git repository.
|
||||
BRIDGE_APP_VERSION?=3.7.1+git
|
||||
BRIDGE_APP_VERSION?=3.8.0+git
|
||||
APP_VERSION:=${BRIDGE_APP_VERSION}
|
||||
APP_FULL_NAME:=Proton Mail Bridge
|
||||
APP_VENDOR:=Proton AG
|
||||
|
||||
69
ci/build.yml
Normal file
69
ci/build.yml
Normal file
@ -0,0 +1,69 @@
|
||||
|
||||
---
|
||||
|
||||
.script-build:
|
||||
stage: build
|
||||
needs: ["lint"]
|
||||
extends:
|
||||
- .rules-branch-and-MR-manual
|
||||
script:
|
||||
- make build
|
||||
- git diff && git diff-index --quiet HEAD
|
||||
- make vault-editor
|
||||
artifacts:
|
||||
expire_in: 1 day
|
||||
when: always
|
||||
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
|
||||
paths:
|
||||
- bridge_*.tgz
|
||||
- vault-editor
|
||||
|
||||
build-linux:
|
||||
extends:
|
||||
- .script-build
|
||||
- .env-linux-build
|
||||
|
||||
build-linux-qa:
|
||||
extends:
|
||||
- build-linux
|
||||
- .rules-branch-manual-MR-and-devel-always
|
||||
variables:
|
||||
BUILD_TAGS: "build_qa"
|
||||
|
||||
build-darwin:
|
||||
extends:
|
||||
- .script-build
|
||||
- .env-darwin
|
||||
|
||||
build-darwin-qa:
|
||||
extends:
|
||||
- build-darwin
|
||||
variables:
|
||||
BUILD_TAGS: "build_qa"
|
||||
|
||||
build-windows:
|
||||
extends:
|
||||
- .script-build
|
||||
- .env-windows
|
||||
|
||||
build-windows-qa:
|
||||
extends:
|
||||
- build-windows
|
||||
variables:
|
||||
BUILD_TAGS: "build_qa"
|
||||
|
||||
trigger-qa-installer:
|
||||
stage: build
|
||||
needs: ["lint"]
|
||||
extends:
|
||||
- .rules-br-tag-always-branch-and-MR-manual
|
||||
variables:
|
||||
APP: bridge
|
||||
WORKFLOW: build-all
|
||||
SRC_TAG: $CI_COMMIT_BRANCH
|
||||
TAG: $CI_COMMIT_TAG
|
||||
SRC_HASH: $CI_COMMIT_SHA
|
||||
trigger:
|
||||
project: "jcuth/bridge-release"
|
||||
branch: master
|
||||
|
||||
62
ci/env.yml
Normal file
62
ci/env.yml
Normal file
@ -0,0 +1,62 @@
|
||||
|
||||
---
|
||||
|
||||
.env-windows:
|
||||
before_script:
|
||||
- export BRIDGE_SYNC_FORCE_MINIMUM_SPEC=1
|
||||
- export GOROOT=/c/Go1.20/
|
||||
- export PATH=$GOROOT/bin:$PATH
|
||||
- export GOARCH=amd64
|
||||
- export GOPATH=~/go1.20
|
||||
- export GO111MODULE=on
|
||||
- export PATH="${GOPATH}/bin:${PATH}"
|
||||
- export MSYSTEM=
|
||||
- export QT6DIR=/c/grrrQt/6.4.3/msvc2019_64
|
||||
- export PATH=$PATH:${QT6DIR}/bin
|
||||
- export PATH="/c/Program Files/Microsoft Visual Studio/2022/Community/Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/bin:$PATH"
|
||||
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
|
||||
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
|
||||
- git config --global safe.directory '*'
|
||||
- git status --porcelain
|
||||
cache: {}
|
||||
tags:
|
||||
- windows-bridge
|
||||
|
||||
.env-darwin:
|
||||
before_script:
|
||||
- export BRIDGE_SYNC_FORCE_MINIMUM_SPEC=1
|
||||
- export PATH=/usr/local/bin:$PATH
|
||||
- export PATH=/usr/local/opt/git/bin:$PATH
|
||||
- export PATH=/usr/local/opt/make/libexec/gnubin:$PATH
|
||||
- export PATH=/usr/local/opt/gnu-sed/libexec/gnubin:$PATH
|
||||
- export GOROOT=~/local/opt/go@1.20
|
||||
- export PATH="${GOROOT}/bin:$PATH"
|
||||
- export GOPATH=~/go1.20
|
||||
- export PATH="${GOPATH}/bin:$PATH"
|
||||
- export QT6DIR=/opt/Qt/6.4.3/macos
|
||||
- export PATH="${QT6DIR}/bin:$PATH"
|
||||
- uname -a
|
||||
cache: {}
|
||||
tags:
|
||||
- macos-m1-bridge
|
||||
|
||||
.env-linux-build:
|
||||
image: gitlab.protontech.ch:4567/go/bridge-internal:build-go1.20-qt6.4.3
|
||||
variables:
|
||||
VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.cache
|
||||
cache:
|
||||
key: linux-vcpkg
|
||||
paths:
|
||||
- .cache
|
||||
when: 'always'
|
||||
before_script:
|
||||
- mkdir -p .cache/bin
|
||||
- export BRIDGE_SYNC_FORCE_MINIMUM_SPEC=1
|
||||
- export PATH=$(pwd)/.cache/bin:$PATH
|
||||
- export GOPATH="$CI_PROJECT_DIR/.cache"
|
||||
- export PATH=$PATH:$QT6DIR/bin
|
||||
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
|
||||
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
|
||||
tags:
|
||||
- shared-large
|
||||
|
||||
58
ci/rules.yml
Normal file
58
ci/rules.yml
Normal file
@ -0,0 +1,58 @@
|
||||
|
||||
---
|
||||
|
||||
.rules-branch-and-MR-manual:
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH || $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
when: manual
|
||||
allow_failure: true
|
||||
- when: never
|
||||
|
||||
.rules-branch-manual-MR-and-devel-always:
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == "devel" || $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
when: always
|
||||
allow_failure: false
|
||||
- if: $CI_COMMIT_BRANCH
|
||||
when: manual
|
||||
allow_failure: true
|
||||
- when: never
|
||||
|
||||
.rules-branch-manual-br-tag-and-MR-and-devel-always:
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == "devel" || $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
when: always
|
||||
allow_failure: false
|
||||
- if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_TAG =~ /^br-\d+/
|
||||
when: always
|
||||
allow_failure: false
|
||||
- if: $CI_COMMIT_BRANCH
|
||||
when: manual
|
||||
allow_failure: true
|
||||
- when: never
|
||||
|
||||
.rules-branch-manual-scheduled-and-test-branch-always:
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "schedule"
|
||||
when: always
|
||||
allow_failure: false
|
||||
- if: $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME=~ /^test/
|
||||
when: always
|
||||
allow_failure: false
|
||||
- if: $CI_COMMIT_BRANCH
|
||||
when: manual
|
||||
allow_failure: true
|
||||
- when: never
|
||||
|
||||
.rules-br-tag-always-branch-and-MR-manual:
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == 'push' && $CI_COMMIT_BRANCH
|
||||
when: manual
|
||||
allow_failure: true
|
||||
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
|
||||
when: manual
|
||||
allow_failure: true
|
||||
- if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_TAG =~ /^br-\d+/
|
||||
when: always
|
||||
- when: never
|
||||
|
||||
109
ci/test.yml
Normal file
109
ci/test.yml
Normal file
@ -0,0 +1,109 @@
|
||||
|
||||
---
|
||||
|
||||
lint:
|
||||
stage: test
|
||||
extends:
|
||||
- .rules-branch-manual-br-tag-and-MR-and-devel-always
|
||||
script:
|
||||
- make lint
|
||||
tags:
|
||||
- shared-medium
|
||||
|
||||
bug-report-preview:
|
||||
stage: test
|
||||
extends:
|
||||
- .rules-branch-and-MR-manual
|
||||
script:
|
||||
- make lint-bug-report-preview
|
||||
tags:
|
||||
- shared-medium
|
||||
|
||||
.script-test:
|
||||
stage: test
|
||||
extends:
|
||||
- .rules-branch-manual-MR-and-devel-always
|
||||
script:
|
||||
- make test
|
||||
artifacts:
|
||||
paths:
|
||||
- coverage/**
|
||||
|
||||
test-linux:
|
||||
extends:
|
||||
- .script-test
|
||||
tags:
|
||||
- shared-large
|
||||
|
||||
fuzz-linux:
|
||||
stage: test
|
||||
extends:
|
||||
- .rules-branch-manual-MR-and-devel-always
|
||||
script:
|
||||
- make fuzz
|
||||
tags:
|
||||
- shared-large
|
||||
|
||||
test-linux-race:
|
||||
extends:
|
||||
- test-linux
|
||||
- .rules-branch-and-MR-manual
|
||||
script:
|
||||
- make test-race
|
||||
|
||||
test-integration:
|
||||
extends:
|
||||
- test-linux
|
||||
script:
|
||||
- make test-integration
|
||||
|
||||
test-integration-race:
|
||||
extends:
|
||||
- test-integration
|
||||
- .rules-branch-and-MR-manual
|
||||
script:
|
||||
- make test-integration-race
|
||||
|
||||
test-integration-nightly:
|
||||
extends:
|
||||
- test-integration
|
||||
- .rules-branch-manual-scheduled-and-test-branch-always
|
||||
needs:
|
||||
- test-integration
|
||||
script:
|
||||
- make test-integration-nightly
|
||||
|
||||
test-windows:
|
||||
extends:
|
||||
- .env-windows
|
||||
- .script-test
|
||||
|
||||
test-darwin:
|
||||
extends:
|
||||
- .env-darwin
|
||||
- .script-test
|
||||
|
||||
test-coverage:
|
||||
stage: test
|
||||
extends:
|
||||
- .rules-branch-manual-scheduled-and-test-branch-always
|
||||
script:
|
||||
- ./utils/coverage.sh
|
||||
coverage: '/total:.*\(statements\).*\d+\.\d+%/'
|
||||
needs:
|
||||
- test-linux
|
||||
- test-windows
|
||||
- test-darwin
|
||||
- test-integration
|
||||
- test-integration-nightly
|
||||
tags:
|
||||
- shared-small
|
||||
artifacts:
|
||||
paths:
|
||||
- coverage*
|
||||
- coverage/**
|
||||
when: 'always'
|
||||
reports:
|
||||
coverage_report:
|
||||
coverage_format: cobertura
|
||||
path: coverage.xml
|
||||
7
go.mod
7
go.mod
@ -7,7 +7,7 @@ require (
|
||||
github.com/Masterminds/semver/v3 v3.2.0
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20231114153341-2ecbdd2739f7
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20231116144214-8a47c8d92fbc
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20231130083229-e8aa47d7a366
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton
|
||||
github.com/PuerkitoBio/goquery v1.8.1
|
||||
github.com/abiosoft/ishell v2.0.0+incompatible
|
||||
@ -16,7 +16,7 @@ require (
|
||||
github.com/cucumber/godog v0.12.5
|
||||
github.com/cucumber/messages-go/v16 v16.0.1
|
||||
github.com/docker/docker-credential-helpers v0.6.3
|
||||
github.com/elastic/go-sysinfo v1.8.1
|
||||
github.com/elastic/go-sysinfo v1.11.2-0.20231129083954-35e55cd2a542
|
||||
github.com/emersion/go-imap v1.2.1
|
||||
github.com/emersion/go-imap-id v0.0.0-20190926060100-f94a56b9ecde
|
||||
github.com/emersion/go-message v0.16.0
|
||||
@ -32,6 +32,7 @@ require (
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba
|
||||
github.com/jeandeaual/go-locale v0.0.0-20220711133428-7de61946b173
|
||||
github.com/keybase/go-keychain v0.0.0
|
||||
github.com/miekg/dns v1.1.50
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58
|
||||
@ -122,6 +123,6 @@ replace (
|
||||
github.com/docker/docker-credential-helpers => github.com/ProtonMail/docker-credential-helpers v1.1.0
|
||||
github.com/emersion/go-message => github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7
|
||||
github.com/emersion/go-smtp => github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865
|
||||
github.com/go-resty/resty/v2 => github.com/LBeernaertProton/resty/v2 v2.0.0-20231030122409-92db8bee3605
|
||||
github.com/go-resty/resty/v2 => github.com/LBeernaertProton/resty/v2 v2.0.0-20231129100320-dddf8030d93a
|
||||
github.com/keybase/go-keychain => github.com/cuthix/go-keychain v0.0.0-20230517073537-fc1740a83768
|
||||
)
|
||||
|
||||
55
go.sum
55
go.sum
@ -11,14 +11,17 @@ cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqCl
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
fyne.io/fyne v1.4.2/go.mod h1:xL4c3WmpE/Tvz5CEm5vqsaizU/EeOCm9DYlL2GtTSiM=
|
||||
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557 h1:l6surSnJ3RP4qA1qmKJ+hQn3UjytosdoG27WGjrDlVs=
|
||||
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557/go.mod h1:sTrmvD/TxuypdOERsDOS7SndZg0rzzcCi1b6wQMXUYM=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/LBeernaertProton/resty/v2 v2.0.0-20231030122409-92db8bee3605 h1:54Fh3JS6s2Tjy6ZIRLtt1amZOqfYDcjErdye45z8fkQ=
|
||||
github.com/LBeernaertProton/resty/v2 v2.0.0-20231030122409-92db8bee3605/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A=
|
||||
github.com/Kodeworks/golang-image-ico v0.0.0-20141118225523-73f0f4cfade9/go.mod h1:7uhhqiBaR4CpN0k9rMjOtjpcfGd6DG2m04zQxKnWQ0I=
|
||||
github.com/LBeernaertProton/resty/v2 v2.0.0-20231129100320-dddf8030d93a h1:eQO/GF/+H8/9udc9QAgieFr+jr1tjXlJo35RAhsUbWY=
|
||||
github.com/LBeernaertProton/resty/v2 v2.0.0-20231129100320-dddf8030d93a/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A=
|
||||
github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
|
||||
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||
github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
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=
|
||||
@ -36,8 +39,8 @@ github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7 h1:+j+Kd/
|
||||
github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20231116144214-8a47c8d92fbc h1:GBRKoFAldApEMkMrsFN1ZxG0eG797w6LTv/dFMDcsqQ=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20231116144214-8a47c8d92fbc/go.mod h1:WEXJqj5DSc2YI77SgXdpMY0nk33Qy92Vu2r4tOEazA8=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20231130083229-e8aa47d7a366 h1:W9P5GdDnuGkB3tbzKnXmUrTjIs6zk/K+4lpPTWzsoRE=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20231130083229-e8aa47d7a366/go.mod h1:t+hb0BfkmZ9fpvzVRpHC7limoowym6ln/j0XL9a8DDw=
|
||||
github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865 h1:EP1gnxLL5Z7xBSymE9nSTM27nRYINuvssAtDmG0suD8=
|
||||
github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
|
||||
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
|
||||
@ -50,6 +53,7 @@ github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma
|
||||
github.com/abiosoft/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzgHf/aS6+zVXRj14cVk9qg=
|
||||
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8=
|
||||
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db/go.mod h1:rB3B4rKii8V21ydCbIzH5hZiCQE7f5E9SzUb/ZZx530=
|
||||
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37 h1:28uU3TtuvQ6KRndxg9TrC868jBWmSKgh0GTXkACCXmA=
|
||||
@ -113,8 +117,12 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/elastic/go-sysinfo v1.8.1 h1:4Yhj+HdV6WjbCRgGdZpPJ8lZQlXZLKDAeIkmQ/VRvi4=
|
||||
github.com/elastic/go-sysinfo v1.8.1/go.mod h1:JfllUnzoQV/JRYymbH3dO1yggI3mV2oTKSXsDHM+uIM=
|
||||
github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
|
||||
github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM=
|
||||
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/elastic/go-sysinfo v1.11.2-0.20231129083954-35e55cd2a542 h1:IFTm6NBbfSgZCaeEzorQhH4T7ZERl4j+1u7oXWzmJcM=
|
||||
github.com/elastic/go-sysinfo v1.11.2-0.20231129083954-35e55cd2a542/go.mod h1:GKqR8bbMK/1ITnez9NIsIfXQr25aLhRJa7AfT8HpBFQ=
|
||||
github.com/elastic/go-windows v1.0.1 h1:AlYZOldA+UJ0/2nBuqWdo90GFCgG9xuyw9SYzGUtJm0=
|
||||
github.com/elastic/go-windows v1.0.1/go.mod h1:FoVvqWSun28vaDQPbj2Elfc0JahhPB7WQEGa3c814Ss=
|
||||
github.com/emersion/go-imap v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA=
|
||||
@ -136,6 +144,9 @@ github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNu
|
||||
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BMXYYRWTLOJKlh+lOBt6nUQgXAfB7oVIQt5cNreqSLI=
|
||||
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:rZfgFAXFS/z/lEd6LJmf9HVZ1LkgYiHx5pHhV5DR16M=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/fyne-io/mobile v0.1.2-0.20201127155338-06aeb98410cc/go.mod h1:/kOrWrZB6sasLbEy2JIvr4arEzQTXBTZGb3Y96yWbHY=
|
||||
github.com/fyne-io/mobile v0.1.2/go.mod h1:/kOrWrZB6sasLbEy2JIvr4arEzQTXBTZGb3Y96yWbHY=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||
github.com/getsentry/sentry-go v0.15.0 h1:CP9bmA7pralrVUedYZsmIHWpq/pBtXTSew7xvVpfLaA=
|
||||
@ -146,7 +157,9 @@ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm
|
||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||
github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7/go.mod h1:482civXOzJJCPzJ4ZOX/pwvXBWSnzD4OKMdH4ClKGbk=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200625191551-73d3c3675aa3/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
@ -162,11 +175,14 @@ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4=
|
||||
github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
|
||||
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gofrs/uuid v4.3.0+incompatible h1:CaSVZxm5B+7o45rtab4jC2G37WGYX1zQfuU2i6DSvnc=
|
||||
github.com/gofrs/uuid v4.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff/go.mod h1:wfqRWLHRBsRgkp5dmbG56SA0DmVtwrF5N3oPdI8t+Aw=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
@ -240,12 +256,16 @@ github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2p
|
||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jackmordaunt/icns v0.0.0-20181231085925-4f16af745526/go.mod h1:UQkeMHVoNcyXYq9otUupF7/h/2tmHlhrS2zw7ZVvUqc=
|
||||
github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba h1:QFQpJdgbON7I0jr2hYW7Bs+XV0qjc3d5tZoDnRFnqTg=
|
||||
github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
|
||||
github.com/jeandeaual/go-locale v0.0.0-20220711133428-7de61946b173 h1:jOONCXyzHWM+ukp+weX77o//U3pMeOj62CNxChJLxIU=
|
||||
github.com/jeandeaual/go-locale v0.0.0-20220711133428-7de61946b173/go.mod h1:uO/uctjf8AcWhNfp5Ili6oPtyFrAoQXEtVY3N798VkQ=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 h1:rp+c0RAYOWj8l6qbCUTSiRLG/iKnW3K3/QfPPuSsBt4=
|
||||
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/josephspurrier/goversioninfo v0.0.0-20200309025242-14b0ab84c6ca/go.mod h1:eJTEwMjXb7kZ633hO3Ln9mBUCOjX2+FlTljvpl9SYdE=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
@ -268,6 +288,7 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
github.com/lucor/goinfo v0.0.0-20200401173949-526b5363a13a/go.mod h1:ORP3/rB5IsulLEBwQZCJyyV6niqmI7P4EWSmkug+1Ng=
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
@ -303,9 +324,14 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.1.1/go.mod h1:d++QJC9ZVf7pa48qrsRWhMJ5pSHIPmS3OLqK1niyLxs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
|
||||
@ -364,6 +390,8 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
|
||||
github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564/go.mod h1:afMbS0qvv1m5tfENCwnOdZGOF8RGR/FsZ7bvBxQGZG4=
|
||||
github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9/go.mod h1:mvWM0+15UqyrFKqdRjY6LuAVJR0HOVhJlEgZ5JWtSWU=
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
@ -397,6 +425,7 @@ github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a h1:DxppxFKRqJ8WD6oJ3+ZXKDY0iMONQDl5UTg2aTyHh8k=
|
||||
@ -432,6 +461,7 @@ golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERs
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
@ -443,6 +473,7 @@ golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
|
||||
@ -461,6 +492,8 @@ golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
@ -482,6 +515,7 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@ -501,8 +535,11 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200720211630-cb9d2d5c5666/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@ -511,6 +548,7 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@ -536,6 +574,7 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
@ -562,11 +601,13 @@ golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBn
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190808195139-e713427fea3f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200328031815-3db5fc6bac03/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
@ -608,6 +649,7 @@ google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw
|
||||
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 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
@ -619,6 +661,7 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
@ -42,7 +42,7 @@ func TestMigratePrefsToVaultWithKeys(t *testing.T) {
|
||||
// Create a new vault.
|
||||
vault, corrupt, err := vault.New(t.TempDir(), t.TempDir(), []byte("my secret key"), async.NoopPanicHandler{})
|
||||
require.NoError(t, err)
|
||||
require.False(t, corrupt)
|
||||
require.NoError(t, corrupt)
|
||||
|
||||
// load the old prefs file.
|
||||
configDir := filepath.Join("testdata", "with_keys")
|
||||
@ -63,7 +63,7 @@ func TestMigratePrefsToVaultWithoutKeys(t *testing.T) {
|
||||
// Create a new vault.
|
||||
vault, corrupt, err := vault.New(t.TempDir(), t.TempDir(), []byte("my secret key"), async.NoopPanicHandler{})
|
||||
require.NoError(t, err)
|
||||
require.False(t, corrupt)
|
||||
require.NoError(t, corrupt)
|
||||
|
||||
// load the old prefs file.
|
||||
configDir := filepath.Join("testdata", "without_keys")
|
||||
@ -173,7 +173,7 @@ func TestUserMigration(t *testing.T) {
|
||||
|
||||
v, corrupt, err := vault.New(settingsFolder, settingsFolder, token, async.NoopPanicHandler{})
|
||||
require.NoError(t, err)
|
||||
require.False(t, corrupt)
|
||||
require.NoError(t, corrupt)
|
||||
|
||||
require.NoError(t, migrateOldAccounts(locations, kcl, v))
|
||||
require.Equal(t, []string{wantCredentials.UserID}, v.GetUserIDs())
|
||||
|
||||
@ -42,21 +42,25 @@ func WithVault(locations *locations.Locations, keychains *keychain.List, panicHa
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"insecure": insecure,
|
||||
"corrupt": corrupt,
|
||||
"corrupt": corrupt != nil,
|
||||
}).Debug("Vault created")
|
||||
|
||||
if corrupt != nil {
|
||||
logrus.WithError(corrupt).Warn("Failed to load existing vault, vault has been reset")
|
||||
}
|
||||
|
||||
cert, _ := encVault.GetBridgeTLSCert()
|
||||
certs.NewInstaller().LogCertInstallStatus(cert)
|
||||
|
||||
// GODT-1950: Add teardown actions (e.g. to close the vault).
|
||||
|
||||
return fn(encVault, insecure, corrupt)
|
||||
return fn(encVault, insecure, corrupt != nil)
|
||||
}
|
||||
|
||||
func newVault(locations *locations.Locations, keychains *keychain.List, panicHandler async.PanicHandler) (*vault.Vault, bool, bool, error) {
|
||||
func newVault(locations *locations.Locations, keychains *keychain.List, panicHandler async.PanicHandler) (*vault.Vault, bool, error, error) {
|
||||
vaultDir, err := locations.ProvideSettingsPath()
|
||||
if err != nil {
|
||||
return nil, false, false, fmt.Errorf("could not get vault dir: %w", err)
|
||||
return nil, false, nil, fmt.Errorf("could not get vault dir: %w", err)
|
||||
}
|
||||
|
||||
logrus.WithField("vaultDir", vaultDir).Debug("Loading vault from directory")
|
||||
@ -78,12 +82,12 @@ func newVault(locations *locations.Locations, keychains *keychain.List, panicHan
|
||||
|
||||
gluonCacheDir, err := locations.ProvideGluonCachePath()
|
||||
if err != nil {
|
||||
return nil, false, false, fmt.Errorf("could not provide gluon path: %w", err)
|
||||
return nil, false, nil, fmt.Errorf("could not provide gluon path: %w", err)
|
||||
}
|
||||
|
||||
vault, corrupt, err := vault.New(vaultDir, gluonCacheDir, vaultKey, panicHandler)
|
||||
if err != nil {
|
||||
return nil, false, false, fmt.Errorf("could not create vault: %w", err)
|
||||
return nil, false, corrupt, fmt.Errorf("could not create vault: %w", err)
|
||||
}
|
||||
|
||||
return vault, insecure, corrupt, nil
|
||||
|
||||
@ -307,7 +307,7 @@ func newBridge(
|
||||
bridge.heartbeat.init(bridge, heartbeatManager)
|
||||
}
|
||||
|
||||
bridge.syncService.Run(bridge.tasks)
|
||||
bridge.syncService.Run()
|
||||
|
||||
return bridge, nil
|
||||
}
|
||||
@ -451,6 +451,8 @@ func (bridge *Bridge) Close(ctx context.Context) {
|
||||
logrus.WithError(err).Error("Failed to close servers")
|
||||
}
|
||||
|
||||
bridge.syncService.Close()
|
||||
|
||||
// Stop all ongoing tasks.
|
||||
bridge.tasks.CancelAndWait()
|
||||
|
||||
|
||||
@ -37,10 +37,10 @@ func (bridge *Bridge) AutoconfigUsed(client string) {
|
||||
}, bridge.usersLock)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) KBArticleOpened(article string) {
|
||||
func (bridge *Bridge) ExternalLinkClicked(article string) {
|
||||
safe.RLock(func() {
|
||||
for _, user := range bridge.users {
|
||||
user.KBArticleOpened(article)
|
||||
user.ExternalLinkClicked(article)
|
||||
}
|
||||
}, bridge.usersLock)
|
||||
}
|
||||
|
||||
@ -641,6 +641,55 @@ func TestBridge_CorruptedVaultClearsPreviousIMAPSyncState(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_AddressOrderChangeDuringSyncInCombinedModeDoesNotTriggerBadEventOnNewMessage(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
// Create a user.
|
||||
userID, addrID, err := s.CreateUser("user", password)
|
||||
require.NoError(t, err)
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
userInfoChanged, done := chToType[events.Event, events.UserChanged](bridge.GetEvents(events.UserChanged{}))
|
||||
defer done()
|
||||
|
||||
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||
createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 300)
|
||||
})
|
||||
|
||||
_, err := bridge.LoginFull(ctx, "user", password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
info, err := bridge.GetUserInfo(userID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(info.Addresses))
|
||||
require.Equal(t, info.Addresses[0], "user@proton.local")
|
||||
|
||||
addrID2, err := s.CreateAddress(userID, "foo@"+s.GetDomain(), password)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, s.SetAddressOrder(userID, []string{addrID2, addrID}))
|
||||
|
||||
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||
createNumMessages(ctx, t, c, addrID2, proton.InboxLabel, 1)
|
||||
})
|
||||
|
||||
// Since we can't intercept events at this time, we sleep for a bit to make sure the
|
||||
// new message does not get combined into the event below. This ensures the newly created
|
||||
// goes through the full code flow which triggered the original bad event.
|
||||
time.Sleep(time.Second)
|
||||
require.NoError(t, s.SetAddressOrder(userID, []string{addrID, addrID2}))
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case e := <-userInfoChanged:
|
||||
require.Equal(t, userID, e.UserID)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func withClient(ctx context.Context, t *testing.T, s *server.Server, username string, password []byte, fn func(context.Context, *proton.Client)) { //nolint:unparam
|
||||
m := proton.New(
|
||||
proton.WithHostURL(s.GetHostURL()),
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.28.0
|
||||
// protoc-gen-go v1.31.0
|
||||
// protoc v3.21.12
|
||||
// source: focus.proto
|
||||
|
||||
|
||||
@ -1,6 +1,23 @@
|
||||
// Copyright (c) 2022 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
// ProtonMail 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.
|
||||
//
|
||||
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.2.0
|
||||
// - protoc-gen-go-grpc v1.3.0
|
||||
// - protoc v3.21.12
|
||||
// source: focus.proto
|
||||
|
||||
@ -20,6 +37,11 @@ import (
|
||||
// Requires gRPC-Go v1.32.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion7
|
||||
|
||||
const (
|
||||
Focus_Raise_FullMethodName = "/focus.Focus/Raise"
|
||||
Focus_Version_FullMethodName = "/focus.Focus/Version"
|
||||
)
|
||||
|
||||
// FocusClient is the client API for Focus service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
@ -38,7 +60,7 @@ func NewFocusClient(cc grpc.ClientConnInterface) FocusClient {
|
||||
|
||||
func (c *focusClient) Raise(ctx context.Context, in *wrapperspb.StringValue, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||
out := new(emptypb.Empty)
|
||||
err := c.cc.Invoke(ctx, "/focus.Focus/Raise", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Focus_Raise_FullMethodName, in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -47,7 +69,7 @@ func (c *focusClient) Raise(ctx context.Context, in *wrapperspb.StringValue, opt
|
||||
|
||||
func (c *focusClient) Version(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*VersionResponse, error) {
|
||||
out := new(VersionResponse)
|
||||
err := c.cc.Invoke(ctx, "/focus.Focus/Version", in, out, opts...)
|
||||
err := c.cc.Invoke(ctx, Focus_Version_FullMethodName, in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -96,7 +118,7 @@ func _Focus_Raise_Handler(srv interface{}, ctx context.Context, dec func(interfa
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/focus.Focus/Raise",
|
||||
FullMethod: Focus_Raise_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(FocusServer).Raise(ctx, req.(*wrapperspb.StringValue))
|
||||
@ -114,7 +136,7 @@ func _Focus_Version_Handler(srv interface{}, ctx context.Context, dec func(inter
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/focus.Focus/Version",
|
||||
FullMethod: Focus_Version_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(FocusServer).Version(ctx, req.(*emptypb.Empty))
|
||||
|
||||
@ -364,7 +364,19 @@ Status GRPCService::ReportBug(ServerContext *, ReportBugRequest const *request,
|
||||
qtProxy_.reportBug(QString::fromStdString(request->ostype()), QString::fromStdString(request->osversion()),
|
||||
QString::fromStdString(request->emailclient()), QString::fromStdString(request->address()), QString::fromStdString(request->description()),
|
||||
request->includelogs());
|
||||
qtProxy_.sendDelayedEvent(tab.nextBugReportWillSucceed() ? newReportBugSuccessEvent() : newReportBugErrorEvent());
|
||||
SPStreamEvent event;
|
||||
switch (tab.nextBugReportResult()) {
|
||||
case SettingsTab::BugReportResult::Success:
|
||||
event = newReportBugSuccessEvent();
|
||||
break;
|
||||
case SettingsTab::BugReportResult::Error:
|
||||
event = newReportBugErrorEvent();
|
||||
break;
|
||||
case SettingsTab::BugReportResult::DataSharingError:
|
||||
event = newReportBugFallbackEvent();
|
||||
break;
|
||||
}
|
||||
qtProxy_.sendDelayedEvent(event);
|
||||
qtProxy_.sendDelayedEvent(newReportBugFinishedEvent());
|
||||
|
||||
return Status::OK;
|
||||
@ -535,7 +547,7 @@ Status GRPCService::SetDiskCachePath(ServerContext *, StringValue const *path, E
|
||||
|
||||
// we mimic the behaviour of Bridge
|
||||
if (!tab.nextCacheChangeWillSucceed()) {
|
||||
qtProxy_.sendDelayedEvent(newDiskCacheErrorEvent(grpc::DiskCacheErrorType(tab.cacheError())));
|
||||
qtProxy_.sendDelayedEvent(newDiskCacheErrorEvent(grpc::DiskCacheErrorType(CANT_MOVE_DISK_CACHE_ERROR)));
|
||||
} else {
|
||||
qtProxy_.setDiskCachePath(qPath);
|
||||
qtProxy_.sendDelayedEvent(newDiskCachePathChangedEvent(qPath));
|
||||
@ -786,7 +798,7 @@ Status GRPCService::InstallTLSCertificate(ServerContext *, Empty const *, Empty
|
||||
app().log().debug(__FUNCTION__);
|
||||
SPStreamEvent event;
|
||||
qtProxy_.installTLSCertificate();
|
||||
switch (app().mainWindow().settingsTab().nextTLSCertIntallResult()) {
|
||||
switch (app().mainWindow().settingsTab().nextTLSCertInstallResult()) {
|
||||
case SettingsTab::TLSCertInstallResult::Success:
|
||||
event = newCertificateInstallSuccessEvent();
|
||||
break;
|
||||
@ -805,7 +817,7 @@ Status GRPCService::InstallTLSCertificate(ServerContext *, Empty const *, Empty
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] request The request.
|
||||
//****************************************************************************************************************************************************
|
||||
Status GRPCService::KBArticleClicked(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) {
|
||||
Status GRPCService::ExternalLinkClicked(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) {
|
||||
app().log().debug(QString("%1 - URL = %2").arg(__FUNCTION__, QString::fromStdString(request->value())));
|
||||
return Status::OK;
|
||||
}
|
||||
|
||||
@ -98,7 +98,7 @@ public: // member functions.
|
||||
grpc::Status ExportTLSCertificates(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
|
||||
grpc::Status ReportBugClicked(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::Empty *) override;
|
||||
grpc::Status AutoconfigClicked(::grpc::ServerContext *context, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
|
||||
grpc::Status KBArticleClicked(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
|
||||
grpc::Status ExternalLinkClicked(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
|
||||
grpc::Status RunEventStream(::grpc::ServerContext *ctx, ::grpc::EventStreamRequest const *request, ::grpc::ServerWriter<::grpc::StreamEvent> *writer) override;
|
||||
grpc::Status StopEventStream(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::Empty *) override;
|
||||
bool sendEvent(bridgepp::SPStreamEvent const &event); ///< Queue an event for sending through the event stream.
|
||||
|
||||
@ -31,6 +31,18 @@ QString const colorSchemeLight = "light"; ///< THe light color scheme name.
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \brief Connect an address error button to the generation of an address error event.
|
||||
///
|
||||
/// \param[in] button The error button.
|
||||
/// \param[in] edit The edit containing the address.
|
||||
/// \param[in] eventGenerator The factory function creating the event.
|
||||
//****************************************************************************************************************************************************
|
||||
void connectAddressError(QPushButton *button, QLineEdit* edit, bridgepp::SPStreamEvent (*eventGenerator)(QString const &)) {
|
||||
QObject::connect(button, &QPushButton::clicked, [edit, eventGenerator]() { app().grpc().sendEvent(eventGenerator(edit->text())); });
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] parent The parent widget of the tab.
|
||||
//****************************************************************************************************************************************************
|
||||
@ -41,20 +53,26 @@ SettingsTab::SettingsTab(QWidget *parent)
|
||||
connect(ui_.buttonInternetOn, &QPushButton::clicked, []() { app().grpc().sendEvent(newInternetStatusEvent(true)); });
|
||||
connect(ui_.buttonInternetOff, &QPushButton::clicked, []() { app().grpc().sendEvent(newInternetStatusEvent(false)); });
|
||||
connect(ui_.buttonShowMainWindow, &QPushButton::clicked, []() { app().grpc().sendEvent(newShowMainWindowEvent()); });
|
||||
connect(ui_.buttonNoKeychain, &QPushButton::clicked, []() { app().grpc().sendEvent(newHasNoKeychainEvent()); });
|
||||
connect(ui_.buttonAPICertIssue, &QPushButton::clicked, []() { app().grpc().sendEvent(newApiCertIssueEvent()); });
|
||||
connect(ui_.buttonDiskCacheUnavailable, &QPushButton::clicked, []() {
|
||||
app().grpc().sendEvent(
|
||||
newDiskCacheErrorEvent(grpc::DiskCacheErrorType::DISK_CACHE_UNAVAILABLE_ERROR));
|
||||
});
|
||||
connect(ui_.buttonDiskFull, &QPushButton::clicked, []() {
|
||||
app().grpc().sendEvent(
|
||||
newDiskCacheErrorEvent(grpc::DiskCacheErrorType::DISK_FULL_ERROR));
|
||||
});
|
||||
connect(ui_.buttonNoActiveKeyForRecipient, &QPushButton::clicked, [&]() {
|
||||
app().grpc().sendEvent(
|
||||
newNoActiveKeyForRecipientEvent(ui_.editNoActiveKeyForRecipient->text()));
|
||||
});
|
||||
connectAddressError(ui_.buttonAddressChanged, ui_.editAddressErrors, newAddressChangedEvent);
|
||||
connectAddressError(ui_.buttonAddressChangedLogout, ui_.editAddressErrors, newAddressChangedLogoutEvent);
|
||||
connect(ui_.checkNextCacheChangeWillSucceed, &QCheckBox::toggled, this, &SettingsTab::updateGUIState);
|
||||
connect(ui_.buttonUpdateError, &QPushButton::clicked, [&]() {
|
||||
app().grpc().sendEvent(newUpdateErrorEvent(static_cast<grpc::UpdateErrorType>(ui_.comboUpdateError->currentIndex())));
|
||||
});
|
||||
connect(ui_.buttonUpdateManualReady, &QPushButton::clicked, [&] {
|
||||
app().grpc().sendEvent(newUpdateManualReadyEvent(ui_.editUpdateVersion->text()));
|
||||
});
|
||||
connect(ui_.buttonUpdateForce, &QPushButton::clicked, [&] {
|
||||
app().grpc().sendEvent(newUpdateForceEvent(ui_.editUpdateVersion->text()));
|
||||
});
|
||||
connect(ui_.buttonUpdateManualRestart, &QPushButton::clicked, []() { app().grpc().sendEvent(newUpdateManualRestartNeededEvent()); });
|
||||
connect(ui_.buttonUpdateSilentRestart, &QPushButton::clicked, []() { app().grpc().sendEvent(newUpdateSilentRestartNeededEvent()); });
|
||||
connect(ui_.buttonUpdateIsLatest, &QPushButton::clicked, []() { app().grpc().sendEvent(newUpdateIsLatestVersionEvent()); });
|
||||
connect(ui_.buttonUpdateCheckFinished, &QPushButton::clicked, []() { app().grpc().sendEvent(newUpdateCheckFinishedEvent()); });
|
||||
connect(ui_.buttonUpdateVersionChanged, &QPushButton::clicked, []() { app().grpc().sendEvent(newUpdateVersionChangedEvent()); });
|
||||
|
||||
this->resetUI();
|
||||
this->updateGUIState();
|
||||
}
|
||||
@ -68,7 +86,6 @@ void SettingsTab::updateGUIState() {
|
||||
for (QWidget *widget: { ui_.groupVersion, ui_.groupGeneral, ui_.groupMail, ui_.groupPaths, ui_.groupCache }) {
|
||||
widget->setEnabled(!connected);
|
||||
}
|
||||
ui_.comboCacheError->setEnabled(!ui_.checkNextCacheChangeWillSucceed->isChecked());
|
||||
}
|
||||
|
||||
|
||||
@ -139,7 +156,7 @@ bool SettingsTab::showSplashScreen() const {
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return true iff autosart is on.
|
||||
/// \return true iff autostart is on.
|
||||
//****************************************************************************************************************************************************
|
||||
bool SettingsTab::isAutostartOn() const {
|
||||
return ui_.checkAutostart->isChecked();
|
||||
@ -290,7 +307,7 @@ void SettingsTab::setBugReport(QString const &osType, QString const &osVersion,
|
||||
//****************************************************************************************************************************************************
|
||||
void SettingsTab::installTLSCertificate() {
|
||||
ui_.labelLastTLSCertInstall->setText(QString("Last install: %1").arg(QDateTime::currentDateTime().toString(Qt::ISODateWithMs)));
|
||||
ui_.checkTLSCertIsInstalled->setChecked(this->nextTLSCertIntallResult() == TLSCertInstallResult::Success);
|
||||
ui_.checkTLSCertIsInstalled->setChecked(this->nextTLSCertInstallResult() == TLSCertInstallResult::Success);
|
||||
}
|
||||
|
||||
|
||||
@ -298,17 +315,15 @@ void SettingsTab::installTLSCertificate() {
|
||||
/// \param[in] folderPath The folder path.
|
||||
//****************************************************************************************************************************************************
|
||||
void SettingsTab::exportTLSCertificates(QString const &folderPath) {
|
||||
ui_.labeLastTLSCertExport->setText(QString("%1 Export to %2")
|
||||
.arg(QDateTime::currentDateTime().toString(Qt::ISODateWithMs))
|
||||
.arg(folderPath));
|
||||
ui_.labeLastTLSCertExport->setText(QString("%1 Export to %2").arg(QDateTime::currentDateTime().toString(Qt::ISODateWithMs),folderPath));
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The state of the check box.
|
||||
//****************************************************************************************************************************************************
|
||||
bool SettingsTab::nextBugReportWillSucceed() const {
|
||||
return ui_.checkNextBugReportWillSucceed->isChecked();
|
||||
SettingsTab::BugReportResult SettingsTab::nextBugReportResult() const {
|
||||
return BugReportResult(ui_.comboBugReportResult->currentIndex());
|
||||
}
|
||||
|
||||
|
||||
@ -323,7 +338,7 @@ bool SettingsTab::isTLSCertificateInstalled() const {
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The value for the 'Next TLS cert install result'.
|
||||
//****************************************************************************************************************************************************
|
||||
SettingsTab::TLSCertInstallResult SettingsTab::nextTLSCertIntallResult() const {
|
||||
SettingsTab::TLSCertInstallResult SettingsTab::nextTLSCertInstallResult() const {
|
||||
return TLSCertInstallResult(ui_.comboNextTLSCertInstallResult->currentIndex());
|
||||
}
|
||||
|
||||
@ -446,14 +461,6 @@ bool SettingsTab::nextCacheChangeWillSucceed() const {
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The index of the selected cache error.
|
||||
//****************************************************************************************************************************************************
|
||||
qint32 SettingsTab::cacheError() const {
|
||||
return ui_.comboCacheError->currentIndex();
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return the value for the 'Automatic Update' check.
|
||||
//****************************************************************************************************************************************************
|
||||
@ -514,7 +521,7 @@ void SettingsTab::resetUI() {
|
||||
ui_.editAddress->setText(QString());
|
||||
ui_.editDescription->setPlainText(QString());
|
||||
ui_.labelIncludeLogsValue->setText(QString());
|
||||
ui_.checkNextBugReportWillSucceed->setChecked(true);
|
||||
ui_.comboBugReportResult->setCurrentIndex(0);
|
||||
|
||||
ui_.editHostname->setText("localhost");
|
||||
ui_.spinPortIMAP->setValue(1143);
|
||||
@ -527,7 +534,6 @@ void SettingsTab::resetUI() {
|
||||
QDir().mkpath(cacheDir);
|
||||
ui_.editDiskCachePath->setText(QDir::toNativeSeparators(cacheDir));
|
||||
ui_.checkNextCacheChangeWillSucceed->setChecked(true);
|
||||
ui_.comboCacheError->setCurrentIndex(0);
|
||||
|
||||
ui_.checkAutomaticUpdate->setChecked(true);
|
||||
|
||||
|
||||
@ -33,13 +33,19 @@ public: // data types.
|
||||
Success = 0,
|
||||
Canceled = 1,
|
||||
Failure = 2
|
||||
}; ///< Enumberation for the result of a TLS certificate installation.
|
||||
}; ///< Enumeration for the result of a TLS certificate installation.
|
||||
|
||||
enum class BugReportResult {
|
||||
Success = 0,
|
||||
Error = 1,
|
||||
DataSharingError = 2,
|
||||
}; ///< Enumeration for the result of bug report sending
|
||||
|
||||
public: // member functions.
|
||||
explicit SettingsTab(QWidget *parent = nullptr); ///< Default constructor.
|
||||
SettingsTab(SettingsTab const &) = delete; ///< Disabled copy-constructor.
|
||||
SettingsTab(SettingsTab &&) = delete; ///< Disabled assignment copy-constructor.
|
||||
~SettingsTab() = default; ///< Destructor.
|
||||
~SettingsTab() override = default; ///< Destructor.
|
||||
SettingsTab &operator=(SettingsTab const &) = delete; ///< Disabled assignment operator.
|
||||
SettingsTab &operator=(SettingsTab &&) = delete; ///< Disabled move assignment operator.
|
||||
|
||||
@ -60,9 +66,9 @@ public: // member functions.
|
||||
QString releaseNotesPageLink() const; ///< Get the content of the 'Release Notes Page Link' edit.
|
||||
QString dependencyLicenseLink() const; ///< Get the content of the 'Dependency License Link' edit.
|
||||
QString landingPageLink() const; ///< Get the content of the 'Landing Page Link' edit.
|
||||
bool nextBugReportWillSucceed() const; ///< Get the status of the 'Next Bug Report Will Fail' check box.
|
||||
BugReportResult nextBugReportResult() const; ///< Get the value of the 'Next bug report result' combo box.
|
||||
bool isTLSCertificateInstalled() const; ///< Get the status of the 'TLS Certificate is installed' check box.
|
||||
TLSCertInstallResult nextTLSCertIntallResult() const; ///< Get the value of the 'Next TLS Certificate install result' combo box.
|
||||
TLSCertInstallResult nextTLSCertInstallResult() const; ///< Get the value of the 'Next TLS Certificate install result' combo box.
|
||||
bool nextTLSCertExportWillSucceed() const; ///< Get the status of the 'Next TLS Cert export will succeed' check box.
|
||||
bool nextTLSKeyExportWillSucceed() const; ///< Get the status of the 'Next TLS Key export will succeed' check box.
|
||||
QString hostname() const; ///< Get the value of the 'Hostname' edit.
|
||||
@ -74,7 +80,6 @@ public: // member functions.
|
||||
bool isPortFree() const; ///< Get the value for the "Is Port Free" check box.
|
||||
QString diskCachePath() const; ///< Get the value for the 'Disk Cache Path' edit.
|
||||
bool nextCacheChangeWillSucceed() const; ///< Get the value for the 'Next Cache Change will succeed' edit.
|
||||
qint32 cacheError() const; ///< Return the index of the selected cache error.
|
||||
bool isAutomaticUpdateOn() const; ///<Get the value for the 'Automatic Update' check box.
|
||||
|
||||
public slots:
|
||||
@ -99,7 +104,7 @@ private: // member functions.
|
||||
void resetUI(); ///< Reset the widget.
|
||||
|
||||
private: // data members.
|
||||
Ui::SettingsTab ui_; ///< The GUI for the tab
|
||||
Ui::SettingsTab ui_ {}; ///< The GUI for the tab
|
||||
};
|
||||
|
||||
|
||||
|
||||
@ -6,8 +6,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1127</width>
|
||||
<height>808</height>
|
||||
<width>1160</width>
|
||||
<height>777</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
@ -18,6 +18,9 @@
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_5">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="spacing">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupVersion">
|
||||
<property name="minimumSize">
|
||||
@ -103,6 +106,9 @@
|
||||
<string>General Settings</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<property name="verticalSpacing">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<widget class="QCheckBox" name="checkShowOnStartup">
|
||||
<property name="text">
|
||||
@ -186,6 +192,9 @@
|
||||
<string>Mail</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_6">
|
||||
<property name="spacing">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_6" stretch="0,0">
|
||||
<item>
|
||||
@ -287,6 +296,9 @@
|
||||
<string>Paths && Links</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<property name="verticalSpacing">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="labelLogsPath">
|
||||
<property name="text">
|
||||
@ -381,6 +393,9 @@
|
||||
<string>TLS Certficates</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_4" columnstretch="1,1">
|
||||
<property name="verticalSpacing">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<widget class="QCheckBox" name="checkTLSCertIsInstalled">
|
||||
<property name="text">
|
||||
@ -487,6 +502,9 @@
|
||||
<string>Status</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
@ -596,8 +614,8 @@
|
||||
<string>Bug Report</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_3">
|
||||
<item row="0" column="3">
|
||||
<widget class="QLineEdit" name="editOSVersion">
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="editEmailClient">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
@ -610,6 +628,37 @@
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="labelOSType">
|
||||
<property name="text">
|
||||
<string>OS Type</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QLabel" name="labelAddress">
|
||||
<property name="text">
|
||||
<string>Address</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="labelEmailClient">
|
||||
<property name="text">
|
||||
<string>Email Cient</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1" colspan="3">
|
||||
<widget class="QPlainTextEdit" name="editDescription">
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
@ -628,13 +677,6 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="labelOSType">
|
||||
<property name="text">
|
||||
<string>OS Type</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="labelIncludeLogs">
|
||||
<property name="text">
|
||||
@ -642,22 +684,18 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QLabel" name="labelAddress">
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="labelDescription">
|
||||
<property name="text">
|
||||
<string>Address</string>
|
||||
<string>Description</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QLabel" name="labelOSVersion">
|
||||
<property name="text">
|
||||
<string>OS Version</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLineEdit" name="editOSType">
|
||||
<item row="0" column="3">
|
||||
<widget class="QLineEdit" name="editOSVersion">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
@ -682,43 +720,29 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="labelDescription">
|
||||
<property name="text">
|
||||
<string>Description</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="editEmailClient">
|
||||
<item row="0" column="1">
|
||||
<widget class="QLineEdit" name="editOSType">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
<property name="baseSize">
|
||||
<size>
|
||||
<width>250</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="labelEmailClient">
|
||||
<item row="0" column="2">
|
||||
<widget class="QLabel" name="labelOSVersion">
|
||||
<property name="text">
|
||||
<string>Email Cient</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1" colspan="4">
|
||||
<widget class="QPlainTextEdit" name="editDescription">
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
<string>OS Version</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@ -737,6 +761,9 @@
|
||||
<string>Events && Errors</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_8">
|
||||
<property name="spacing">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_11">
|
||||
<item>
|
||||
@ -754,6 +781,9 @@
|
||||
<property name="suffix">
|
||||
<string> ms</string>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>-1</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>3600000</number>
|
||||
</property>
|
||||
@ -761,7 +791,7 @@
|
||||
<number>100</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>1000</number>
|
||||
<number>0</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@ -781,51 +811,141 @@
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_9">
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonInternetOff">
|
||||
<property name="text">
|
||||
<string>Internet Off</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QGridLayout" name="gridLayout_5">
|
||||
<property name="horizontalSpacing">
|
||||
<number>-1</number>
|
||||
</property>
|
||||
<property name="verticalSpacing">
|
||||
<number>8</number>
|
||||
</property>
|
||||
<item row="0" column="1">
|
||||
<widget class="QPushButton" name="buttonInternetOn">
|
||||
<property name="text">
|
||||
<string>Internet On</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QPushButton" name="buttonShowMainWindow">
|
||||
<property name="text">
|
||||
<string>Show Main Window</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QPushButton" name="buttonInternetOff">
|
||||
<property name="text">
|
||||
<string>Internet Off</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QPushButton" name="buttonAPICertIssue">
|
||||
<property name="text">
|
||||
<string>API cert. Issue</string>
|
||||
<string>API Certficate Issue</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QPushButton" name="buttonNoKeychain">
|
||||
<property name="text">
|
||||
<string>No Keychain</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Address related errors</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_9">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_12">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Address</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="editAddressErrors">
|
||||
<property name="text">
|
||||
<string>dummy.user@proton.me</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_15">
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonAddressChanged">
|
||||
<property name="text">
|
||||
<string>Address Changed</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonAddressChangedLogout">
|
||||
<property name="text">
|
||||
<string>Address Changed Logout</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkIsPortFree">
|
||||
<property name="text">
|
||||
<string>Reply true to the next 'Is Port Free' request.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkNextCacheChangeWillSucceed">
|
||||
<property name="text">
|
||||
<string>Next Cache Change will succeed</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_13">
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonDiskCacheUnavailable">
|
||||
<widget class="QLabel" name="labelNextBugReportResult">
|
||||
<property name="text">
|
||||
<string>Disk Cache Unavailable</string>
|
||||
<string>Next bug report result</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonDiskFull">
|
||||
<property name="text">
|
||||
<string>Disk Full</string>
|
||||
</property>
|
||||
<widget class="QComboBox" name="comboBugReportResult">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Success</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Error</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Data sharing error</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
@ -844,52 +964,96 @@
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_12">
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonNoActiveKeyForRecipient">
|
||||
<layout class="QGridLayout" name="gridLayout_6">
|
||||
<property name="verticalSpacing">
|
||||
<number>8</number>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<widget class="QPushButton" name="buttonUpdateManualReady">
|
||||
<property name="text">
|
||||
<string>No Active Key For Recipient</string>
|
||||
<string>Update Manual Ready</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="editNoActiveKeyForRecipient">
|
||||
<item row="0" column="2">
|
||||
<widget class="QLineEdit" name="editUpdateVersion">
|
||||
<property name="text">
|
||||
<string>dummy.user@proton.me</string>
|
||||
<string>4.0</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkIsPortFree">
|
||||
<property name="text">
|
||||
<string>Reply true to the next 'Is Port Free' request.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_8">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkNextCacheChangeWillSucceed">
|
||||
<item row="4" column="0">
|
||||
<widget class="QPushButton" name="buttonUpdateVersionChanged">
|
||||
<property name="text">
|
||||
<string>Next Cache Change will succeed</string>
|
||||
<string>Update version changed</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QPushButton" name="buttonUpdateForce">
|
||||
<property name="text">
|
||||
<string>Update Force</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QPushButton" name="buttonUpdateManualRestart">
|
||||
<property name="text">
|
||||
<string>Update manual restart</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QPushButton" name="buttonUpdateCheckFinished">
|
||||
<property name="text">
|
||||
<string>Update check finished</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QPushButton" name="buttonUpdateSilentRestart">
|
||||
<property name="text">
|
||||
<string>Update silent restart</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="2">
|
||||
<widget class="QComboBox" name="comboUpdateError">
|
||||
<property name="sizeAdjustPolicy">
|
||||
<enum>QComboBox::AdjustToContents</enum>
|
||||
</property>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Update manual error</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Update force error</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Update silent error</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QPushButton" name="buttonUpdateError">
|
||||
<property name="text">
|
||||
<string>Update error</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QPushButton" name="buttonUpdateIsLatest">
|
||||
<property name="text">
|
||||
<string>Update is latest</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1" colspan="2">
|
||||
<spacer name="horizontalSpacer_6">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
@ -902,34 +1066,8 @@
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="comboCacheError">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Disk Cache Unavailable</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Can't Move Disk Cache</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Disk Full</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkNextBugReportWillSucceed">
|
||||
<property name="text">
|
||||
<string>Next Bug Report Will Succeed</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
@ -956,19 +1094,10 @@
|
||||
<tabstop>editDependencyLicenseLink</tabstop>
|
||||
<tabstop>editLandingPageLink</tabstop>
|
||||
<tabstop>editDiskCachePath</tabstop>
|
||||
<tabstop>editOSType</tabstop>
|
||||
<tabstop>editOSVersion</tabstop>
|
||||
<tabstop>editEmailClient</tabstop>
|
||||
<tabstop>editAddress</tabstop>
|
||||
<tabstop>editDescription</tabstop>
|
||||
<tabstop>spinEventDelay</tabstop>
|
||||
<tabstop>buttonInternetOff</tabstop>
|
||||
<tabstop>buttonInternetOn</tabstop>
|
||||
<tabstop>buttonShowMainWindow</tabstop>
|
||||
<tabstop>checkIsPortFree</tabstop>
|
||||
<tabstop>checkNextCacheChangeWillSucceed</tabstop>
|
||||
<tabstop>comboCacheError</tabstop>
|
||||
<tabstop>checkNextBugReportWillSucceed</tabstop>
|
||||
</tabstops>
|
||||
<resources/>
|
||||
<connections/>
|
||||
|
||||
@ -58,7 +58,7 @@ UsersTab::UsersTab(QWidget *parent)
|
||||
connect(ui_.checkSync, &QCheckBox::toggled, this, &UsersTab::onCheckSyncToggled);
|
||||
connect(ui_.sliderSync, &QSlider::valueChanged, this, &UsersTab::onSliderSyncValueChanged);
|
||||
|
||||
users_.append(randomUser());
|
||||
users_.append(defaultUser());
|
||||
|
||||
this->updateGUIState();
|
||||
}
|
||||
|
||||
@ -294,11 +294,11 @@ bool QMLBackend::isTLSCertificateInstalled() {
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] url The URL of the knowledge base article. If empty/invalid, the home page for the Bridge knowledge base is opened.
|
||||
//****************************************************************************************************************************************************
|
||||
void QMLBackend::openKBArticle(QString const &url) {
|
||||
void QMLBackend::openExternalLink(QString const &url) {
|
||||
HANDLE_EXCEPTION(
|
||||
QString const u = url.isEmpty() ? bridgeKBUrl : url;
|
||||
QDesktopServices::openUrl(u);
|
||||
emit notifyKBArticleClicked(u);
|
||||
emit notifyExternalLinkClicked(u);
|
||||
)
|
||||
}
|
||||
|
||||
@ -1062,9 +1062,9 @@ void QMLBackend::notifyAutoconfigClicked(QString const &client) const {
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] article The url of the KB article.
|
||||
//****************************************************************************************************************************************************
|
||||
void QMLBackend::notifyKBArticleClicked(QString const &article) const {
|
||||
void QMLBackend::notifyExternalLinkClicked(QString const &article) const {
|
||||
HANDLE_EXCEPTION(
|
||||
app().grpc().KBArticleClicked(article);
|
||||
app().grpc().externalLinkClicked(article);
|
||||
)
|
||||
}
|
||||
|
||||
@ -1307,9 +1307,7 @@ void QMLBackend::connectGrpcEvents() {
|
||||
connect(client, &GRPCClient::showMainWindow, [&]() { this->showMainWindow("gRPC showMainWindow event"); });
|
||||
|
||||
// cache events
|
||||
connect(client, &GRPCClient::diskCacheUnavailable, this, &QMLBackend::diskCacheUnavailable);
|
||||
connect(client, &GRPCClient::cantMoveDiskCache, this, &QMLBackend::cantMoveDiskCache);
|
||||
connect(client, &GRPCClient::diskFull, this, &QMLBackend::diskFull);
|
||||
connect(client, &GRPCClient::diskCachePathChanged, this, &QMLBackend::diskCachePathChanged);
|
||||
connect(client, &GRPCClient::diskCachePathChangeFinished, this, &QMLBackend::diskCachePathChangeFinished);
|
||||
|
||||
@ -1354,7 +1352,6 @@ void QMLBackend::connectGrpcEvents() {
|
||||
connect(client, &GRPCClient::rebuildKeychain, this, &QMLBackend::notifyRebuildKeychain);
|
||||
|
||||
// mail events
|
||||
connect(client, &GRPCClient::noActiveKeyForRecipient, this, &QMLBackend::noActiveKeyForRecipient);
|
||||
connect(client, &GRPCClient::addressChanged, this, &QMLBackend::addressChanged);
|
||||
connect(client, &GRPCClient::addressChangedLogout, this, &QMLBackend::addressChangedLogout);
|
||||
connect(client, &GRPCClient::apiCertIssue, this, &QMLBackend::apiCertIssue);
|
||||
|
||||
@ -65,7 +65,7 @@ public: // member functions.
|
||||
Q_INVOKABLE QString collectAnswers(quint8 categoryId) const; ///< Collect answer for a given set of questions.
|
||||
Q_INVOKABLE void clearAnswers(); ///< Clear all collected answers.
|
||||
Q_INVOKABLE bool isTLSCertificateInstalled(); ///< Check if the bridge certificate is installed in the OS keychain.
|
||||
Q_INVOKABLE void openKBArticle(QString const & url = QString()); ///< Open a knowledge base article.
|
||||
Q_INVOKABLE void openExternalLink(QString const & url = QString()); ///< Open a knowledge base article.
|
||||
|
||||
public: // Qt/QML properties. Note that the NOTIFY-er signal is required even for read-only properties (QML warning otherwise)
|
||||
Q_PROPERTY(bool showOnStartup READ showOnStartup NOTIFY showOnStartupChanged)
|
||||
@ -205,7 +205,7 @@ public slots: // slot for signals received from QML -> To be forwarded to Bridge
|
||||
void sendBadEventUserFeedback(QString const &userID, bool doResync); ///< Slot the providing user feedback for a bad event.
|
||||
void notifyReportBugClicked() const; ///< Slot for the ReportBugClicked gRPC event.
|
||||
void notifyAutoconfigClicked(QString const &client) const; ///< Slot for gAutoconfigClicked gRPC event.
|
||||
void notifyKBArticleClicked(QString const &article) const; ///< Slot for KBArticleClicked gRPC event.
|
||||
void notifyExternalLinkClicked(QString const &article) const; ///< Slot for KBArticleClicked gRPC event.
|
||||
|
||||
public slots: // slots for functions that need to be processed locally.
|
||||
void setNormalTrayIcon(); ///< Set the tray icon to normal.
|
||||
@ -224,10 +224,8 @@ public slots: // slot for signals received from gRPC that need transformation in
|
||||
|
||||
signals: // Signals received from the Go backend, to be forwarded to QML
|
||||
void toggleAutostartFinished(); ///< Signal for the 'toggleAutostartFinished' gRPC stream event.
|
||||
void diskCacheUnavailable(); ///< Signal for the 'diskCacheUnavailable' gRPC stream event.
|
||||
void cantMoveDiskCache(); ///< Signal for the 'cantMoveDiskCache' gRPC stream event.
|
||||
void diskCachePathChangeFinished(); ///< Signal for the 'diskCachePathChangeFinished' gRPC stream event.
|
||||
void diskFull(); ///< Signal for the 'diskFull' gRPC stream event.
|
||||
void loginUsernamePasswordError(QString const &errorMsg); ///< Signal for the 'loginUsernamePasswordError' gRPC stream event.
|
||||
void loginFreeUserError(); ///< Signal for the 'loginFreeUserError' gRPC stream event.
|
||||
void loginConnectionError(QString const &errorMsg); ///< Signal for the 'loginConnectionError' gRPC stream event.
|
||||
@ -258,7 +256,6 @@ signals: // Signals received from the Go backend, to be forwarded to QML
|
||||
void changeKeychainFinished(); ///< Signal for the 'changeKeychainFinished' gRPC stream event.
|
||||
void notifyHasNoKeychain(); ///< Signal for the 'notifyHasNoKeychain' gRPC stream event.
|
||||
void notifyRebuildKeychain(); ///< Signal for the 'notifyRebuildKeychain' gRPC stream event.
|
||||
void noActiveKeyForRecipient(QString const &email); ///< Signal for the 'noActiveKeyForRecipient' gRPC stream event.
|
||||
void addressChanged(QString const &address); ///< Signal for the 'addressChanged' gRPC stream event.
|
||||
void addressChangedLogout(QString const &address); ///< Signal for the 'addressChangedLogout' gRPC stream event.
|
||||
void apiCertIssue(); ///< Signal for the 'apiCertIssue' gRPC stream event.
|
||||
|
||||
@ -84,6 +84,7 @@ Popup {
|
||||
anchors.topMargin: 14
|
||||
spacing: 8
|
||||
|
||||
|
||||
ColorImage {
|
||||
Layout.preferredHeight: 24
|
||||
Layout.preferredWidth: 24
|
||||
@ -108,14 +109,29 @@ Popup {
|
||||
sourceSize.width: 24
|
||||
width: 24
|
||||
}
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
ColumnLayout {
|
||||
Layout.alignment: Qt.AlignTop
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
color: root.colorScheme.text_invert
|
||||
colorScheme: root.colorScheme
|
||||
text: root.notification ? root.notification.description : ""
|
||||
wrapMode: Text.WordWrap
|
||||
Label {
|
||||
id: messageLabel
|
||||
Layout.alignment: Qt.AlignTop
|
||||
Layout.fillWidth: true
|
||||
color: root.colorScheme.text_invert
|
||||
colorScheme: root.colorScheme
|
||||
text: root.notification ? root.notification.description : ""
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
LinkLabel {
|
||||
Layout.alignment: Qt.AlignTop | Qt.AlignLeft
|
||||
Layout.fillWidth: true
|
||||
colorScheme: root.colorScheme
|
||||
color: messageLabel.color
|
||||
external: true
|
||||
link: root.notification ? root.notification.linkUrl : ""
|
||||
text: root.notification ? root.notification.linkText : ""
|
||||
visible: root.notification && root.notification.linkUrl.length > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,9 +34,6 @@ QtObject {
|
||||
function onColorSchemeNameChanged(scheme) {
|
||||
root.setColorScheme();
|
||||
}
|
||||
function onDiskCacheUnavailable() {
|
||||
mainWindow.showAndRise();
|
||||
}
|
||||
function onHideMainWindow() {
|
||||
mainWindow.hide();
|
||||
}
|
||||
|
||||
@ -113,7 +113,7 @@ SettingsView {
|
||||
secondary: true
|
||||
text: qsTr("View logs")
|
||||
|
||||
onClicked: Qt.openUrlExternally(Backend.logsPath)
|
||||
onClicked: Backend.openExternalLink(Backend.logsPath)
|
||||
}
|
||||
}
|
||||
TextEdit {
|
||||
|
||||
@ -36,7 +36,7 @@ SettingsView {
|
||||
type: SettingsItem.PrimaryButton
|
||||
|
||||
onClicked: {
|
||||
Backend.openKBArticle();
|
||||
Backend.openExternalLink();
|
||||
}
|
||||
}
|
||||
SettingsItem {
|
||||
@ -70,7 +70,7 @@ SettingsView {
|
||||
text: qsTr("Logs")
|
||||
type: SettingsItem.Button
|
||||
|
||||
onClicked: Qt.openUrlExternally(Backend.logsPath)
|
||||
onClicked: Backend.openExternalLink(Backend.logsPath)
|
||||
}
|
||||
SettingsItem {
|
||||
id: reportBug
|
||||
@ -103,7 +103,7 @@ SettingsView {
|
||||
type: Label.Caption
|
||||
|
||||
onLinkActivated: function (link) {
|
||||
Qt.openUrlExternally(link)
|
||||
Backend.openExternalLink(link)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,7 +71,7 @@ Dialog {
|
||||
wrapMode: Text.WordWrap
|
||||
|
||||
onLinkActivated: function (link) {
|
||||
Qt.openUrlExternally(link);
|
||||
Backend.openExternalLink(link);
|
||||
}
|
||||
}
|
||||
Item {
|
||||
@ -82,6 +82,17 @@ Dialog {
|
||||
implicitWidth: additionalChildrenContainer.childrenRect.width
|
||||
visible: children.length > 0
|
||||
}
|
||||
LinkLabel {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.bottomMargin: 32
|
||||
colorScheme: root.colorScheme
|
||||
external: true
|
||||
link: notification.linkUrl
|
||||
text: notification.linkText
|
||||
visible: notification.linkUrl.length > 0
|
||||
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 8
|
||||
|
||||
|
||||
@ -61,18 +61,10 @@ Item {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.enableBeta
|
||||
}
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.cacheUnavailable
|
||||
}
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.cacheCantMove
|
||||
}
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.diskFull
|
||||
}
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.enableSplitMode
|
||||
@ -101,10 +93,6 @@ Item {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.apiCertIssue
|
||||
}
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.noActiveKeyForRecipient
|
||||
}
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.userBadEvent
|
||||
|
||||
@ -24,19 +24,17 @@ QtObject {
|
||||
|
||||
property list<Action> action
|
||||
property bool active: false
|
||||
// brief is used in status view only
|
||||
property string brief
|
||||
property string brief // brief is used in status view only
|
||||
default property var children
|
||||
property var data
|
||||
// description is used in banners and in dialogs as description
|
||||
property string description
|
||||
property string description // description is used in banners and in dialogs as description
|
||||
property bool dismissed: false
|
||||
property int group
|
||||
property string icon
|
||||
property string linkUrl: ""
|
||||
property string linkText: ""
|
||||
readonly property var occurred: active ? new Date() : undefined
|
||||
|
||||
// title is used in dialogs only
|
||||
property string title
|
||||
property string title // title is used in dialogs only
|
||||
property int type
|
||||
|
||||
onActiveChanged: {
|
||||
|
||||
@ -28,31 +28,15 @@ QtObject {
|
||||
Dialogs = 64
|
||||
}
|
||||
|
||||
// Other
|
||||
property Notification accountChanged: Notification {
|
||||
brief: qsTr("Address list changed")
|
||||
description: qsTr("The address list for .... account has changed. You need to reconfigure your email client.")
|
||||
group: Notifications.Group.Configuration
|
||||
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||
type: Notification.NotificationType.Danger
|
||||
|
||||
action: Action {
|
||||
text: qsTr("Reconfigure")
|
||||
|
||||
onTriggered:
|
||||
// TODO: open configuration window here
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
property Notification addressChanged: Notification {
|
||||
brief: title
|
||||
description: qsTr("The address list for your account has changed. You might need to reconfigure your email client.")
|
||||
group: Notifications.Group.Configuration
|
||||
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||
linkText: qsTr("Learn more about address list changes")
|
||||
linkUrl: "https://proton.me/support/bridge-address-list-has-changed"
|
||||
title: qsTr("Address list changes")
|
||||
type: Notification.NotificationType.Warning
|
||||
|
||||
action: [
|
||||
Action {
|
||||
text: qsTr("OK")
|
||||
@ -76,7 +60,7 @@ QtObject {
|
||||
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.cacheUnavailable, root.cacheCantMove, root.accountChanged, root.diskFull, root.cacheLocationChangeSuccess, root.enableSplitMode, root.resetBridge, root.changeAllMailVisibility, root.deleteAccount, root.noKeychain, root.rebuildKeychain, root.addressChanged, root.apiCertIssue, root.noActiveKeyForRecipient, 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]
|
||||
property Notification alreadyLoggedIn: Notification {
|
||||
brief: qsTr("Already signed in")
|
||||
description: qsTr("This account is already signed in.")
|
||||
@ -104,9 +88,11 @@ QtObject {
|
||||
}
|
||||
property Notification apiCertIssue: Notification {
|
||||
brief: qsTr("Cannot establish secure connection")
|
||||
description: qsTr("Bridge cannot verify the authenticity of Proton servers on your current network due to a TLS certificate error. " + "Start Bridge again after ensuring your connection is secure and/or connecting to a VPN. Learn more about TLS pinning " + "<a href=\"https://proton.me/blog/tls-ssl-certificate#Extra-security-precautions-taken-by-ProtonMail\">here</a>.")
|
||||
description: qsTr("Bridge cannot verify the authenticity of Proton servers on your current network due to a TLS certificate error. Start Bridge again after ensuring your connection is secure and/or connecting to a VPN.")
|
||||
group: Notifications.Group.Dialogs | Notifications.Group.Connection
|
||||
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||
linkText: qsTr("Learn mode about TLS pinning")
|
||||
linkUrl: "https://proton.me/blog/tls-ssl-certificate#Extra-security-precautions-taken-by-ProtonMail"
|
||||
title: qsTr("Unable to establish a \nsecure connection to \nProton servers")
|
||||
type: Notification.NotificationType.Danger
|
||||
|
||||
@ -208,6 +194,8 @@ QtObject {
|
||||
description: qsTr("The location you have selected is not available. Make sure you have enough free space or choose another location.")
|
||||
group: Notifications.Group.Configuration | Notifications.Group.Dialogs
|
||||
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||
linkText: qsTr("Learn more about cache relocation issues")
|
||||
linkUrl: "https://proton.me/support/bridge-cant-move-cache"
|
||||
title: qsTr("Can’t move cache")
|
||||
type: Notification.NotificationType.Warning
|
||||
|
||||
@ -263,43 +251,6 @@ QtObject {
|
||||
target: Backend
|
||||
}
|
||||
}
|
||||
|
||||
// Cache
|
||||
property Notification cacheUnavailable: Notification {
|
||||
brief: title
|
||||
description: qsTr("The current cache location is unavailable. Check the directory or change it in your settings.")
|
||||
group: Notifications.Group.Configuration | Notifications.Group.Dialogs
|
||||
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||
title: qsTr("Cache location is unavailable")
|
||||
type: Notification.NotificationType.Warning
|
||||
|
||||
action: [
|
||||
Action {
|
||||
text: qsTr("Quit Bridge")
|
||||
|
||||
onTriggered: {
|
||||
Backend.quit();
|
||||
root.cacheUnavailable.active = false;
|
||||
}
|
||||
},
|
||||
Action {
|
||||
text: qsTr("Change location")
|
||||
|
||||
onTriggered: {
|
||||
root.cacheUnavailable.active = false;
|
||||
root.frontendMain.showLocalCacheSettings();
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Connections {
|
||||
function onDiskCacheUnavailable() {
|
||||
root.cacheUnavailable.active = true;
|
||||
}
|
||||
|
||||
target: Backend
|
||||
}
|
||||
}
|
||||
property Notification changeAllMailVisibility: Notification {
|
||||
property var isVisibleNow
|
||||
|
||||
@ -378,41 +329,6 @@ QtObject {
|
||||
target: root
|
||||
}
|
||||
}
|
||||
property Notification diskFull: Notification {
|
||||
brief: title
|
||||
description: qsTr("Quit Bridge and free disk space or move the local cache to another disk.")
|
||||
group: Notifications.Group.Configuration | Notifications.Group.Dialogs
|
||||
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||
title: qsTr("Your disk is almost full")
|
||||
type: Notification.NotificationType.Warning
|
||||
|
||||
action: [
|
||||
Action {
|
||||
text: qsTr("Quit Bridge")
|
||||
|
||||
onTriggered: {
|
||||
Backend.quit();
|
||||
root.diskFull.active = false;
|
||||
}
|
||||
},
|
||||
Action {
|
||||
text: qsTr("Settings")
|
||||
|
||||
onTriggered: {
|
||||
root.diskFull.active = false;
|
||||
root.frontendMain.showLocalCacheSettings();
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Connections {
|
||||
function onDiskFull() {
|
||||
root.diskFull.active = true;
|
||||
}
|
||||
|
||||
target: Backend
|
||||
}
|
||||
}
|
||||
property Notification enableBeta: Notification {
|
||||
brief: title
|
||||
description: qsTr("Be the first to get new updates and use new features. Bridge will update to the latest beta version.")
|
||||
@ -454,6 +370,8 @@ QtObject {
|
||||
description: qsTr("Changing between split and combined address mode will require you to delete your account(s) from your email client and begin the setup process from scratch.")
|
||||
group: Notifications.Group.Configuration | Notifications.Group.Dialogs
|
||||
icon: "./icons/ic-question-circle.svg"
|
||||
linkText: qsTr("Learn more about split mode")
|
||||
linkUrl: "https://proton.me/support/difference-combined-addresses-mode-split-addresses-mode"
|
||||
title: qsTr("Enable split mode?")
|
||||
type: Notification.NotificationType.Warning
|
||||
|
||||
@ -600,7 +518,9 @@ QtObject {
|
||||
description: "#PlaceHolderText"
|
||||
group: Notifications.Group.Connection
|
||||
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||
title: qsTr("IMAP Login failed")
|
||||
linkText: qsTr("Learn more about IMAP login issues")
|
||||
linkUrl: "https://proton.me/support/bridge-imap-login-failed"
|
||||
title: qsTr("IMAP login failed")
|
||||
type: Notification.NotificationType.Danger
|
||||
|
||||
action: [
|
||||
@ -627,6 +547,8 @@ QtObject {
|
||||
description: qsTr("The IMAP port could not be changed.")
|
||||
group: Notifications.Group.Connection
|
||||
icon: "./icons/ic-alert.svg"
|
||||
linkText: qsTr("Learn more about IMAP port issues")
|
||||
linkUrl: "https://proton.me/support/port-already-occupied-error"
|
||||
type: Notification.NotificationType.Danger
|
||||
|
||||
Connections {
|
||||
@ -642,6 +564,8 @@ QtObject {
|
||||
description: qsTr("The IMAP server could not be started. Please check or change the IMAP port.")
|
||||
group: Notifications.Group.Connection
|
||||
icon: "./icons/ic-alert.svg"
|
||||
linkText: qsTr("Learn more about IMAP port issues")
|
||||
linkUrl: "https://proton.me/support/port-already-occupied-error"
|
||||
type: Notification.NotificationType.Danger
|
||||
|
||||
Connections {
|
||||
@ -679,33 +603,6 @@ QtObject {
|
||||
target: Backend
|
||||
}
|
||||
}
|
||||
property Notification noActiveKeyForRecipient: Notification {
|
||||
brief: title
|
||||
description: "#PlaceholderText#"
|
||||
group: Notifications.Group.Dialogs | Notifications.Group.Connection
|
||||
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||
title: qsTr("Unable to send \nencrypted message")
|
||||
type: Notification.NotificationType.Danger
|
||||
|
||||
action: [
|
||||
Action {
|
||||
text: qsTr("OK")
|
||||
|
||||
onTriggered: {
|
||||
root.noActiveKeyForRecipient.active = false;
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Connections {
|
||||
function onNoActiveKeyForRecipient(email) {
|
||||
root.noActiveKeyForRecipient.description = qsTr("There are no active keys to encrypt your message to %1. " + "Please update the setting for this contact.").arg(email);
|
||||
root.noActiveKeyForRecipient.active = true;
|
||||
}
|
||||
|
||||
target: Backend
|
||||
}
|
||||
}
|
||||
|
||||
// Connection
|
||||
property Notification noInternet: Notification {
|
||||
@ -714,7 +611,6 @@ QtObject {
|
||||
group: Notifications.Group.Connection
|
||||
icon: "./icons/ic-no-connection.svg"
|
||||
type: Notification.NotificationType.Danger
|
||||
|
||||
Connections {
|
||||
function onInternetOff() {
|
||||
root.noInternet.active = true;
|
||||
@ -730,9 +626,11 @@ QtObject {
|
||||
brief: title
|
||||
description: Backend.goos === "darwin" ?
|
||||
qsTr("Bridge is not able to access your keychain. Please make sure your keychain is not locked and restart the application.") :
|
||||
qsTr("Bridge is not able to detect a supported password manager (pass or secret-service). Please install and setup supported password manager and restart the application.")
|
||||
qsTr("Bridge is not able to detect a supported password manager (pass or secret-service). Please install and setup a supported password manager and restart the application.")
|
||||
group: Notifications.Group.Dialogs | Notifications.Group.Configuration
|
||||
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||
linkText: qsTr("Learn more about keychain issues")
|
||||
linkUrl: "https://proton.me/support/bridge-cannot-access-keychain"
|
||||
title: Backend.goos === "darwin" ? qsTr("Cannot access keychain") : qsTr("No keychain available")
|
||||
type: Notification.NotificationType.Danger
|
||||
|
||||
@ -775,7 +673,7 @@ QtObject {
|
||||
text: qsTr("Upgrade")
|
||||
|
||||
onTriggered: {
|
||||
Qt.openUrlExternally(root.onlyPaidUsers.pricingLink);
|
||||
Backend.openExternalLink(root.onlyPaidUsers.pricingLink);
|
||||
root.onlyPaidUsers.active = false;
|
||||
}
|
||||
}
|
||||
@ -802,7 +700,7 @@ QtObject {
|
||||
text: qsTr("Open the support page")
|
||||
|
||||
onTriggered: {
|
||||
Backend.openKBArticle();
|
||||
Backend.openExternalLink();
|
||||
Backend.quit();
|
||||
}
|
||||
}
|
||||
@ -893,6 +791,8 @@ QtObject {
|
||||
description: qsTr("The SMTP port could not be changed.")
|
||||
group: Notifications.Group.Connection
|
||||
icon: "./icons/ic-alert.svg"
|
||||
linkText: qsTr("Learn more about SMTP port issues")
|
||||
linkUrl: "https://proton.me/support/port-already-occupied-error"
|
||||
type: Notification.NotificationType.Danger
|
||||
|
||||
Connections {
|
||||
@ -908,6 +808,8 @@ QtObject {
|
||||
description: qsTr("The SMTP server could not be started. Please check or change the SMTP port.")
|
||||
group: Notifications.Group.Connection
|
||||
icon: "./icons/ic-alert.svg"
|
||||
linkText: qsTr("Learn more about SMTP port issues")
|
||||
linkUrl: "https://proton.me/support/port-already-occupied-error"
|
||||
type: Notification.NotificationType.Danger
|
||||
|
||||
Connections {
|
||||
@ -939,7 +841,7 @@ QtObject {
|
||||
text: qsTr("Update manually")
|
||||
|
||||
onTriggered: {
|
||||
Qt.openUrlExternally(Backend.landingPageLink);
|
||||
Backend.openExternalLink(Backend.landingPageLink);
|
||||
root.updateForce.active = false;
|
||||
}
|
||||
},
|
||||
@ -969,6 +871,8 @@ QtObject {
|
||||
description: qsTr("You must update manually. Go to: https://proton.me/mail/bridge#download")
|
||||
group: Notifications.Group.Update | Notifications.Group.Dialogs
|
||||
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||
linkText: qsTr("Learn more about Bridge updates")
|
||||
linkUrl: "https://proton.me/support/protonmail-bridge-manual-update"
|
||||
title: qsTr("Bridge couldn't update")
|
||||
type: Notification.NotificationType.Danger
|
||||
|
||||
@ -977,7 +881,7 @@ QtObject {
|
||||
text: qsTr("Update manually")
|
||||
|
||||
onTriggered: {
|
||||
Qt.openUrlExternally(Backend.landingPageLink);
|
||||
Backend.openExternalLink(Backend.landingPageLink);
|
||||
root.updateForceError.active = false;
|
||||
}
|
||||
},
|
||||
@ -1027,6 +931,8 @@ QtObject {
|
||||
description: qsTr("Please follow manual installation in order to update Bridge.")
|
||||
group: Notifications.Group.Update
|
||||
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||
linkText: qsTr("Learn more about Bridge updates")
|
||||
linkUrl: "https://proton.me/support/protonmail-bridge-manual-update"
|
||||
title: qsTr("Bridge couldn’t update")
|
||||
type: Notification.NotificationType.Warning
|
||||
|
||||
@ -1035,7 +941,7 @@ QtObject {
|
||||
text: qsTr("Update manually")
|
||||
|
||||
onTriggered: {
|
||||
Qt.openUrlExternally(Backend.landingPageLink);
|
||||
Backend.openExternalLink(Backend.landingPageLink);
|
||||
root.updateManualError.active = false;
|
||||
Backend.quit();
|
||||
}
|
||||
@ -1085,7 +991,7 @@ QtObject {
|
||||
text: qsTr("Update manually")
|
||||
|
||||
onTriggered: {
|
||||
Qt.openUrlExternally(Backend.landingPageLink);
|
||||
Backend.openExternalLink(Backend.landingPageLink);
|
||||
root.updateManualReady.active = false;
|
||||
}
|
||||
},
|
||||
@ -1138,13 +1044,15 @@ QtObject {
|
||||
description: qsTr("Bridge couldn't update")
|
||||
group: Notifications.Group.Update
|
||||
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||
linkText: qsTr("Learn more about Bridge updates")
|
||||
linkUrl: "https://proton.me/support/protonmail-bridge-manual-update"
|
||||
type: Notification.NotificationType.Warning
|
||||
|
||||
action: Action {
|
||||
text: qsTr("Update manually")
|
||||
|
||||
onTriggered: {
|
||||
Qt.openUrlExternally(Backend.landingPageLink);
|
||||
Backend.openExternalLink(Backend.landingPageLink);
|
||||
root.updateSilentError.active = false;
|
||||
}
|
||||
}
|
||||
@ -1188,6 +1096,8 @@ QtObject {
|
||||
description: "#PlaceHolderText"
|
||||
group: Notifications.Group.Connection | Notifications.Group.Dialogs
|
||||
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||
linkText: qsTr("Learn more about internal errors")
|
||||
linkUrl: "https://proton.me/support/bridge-internal-error"
|
||||
title: qsTr("Internal error")
|
||||
type: Notification.NotificationType.Danger
|
||||
|
||||
|
||||
@ -22,6 +22,7 @@ RowLayout {
|
||||
property bool external: false
|
||||
property string link: "#"
|
||||
property string text: ""
|
||||
property color color: colorScheme.interaction_norm
|
||||
|
||||
function clear() {
|
||||
root.callback = null;
|
||||
@ -49,12 +50,12 @@ RowLayout {
|
||||
id: label
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
colorScheme: root.colorScheme
|
||||
linkColor: root.color
|
||||
text: label.link(root.link, root.text)
|
||||
type: Label.LabelType.Body
|
||||
|
||||
onLinkActivated: function (link) {
|
||||
if ((link !== "#") && (link.length > 0)) {
|
||||
Qt.openUrlExternally(link);
|
||||
Backend.openExternalLink(link);
|
||||
}
|
||||
if (callback) {
|
||||
callback();
|
||||
|
||||
@ -79,7 +79,7 @@ Rectangle {
|
||||
text: qsTr("Open guide")
|
||||
|
||||
onClicked: function () {
|
||||
Backend.openKBArticle(wizard.setupGuideLink());
|
||||
Backend.openExternalLink(wizard.setupGuideLink());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -49,7 +49,7 @@ Button {
|
||||
text: qsTr("Get help")
|
||||
|
||||
onClicked: {
|
||||
Backend.openKBArticle();
|
||||
Backend.openExternalLink();
|
||||
}
|
||||
}
|
||||
MenuItem {
|
||||
|
||||
@ -37,7 +37,7 @@ Item {
|
||||
function showAppleMailAutoconfigCertificateInstall() {
|
||||
showAppleMailAutoconfigCommon();
|
||||
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.openKBArticle("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);
|
||||
linkLabel2.clear();
|
||||
}
|
||||
function showAppleMailAutoconfigCommon() {
|
||||
@ -51,7 +51,7 @@ Item {
|
||||
function showAppleMailAutoconfigProfileInstall() {
|
||||
showAppleMailAutoconfigCommon();
|
||||
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.openKBArticle("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);
|
||||
}
|
||||
function showClientSelector(newAccount = true) {
|
||||
@ -86,7 +86,7 @@ Item {
|
||||
function showOnboarding() {
|
||||
titleLabel.text = (Backend.users.count === 0) ? welcomeTitle : addAccountTitle;
|
||||
descriptionLabel.text = welcomeDescription
|
||||
linkLabel1.setCallback(function() { Backend.openKBArticle("https://proton.me/support/why-you-need-bridge"); }, qsTr("Why do I need Bridge?"), true);
|
||||
linkLabel1.setCallback(function() { Backend.openExternalLink("https://proton.me/support/why-you-need-bridge"); }, qsTr("Why do I need Bridge?"), true);
|
||||
linkLabel2.clear();
|
||||
root.iconSource = welcomeImage;
|
||||
root.iconHeight = welcomeImageHeight;
|
||||
|
||||
@ -162,7 +162,7 @@ Dialog {
|
||||
wrapMode: Text.WordWrap
|
||||
|
||||
onLinkActivated: function (link) {
|
||||
Qt.openUrlExternally(link);
|
||||
Backend.openExternalLink(link);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -208,17 +208,19 @@ QString randomLastName() {
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
/// \param[in] firstName The user's first name. If empty, a random common US first name is used.
|
||||
/// \param[in] lastName The user's last name. If empty, a random common US last name is used.
|
||||
/// \return The user
|
||||
//****************************************************************************************************************************************************
|
||||
SPUser randomUser() {
|
||||
SPUser randomUser(QString const &firstName, QString const &lastName) {
|
||||
SPUser user = User::newUser(nullptr);
|
||||
user->setID(QUuid::createUuid().toString());
|
||||
QString const firstName = randomFirstName();
|
||||
QString const lastName = randomLastName();
|
||||
QString const username = QString("%1.%2").arg(firstName.toLower(), lastName.toLower());
|
||||
QString const first = firstName.isEmpty() ? randomFirstName() : firstName;
|
||||
QString const last = lastName.isEmpty() ? randomLastName() : lastName;
|
||||
QString const username = QString("%1.%2").arg(first.toLower(), last.toLower());
|
||||
user->setUsername(username);
|
||||
user->setAddresses(QStringList() << (username + "@proton.me") << (username + "@protonmail.com"));
|
||||
user->setPassword(QUuid().createUuid().toString(QUuid::StringFormat::WithoutBraces).left(20));
|
||||
user->setPassword(QUuid::createUuid().toString(QUuid::StringFormat::WithoutBraces).left(20));
|
||||
user->setAvatarText(firstName.left(1) + lastName.left(1));
|
||||
user->setState(UserState::Connected);
|
||||
user->setSplitMode(false);
|
||||
@ -229,6 +231,16 @@ SPUser randomUser() {
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The default user. The name Eric Norbert is used on the proton.me website, and should be used for screenshots.
|
||||
//****************************************************************************************************************************************************
|
||||
SPUser defaultUser() {
|
||||
SPUser user = randomUser("Eric", "Norbert");
|
||||
user->setAddresses({"eric.norbert@proton.me", "eric_norbert_writes@protonmail.com"}); // we override the address list with addresses commonly used on screenshots proton.me
|
||||
return user;
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The OS the application is running on.
|
||||
//****************************************************************************************************************************************************
|
||||
|
||||
@ -44,7 +44,8 @@ QString goos(); ///< return the value of Go's GOOS for the current platform ("d
|
||||
qint64 randN(qint64 n); ///< return a random integer in the half open range [0,n)
|
||||
QString randomFirstName(); ///< Get a random first name from a pre-determined list.
|
||||
QString randomLastName(); ///< Get a random first name from a pre-determined list.
|
||||
SPUser randomUser(); ///< Get a random user.
|
||||
SPUser defaultUser(); ///< Return The default user, with the name and addresses used on screenshots on proton.me
|
||||
SPUser randomUser(QString const &firstName = "", QString const &lastName = ""); ///< Get a random user.
|
||||
OS os(); ///< Return the operating system.
|
||||
bool onLinux(); ///< Check if the OS is Linux.
|
||||
bool onMacOS(); ///< Check if the OS is macOS.
|
||||
|
||||
@ -202,6 +202,18 @@ SPStreamEvent newReportBugErrorEvent() {
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The event.
|
||||
//****************************************************************************************************************************************************
|
||||
SPStreamEvent newReportBugFallbackEvent() {
|
||||
auto event = new grpc::ReportBugFallbackEvent;
|
||||
auto appEvent = new grpc::AppEvent;
|
||||
appEvent->set_allocated_reportbugfallback(event);
|
||||
return wrapAppEvent(appEvent);
|
||||
}
|
||||
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The event.
|
||||
//****************************************************************************************************************************************************
|
||||
@ -368,7 +380,7 @@ SPStreamEvent newUpdateForceEvent(QString const &version) {
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return the event.
|
||||
//****************************************************************************************************************************************************
|
||||
SPStreamEvent newUpdateSilentRestartNeeded() {
|
||||
SPStreamEvent newUpdateSilentRestartNeededEvent() {
|
||||
auto event = new grpc::UpdateSilentRestartNeeded;
|
||||
auto updateEvent = new grpc::UpdateEvent;
|
||||
updateEvent->set_allocated_silentrestartneeded(event);
|
||||
@ -379,7 +391,7 @@ SPStreamEvent newUpdateSilentRestartNeeded() {
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The event.
|
||||
//****************************************************************************************************************************************************
|
||||
SPStreamEvent newUpdateIsLatestVersion() {
|
||||
SPStreamEvent newUpdateIsLatestVersionEvent() {
|
||||
auto event = new grpc::UpdateIsLatestVersion;
|
||||
auto updateEvent = new grpc::UpdateEvent;
|
||||
updateEvent->set_allocated_islatestversion(event);
|
||||
@ -390,7 +402,7 @@ SPStreamEvent newUpdateIsLatestVersion() {
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The event.
|
||||
//****************************************************************************************************************************************************
|
||||
SPStreamEvent newUpdateCheckFinished() {
|
||||
SPStreamEvent newUpdateCheckFinishedEvent() {
|
||||
auto event = new grpc::UpdateCheckFinished;
|
||||
auto updateEvent = new grpc::UpdateEvent;
|
||||
updateEvent->set_allocated_checkfinished(event);
|
||||
@ -398,6 +410,17 @@ SPStreamEvent newUpdateCheckFinished() {
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The event.
|
||||
//****************************************************************************************************************************************************
|
||||
SPStreamEvent newUpdateVersionChangedEvent() {
|
||||
auto event = new grpc::UpdateVersionChanged;
|
||||
auto updateEvent = new grpc::UpdateEvent;
|
||||
updateEvent->set_allocated_versionchanged(event);
|
||||
return wrapUpdateEvent(updateEvent);
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] errorType The error type.
|
||||
/// \return The event.
|
||||
@ -505,19 +528,6 @@ SPStreamEvent newRebuildKeychainEvent() {
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] email The email.
|
||||
/// \return The event.
|
||||
//****************************************************************************************************************************************************
|
||||
SPStreamEvent newNoActiveKeyForRecipientEvent(QString const &email) {
|
||||
auto event = new grpc::NoActiveKeyForRecipientEvent;
|
||||
event->set_email(email.toStdString());
|
||||
auto mailEvent = new grpc::MailEvent;
|
||||
mailEvent->set_allocated_noactivekeyforrecipientevent(event);
|
||||
return wrapMailEvent(mailEvent);
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] address The address.
|
||||
/// /// \return The event.
|
||||
|
||||
@ -34,6 +34,7 @@ SPStreamEvent newResetFinishedEvent(); ///< Create a new ResetFinishedEvent even
|
||||
SPStreamEvent newReportBugFinishedEvent(); ///< Create a new ReportBugFinishedEvent event.
|
||||
SPStreamEvent newReportBugSuccessEvent(); ///< Create a new ReportBugSuccessEvent event.
|
||||
SPStreamEvent newReportBugErrorEvent(); ///< Create a new ReportBugErrorEvent event.
|
||||
SPStreamEvent newReportBugFallbackEvent(); ///< Create a new ReportBugFallbackEvent event.
|
||||
SPStreamEvent newCertificateInstallSuccessEvent(); ///< Create a new CertificateInstallSuccessEvent event.
|
||||
SPStreamEvent newCertificateInstallCanceledEvent(); ///< Create a new CertificateInstallCanceledEvent event.
|
||||
SPStreamEvent newCertificateInstallFailedEvent(); ///< Create anew CertificateInstallFailedEvent event.
|
||||
@ -51,9 +52,10 @@ SPStreamEvent newUpdateErrorEvent(grpc::UpdateErrorType errorType); ///< Create
|
||||
SPStreamEvent newUpdateManualReadyEvent(QString const &version); ///< Create a new UpdateManualReadyEvent event.
|
||||
SPStreamEvent newUpdateManualRestartNeededEvent(); ///< Create a new UpdateManualRestartNeededEvent event.
|
||||
SPStreamEvent newUpdateForceEvent(QString const &version); ///< Create a new UpdateForceEvent event.
|
||||
SPStreamEvent newUpdateSilentRestartNeeded(); ///< Create a new UpdateSilentRestartNeeded event.
|
||||
SPStreamEvent newUpdateIsLatestVersion(); ///< Create a new UpdateIsLatestVersion event.
|
||||
SPStreamEvent newUpdateCheckFinished(); ///< Create a new UpdateCheckFinished event.
|
||||
SPStreamEvent newUpdateSilentRestartNeededEvent(); ///< Create a new UpdateSilentRestartNeeded event.
|
||||
SPStreamEvent newUpdateIsLatestVersionEvent(); ///< Create a new UpdateIsLatestVersion event.
|
||||
SPStreamEvent newUpdateCheckFinishedEvent(); ///< Create a new UpdateCheckFinished event.
|
||||
SPStreamEvent newUpdateVersionChangedEvent(); ///< Create a new updateVersionChanged event.
|
||||
|
||||
// Cache on disk related events
|
||||
SPStreamEvent newDiskCacheErrorEvent(grpc::DiskCacheErrorType errorType); ///< Create a new DiskCacheErrorEvent event.
|
||||
@ -71,7 +73,6 @@ SPStreamEvent newHasNoKeychainEvent(); ///< Create a new HasNoKeychainEvent even
|
||||
SPStreamEvent newRebuildKeychainEvent(); ///< Create a new RebuildKeychainEvent event.
|
||||
|
||||
// Mail related events
|
||||
SPStreamEvent newNoActiveKeyForRecipientEvent(QString const &email); ///< Create a new NoActiveKeyForRecipientEvent event.
|
||||
SPStreamEvent newAddressChangedEvent(QString const &address); ///< Create a new AddressChangedEvent event.
|
||||
SPStreamEvent newAddressChangedLogoutEvent(QString const &address); ///< Create a new AddressChangedLogoutEvent event.
|
||||
SPStreamEvent newApiCertIssueEvent(); ///< Create a new ApiCertIssueEvent event.
|
||||
|
||||
@ -1298,18 +1298,10 @@ void GRPCClient::processCacheEvent(DiskCacheEvent const &event) {
|
||||
switch (event.event_case()) {
|
||||
case DiskCacheEvent::kError: {
|
||||
switch (event.error().type()) {
|
||||
case DISK_CACHE_UNAVAILABLE_ERROR:
|
||||
this->logError("Cache error received: diskCacheUnavailable.");
|
||||
emit diskCacheUnavailable();
|
||||
break;
|
||||
case CANT_MOVE_DISK_CACHE_ERROR:
|
||||
this->logError("Cache error received: cantMoveDiskCache.");
|
||||
emit cantMoveDiskCache();
|
||||
break;
|
||||
case DISK_FULL_ERROR:
|
||||
this->logError("Cache error received: diskFull.");
|
||||
emit diskFull();
|
||||
break;
|
||||
default:
|
||||
this->logError("Unknown cache error event received.");
|
||||
break;
|
||||
@ -1409,12 +1401,6 @@ void GRPCClient::processKeychainEvent(KeychainEvent const &event) {
|
||||
//****************************************************************************************************************************************************
|
||||
void GRPCClient::processMailEvent(MailEvent const &event) {
|
||||
switch (event.event_case()) {
|
||||
case MailEvent::kNoActiveKeyForRecipientEvent: {
|
||||
QString const email = QString::fromStdString(event.noactivekeyforrecipientevent().email());
|
||||
this->logTrace(QString("Mail event received: NoActiveKeyForRecipient (email = %1).").arg(email));
|
||||
emit noActiveKeyForRecipient(email);
|
||||
break;
|
||||
}
|
||||
case MailEvent::kAddressChanged:
|
||||
this->logTrace("Mail event received: AddressChanged.");
|
||||
emit addressChanged(QString::fromStdString(event.addresschanged().address()));
|
||||
@ -1527,24 +1513,30 @@ UPClientContext GRPCClient::clientContext() const {
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] userID The user ID.
|
||||
/// \param[in] address The email address.
|
||||
/// \return the status for the gRPC call.
|
||||
//****************************************************************************************************************************************************
|
||||
grpc::Status GRPCClient::reportBugClicked() {
|
||||
return this->logGRPCCallStatus(stub_->ReportBugClicked(this->clientContext().get(), empty, &empty), __FUNCTION__);
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] client The client string.
|
||||
/// \return the status for the gRPC call.
|
||||
//****************************************************************************************************************************************************
|
||||
grpc::Status GRPCClient::autoconfigClicked(QString const &client) {
|
||||
StringValue s;
|
||||
s.set_value(client.toStdString());
|
||||
return this->logGRPCCallStatus(stub_->AutoconfigClicked(this->clientContext().get(), s, &empty), __FUNCTION__);
|
||||
}
|
||||
|
||||
grpc::Status GRPCClient::KBArticleClicked(QString const &article) {
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] link The clicked link.
|
||||
/// \return the status for the gRPC call.
|
||||
//****************************************************************************************************************************************************
|
||||
grpc::Status GRPCClient::externalLinkClicked(QString const &link) {
|
||||
StringValue s;
|
||||
s.set_value(article.toStdString());
|
||||
return this->logGRPCCallStatus(stub_->KBArticleClicked(this->clientContext().get(), s, &empty), __FUNCTION__);
|
||||
s.set_value(link.toStdString());
|
||||
return this->logGRPCCallStatus(stub_->ExternalLinkClicked(this->clientContext().get(), s, &empty), __FUNCTION__);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -113,9 +113,7 @@ public:
|
||||
grpc::Status setDiskCachePath(QUrl const &path); ///< Performs the 'setDiskCachePath' call
|
||||
|
||||
signals:
|
||||
void diskCacheUnavailable();
|
||||
void cantMoveDiskCache();
|
||||
void diskFull();
|
||||
void diskCachePathChanged(QUrl const &path);
|
||||
void diskCachePathChangeFinished();
|
||||
|
||||
@ -196,7 +194,7 @@ signals:
|
||||
public: // telemetry related calls
|
||||
grpc::Status reportBugClicked(); ///< Performs the 'reportBugClicked' call.
|
||||
grpc::Status autoconfigClicked(QString const &userID); ///< Performs the 'AutoconfigClicked' call.
|
||||
grpc::Status KBArticleClicked(QString const &userID); ///< Performs the 'KBArticleClicked' call.
|
||||
grpc::Status externalLinkClicked(QString const &userID); ///< Performs the 'KBArticleClicked' call.
|
||||
|
||||
public: // keychain related calls
|
||||
grpc::Status availableKeychains(QStringList &outKeychains);
|
||||
@ -215,7 +213,6 @@ signals:
|
||||
void certIsReady();
|
||||
|
||||
signals: // mail related events
|
||||
void noActiveKeyForRecipient(QString const &email);
|
||||
void addressChanged(QString const &address);
|
||||
void addressChangedLogout(QString const &address);
|
||||
void apiCertIssue();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -100,7 +100,7 @@ service Bridge {
|
||||
// Telemetry
|
||||
rpc ReportBugClicked(google.protobuf.Empty) returns (google.protobuf.Empty);
|
||||
rpc AutoconfigClicked(google.protobuf.StringValue) returns (google.protobuf.Empty);
|
||||
rpc KBArticleClicked(google.protobuf.StringValue) returns (google.protobuf.Empty);
|
||||
rpc ExternalLinkClicked(google.protobuf.StringValue) returns (google.protobuf.Empty);
|
||||
|
||||
// TLS certificate related calls
|
||||
rpc IsTLSCertificateInstalled(google.protobuf.Empty) returns (google.protobuf.BoolValue);
|
||||
@ -385,9 +385,7 @@ message DiskCacheEvent {
|
||||
}
|
||||
|
||||
enum DiskCacheErrorType {
|
||||
DISK_CACHE_UNAVAILABLE_ERROR = 0;
|
||||
CANT_MOVE_DISK_CACHE_ERROR = 1;
|
||||
DISK_FULL_ERROR = 2;
|
||||
CANT_MOVE_DISK_CACHE_ERROR = 0;
|
||||
};
|
||||
|
||||
message DiskCacheErrorEvent {
|
||||
@ -446,17 +444,12 @@ message RebuildKeychainEvent {}
|
||||
//**********************************************************
|
||||
message MailEvent {
|
||||
oneof event {
|
||||
NoActiveKeyForRecipientEvent noActiveKeyForRecipientEvent = 1;
|
||||
AddressChangedEvent addressChanged = 2;
|
||||
AddressChangedLogoutEvent addressChangedLogout = 3;
|
||||
ApiCertIssueEvent apiCertIssue = 6;
|
||||
AddressChangedEvent addressChanged = 1;
|
||||
AddressChangedLogoutEvent addressChangedLogout = 2;
|
||||
ApiCertIssueEvent apiCertIssue = 3;
|
||||
}
|
||||
}
|
||||
|
||||
message NoActiveKeyForRecipientEvent {
|
||||
string email = 1;
|
||||
}
|
||||
|
||||
message AddressChangedEvent {
|
||||
string address = 1;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -161,10 +161,6 @@ func NewKeychainRebuildKeychainEvent() *StreamEvent {
|
||||
return keychainEvent(&KeychainEvent{Event: &KeychainEvent_RebuildKeychain{RebuildKeychain: &RebuildKeychainEvent{}}})
|
||||
}
|
||||
|
||||
func NewMailNoActiveKeyForRecipientEvent(email string) *StreamEvent {
|
||||
return mailEvent(&MailEvent{Event: &MailEvent_NoActiveKeyForRecipientEvent{NoActiveKeyForRecipientEvent: &NoActiveKeyForRecipientEvent{Email: email}}})
|
||||
}
|
||||
|
||||
func NewMailAddressChangeEvent(email string) *StreamEvent {
|
||||
return mailEvent(&MailEvent{Event: &MailEvent_AddressChanged{AddressChanged: &AddressChangedEvent{Address: email}}})
|
||||
}
|
||||
|
||||
@ -166,7 +166,6 @@ func (s *Service) StartEventTest() error {
|
||||
NewKeychainRebuildKeychainEvent(),
|
||||
|
||||
// mail
|
||||
NewMailNoActiveKeyForRecipientEvent(dummyAddress),
|
||||
NewMailAddressChangeEvent(dummyAddress),
|
||||
NewMailAddressChangeLogoutEvent(dummyAddress),
|
||||
NewMailApiCertIssue(),
|
||||
|
||||
@ -34,7 +34,7 @@ func (s *Service) AutoconfigClicked(_ context.Context, client *wrapperspb.String
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (s *Service) KBArticleClicked(_ context.Context, article *wrapperspb.StringValue) (*emptypb.Empty, error) {
|
||||
s.bridge.KBArticleOpened(article.Value)
|
||||
func (s *Service) ExternalLinkClicked(_ context.Context, article *wrapperspb.StringValue) (*emptypb.Empty, error) {
|
||||
s.bridge.ExternalLinkClicked(article.Value)
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
@ -15,18 +15,18 @@
|
||||
// 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/>.
|
||||
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package sentry
|
||||
|
||||
import "os"
|
||||
import (
|
||||
"github.com/jeandeaual/go-locale"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func GetSystemLang() string {
|
||||
lang := os.Getenv("LC_ALL")
|
||||
if lang == "" {
|
||||
lang = os.Getenv("LANG")
|
||||
lang, err := locale.GetLanguage()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Failed to get system language")
|
||||
lang = "Unknown"
|
||||
}
|
||||
|
||||
return lang
|
||||
}
|
||||
|
||||
@ -1,67 +0,0 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package sentry
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultLocaleUser = "GetUserDefaultLocaleName" // https://learn.microsoft.com/en-us/windows/win32/api/winnls/nf-winnls-getuserdefaultlocalename
|
||||
defaultLocaleSystem = "GetSystemDefaultLocaleName" // https://learn.microsoft.com/en-us/windows/win32/api/winnls/nf-winnls-getsystemdefaultlocalename
|
||||
localeNameMaxLength = 85 // https://learn.microsoft.com/en-us/windows/win32/intl/locale-name-constants
|
||||
)
|
||||
|
||||
func getLocale(dll *syscall.DLL, procName string) (string, error) {
|
||||
proc, err := dll.FindProc(procName)
|
||||
if err != nil {
|
||||
return "errProc", err
|
||||
}
|
||||
|
||||
b := make([]uint16, localeNameMaxLength)
|
||||
|
||||
r, _, err := proc.Call(uintptr(unsafe.Pointer(&b[0])), uintptr(localeNameMaxLength))
|
||||
if r == 0 || err != nil {
|
||||
return "errCall", err
|
||||
}
|
||||
|
||||
return syscall.UTF16ToString(b), nil
|
||||
}
|
||||
|
||||
func GetSystemLang() string {
|
||||
dll, err := syscall.LoadDLL("kernel32")
|
||||
if err != nil {
|
||||
return "errDll"
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = dll.Release()
|
||||
}()
|
||||
|
||||
if lang, err := getLocale(dll, defaultLocaleUser); err == nil {
|
||||
return lang
|
||||
}
|
||||
|
||||
lang, _ := getLocale(dll, defaultLocaleSystem)
|
||||
|
||||
return lang
|
||||
}
|
||||
@ -678,17 +678,27 @@ func (s *Connector) importMessage(
|
||||
|
||||
if err := s.identityState.WithAddrKR(s.addrID, func(_, addrKR *crypto.KeyRing) error {
|
||||
var messageID string
|
||||
|
||||
p, err2 := parser.New(bytes.NewReader(literal))
|
||||
if err2 != nil {
|
||||
return fmt.Errorf("failed to parse literal: %w", err2)
|
||||
}
|
||||
if slices.Contains(labelIDs, proton.DraftsLabel) {
|
||||
msg, err := s.createDraft(ctx, literal, addrKR, addr)
|
||||
msg, err := s.createDraftWithParser(ctx, p, addrKR, addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create draft: %w", err)
|
||||
}
|
||||
|
||||
// apply labels
|
||||
|
||||
messageID = msg.ID
|
||||
} else {
|
||||
// multipart body requires at least one text part to be properly encrypted.
|
||||
if p.AttachEmptyTextPartIfNoneExists() {
|
||||
buf := new(bytes.Buffer)
|
||||
if err := p.NewWriter().Write(buf); err != nil {
|
||||
return fmt.Errorf("failed build new MIMEBody: %w", err)
|
||||
}
|
||||
literal = buf.Bytes()
|
||||
}
|
||||
str, err := s.client.ImportMessages(ctx, addrKR, 1, 1, []proton.ImportReq{{
|
||||
Metadata: proton.ImportMetadata{
|
||||
AddressID: s.addrID,
|
||||
@ -728,13 +738,7 @@ func (s *Connector) importMessage(
|
||||
return toIMAPMessage(full.MessageMetadata), literal, nil
|
||||
}
|
||||
|
||||
func (s *Connector) createDraft(ctx context.Context, literal []byte, addrKR *crypto.KeyRing, sender proton.Address) (proton.Message, error) {
|
||||
// Create a new message parser from the reader.
|
||||
parser, err := parser.New(bytes.NewReader(literal))
|
||||
if err != nil {
|
||||
return proton.Message{}, fmt.Errorf("failed to create parser: %w", err)
|
||||
}
|
||||
|
||||
func (s *Connector) createDraftWithParser(ctx context.Context, parser *parser.Parser, addrKR *crypto.KeyRing, sender proton.Address) (proton.Message, error) {
|
||||
message, err := message.ParseWithParser(parser, true)
|
||||
if err != nil {
|
||||
return proton.Message{}, fmt.Errorf("failed to parse message: %w", err)
|
||||
|
||||
@ -274,6 +274,10 @@ func (s *Service) HandleRefreshEvent(ctx context.Context, _ proton.RefreshFlag)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.rebuildConnectors(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.syncStateProvider.ClearSyncStatus(ctx); err != nil {
|
||||
return fmt.Errorf("failed to clear sync status:%w", err)
|
||||
}
|
||||
@ -292,6 +296,7 @@ func (s *Service) HandleUserEvent(_ context.Context, user *proton.User) error {
|
||||
|
||||
return s.identityState.Write(func(identity *useridentity.State) error {
|
||||
identity.OnUserEvent(*user)
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
@ -24,12 +24,18 @@ import (
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/useridentity"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/usertypes"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (s *Service) HandleAddressEvents(ctx context.Context, events []proton.AddressEvent) error {
|
||||
s.log.Debug("handling address event")
|
||||
|
||||
if s.addressMode == usertypes.AddressModeCombined {
|
||||
oldPrimaryAddr, err := s.identityState.GetPrimaryAddress()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get primary addr: %w", err)
|
||||
}
|
||||
|
||||
if err := s.identityState.Write(func(identity *useridentity.State) error {
|
||||
identity.OnAddressEvents(events)
|
||||
return nil
|
||||
@ -38,6 +44,28 @@ func (s *Service) HandleAddressEvents(ctx context.Context, events []proton.Addre
|
||||
return err
|
||||
}
|
||||
|
||||
newPrimaryAddr, err := s.identityState.GetPrimaryAddress()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get primary addr after update: %w", err)
|
||||
}
|
||||
|
||||
if oldPrimaryAddr.ID == newPrimaryAddr.ID {
|
||||
return nil
|
||||
}
|
||||
|
||||
connector, ok := s.connectors[oldPrimaryAddr.ID]
|
||||
if !ok {
|
||||
return fmt.Errorf("could not find old primary addr conncetor after default address change")
|
||||
}
|
||||
|
||||
s.connectors[newPrimaryAddr.ID] = connector
|
||||
delete(s.connectors, oldPrimaryAddr.ID)
|
||||
|
||||
s.log.WithFields(logrus.Fields{
|
||||
"old": oldPrimaryAddr.Email,
|
||||
"new": newPrimaryAddr.Email,
|
||||
}).Debug("Primary address changed")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@ -19,15 +19,13 @@ package imapservice
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
)
|
||||
|
||||
type syncReporter struct {
|
||||
userID string
|
||||
eventPublisher events.EventPublisher
|
||||
|
||||
type syncData struct {
|
||||
start time.Time
|
||||
total int64
|
||||
count int64
|
||||
@ -36,8 +34,25 @@ type syncReporter struct {
|
||||
freq time.Duration
|
||||
}
|
||||
|
||||
type syncReporter struct {
|
||||
userID string
|
||||
eventPublisher events.EventPublisher
|
||||
|
||||
dataLock sync.Mutex
|
||||
data syncData
|
||||
}
|
||||
|
||||
func (rep *syncReporter) withData(f func(s *syncData)) {
|
||||
rep.dataLock.Lock()
|
||||
defer rep.dataLock.Unlock()
|
||||
|
||||
f(&rep.data)
|
||||
}
|
||||
|
||||
func (rep *syncReporter) OnStart(ctx context.Context) {
|
||||
rep.start = time.Now()
|
||||
rep.withData(func(s *syncData) {
|
||||
s.start = time.Now()
|
||||
})
|
||||
rep.eventPublisher.PublishEvent(ctx, events.SyncStarted{UserID: rep.userID})
|
||||
}
|
||||
|
||||
@ -55,35 +70,38 @@ func (rep *syncReporter) OnError(ctx context.Context, err error) {
|
||||
}
|
||||
|
||||
func (rep *syncReporter) OnProgress(ctx context.Context, delta int64) {
|
||||
rep.count += delta
|
||||
rep.withData(func(s *syncData) {
|
||||
s.count += delta
|
||||
var progress float64
|
||||
var remaining time.Duration
|
||||
|
||||
var progress float64
|
||||
var remaining time.Duration
|
||||
// It's possible for count to be bigger or smaller than total depending on when the sync begins and whether new
|
||||
// messages are added/removed during this period. When this happens just limited the progress to 100%.
|
||||
if s.count > s.total {
|
||||
progress = 1
|
||||
} else {
|
||||
progress = float64(s.count) / float64(s.total)
|
||||
remaining = time.Since(s.start) * time.Duration(s.total-(s.count+1)) / time.Duration(s.count+1)
|
||||
}
|
||||
|
||||
// It's possible for count to be bigger or smaller than total depending on when the sync begins and whether new
|
||||
// messages are added/removed during this period. When this happens just limited the progress to 100%.
|
||||
if rep.count > rep.total {
|
||||
progress = 1
|
||||
} else {
|
||||
progress = float64(rep.count) / float64(rep.total)
|
||||
remaining = time.Since(rep.start) * time.Duration(rep.total-(rep.count+1)) / time.Duration(rep.count+1)
|
||||
}
|
||||
if time.Since(s.last) > s.freq {
|
||||
rep.eventPublisher.PublishEvent(ctx, events.SyncProgress{
|
||||
UserID: rep.userID,
|
||||
Progress: progress,
|
||||
Elapsed: time.Since(s.start),
|
||||
Remaining: remaining,
|
||||
})
|
||||
|
||||
if time.Since(rep.last) > rep.freq {
|
||||
rep.eventPublisher.PublishEvent(ctx, events.SyncProgress{
|
||||
UserID: rep.userID,
|
||||
Progress: progress,
|
||||
Elapsed: time.Since(rep.start),
|
||||
Remaining: remaining,
|
||||
})
|
||||
|
||||
rep.last = time.Now()
|
||||
}
|
||||
s.last = time.Now()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (rep *syncReporter) InitializeProgressCounter(_ context.Context, current int64, total int64) {
|
||||
rep.count = current
|
||||
rep.total = total
|
||||
rep.withData(func(s *syncData) {
|
||||
s.count = current
|
||||
s.total = total
|
||||
})
|
||||
}
|
||||
|
||||
func newSyncReporter(userID string, eventsPublisher events.EventPublisher, freq time.Duration) *syncReporter {
|
||||
@ -91,7 +109,9 @@ func newSyncReporter(userID string, eventsPublisher events.EventPublisher, freq
|
||||
userID: userID,
|
||||
eventPublisher: eventsPublisher,
|
||||
|
||||
start: time.Now(),
|
||||
freq: freq,
|
||||
data: syncData{
|
||||
start: time.Now(),
|
||||
freq: freq,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -102,7 +102,7 @@ func (s *Service) smtpSendMail(ctx context.Context, authID string, from string,
|
||||
}
|
||||
}
|
||||
|
||||
if !fromAddr.Send {
|
||||
if !fromAddr.Send || fromAddr.Status != proton.AddressStatusEnabled {
|
||||
s.log.Errorf("Can't send emails on address: %v", fromAddr.Email)
|
||||
return &ErrCanNotSendOnAddress{address: fromAddr.Email}
|
||||
}
|
||||
@ -138,7 +138,11 @@ func (s *Service) smtpSendMail(ctx context.Context, authID string, from string,
|
||||
return fmt.Errorf("failed to get public key: %w", err)
|
||||
}
|
||||
|
||||
parser.AttachPublicKey(pubKey, fmt.Sprintf("publickey - %v - %v", addrKR.GetIdentities()[0].Name, key.GetFingerprint()[:8]))
|
||||
parser.AttachPublicKey(pubKey, fmt.Sprintf(
|
||||
"publickey - %v - 0x%v",
|
||||
addrKR.GetIdentities()[0].Name,
|
||||
strings.ToUpper(key.GetFingerprint()[:8]),
|
||||
))
|
||||
}
|
||||
|
||||
// Parse the message we want to send (after we have attached the public key).
|
||||
|
||||
@ -210,12 +210,14 @@ func (t *Handler) run(ctx context.Context,
|
||||
stageContext.metadataFetched = syncStatus.NumSyncedMessages
|
||||
stageContext.totalMessageCount = syncStatus.TotalMessageCount
|
||||
|
||||
defer stageContext.Close()
|
||||
|
||||
t.regulator.Sync(ctx, stageContext)
|
||||
if err := t.regulator.Sync(ctx, stageContext); err != nil {
|
||||
stageContext.onError(err)
|
||||
_ = stageContext.waitAndClose(ctx)
|
||||
return fmt.Errorf("failed to start sync job: %w", err)
|
||||
}
|
||||
|
||||
// Wait on reply
|
||||
if err := stageContext.wait(ctx); err != nil {
|
||||
if err := stageContext.waitAndClose(ctx); err != nil {
|
||||
return fmt.Errorf("failed sync messages: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@ -64,7 +64,7 @@ func (s Status) InProgress() bool {
|
||||
|
||||
// Regulator is an abstraction for the sync service, since it regulates the number of concurrent sync activities.
|
||||
type Regulator interface {
|
||||
Sync(ctx context.Context, stage *Job)
|
||||
Sync(ctx context.Context, stage *Job) error
|
||||
}
|
||||
|
||||
type BuildResult struct {
|
||||
|
||||
@ -19,9 +19,6 @@ package syncservice
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
@ -48,10 +45,8 @@ type Job struct {
|
||||
updateApplier UpdateApplier
|
||||
syncReporter Reporter
|
||||
|
||||
log *logrus.Entry
|
||||
errorCh *async.QueuedChannel[error]
|
||||
wg sync.WaitGroup
|
||||
once sync.Once
|
||||
log *logrus.Entry
|
||||
jw *jobWaiter
|
||||
|
||||
panicHandler async.PanicHandler
|
||||
downloadCache *DownloadCache
|
||||
@ -74,7 +69,7 @@ func NewJob(ctx context.Context,
|
||||
) *Job {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
|
||||
return &Job{
|
||||
j := &Job{
|
||||
ctx: ctx,
|
||||
client: client,
|
||||
userID: userID,
|
||||
@ -85,26 +80,23 @@ func NewJob(ctx context.Context,
|
||||
messageBuilder: messageBuilder,
|
||||
updateApplier: updateApplier,
|
||||
syncReporter: syncReporter,
|
||||
errorCh: async.NewQueuedChannel[error](4, 8, panicHandler, fmt.Sprintf("sync-job-error-%v", userID)),
|
||||
panicHandler: panicHandler,
|
||||
downloadCache: cache,
|
||||
jw: newJobWaiter(log.WithField("sync-job", "waiter"), panicHandler),
|
||||
}
|
||||
|
||||
j.jw.begin()
|
||||
|
||||
return j
|
||||
}
|
||||
|
||||
func (j *Job) Close() {
|
||||
j.errorCh.CloseAndDiscardQueued()
|
||||
j.wg.Wait()
|
||||
func (j *Job) close() {
|
||||
j.jw.close()
|
||||
}
|
||||
|
||||
func (j *Job) onError(err error) {
|
||||
defer j.wg.Done()
|
||||
defer j.jw.onTaskFinished(err)
|
||||
|
||||
// context cancelled is caught & handled in a different location.
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return
|
||||
}
|
||||
|
||||
j.errorCh.Enqueue(err)
|
||||
j.cancel()
|
||||
}
|
||||
|
||||
@ -119,55 +111,40 @@ func (j *Job) onJobFinished(ctx context.Context, lastMessageID string, count int
|
||||
return
|
||||
}
|
||||
|
||||
// j.onError() also calls j.wg.Done().
|
||||
j.wg.Done()
|
||||
// j.onError() also calls j.jw.onTaskFinished().
|
||||
defer j.jw.onTaskFinished(nil)
|
||||
j.syncReporter.OnProgress(ctx, count)
|
||||
}
|
||||
|
||||
// begin is expected to be called once the job enters the pipeline.
|
||||
func (j *Job) begin() {
|
||||
j.log.Info("Job started")
|
||||
j.wg.Add(1)
|
||||
j.startChildWaiter()
|
||||
}
|
||||
|
||||
// end is expected to be called once the job has no further work left.
|
||||
func (j *Job) end() {
|
||||
j.log.Info("Job finished")
|
||||
j.wg.Done()
|
||||
j.jw.onTaskFinished(nil)
|
||||
}
|
||||
|
||||
// wait waits until the job has finished, the context got cancelled or an error occurred.
|
||||
func (j *Job) wait(ctx context.Context) error {
|
||||
defer j.wg.Wait()
|
||||
|
||||
// waitAndClose waits until the job has finished, the context got cancelled or an error occurred.
|
||||
func (j *Job) waitAndClose(ctx context.Context) error {
|
||||
defer j.close()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
j.cancel()
|
||||
<-j.jw.doneCh
|
||||
return ctx.Err()
|
||||
case err := <-j.errorCh.GetChannel():
|
||||
return err
|
||||
case e := <-j.jw.doneCh:
|
||||
return e
|
||||
}
|
||||
}
|
||||
|
||||
func (j *Job) newChildJob(messageID string, messageCount int64) childJob {
|
||||
j.log.Infof("Creating new child job")
|
||||
j.wg.Add(1)
|
||||
j.jw.onTaskCreated()
|
||||
return childJob{job: j, lastMessageID: messageID, messageCount: messageCount}
|
||||
}
|
||||
|
||||
func (j *Job) startChildWaiter() {
|
||||
j.once.Do(func() {
|
||||
go func() {
|
||||
defer async.HandlePanic(j.panicHandler)
|
||||
|
||||
j.wg.Wait()
|
||||
j.log.Info("All child jobs succeeded")
|
||||
j.errorCh.Enqueue(j.ctx.Err())
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
// childJob represents a batch of work that goes down the pipeline. It keeps track of the message ID that is in the
|
||||
// batch and the number of messages in the batch.
|
||||
type childJob struct {
|
||||
@ -232,7 +209,7 @@ func (s *childJob) checkCancelled() bool {
|
||||
err := s.job.ctx.Err()
|
||||
if err != nil {
|
||||
s.job.log.Infof("Child job exit due to context cancelled")
|
||||
s.job.wg.Done()
|
||||
s.job.jw.onTaskFinished(err)
|
||||
return true
|
||||
}
|
||||
|
||||
@ -242,3 +219,95 @@ func (s *childJob) checkCancelled() bool {
|
||||
func (s *childJob) getContext() context.Context {
|
||||
return s.job.ctx
|
||||
}
|
||||
|
||||
type JobWaiterMessage int
|
||||
|
||||
const (
|
||||
JobWaiterMessageCreated JobWaiterMessage = iota
|
||||
JobWaiterMessageFinished
|
||||
)
|
||||
|
||||
type jobWaiterMessagePair struct {
|
||||
m JobWaiterMessage
|
||||
err error
|
||||
}
|
||||
|
||||
// jobWaiter is meant to be used to track ongoing sync batches. Once all the child jobs
|
||||
// have completed, the first recorded error (if any) will be written to doneCh and then this
|
||||
// channel will be closed.
|
||||
type jobWaiter struct {
|
||||
ch chan jobWaiterMessagePair
|
||||
doneCh chan error
|
||||
log *logrus.Entry
|
||||
panicHandler async.PanicHandler
|
||||
}
|
||||
|
||||
func newJobWaiter(log *logrus.Entry, panicHandler async.PanicHandler) *jobWaiter {
|
||||
return &jobWaiter{
|
||||
ch: make(chan jobWaiterMessagePair),
|
||||
doneCh: make(chan error, 2),
|
||||
log: log,
|
||||
panicHandler: panicHandler,
|
||||
}
|
||||
}
|
||||
|
||||
func (j *jobWaiter) close() {
|
||||
close(j.ch)
|
||||
}
|
||||
|
||||
func (j *jobWaiter) sendMessage(m JobWaiterMessage, err error) {
|
||||
j.ch <- jobWaiterMessagePair{
|
||||
m: m,
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
func (j *jobWaiter) onTaskFinished(err error) {
|
||||
j.sendMessage(JobWaiterMessageFinished, err)
|
||||
}
|
||||
|
||||
func (j *jobWaiter) onTaskCreated() {
|
||||
j.sendMessage(JobWaiterMessageCreated, nil)
|
||||
}
|
||||
|
||||
func (j *jobWaiter) begin() {
|
||||
go func() {
|
||||
defer async.HandlePanic(j.panicHandler)
|
||||
|
||||
total := 1
|
||||
var err error
|
||||
|
||||
defer func() {
|
||||
j.doneCh <- err
|
||||
close(j.doneCh)
|
||||
}()
|
||||
|
||||
for {
|
||||
m, ok := <-j.ch
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
switch m.m {
|
||||
case JobWaiterMessageCreated:
|
||||
total++
|
||||
case JobWaiterMessageFinished:
|
||||
total--
|
||||
if m.err != nil && err == nil {
|
||||
err = m.err
|
||||
}
|
||||
default:
|
||||
j.log.Errorf("Unknown message type: %v", m.m)
|
||||
continue
|
||||
}
|
||||
|
||||
if total <= 0 {
|
||||
if total < 0 {
|
||||
logrus.Errorf("Child count less than 0, shouldn't happen...")
|
||||
}
|
||||
j.log.Info("All child jobs completed")
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@ -20,6 +20,7 @@ package syncservice
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
@ -56,8 +57,7 @@ func TestJob_WaitsOnChildren(t *testing.T) {
|
||||
tj.job.end()
|
||||
}()
|
||||
|
||||
require.NoError(t, tj.job.wait(context.Background()))
|
||||
tj.job.Close()
|
||||
require.NoError(t, tj.job.waitAndClose(context.Background()))
|
||||
}
|
||||
|
||||
func TestJob_WaitsOnAllChildrenOnError(t *testing.T) {
|
||||
@ -73,18 +73,23 @@ func TestJob_WaitsOnAllChildrenOnError(t *testing.T) {
|
||||
|
||||
jobErr := errors.New("failed")
|
||||
|
||||
startCh := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
job1 := tj.job.newChildJob("1", 0)
|
||||
job2 := tj.job.newChildJob("2", 1)
|
||||
|
||||
<-startCh
|
||||
|
||||
job1.onFinished(context.Background())
|
||||
job2.onError(jobErr)
|
||||
tj.job.end()
|
||||
}()
|
||||
|
||||
err := tj.job.wait(context.Background())
|
||||
close(startCh)
|
||||
err := tj.job.waitAndClose(context.Background())
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, jobErr)
|
||||
tj.job.Close()
|
||||
}
|
||||
|
||||
func TestJob_MultipleChildrenReportError(t *testing.T) {
|
||||
@ -99,20 +104,23 @@ func TestJob_MultipleChildrenReportError(t *testing.T) {
|
||||
|
||||
startCh := make(chan struct{})
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
for i := 0; i < 10; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
job := tj.job.newChildJob("1", 0)
|
||||
wg.Done()
|
||||
<-startCh
|
||||
|
||||
job.onError(jobErr)
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
tj.job.end()
|
||||
close(startCh)
|
||||
err := tj.job.wait(context.Background())
|
||||
err := tj.job.waitAndClose(context.Background())
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, jobErr)
|
||||
tj.job.Close()
|
||||
}
|
||||
|
||||
func TestJob_ChildFailureCancelsAllOtherChildJobs(t *testing.T) {
|
||||
@ -127,8 +135,12 @@ func TestJob_ChildFailureCancelsAllOtherChildJobs(t *testing.T) {
|
||||
|
||||
failJob := tj.job.newChildJob("0", 1)
|
||||
|
||||
tj.job.begin()
|
||||
wg := sync.WaitGroup{}
|
||||
for i := 0; i < 10; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
job := tj.job.newChildJob("1", 0)
|
||||
<-job.getContext().Done()
|
||||
require.ErrorIs(t, job.getContext().Err(), context.Canceled)
|
||||
@ -137,12 +149,13 @@ func TestJob_ChildFailureCancelsAllOtherChildJobs(t *testing.T) {
|
||||
}
|
||||
go func() {
|
||||
failJob.onError(jobErr)
|
||||
wg.Wait()
|
||||
tj.job.end()
|
||||
}()
|
||||
|
||||
err := tj.job.wait(context.Background())
|
||||
err := tj.job.waitAndClose(context.Background())
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, jobErr)
|
||||
tj.job.Close()
|
||||
}
|
||||
|
||||
func TestJob_CtxCancelCancelsAllChildren(t *testing.T) {
|
||||
@ -154,9 +167,12 @@ func TestJob_CtxCancelCancelsAllChildren(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
tj := newTestJob(ctx, mockCtrl, "u", getTestLabels())
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
for i := 0; i < 10; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
job := tj.job.newChildJob("1", 0)
|
||||
wg.Done()
|
||||
<-job.getContext().Done()
|
||||
require.ErrorIs(t, job.getContext().Err(), context.Canceled)
|
||||
require.True(t, job.checkCancelled())
|
||||
@ -164,13 +180,37 @@ func TestJob_CtxCancelCancelsAllChildren(t *testing.T) {
|
||||
}
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
tj.job.end()
|
||||
cancel()
|
||||
}()
|
||||
|
||||
err := tj.job.wait(ctx)
|
||||
err := tj.job.waitAndClose(ctx)
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, context.Canceled)
|
||||
}
|
||||
|
||||
func TestJob_CtxCancelBeforeBegin(t *testing.T) {
|
||||
options := setupGoLeak()
|
||||
defer goleak.VerifyNone(t, options)
|
||||
|
||||
mockCtrl := gomock.NewController(t)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
tj := newTestJob(ctx, mockCtrl, "u", getTestLabels())
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
wg.Wait()
|
||||
cancel()
|
||||
tj.job.end()
|
||||
}()
|
||||
|
||||
wg.Done()
|
||||
err := tj.job.waitAndClose(ctx)
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, context.Canceled)
|
||||
tj.job.Close()
|
||||
}
|
||||
|
||||
func TestJob_WithoutChildJobsCanBeTerminated(t *testing.T) {
|
||||
@ -186,9 +226,8 @@ func TestJob_WithoutChildJobsCanBeTerminated(t *testing.T) {
|
||||
tj.job.begin()
|
||||
tj.job.end()
|
||||
}()
|
||||
err := tj.job.wait(ctx)
|
||||
err := tj.job.waitAndClose(context.Background())
|
||||
require.NoError(t, err)
|
||||
tj.job.Close()
|
||||
}
|
||||
|
||||
type tjob struct {
|
||||
|
||||
@ -127,9 +127,11 @@ func (mr *MockBuildStageOutputMockRecorder) Close() *gomock.Call {
|
||||
}
|
||||
|
||||
// Produce mocks base method.
|
||||
func (m *MockBuildStageOutput) Produce(arg0 context.Context, arg1 ApplyRequest) {
|
||||
func (m *MockBuildStageOutput) Produce(arg0 context.Context, arg1 ApplyRequest) error {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Produce", arg0, arg1)
|
||||
ret := m.ctrl.Call(m, "Produce", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Produce indicates an expected call of Produce.
|
||||
@ -212,9 +214,11 @@ func (mr *MockDownloadStageOutputMockRecorder) Close() *gomock.Call {
|
||||
}
|
||||
|
||||
// Produce mocks base method.
|
||||
func (m *MockDownloadStageOutput) Produce(arg0 context.Context, arg1 BuildRequest) {
|
||||
func (m *MockDownloadStageOutput) Produce(arg0 context.Context, arg1 BuildRequest) error {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Produce", arg0, arg1)
|
||||
ret := m.ctrl.Call(m, "Produce", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Produce indicates an expected call of Produce.
|
||||
@ -297,9 +301,11 @@ func (mr *MockMetadataStageOutputMockRecorder) Close() *gomock.Call {
|
||||
}
|
||||
|
||||
// Produce mocks base method.
|
||||
func (m *MockMetadataStageOutput) Produce(arg0 context.Context, arg1 DownloadRequest) {
|
||||
func (m *MockMetadataStageOutput) Produce(arg0 context.Context, arg1 DownloadRequest) error {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Produce", arg0, arg1)
|
||||
ret := m.ctrl.Call(m, "Produce", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Produce indicates an expected call of Produce.
|
||||
@ -478,9 +484,11 @@ func (m *MockRegulator) EXPECT() *MockRegulatorMockRecorder {
|
||||
}
|
||||
|
||||
// Sync mocks base method.
|
||||
func (m *MockRegulator) Sync(arg0 context.Context, arg1 *Job) {
|
||||
func (m *MockRegulator) Sync(arg0 context.Context, arg1 *Job) error {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Sync", arg0, arg1)
|
||||
ret := m.ctrl.Call(m, "Sync", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Sync indicates an expected call of Sync.
|
||||
|
||||
@ -33,7 +33,7 @@ type Service struct {
|
||||
applyStage *ApplyStage
|
||||
limits syncLimits
|
||||
metaCh *ChannelConsumerProducer[*Job]
|
||||
panicHandler async.PanicHandler
|
||||
group *async.Group
|
||||
}
|
||||
|
||||
func NewService(reporter reporter.Reporter,
|
||||
@ -53,26 +53,22 @@ func NewService(reporter reporter.Reporter,
|
||||
buildStage: NewBuildStage(buildCh, applyCh, limits.MessageBuildMem, panicHandler, reporter),
|
||||
applyStage: NewApplyStage(applyCh),
|
||||
metaCh: metaCh,
|
||||
panicHandler: panicHandler,
|
||||
group: async.NewGroup(context.Background(), panicHandler),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Run(group *async.Group) {
|
||||
group.Once(func(ctx context.Context) {
|
||||
syncGroup := async.NewGroup(ctx, s.panicHandler)
|
||||
|
||||
s.metadataStage.Run(syncGroup)
|
||||
s.downloadStage.Run(syncGroup)
|
||||
s.buildStage.Run(syncGroup)
|
||||
s.applyStage.Run(syncGroup)
|
||||
|
||||
defer s.metaCh.Close()
|
||||
defer syncGroup.CancelAndWait()
|
||||
|
||||
<-ctx.Done()
|
||||
})
|
||||
func (s *Service) Run() {
|
||||
s.metadataStage.Run(s.group)
|
||||
s.downloadStage.Run(s.group)
|
||||
s.buildStage.Run(s.group)
|
||||
s.applyStage.Run(s.group)
|
||||
}
|
||||
|
||||
func (s *Service) Sync(ctx context.Context, stage *Job) {
|
||||
s.metaCh.Produce(ctx, stage)
|
||||
func (s *Service) Sync(ctx context.Context, stage *Job) error {
|
||||
return s.metaCh.Produce(ctx, stage)
|
||||
}
|
||||
|
||||
func (s *Service) Close() {
|
||||
s.group.CancelAndWait()
|
||||
s.metaCh.Close()
|
||||
}
|
||||
|
||||
@ -50,12 +50,12 @@ func TestApplyStage_CancelledJobIsDiscarded(t *testing.T) {
|
||||
}()
|
||||
|
||||
jobCancel()
|
||||
input.Produce(ctx, ApplyRequest{
|
||||
require.NoError(t, input.Produce(ctx, ApplyRequest{
|
||||
childJob: childJob,
|
||||
messages: nil,
|
||||
})
|
||||
}))
|
||||
|
||||
err := tj.job.wait(ctx)
|
||||
err := tj.job.waitAndClose(ctx)
|
||||
require.ErrorIs(t, err, context.Canceled)
|
||||
cancel()
|
||||
}
|
||||
@ -84,12 +84,12 @@ func TestApplyStage_JobWithNoMessagesIsFinalized(t *testing.T) {
|
||||
stage.run(ctx)
|
||||
}()
|
||||
|
||||
input.Produce(ctx, ApplyRequest{
|
||||
require.NoError(t, input.Produce(ctx, ApplyRequest{
|
||||
childJob: childJob,
|
||||
messages: nil,
|
||||
})
|
||||
}))
|
||||
|
||||
err := tj.job.wait(ctx)
|
||||
err := tj.job.waitAndClose(ctx)
|
||||
cancel()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@ -127,12 +127,12 @@ func TestApplyStage_ErrorOnApplyIsReportedAndJobFails(t *testing.T) {
|
||||
stage.run(ctx)
|
||||
}()
|
||||
|
||||
input.Produce(ctx, ApplyRequest{
|
||||
require.NoError(t, input.Produce(ctx, ApplyRequest{
|
||||
childJob: childJob,
|
||||
messages: buildResults,
|
||||
})
|
||||
}))
|
||||
|
||||
err := tj.job.wait(ctx)
|
||||
err := tj.job.waitAndClose(ctx)
|
||||
cancel()
|
||||
require.ErrorIs(t, err, applyErr)
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
@ -182,10 +183,12 @@ func (b *BuildStage) run(ctx context.Context) {
|
||||
|
||||
outJob.onStageCompleted(ctx)
|
||||
|
||||
b.output.Produce(ctx, ApplyRequest{
|
||||
if err := b.output.Produce(ctx, ApplyRequest{
|
||||
childJob: outJob,
|
||||
messages: success,
|
||||
})
|
||||
}); err != nil {
|
||||
return fmt.Errorf("failed to produce output for next stage: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@ -111,7 +111,7 @@ func TestBuildStage_SuccessRemovesFailedMessage(t *testing.T) {
|
||||
stage.run(ctx)
|
||||
}()
|
||||
|
||||
input.Produce(ctx, BuildRequest{childJob: childJob, batch: []proton.FullMessage{msg}})
|
||||
require.NoError(t, input.Produce(ctx, BuildRequest{childJob: childJob, batch: []proton.FullMessage{msg}}))
|
||||
|
||||
req, err := output.Consume(ctx)
|
||||
cancel()
|
||||
@ -170,7 +170,7 @@ func TestBuildStage_BuildFailureIsReportedButDoesNotCancelJob(t *testing.T) {
|
||||
stage.run(ctx)
|
||||
}()
|
||||
|
||||
input.Produce(ctx, BuildRequest{childJob: childJob, batch: []proton.FullMessage{msg}})
|
||||
require.NoError(t, input.Produce(ctx, BuildRequest{childJob: childJob, batch: []proton.FullMessage{msg}}))
|
||||
|
||||
req, err := output.Consume(ctx)
|
||||
cancel()
|
||||
@ -222,7 +222,7 @@ func TestBuildStage_FailedToLocateKeyRingIsReportedButDoesNotFailBuild(t *testin
|
||||
stage.run(ctx)
|
||||
}()
|
||||
|
||||
input.Produce(ctx, BuildRequest{childJob: childJob, batch: []proton.FullMessage{msg}})
|
||||
require.NoError(t, input.Produce(ctx, BuildRequest{childJob: childJob, batch: []proton.FullMessage{msg}}))
|
||||
|
||||
req, err := output.Consume(ctx)
|
||||
cancel()
|
||||
@ -267,9 +267,9 @@ func TestBuildStage_OtherErrorsFailJob(t *testing.T) {
|
||||
stage.run(ctx)
|
||||
}()
|
||||
|
||||
input.Produce(ctx, BuildRequest{childJob: childJob, batch: []proton.FullMessage{msg}})
|
||||
require.NoError(t, input.Produce(ctx, BuildRequest{childJob: childJob, batch: []proton.FullMessage{msg}}))
|
||||
|
||||
err := tj.job.wait(ctx)
|
||||
err := tj.job.waitAndClose(ctx)
|
||||
require.Equal(t, expectedErr, err)
|
||||
|
||||
cancel()
|
||||
@ -311,10 +311,10 @@ func TestBuildStage_CancelledJobIsDiscarded(t *testing.T) {
|
||||
}()
|
||||
|
||||
jobCancel()
|
||||
input.Produce(ctx, BuildRequest{
|
||||
require.NoError(t, input.Produce(ctx, BuildRequest{
|
||||
childJob: childJob,
|
||||
batch: []proton.FullMessage{msg},
|
||||
})
|
||||
}))
|
||||
|
||||
go func() { cancel() }()
|
||||
|
||||
|
||||
@ -21,6 +21,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
@ -183,10 +184,12 @@ func (d *DownloadStage) run(ctx context.Context) {
|
||||
// Step 5: Publish result.
|
||||
request.onStageCompleted(ctx)
|
||||
|
||||
d.output.Produce(ctx, BuildRequest{
|
||||
if err := d.output.Produce(ctx, BuildRequest{
|
||||
batch: result,
|
||||
childJob: request.childJob,
|
||||
})
|
||||
}); err != nil {
|
||||
request.job.onError(fmt.Errorf("failed to produce output for next stage: %w", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -189,10 +189,10 @@ func TestDownloadStage_Run(t *testing.T) {
|
||||
stage.run(ctx)
|
||||
}()
|
||||
|
||||
input.Produce(ctx, DownloadRequest{
|
||||
require.NoError(t, input.Produce(ctx, DownloadRequest{
|
||||
childJob: childJob,
|
||||
ids: msgIDs,
|
||||
})
|
||||
}))
|
||||
|
||||
out, err := output.Consume(ctx)
|
||||
require.NoError(t, err)
|
||||
@ -232,10 +232,10 @@ func TestDownloadStage_RunWith422(t *testing.T) {
|
||||
stage.run(ctx)
|
||||
}()
|
||||
|
||||
input.Produce(ctx, DownloadRequest{
|
||||
require.NoError(t, input.Produce(ctx, DownloadRequest{
|
||||
childJob: childJob,
|
||||
ids: msgIDs,
|
||||
})
|
||||
}))
|
||||
|
||||
out, err := output.Consume(ctx)
|
||||
require.NoError(t, err)
|
||||
@ -271,10 +271,11 @@ func TestDownloadStage_CancelledJobIsDiscarded(t *testing.T) {
|
||||
}()
|
||||
|
||||
jobCancel()
|
||||
input.Produce(ctx, DownloadRequest{
|
||||
|
||||
require.NoError(t, input.Produce(ctx, DownloadRequest{
|
||||
childJob: childJob,
|
||||
ids: nil,
|
||||
})
|
||||
}))
|
||||
|
||||
go func() { cancel() }()
|
||||
|
||||
@ -308,12 +309,12 @@ func TestDownloadStage_JobAbortsOnMessageDownloadError(t *testing.T) {
|
||||
stage.run(ctx)
|
||||
}()
|
||||
|
||||
input.Produce(ctx, DownloadRequest{
|
||||
require.NoError(t, input.Produce(ctx, DownloadRequest{
|
||||
childJob: childJob,
|
||||
ids: []string{"foo"},
|
||||
})
|
||||
}))
|
||||
|
||||
err := tj.job.wait(ctx)
|
||||
err := tj.job.waitAndClose(ctx)
|
||||
require.Equal(t, expectedErr, err)
|
||||
|
||||
cancel()
|
||||
@ -359,12 +360,12 @@ func TestDownloadStage_JobAbortsOnAttachmentDownloadError(t *testing.T) {
|
||||
stage.run(ctx)
|
||||
}()
|
||||
|
||||
input.Produce(ctx, DownloadRequest{
|
||||
require.NoError(t, input.Produce(ctx, DownloadRequest{
|
||||
childJob: childJob,
|
||||
ids: []string{"foo"},
|
||||
})
|
||||
}))
|
||||
|
||||
err := tj.job.wait(ctx)
|
||||
err := tj.job.waitAndClose(ctx)
|
||||
require.Equal(t, expectedErr, err)
|
||||
|
||||
cancel()
|
||||
|
||||
@ -20,6 +20,7 @@ package syncservice
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/ProtonMail/gluon/logging"
|
||||
@ -115,7 +116,10 @@ func (m *MetadataStage) run(ctx context.Context, metadataPageSize int, maxMessag
|
||||
|
||||
output.onStageCompleted(ctx)
|
||||
|
||||
m.output.Produce(ctx, output)
|
||||
if err := m.output.Produce(ctx, output); err != nil {
|
||||
job.onError(fmt.Errorf("failed to produce output for next stage: %w", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// If this job has no more work left, signal completion.
|
||||
|
||||
@ -21,6 +21,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
@ -59,7 +60,7 @@ func TestMetadataStage_RunFinishesWith429(t *testing.T) {
|
||||
metadata.run(ctx, TestMetadataPageSize, TestMaxMessages, &network.NoCoolDown{})
|
||||
}()
|
||||
|
||||
input.Produce(ctx, tj.job)
|
||||
require.NoError(t, input.Produce(ctx, tj.job))
|
||||
|
||||
for _, chunk := range xslices.Chunk(msgs, TestMaxMessages) {
|
||||
tj.syncReporter.EXPECT().OnProgress(gomock.Any(), gomock.Eq(int64(len(chunk))))
|
||||
@ -93,7 +94,10 @@ func TestMetadataStage_JobCorrectlyFinishesAfterCancel(t *testing.T) {
|
||||
metadata.run(ctx, TestMetadataPageSize, TestMaxMessages, &network.NoCoolDown{})
|
||||
}()
|
||||
|
||||
input.Produce(ctx, tj.job)
|
||||
{
|
||||
err := input.Produce(ctx, tj.job)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// read one output then cancel
|
||||
request, err := output.Consume(ctx)
|
||||
@ -102,8 +106,11 @@ func TestMetadataStage_JobCorrectlyFinishesAfterCancel(t *testing.T) {
|
||||
// cancel job context
|
||||
jobCancel()
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
// The next stages should check whether the job has been cancelled or not. Here we need to do it manually.
|
||||
go func() {
|
||||
wg.Done()
|
||||
for {
|
||||
req, err := output.Consume(ctx)
|
||||
if err != nil {
|
||||
@ -113,8 +120,9 @@ func TestMetadataStage_JobCorrectlyFinishesAfterCancel(t *testing.T) {
|
||||
req.checkCancelled()
|
||||
}
|
||||
}()
|
||||
|
||||
err = tj.job.wait(context.Background())
|
||||
wg.Wait()
|
||||
err = tj.job.waitAndClose(ctx)
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, context.Canceled)
|
||||
cancel()
|
||||
}
|
||||
@ -149,8 +157,8 @@ func TestMetadataStage_RunInterleaved(t *testing.T) {
|
||||
}()
|
||||
|
||||
go func() {
|
||||
input.Produce(ctx, tj1.job)
|
||||
input.Produce(ctx, tj2.job)
|
||||
require.NoError(t, input.Produce(ctx, tj1.job))
|
||||
require.NoError(t, input.Produce(ctx, tj2.job))
|
||||
}()
|
||||
|
||||
go func() {
|
||||
@ -165,8 +173,8 @@ func TestMetadataStage_RunInterleaved(t *testing.T) {
|
||||
}
|
||||
}()
|
||||
|
||||
require.NoError(t, tj1.job.wait(ctx))
|
||||
require.NoError(t, tj2.job.wait(ctx))
|
||||
require.NoError(t, tj1.job.waitAndClose(ctx))
|
||||
require.NoError(t, tj2.job.waitAndClose(ctx))
|
||||
cancel()
|
||||
}
|
||||
|
||||
|
||||
@ -23,7 +23,7 @@ import (
|
||||
)
|
||||
|
||||
type StageOutputProducer[T any] interface {
|
||||
Produce(ctx context.Context, value T)
|
||||
Produce(ctx context.Context, value T) error
|
||||
Close()
|
||||
}
|
||||
|
||||
@ -41,10 +41,12 @@ func NewChannelConsumerProducer[T any]() *ChannelConsumerProducer[T] {
|
||||
return &ChannelConsumerProducer[T]{ch: make(chan T)}
|
||||
}
|
||||
|
||||
func (c ChannelConsumerProducer[T]) Produce(ctx context.Context, value T) {
|
||||
func (c ChannelConsumerProducer[T]) Produce(ctx context.Context, value T) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case c.ch <- value:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -193,21 +193,33 @@ func (user *User) AutoconfigUsed(client string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (user *User) KBArticleOpened(article string) {
|
||||
func (user *User) ExternalLinkClicked(article string) {
|
||||
if !user.configStatus.IsPending() {
|
||||
return
|
||||
}
|
||||
|
||||
var kb_articles_to_track = [...]string{
|
||||
var trackedLinks = [...]string{
|
||||
"https://proton.me/support/bridge",
|
||||
"https://proton.me/support/protonmail-bridge-clients-apple-mail",
|
||||
"https://proton.me/support/protonmail-bridge-clients-macos-outlook-2019",
|
||||
"https://proton.me/support/protonmail-bridge-clients-windows-outlook-2019",
|
||||
"https://proton.me/support/protonmail-bridge-clients-windows-thunderbird",
|
||||
"https://proton.me/support/protonmail-bridge-configure-client",
|
||||
"https://proton.me/support/bridge-address-list-has-changed",
|
||||
"https://proton.me/blog/tls-ssl-certificate#Extra-security-precautions-taken-by-ProtonMail",
|
||||
"https://proton.me/support/bridge-cant-move-cache",
|
||||
"https://proton.me/support/difference-combined-addresses-mode-split-addresses-mode",
|
||||
"https://proton.me/support/bridge-imap-login-failed",
|
||||
"https://proton.me/support/port-already-occupied-error",
|
||||
"https://proton.me/support/bridge-cannot-access-keychain",
|
||||
"https://proton.me/support/protonmail-bridge-manual-update",
|
||||
"https://proton.me/support/bridge-internal-error",
|
||||
"https://proton.me/support/apple-mail-certificate",
|
||||
"https://proton.me/support/macos-certificate-warning",
|
||||
"https://proton.me/support/why-you-need-bridge",
|
||||
}
|
||||
|
||||
for id, url := range kb_articles_to_track {
|
||||
for id, url := range trackedLinks {
|
||||
if url == article {
|
||||
if err := user.configStatus.RecordLinkClicked(uint(id)); err != nil {
|
||||
user.log.WithError(err).Error("Failed to log LinkClicked in config_status.")
|
||||
|
||||
@ -133,7 +133,7 @@ func withUser(tb testing.TB, ctx context.Context, _ *server.Server, m *proton.Ma
|
||||
|
||||
v, corrupt, err := vault.New(tb.TempDir(), tb.TempDir(), []byte("my secret key"), nil)
|
||||
require.NoError(tb, err)
|
||||
require.False(tb, corrupt)
|
||||
require.NoError(tb, corrupt)
|
||||
|
||||
vaultUser, err := v.AddUser(apiUser.ID, username, username+"@pm.me", apiAuth.UID, apiAuth.RefreshToken, saltedKeyPass)
|
||||
require.NoError(tb, err)
|
||||
|
||||
@ -55,7 +55,7 @@ func TestMigrate(t *testing.T) {
|
||||
// Migrate the vault.
|
||||
s, corrupt, err := New(dir, "default-gluon-dir", []byte("my secret key"), async.NoopPanicHandler{})
|
||||
require.NoError(t, err)
|
||||
require.False(t, corrupt)
|
||||
require.NoError(t, corrupt)
|
||||
|
||||
// Check the migrated vault.
|
||||
require.Equal(t, "v2.3.x-gluon-dir", s.GetGluonCacheDir())
|
||||
|
||||
@ -68,7 +68,7 @@ func TestVault_Settings_GluonDir(t *testing.T) {
|
||||
// create a new test vault.
|
||||
s, corrupt, err := vault.New(t.TempDir(), "/path/to/gluon", []byte("my secret key"), async.NoopPanicHandler{})
|
||||
require.NoError(t, err)
|
||||
require.False(t, corrupt)
|
||||
require.NoError(t, corrupt)
|
||||
|
||||
// Check the default gluon dir.
|
||||
require.Equal(t, "/path/to/gluon", s.GetGluonCacheDir())
|
||||
|
||||
@ -19,6 +19,7 @@ package vault
|
||||
|
||||
import (
|
||||
"crypto/cipher"
|
||||
"fmt"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/vmihailenco/msgpack/v5"
|
||||
@ -34,12 +35,12 @@ func unmarshalFile[T any](gcm cipher.AEAD, b []byte, data *T) error {
|
||||
var f File
|
||||
|
||||
if err := msgpack.Unmarshal(b, &f); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("%w: %v", ErrUnmarshal, err)
|
||||
}
|
||||
|
||||
dec, err := gcm.Open(nil, f.Data[:gcm.NonceSize()], f.Data[gcm.NonceSize():], nil)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("%w: %v", ErrDecryptFailed, err)
|
||||
}
|
||||
|
||||
for v := f.Version; v < Current; v++ {
|
||||
@ -48,7 +49,11 @@ func unmarshalFile[T any](gcm cipher.AEAD, b []byte, data *T) error {
|
||||
}
|
||||
}
|
||||
|
||||
return msgpack.Unmarshal(dec, data)
|
||||
if err := msgpack.Unmarshal(dec, data); err != nil {
|
||||
return fmt.Errorf("%w: %v", ErrUnmarshal, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func marshalFile[T any](gcm cipher.AEAD, t T) ([]byte, error) {
|
||||
|
||||
@ -49,27 +49,31 @@ type Vault struct {
|
||||
panicHandler async.PanicHandler
|
||||
}
|
||||
|
||||
var ErrDecryptFailed = errors.New("failed to decrypt vault")
|
||||
var ErrUnmarshal = errors.New("vault contents are corrupt")
|
||||
|
||||
// New constructs a new encrypted data vault at the given filepath using the given encryption key.
|
||||
func New(vaultDir, gluonCacheDir string, key []byte, panicHandler async.PanicHandler) (*Vault, bool, error) {
|
||||
// The first error is a corruption error for an existing vault, the second errors refrain to all other errors.
|
||||
func New(vaultDir, gluonCacheDir string, key []byte, panicHandler async.PanicHandler) (*Vault, error, error) {
|
||||
if err := os.MkdirAll(vaultDir, 0o700); err != nil {
|
||||
return nil, false, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
hash256 := sha256.Sum256(key)
|
||||
|
||||
aes, err := aes.NewCipher(hash256[:])
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(aes)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
vault, corrupt, err := newVault(filepath.Join(vaultDir, "vault.enc"), gluonCacheDir, gcm)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
return nil, corrupt, err
|
||||
}
|
||||
|
||||
vault.panicHandler = panicHandler
|
||||
@ -341,28 +345,28 @@ func (vault *Vault) detachUser(userID string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func newVault(path, gluonDir string, gcm cipher.AEAD) (*Vault, bool, error) {
|
||||
func newVault(path, gluonDir string, gcm cipher.AEAD) (*Vault, error, error) {
|
||||
if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) {
|
||||
if _, err := initVault(path, gluonDir, gcm); err != nil {
|
||||
return nil, false, err
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
enc, err := os.ReadFile(filepath.Clean(path))
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var corrupt bool
|
||||
var corrupt error
|
||||
|
||||
if err := unmarshalFile(gcm, enc, new(Data)); err != nil {
|
||||
corrupt = true
|
||||
corrupt = err
|
||||
}
|
||||
|
||||
if corrupt {
|
||||
if corrupt != nil {
|
||||
newEnc, err := initVault(path, gluonDir, gcm)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
return nil, corrupt, err
|
||||
}
|
||||
|
||||
enc = newEnc
|
||||
|
||||
@ -34,7 +34,7 @@ func BenchmarkVault(b *testing.B) {
|
||||
// Create a new vault.
|
||||
s, corrupt, err := vault.New(vaultDir, gluonDir, []byte("my secret key"), async.NoopPanicHandler{})
|
||||
require.NoError(b, err)
|
||||
require.False(b, corrupt)
|
||||
require.NoError(b, corrupt)
|
||||
|
||||
// Add 10kB of cookies to the vault.
|
||||
require.NoError(b, s.SetCookies(bytes.Repeat([]byte("a"), 10_000)))
|
||||
|
||||
@ -34,19 +34,19 @@ func TestVault_Corrupt(t *testing.T) {
|
||||
{
|
||||
_, corrupt, err := vault.New(vaultDir, gluonDir, []byte("my secret key"), async.NoopPanicHandler{})
|
||||
require.NoError(t, err)
|
||||
require.False(t, corrupt)
|
||||
require.NoError(t, corrupt)
|
||||
}
|
||||
|
||||
{
|
||||
_, corrupt, err := vault.New(vaultDir, gluonDir, []byte("my secret key"), async.NoopPanicHandler{})
|
||||
require.NoError(t, err)
|
||||
require.False(t, corrupt)
|
||||
require.NoError(t, corrupt)
|
||||
}
|
||||
|
||||
{
|
||||
_, corrupt, err := vault.New(vaultDir, gluonDir, []byte("bad key"), async.NoopPanicHandler{})
|
||||
require.NoError(t, err)
|
||||
require.True(t, corrupt)
|
||||
require.ErrorIs(t, corrupt, vault.ErrDecryptFailed)
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,13 +56,13 @@ func TestVault_Corrupt_JunkData(t *testing.T) {
|
||||
{
|
||||
_, corrupt, err := vault.New(vaultDir, gluonDir, []byte("my secret key"), async.NoopPanicHandler{})
|
||||
require.NoError(t, err)
|
||||
require.False(t, corrupt)
|
||||
require.NoError(t, corrupt)
|
||||
}
|
||||
|
||||
{
|
||||
_, corrupt, err := vault.New(vaultDir, gluonDir, []byte("my secret key"), async.NoopPanicHandler{})
|
||||
require.NoError(t, err)
|
||||
require.False(t, corrupt)
|
||||
require.NoError(t, corrupt)
|
||||
}
|
||||
|
||||
{
|
||||
@ -75,7 +75,7 @@ func TestVault_Corrupt_JunkData(t *testing.T) {
|
||||
|
||||
_, corrupt, err := vault.New(vaultDir, gluonDir, []byte("my secret key"), async.NoopPanicHandler{})
|
||||
require.NoError(t, err)
|
||||
require.True(t, corrupt)
|
||||
require.ErrorIs(t, corrupt, vault.ErrUnmarshal)
|
||||
}
|
||||
}
|
||||
|
||||
@ -103,7 +103,7 @@ func newVault(t *testing.T) *vault.Vault {
|
||||
|
||||
s, corrupt, err := vault.New(t.TempDir(), t.TempDir(), []byte("my secret key"), async.NoopPanicHandler{})
|
||||
require.NoError(t, err)
|
||||
require.False(t, corrupt)
|
||||
require.NoError(t, corrupt)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
@ -18,8 +18,8 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"strings"
|
||||
|
||||
"github.com/emersion/go-message"
|
||||
@ -70,10 +70,12 @@ func (p *Parser) Root() *Part {
|
||||
}
|
||||
|
||||
func (p *Parser) AttachPublicKey(key, keyName string) {
|
||||
h := message.Header{}
|
||||
encName := mime.QEncoding.Encode("utf-8", keyName+".asc")
|
||||
params := map[string]string{"name": encName, "filename": encName}
|
||||
|
||||
h.Set("Content-Type", fmt.Sprintf(`application/pgp-keys; name="%v.asc"; filename="%v.asc"`, keyName, keyName))
|
||||
h.Set("Content-Disposition", fmt.Sprintf(`attachment; name="%v.asc"; filename="%v.asc"`, keyName, keyName))
|
||||
h := message.Header{}
|
||||
h.Set("Content-Type", mime.FormatMediaType("application/pgp-keys", params))
|
||||
h.Set("Content-Disposition", mime.FormatMediaType("attachment", params))
|
||||
h.Set("Content-Transfer-Encoding", "base64")
|
||||
|
||||
p.Root().AddChild(&Part{
|
||||
@ -82,7 +84,7 @@ func (p *Parser) AttachPublicKey(key, keyName string) {
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Parser) AttachEmptyTextPartIfNoneExists() {
|
||||
func (p *Parser) AttachEmptyTextPartIfNoneExists() bool {
|
||||
root := p.Root()
|
||||
if root.isMultipartMixed() {
|
||||
for _, v := range root.children {
|
||||
@ -93,14 +95,14 @@ func (p *Parser) AttachEmptyTextPartIfNoneExists() {
|
||||
contentType, _, err := v.Header.ContentType()
|
||||
if err == nil && strings.HasPrefix(contentType, "text/") {
|
||||
// Message already has text part
|
||||
return
|
||||
return false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
contentType, _, err := root.Header.ContentType()
|
||||
if err == nil && strings.HasPrefix(contentType, "text/") {
|
||||
// Message already has text part
|
||||
return
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@ -113,6 +115,7 @@ func (p *Parser) AttachEmptyTextPartIfNoneExists() {
|
||||
Header: h,
|
||||
Body: nil,
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
// Section returns the message part referred to by the given section. A section
|
||||
|
||||
@ -137,15 +137,15 @@ func (p *Part) ConvertMetaCharset() error {
|
||||
if val, ok := sel.Attr("content"); ok {
|
||||
t, params, err := pmmime.ParseMediaType(val)
|
||||
if err != nil {
|
||||
logrus.WithField("pkg", "parser").WithError(err).Error("Meta tag parsing fails.")
|
||||
return
|
||||
}
|
||||
|
||||
if charset, ok := params["charset"]; ok && charset != utf8Charset {
|
||||
params["charset"] = utf8Charset
|
||||
sel.SetAttr("content", mime.FormatMediaType(t, params))
|
||||
metaModified = true
|
||||
}
|
||||
|
||||
sel.SetAttr("content", mime.FormatMediaType(t, params))
|
||||
metaModified = true
|
||||
}
|
||||
|
||||
if charset, ok := sel.Attr("charset"); ok && charset != utf8Charset {
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
@ -71,3 +72,45 @@ func getSectionNumber(s string) (part []int) {
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func TestPart_ConvertMetaCharset(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
wantErr bool
|
||||
wantSame bool
|
||||
}{
|
||||
{
|
||||
"html no meta",
|
||||
"<body></body>",
|
||||
false,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"html meta no charset",
|
||||
"<header><meta name=ProgId content=Word.Document></header><body><meta></body>",
|
||||
false,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"html meta UTF-8 charset",
|
||||
"<header><meta charset=UTF-8></header><body><meta></body>",
|
||||
false,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"html meta not UTF-8 charset",
|
||||
"<header><meta charset=UTF-7></header><body><meta></body>",
|
||||
false,
|
||||
false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var p = Part{Body: []byte(tt.body)}
|
||||
err := p.ConvertMetaCharset()
|
||||
assert.Equal(t, tt.wantErr, err != nil)
|
||||
assert.Equal(t, tt.wantSame, reflect.DeepEqual([]byte(tt.body), p.Body))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -552,14 +552,15 @@ func TestParseMultipartAlternative(t *testing.T) {
|
||||
assert.Equal(t, `"schizofrenic" <schizofrenic@pm.me>`, m.Sender.String())
|
||||
assert.Equal(t, `<pmbridgeietest@outlook.com>`, m.ToList[0].String())
|
||||
|
||||
assert.Equal(t, `<html><head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8"/>
|
||||
assert.Equal(t, `<html>
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
|
||||
</head>
|
||||
<body>
|
||||
<b>aoeuaoeu</b>
|
||||
|
||||
|
||||
</body></html>`, string(m.RichBody))
|
||||
</body>
|
||||
</html>
|
||||
`, string(m.RichBody))
|
||||
|
||||
assert.Equal(t, "*aoeuaoeu*\n\n", string(m.PlainBody))
|
||||
}
|
||||
@ -573,14 +574,15 @@ func TestParseMultipartAlternativeNested(t *testing.T) {
|
||||
assert.Equal(t, `"schizofrenic" <schizofrenic@pm.me>`, m.Sender.String())
|
||||
assert.Equal(t, `<pmbridgeietest@outlook.com>`, m.ToList[0].String())
|
||||
|
||||
assert.Equal(t, `<html><head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8"/>
|
||||
assert.Equal(t, `<html>
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
|
||||
</head>
|
||||
<body>
|
||||
<b>multipart 2.2</b>
|
||||
|
||||
|
||||
</body></html>`, string(m.RichBody))
|
||||
</body>
|
||||
</html>
|
||||
`, string(m.RichBody))
|
||||
|
||||
assert.Equal(t, "*multipart 2.1*\n\n", string(m.PlainBody))
|
||||
}
|
||||
|
||||
@ -261,13 +261,13 @@ func ParseMediaType(v string) (string, map[string]string, error) {
|
||||
}
|
||||
decoded, err := DecodeHeader(v)
|
||||
if err != nil {
|
||||
logrus.WithField("value", v).WithError(err).Error("Media Type parsing error.")
|
||||
logrus.WithField("value", v).WithField("pkg", "mime").WithError(err).Error("Cannot decode Headers.")
|
||||
return "", nil, err
|
||||
}
|
||||
v, _ = changeEncodingAndKeepLastParamDefinition(decoded)
|
||||
mediatype, params, err := mime.ParseMediaType(v)
|
||||
if err != nil {
|
||||
logrus.WithField("value", v).WithError(err).Error("Media Type parsing error.")
|
||||
logrus.WithField("value", v).WithField("pkg", "mime").WithError(err).Error("Media Type parsing error.")
|
||||
return "", nil, err
|
||||
}
|
||||
return mediatype, params, err
|
||||
|
||||
@ -112,8 +112,8 @@ func (t *testCtx) initBridge() (<-chan events.Event, error) {
|
||||
vault, corrupt, err := vault.New(vaultDir, gluonCacheDir, t.storeKey, async.NoopPanicHandler{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create vault: %w", err)
|
||||
} else if corrupt {
|
||||
return nil, fmt.Errorf("vault is corrupt")
|
||||
} else if corrupt != nil {
|
||||
return nil, fmt.Errorf("vault is corrupt: %w", corrupt)
|
||||
}
|
||||
t.vault = vault
|
||||
|
||||
|
||||
@ -20,6 +20,7 @@ package tests
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -29,6 +30,7 @@ import (
|
||||
)
|
||||
|
||||
type heartbeatRecorder struct {
|
||||
lock sync.Mutex
|
||||
heartbeat telemetry.HeartbeatData
|
||||
bridge *bridge.Bridge
|
||||
reject bool
|
||||
@ -74,10 +76,19 @@ func (hb *heartbeatRecorder) SendHeartbeat(_ context.Context, metrics *telemetry
|
||||
if hb.reject {
|
||||
return false
|
||||
}
|
||||
hb.lock.Lock()
|
||||
defer hb.lock.Unlock()
|
||||
hb.heartbeat = *metrics
|
||||
return true
|
||||
}
|
||||
|
||||
func (hb *heartbeatRecorder) GetRecordedHeartbeat() telemetry.HeartbeatData {
|
||||
hb.lock.Lock()
|
||||
defer hb.lock.Unlock()
|
||||
|
||||
return hb.heartbeat
|
||||
}
|
||||
|
||||
func (hb *heartbeatRecorder) SetLastHeartbeatSent(timestamp time.Time) error {
|
||||
if hb.bridge == nil {
|
||||
return errors.New("no bridge initialized")
|
||||
|
||||
@ -440,21 +440,227 @@ Feature: IMAP import messages
|
||||
"from": "Bridge Second Test <bridge_second@test.com>",
|
||||
"subject": "MESSAGE WITH REMOTE CONTENT",
|
||||
"content": {
|
||||
"content-type": "multipart/alternative",
|
||||
"content-type": "multipart/mixed",
|
||||
"sections":[
|
||||
{
|
||||
"content-type": "text/plain",
|
||||
"content-type-charset": "utf-8",
|
||||
"transfer-encoding": "7bit",
|
||||
"body-is": "Remote content\n\n\nBridge\n\n\nRemote content"
|
||||
},
|
||||
{
|
||||
"content-type": "text/html",
|
||||
"content-type-charset": "utf-8",
|
||||
"transfer-encoding": "7bit",
|
||||
"body-is": "<!DOCTYPE html>\n<html>\n <head>\n\n <meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\">\n </head>\n <body>\n <p><tt>Remote content</tt></p>\n <p><tt><br>\n </tt></p>\n <p><img\n src=\"https://bridgeteam.protontech.ch/bridgeteam/tmp/bridge.jpg\"\n alt=\"Bridge\" width=\"180\" height=\"180\"></p>\n <p><br>\n </p>\n <p><tt>Remote content</tt><br>\n </p>\n <br>\n </body>\n</html>"
|
||||
"content-type": "multipart/alternative",
|
||||
"sections":[
|
||||
{
|
||||
"content-type": "text/plain",
|
||||
"content-type-charset": "utf-8",
|
||||
"transfer-encoding": "7bit",
|
||||
"body-is": "Remote content\n\n\nBridge\n\n\nRemote content"
|
||||
},
|
||||
{
|
||||
"content-type": "text/html",
|
||||
"content-type-charset": "utf-8",
|
||||
"transfer-encoding": "7bit",
|
||||
"body-is": "<!DOCTYPE html>\n<html>\n <head>\n\n <meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\">\n </head>\n <body>\n <p><tt>Remote content</tt></p>\n <p><tt><br>\n </tt></p>\n <p><img\n src=\"https://bridgeteam.protontech.ch/bridgeteam/tmp/bridge.jpg\"\n alt=\"Bridge\" width=\"180\" height=\"180\"></p>\n <p><br>\n </p>\n <p><tt>Remote content</tt><br>\n </p>\n <br>\n </body>\n</html>"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
Scenario: Import message with inline image
|
||||
When IMAP client "1" appends the following message to "Inbox":
|
||||
"""
|
||||
Date: 01 Jan 1980 00:00:00 +0000
|
||||
From: Bridge Second Test <bridge_second@test.com>
|
||||
To: Bridge Test <bridge@test.com>
|
||||
Subject: Html Inline Importing
|
||||
Content-Disposition: inline
|
||||
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Thunderbird/60.5.0
|
||||
MIME-Version: 1.0
|
||||
Content-Language: en-US
|
||||
Content-Type: multipart/related; boundary="61FA22A41A3F46E8E90EF528"
|
||||
|
||||
This is a multi-part message in MIME format.
|
||||
--61FA22A41A3F46E8E90EF528
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
|
||||
</head>
|
||||
<body text="#000000" bgcolor="#FFFFFF">
|
||||
<p><br>
|
||||
</p>
|
||||
<p>Behold! An inline <img moz-do-not-send="false"
|
||||
src="cid:part1.D96BFAE9.E2E1CAE3@protonmail.com" alt=""
|
||||
width="24" height="24"><br>
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
--61FA22A41A3F46E8E90EF528
|
||||
Content-Type: image/gif; name="email-action-left.gif"
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-ID: <part1.D96BFAE9.E2E1CAE3@protonmail.com>
|
||||
Content-Disposition: inline; filename="email-action-left.gif"
|
||||
|
||||
R0lGODlhGAAYANUAACcsKOHs4kppTH6tgYWxiIq0jTVENpG5lDI/M7bRuEaJSkqOTk2RUU+P
|
||||
U16lYl+lY2iva262cXS6d3rDfYLNhWeeamKTZGSVZkNbRGqhbOPt4////+7u7qioqFZWVlNT
|
||||
UyIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAGAAYAAAG
|
||||
/8CNcLjRJAqVRqNSSGiI0GFgoKhar4NAdHioMhyRCYUyiTgY1cOWUH1ILgIDAGAQXCSPKgHa
|
||||
XUAyGCCCg4IYGRALCmpCAVUQFgiEkiAIFhBVWhtUDxmRk5IIGXkDRQoMEoGfHpIYEmhGCg4X
|
||||
nyAdHB+SFw4KRwoRArQdG7eEAhEKSAoTBoIdzs/Cw7iCBhMKSQoUAIJbQ8QgABQKStnbIN1C
|
||||
3+HjFcrMtdDO6dMg1dcFvsCfwt+CxsgJYs3a10+QLl4aTKGitYpQq1eaFHDyREtQqFGMHEGq
|
||||
SMkSJi4K/ACiZQiRIihsJL6JM6fOnTwK9kTpYgqMGDJm0JzsNuWKTw0FWdANMYJECRMnW4IA
|
||||
ADs=
|
||||
|
||||
--61FA22A41A3F46E8E90EF528--
|
||||
|
||||
"""
|
||||
Then it succeeds
|
||||
And IMAP client "1" eventually sees the following message in "Inbox" with this structure:
|
||||
"""
|
||||
{
|
||||
"date": "01 Jan 80 00:00 +0000",
|
||||
"to": "Bridge Test <bridge@test.com>",
|
||||
"from": "Bridge Second Test <bridge_second@test.com>",
|
||||
"subject": "Html Inline Importing",
|
||||
"content": {
|
||||
"content-type": "multipart/mixed",
|
||||
"sections":[
|
||||
{
|
||||
"content-type": "multipart/related",
|
||||
"sections":[
|
||||
{
|
||||
"content-type": "text/html",
|
||||
"content-type-charset": "utf-8",
|
||||
"transfer-encoding": "7bit",
|
||||
"body-is": "<html>\n<head>\n<meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\">\n</head>\n<body text=\"#000000\" bgcolor=\"#FFFFFF\">\n<p><br>\n</p>\n<p>Behold! An inline <img moz-do-not-send=\"false\"\nsrc=\"cid:part1.D96BFAE9.E2E1CAE3@protonmail.com\" alt=\"\"\nwidth=\"24\" height=\"24\"><br>\n</p>\n</body>\n</html>"
|
||||
},
|
||||
{
|
||||
"content-type": "image/gif",
|
||||
"content-type-name": "email-action-left.gif",
|
||||
"content-disposition": "inline",
|
||||
"content-disposition-filename": "email-action-left.gif",
|
||||
"transfer-encoding": "base64",
|
||||
"body-is": "R0lGODlhGAAYANUAACcsKOHs4kppTH6tgYWxiIq0jTVENpG5lDI/M7bRuEaJSkqOTk2RUU+PU16l\r\nYl+lY2iva262cXS6d3rDfYLNhWeeamKTZGSVZkNbRGqhbOPt4////+7u7qioqFZWVlNTUyIiIgAA\r\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\r\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAGAAYAAAG/8CNcLjRJAqVRqNS\r\nSGiI0GFgoKhar4NAdHioMhyRCYUyiTgY1cOWUH1ILgIDAGAQXCSPKgHaXUAyGCCCg4IYGRALCmpC\r\nAVUQFgiEkiAIFhBVWhtUDxmRk5IIGXkDRQoMEoGfHpIYEmhGCg4XnyAdHB+SFw4KRwoRArQdG7eE\r\nAhEKSAoTBoIdzs/Cw7iCBhMKSQoUAIJbQ8QgABQKStnbIN1C3+HjFcrMtdDO6dMg1dcFvsCfwt+C\r\nxsgJYs3a10+QLl4aTKGitYpQq1eaFHDyREtQqFGMHEGqSMkSJi4K/ACiZQiRIihsJL6JM6fOnTwK\r\n9kTpYgqMGDJm0JzsNuWKTw0FWdANMYJECRMnW4IAADs="
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
Scenario: Message import with text part and attachment
|
||||
When IMAP client "1" appends the following message to "INBOX":
|
||||
"""
|
||||
From: Bridge Test <bridgetest@pm.test>
|
||||
Date: 01 Jan 1980 00:00:00 +0000
|
||||
To: Internal Bridge <bridgetest@example.com>
|
||||
Received: by 2002:0:0:0:0:0:0:0 with SMTP id 0123456789abcdef; Wed, 30 Dec 2020 01:23:45 0000
|
||||
Subject: Message import with text part
|
||||
Content-Type: multipart/mixed; boundary="BOUNDARY"
|
||||
|
||||
This is a multi-part message in MIME format.
|
||||
|
||||
--BOUNDARY
|
||||
Content-Type: text/plain; charset=utf-8; format=flowed
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
Hello World
|
||||
|
||||
--BOUNDARY
|
||||
Content-Disposition: attachment; filename=image.png
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Type: image/png
|
||||
|
||||
iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAALVBMVEUAAAD/////////////////
|
||||
//////////////////////////////////////+hSKubAAAADnRSTlMAgO8QQM+/IJ9gj1AwcIQd
|
||||
OXUAAAGdSURBVDjLXJC9SgNBFIVPXDURTYhgIQghINgowyLYCAYtRFAIgtYhpAjYhC0srCRW6YIg
|
||||
WNpoHVSsg/gEii+Qnfxq4DyDc3cyMfrBwl2+O+fOHTi8p7LS5RUf/9gpMKL7iT9sK47Q95ggpkzv
|
||||
1cvRcsGYNMYsmP+zKN27NR2vcDyTNVdfkOuuniNPMWafvIbljt+YoMEvW8y7lt+ARwhvrgPjhA0I
|
||||
BTng7S1GLPlypBvtIBPidY4YBDJFdtnkscQ5JGaGqxC9i7jSDwcwnB8qHWBaQjw1ABI8wYgtVoG6
|
||||
9pFkH8iZIiJeulFt4JLvJq8I5N2GMWYbHWDWzM3JZTMdeSWla0kW86FcuI0mfStiNKQ/AhEeh8h0
|
||||
YUTffFwrMTT5oSwdojIQ0UKcocgAKRH1HiqhFQmmJa5qRaYHNbRiSsOgslY0NdixItUTUWlZkedP
|
||||
HXVyAgAIA1F0wP5btQZPIyTwvAqa/Fl4oacuP+e4XHAjSYpkQkxSiMX+T7FPoZJToSStzED70HCy
|
||||
KE3NGCg4jJrC6Ti7AFwZLhnW0gMbzFZc0RmmeAAAAABJRU5ErkJggg==
|
||||
|
||||
--BOUNDARY--
|
||||
"""
|
||||
Then it succeeds
|
||||
And IMAP client "1" eventually sees the following message in "INBOX" with this structure:
|
||||
"""
|
||||
{
|
||||
"from": "Bridge Test <bridgetest@pm.test>",
|
||||
"date": "01 Jan 80 00:00 +0000",
|
||||
"to": "Internal Bridge <bridgetest@example.com>",
|
||||
"subject": "Message import with text part",
|
||||
"content": {
|
||||
"content-type": "multipart/mixed",
|
||||
"sections":[
|
||||
{
|
||||
"content-type": "text/plain",
|
||||
"body-is": "Hello World"
|
||||
},
|
||||
{
|
||||
"content-type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
Scenario: Message import without text part
|
||||
When IMAP client "1" appends the following message to "INBOX":
|
||||
"""
|
||||
From: Bridge Test <bridgetest@pm.test>
|
||||
Date: 01 Jan 1980 00:00:00 +0000
|
||||
To: Internal Bridge <bridgetest@example.com>
|
||||
Received: by 2002:0:0:0:0:0:0:0 with SMTP id 0123456789abcdef; Wed, 30 Dec 2020 01:23:45 0000
|
||||
Subject: Message import without text part
|
||||
Content-Type: multipart/mixed; boundary="BOUNDARY"
|
||||
|
||||
This is a multi-part message in MIME format.
|
||||
|
||||
--BOUNDARY
|
||||
Content-Disposition: attachment; filename=image.png
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Type: image/png
|
||||
|
||||
iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAALVBMVEUAAAD/////////////////
|
||||
//////////////////////////////////////+hSKubAAAADnRSTlMAgO8QQM+/IJ9gj1AwcIQd
|
||||
OXUAAAGdSURBVDjLXJC9SgNBFIVPXDURTYhgIQghINgowyLYCAYtRFAIgtYhpAjYhC0srCRW6YIg
|
||||
WNpoHVSsg/gEii+Qnfxq4DyDc3cyMfrBwl2+O+fOHTi8p7LS5RUf/9gpMKL7iT9sK47Q95ggpkzv
|
||||
1cvRcsGYNMYsmP+zKN27NR2vcDyTNVdfkOuuniNPMWafvIbljt+YoMEvW8y7lt+ARwhvrgPjhA0I
|
||||
BTng7S1GLPlypBvtIBPidY4YBDJFdtnkscQ5JGaGqxC9i7jSDwcwnB8qHWBaQjw1ABI8wYgtVoG6
|
||||
9pFkH8iZIiJeulFt4JLvJq8I5N2GMWYbHWDWzM3JZTMdeSWla0kW86FcuI0mfStiNKQ/AhEeh8h0
|
||||
YUTffFwrMTT5oSwdojIQ0UKcocgAKRH1HiqhFQmmJa5qRaYHNbRiSsOgslY0NdixItUTUWlZkedP
|
||||
HXVyAgAIA1F0wP5btQZPIyTwvAqa/Fl4oacuP+e4XHAjSYpkQkxSiMX+T7FPoZJToSStzED70HCy
|
||||
KE3NGCg4jJrC6Ti7AFwZLhnW0gMbzFZc0RmmeAAAAABJRU5ErkJggg==
|
||||
|
||||
--BOUNDARY--
|
||||
"""
|
||||
Then it succeeds
|
||||
And IMAP client "1" eventually sees the following message in "INBOX" with this structure:
|
||||
"""
|
||||
{
|
||||
"from": "Bridge Test <bridgetest@pm.test>",
|
||||
"date": "01 Jan 80 00:00 +0000",
|
||||
"to": "Internal Bridge <bridgetest@example.com>",
|
||||
"subject": "Message import without text part",
|
||||
"content": {
|
||||
"content-type": "multipart/mixed",
|
||||
"sections":[
|
||||
{
|
||||
"content-type": "text/plain",
|
||||
"body-is": ""
|
||||
},
|
||||
{
|
||||
"content-type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
"""
|
||||
@ -30,3 +30,22 @@ Feature: IMAP marks messages as forwarded
|
||||
And it succeeds
|
||||
Then IMAP client "1" eventually sees that message at row 1 does not have the flag "forwarded"
|
||||
And it succeeds
|
||||
|
||||
Scenario: Mark message as replied
|
||||
When IMAP client "1" selects "Folders/mbox"
|
||||
And IMAP client "1" marks message 1 as "replied"
|
||||
And it succeeds
|
||||
Then IMAP client "1" eventually sees that message at row 1 has the flag "\Answered"
|
||||
And it succeeds
|
||||
|
||||
@regression
|
||||
Scenario: Mark message as replied and then revert
|
||||
When IMAP client "1" selects "Folders/mbox"
|
||||
And IMAP client "1" marks message 1 as "replied"
|
||||
And it succeeds
|
||||
Then IMAP client "1" eventually sees that message at row 1 has the flag "\Answered"
|
||||
And it succeeds
|
||||
And IMAP client "1" marks message 1 as "unreplied"
|
||||
And it succeeds
|
||||
Then IMAP client "1" eventually sees that message at row 1 does not have the flag "\Answered"
|
||||
And it succeeds
|
||||
@ -1,6 +1,7 @@
|
||||
Feature: SMTP wrong messages
|
||||
Background:
|
||||
Given there exists an account with username "[user:user]" and password "password"
|
||||
And the account "[user:user]" has additional disabled address "[user:disabled]@[domain]"
|
||||
And there exists an account with username "[user:to]" and password "password"
|
||||
Then it succeeds
|
||||
When bridge starts
|
||||
@ -54,4 +55,14 @@ Feature: SMTP wrong messages
|
||||
hello
|
||||
|
||||
"""
|
||||
Then it fails
|
||||
Then it fails with error "invalid return path"
|
||||
|
||||
Scenario: Send from a valid address that cannot send
|
||||
When SMTP client "1" sends the following message from "[user:disabled]@[domain]" to "[user:to]@[domain]":
|
||||
"""
|
||||
From: Bridge Test Disabled <[user:disabled]@[domain]>
|
||||
To: Internal Bridge <[user:to]@[domain]>
|
||||
|
||||
Hello
|
||||
"""
|
||||
And it fails with error "Error: can't send on address: [user:disabled]@[domain]"
|
||||
|
||||
@ -141,7 +141,7 @@ Feature: SMTP sending of plain messages
|
||||
"content-type": "text/html",
|
||||
"content-type-charset": "utf-8",
|
||||
"transfer-encoding": "quoted-printable",
|
||||
"body-is": "<html><head>\r\n<meta http-equiv=3D\"content-type\" content=3D\"text/html; charset=3DUTF-8\"/>\r\n</head>\r\n<body text=3D\"#000000\" bgcolor=3D\"#FFFFFF\">\r\n<p><br/>\r\n</p>\r\n<p>Behold! An inline <img moz-do-not-send=3D\"false\" src=3D\"cid:part1.D96BFA=\r\nE9.E2E1CAE3@protonmail.com\" alt=3D\"\" width=3D\"24\" height=3D\"24\"/><br/>\r\n</p>\r\n\r\n\r\n</body></html>"
|
||||
"body-is": "<html>\r\n<head>\r\n<meta http-equiv=3D\"content-type\" content=3D\"text/html; charset=3DUTF-8\">\r\n</head>\r\n<body text=3D\"#000000\" bgcolor=3D\"#FFFFFF\">\r\n<p><br>\r\n</p>\r\n<p>Behold! An inline <img moz-do-not-send=3D\"false\"\r\nsrc=3D\"cid:part1.D96BFAE9.E2E1CAE3@protonmail.com\" alt=3D\"\"\r\nwidth=3D\"24\" height=3D\"24\"><br>\r\n</p>\r\n</body>\r\n</html>"
|
||||
},
|
||||
{
|
||||
"content-type": "image/gif",
|
||||
@ -476,7 +476,7 @@ Feature: SMTP sending of plain messages
|
||||
"content-type": "text/html",
|
||||
"content-type-charset": "utf-8",
|
||||
"transfer-encoding": "quoted-printable",
|
||||
"body-is": "<!DOCTYPE html><html><head>\n\n <meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\"/>\n </head>\n <body>\n <p><tt>Remote content</tt></p>\n <p><tt><br/>\n </tt></p>\n <p><img src=\"https://bridgeteam.protontech.ch/bridgeteam/tmp/bridge.jpg\" alt=\"Bridge\" width=\"180\" height=\"180\"/></p>\n <p><br/>\n </p>\n <p><tt>Remote content</tt><br/>\n </p>\n <br/>\n \n\n</body></html>"
|
||||
"body-is": "<!DOCTYPE html><html><head>\r\n\r\n <meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\"/>\r\n </head>\r\n <body>\r\n <p><tt>Remote content</tt></p>\r\n <p><tt><br/>\r\n </tt></p>\r\n <p><img src=\"https://bridgeteam.protontech.ch/bridgeteam/tmp/bridge.jpg\" alt=\"Bridge\" width=\"180\" height=\"180\"/></p>\r\n <p><br/>\r\n </p>\r\n <p><tt>Remote content</tt><br/>\r\n </p>\r\n <br/>\r\n \r\n\r\n</body></html>"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -1,14 +1,14 @@
|
||||
Feature: A user can login
|
||||
Background:
|
||||
Given there exists an account with username "[user:user]" and password "password"
|
||||
And there exists an account with username "[user:MixedCaps]" and password "password"
|
||||
And there exists a disabled account with username "[user:disabled]" and password "password"
|
||||
Given there exists an account with username "[user:user]" and password "password2"
|
||||
And there exists an account with username "[user:MixedCaps]" and password "password3"
|
||||
And there exists a disabled account with username "[user:disabled]" and password "password4"
|
||||
Then it succeeds
|
||||
And bridge starts
|
||||
Then it succeeds
|
||||
|
||||
Scenario: Login to account
|
||||
When the user logs in with username "[user:user]" and password "password"
|
||||
When the user logs in with username "[user:user]" and password "password2"
|
||||
Then user "[user:user]" is eventually listed and connected
|
||||
|
||||
Scenario: Login to account with wrong password
|
||||
@ -21,19 +21,19 @@ Feature: A user can login
|
||||
|
||||
Scenario: Login to account without internet
|
||||
Given the internet is turned off
|
||||
When the user logs in with username "[user:user]" and password "password"
|
||||
When the user logs in with username "[user:user]" and password "password2"
|
||||
Then user "[user:user]" is not listed
|
||||
|
||||
Scenario: Login to account with caps
|
||||
When the user logs in with username "[user:MixedCaps]" and password "password"
|
||||
When the user logs in with username "[user:MixedCaps]" and password "password3"
|
||||
Then user "[user:MixedCaps]" is eventually listed and connected
|
||||
|
||||
Scenario: Login to account with disabled primary
|
||||
When the user logs in with username "[user:disabled]" and password "password"
|
||||
When the user logs in with username "[user:disabled]" and password "password4"
|
||||
Then user "[user:disabled]" is eventually listed and connected
|
||||
|
||||
Scenario: Login to account without internet but the connection is later restored
|
||||
When the user logs in with username "[user:user]" and password "password"
|
||||
When the user logs in with username "[user:user]" and password "password2"
|
||||
And bridge stops
|
||||
And the internet is turned off
|
||||
And bridge starts
|
||||
@ -42,7 +42,7 @@ Feature: A user can login
|
||||
|
||||
Scenario: Login to multiple accounts
|
||||
Given there exists an account with username "[user:additional]" and password "password"
|
||||
When the user logs in with username "[user:user]" and password "password"
|
||||
When the user logs in with username "[user:user]" and password "password2"
|
||||
And the user logs in with username "[user:additional]" and password "password"
|
||||
Then user "[user:user]" is eventually listed and connected
|
||||
And user "[user:additional]" is eventually listed and connected
|
||||
@ -43,7 +43,7 @@ func (s *scenario) bridgeSendsTheFollowingHeartbeat(text *godog.DocString) error
|
||||
return err
|
||||
}
|
||||
|
||||
return matchHeartbeat(s.t.heartbeat.heartbeat, wantHeartbeat)
|
||||
return matchHeartbeat(s.t.heartbeat.GetRecordedHeartbeat(), wantHeartbeat)
|
||||
}
|
||||
|
||||
func (s *scenario) bridgeNeedsToSendHeartbeat() error {
|
||||
|
||||
@ -938,6 +938,18 @@ func clientChangeMessageState(client *client.Client, seq int, messageState strin
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case messageState == "replied":
|
||||
_, err := clientStore(client, seq, seq, isUID, imap.FormatFlagsOp(imap.AddFlags, true), imap.AnsweredFlag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case messageState == "unreplied":
|
||||
_, err := clientStore(client, seq, seq, isUID, imap.FormatFlagsOp(imap.RemoveFlags, true), imap.AnsweredFlag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user