Compare commits

..

114 Commits

Author SHA1 Message Date
dd9a819ea2 chore: Xikou Bridge 3.8.0 changelog. 2023-12-04 14:42:09 +01:00
401e56224b fix(GODT-3142): pass br tag if available 2023-12-04 14:14:52 +01:00
1ee52f0f55 fix(GODT-3151): Fix feature test with non modified HTML part. 2023-12-04 13:11:33 +01:00
9efaf9184c fix(GODT-3151): Only modify HTML Meta content if UTF-8 charset override is needed. 2023-12-04 11:45:47 +01:00
a8f270405f chore: Xikou Bridge 3.8.0 changelog. 2023-11-30 13:59:58 +01:00
38606888fe fix(GODT-2851): Add empty text part if no text part when importing multipart. 2023-11-30 11:03:31 +01:00
1b22c32ef9 fix(GODT-3102): Distinguish Vault Decryption from Serialization Errors
Rather than returning whether the vault was corrupt or not return the
error which caused the vault to be considered as corrupt.
2023-11-30 08:31:14 +01:00
7a1c7e8743 fix(GODT-3124): Handling of sync child jobs
Improve the handling of sync child jobs to ensure it behaves correctly
in all scenarios.

The sync service now uses a isolated context to avoid all the pipeline
stages shutting down before all the sync tasks have had the opportunity
to run their course.

The job waiter now immediately starts with a counter of 1 and waits
until all the child and the parent job finish before considering the
work to be finished.

Finally, we also handle the case where a sync job can't be queued
because the calling context has been cancelled.
2023-11-29 18:04:22 +00:00
9449177553 fix(GODT-3148): bump go-sysinfo to get rid of linker warning on macOS Sonoma. 2023-11-29 14:49:13 +01:00
bbcedc655a fix(GODT-3124): Flaky tests
Bump GPA to include fix for flacky tests.

https://github.com/ProtonMail/go-proton-api/pull/137
2023-11-29 12:02:06 +01:00
40c97ab19e fix(GODT-3022): Handle multipart/related on fake server. 2023-11-28 15:07:26 +00:00
50dd046b82 fix(GODT-3133): Fix GetSystemLanguage 2023-11-28 09:32:40 +01:00
7d13c99710 fix(GODT-3124): Race condition in sync task waiter
Fix incorrect use of `sync.WaitGroup` use to wait on sync jobs that
fail. After calling `WaitGroup.Wait()` it is advised to call
`WaitGroup.Add` until the existing counter has reached 0.

The code has been updated with a different mechanism that achieves the
same behavior which was previously available.
2023-11-28 09:15:28 +01:00
6d7c21b2c9 fix(GODT-3135): fix br tag pipeline rules. 2023-11-27 16:25:49 +00:00
f7434109be fix(GODT-3124): Race conditions reported by race check 2023-11-27 16:30:27 +01:00
414d74d06a test(GODT-3124): Attempt to fix 401 during login
Update GPA to use the simplified locking model and hope that the problem
solves itself. As far as I could tell, this might be a lock acquisition
issue.

https://github.com/ProtonMail/go-proton-api/pull/132
https://github.com/ProtonMail/go-proton-api/pull/133
2023-11-27 13:31:35 +01:00
110cdbf3ae feat(GODT-3046): report all clicked external links to bridge. 2023-11-27 10:41:46 +01:00
ec4ceb4552 feat(GODT-3134): br tag triggers installer 2023-11-24 12:32:01 +01:00
ef62704030 feat(GODT-31134): re-organize pipeline config files: no change 2023-11-24 11:56:19 +01:00
eaba6b6363 fix(GODT-2797): encode attached key name and use same pubkey name as web-app. 2023-11-23 15:24:08 +01:00
e1723fc24b test: Add test scenarios to add an /Answered flag to a replied message and revert 2023-11-23 07:52:05 +00:00
2073513d5e chore: fix case of IMAP login error. 2023-11-22 15:43:47 +01:00
36f7d9672f fix(GODT-3132): Do not allow sending on disabled accounts 2023-11-22 13:07:20 +00:00
ef183e0758 feat(GODT-3046): tester UI cleanup. 2023-11-22 11:01:59 +01:00
0d2a803711 feat(GODT-3046): added all links to KB in error messages. 2023-11-22 09:26:40 +01:00
06b5276981 feat(GODT-3046): fix typo spotted during KB article review. 2023-11-22 08:29:58 +01:00
b2d61da41f feat(GODT-3046): removed 'No active key for recipient. 2023-11-22 08:29:58 +01:00
e51c81fc03 feat(GODT-3046): added ReportBugFallback event support in bridge-gui. 2023-11-22 08:29:58 +01:00
26897f06c4 feat(GODT-3046): added 'no keychain' event to bridge-gui-tester. 2023-11-22 08:29:58 +01:00
5ca9a7db37 feat(GODT-3046): removed unused error notifications, and added default user to bridge-gui-tester. 2023-11-22 08:29:58 +01:00
b34f5d072f feat(GODT-3046): added addressChanged and addressChangedLogout to gui-tester. 2023-11-22 08:29:58 +01:00
eeb514cc81 feat(GODT-3046): removed unused notification. 2023-11-22 08:29:58 +01:00
650ad49ab0 feat(GODT-3046): link in pop-up banner. 2023-11-22 08:29:58 +01:00
0e5715c4e3 feat(GODT-3046): LinkLabel in notification dialog. 2023-11-22 08:29:58 +01:00
b0f1c3d4c5 test(GODT-3113): Inline HTML message and HTML attachment is getting altered 2023-11-21 15:15:02 +00:00
ba935a6cce fix(GODT-3129): Bad Event during after address order change
When syncing an account, if the user creates a new address and then
changes it to be the default address in combined address mode we need
to update the connector maps so that the new primary address ID can be
found in that map.

Includes https://github.com/ProtonMail/go-proton-api/pull/130
2023-11-21 12:24:24 +00:00
1370ff78c5 chore: added update events to bridge GUI tester. 2023-11-21 11:59:02 +01:00
109c15410a fix(GODT-3117): Improve GetAllContacts and GetAllContactsEmail
https://github.com/ProtonMail/go-proton-api/pull/129
2023-11-20 16:02:21 +01:00
3210709810 chore: Wakato Bridge 3.7.1 changelog. 2023-11-20 11:56:03 +01:00
8fd988d7c5 fix(GODT-3054): Only delete drafts after message has been Sent
When editing a draft created by Apple Mail on the web client and then
later sending the draft with Apple Mail, we need to delete the draft
ourselves, or it will remain in the Draft folder.

This patch makes sure that the deletion of said draft only occurs after
the message was successfully sent.
2023-11-20 10:37:04 +01:00
bf89d548d3 fix(GODT-2576): Correctly handle Forwarded messages from Thunderbird
Thunderbird uses `In-Reply-To` with `X-Forwarded-Message-Id` to signal
to the SMTP server that it is forwarding a message.
2023-11-16 16:17:54 +01:00
51229cbb68 feat(GODT-3122): added test, changed interface for accessing display name. 2023-11-16 10:44:59 +00:00
36c5c37dac fix(GODT-3122): use display name as 'Email Account Name' in macOS profile. 2023-11-16 10:44:59 +00:00
5a434fafbc fix(GODT-3125): Heartbeat crash on exit
Ensure that the heartbeat background task is stopped before we close
the users as it accesses data within these instances.

Additionally, we also make sure that when telemetry is disabled, we stop
the background task.

Finally, `HeartbeatManager` now specifies what the desired interval is
so we can better configure the test cases.
2023-11-16 11:05:40 +01:00
ea1c2534df fix(GODT-2617): Validate user can send from the SMTP sender address
https://github.com/ProtonMail/go-proton-api/pull/126
2023-11-15 14:13:21 +01:00
1cafbfcaaa chore: Wakato Bridge 3.7.1 changelog. 2023-11-15 12:54:18 +01:00
2d44ccaee0 fix(GODT-3123): Trigger bad event on empty EventID on existing accounts
See `checkIrrecoverableEventID` for more details.
2023-11-15 11:06:51 +01:00
96517b7fb1 chore: Remove debug prints 2023-11-15 09:09:07 +01:00
bc381407a7 feat(GODT-2576): Forward and $Forward Flag Support
When an IMAP client stores the `Forward` or `$Forward` flags on a
message, the forwarded state is now correctly represented on the Proton
servers.

https://github.com/ProtonMail/go-proton-api/pull/125
https://github.com/ProtonMail/gluon/pull/400
2023-11-15 07:51:00 +01:00
ddc5e775b9 fix(GODT-3118): Do not reset EventID when migrating sync settings 2023-11-14 07:03:28 +00:00
ea26188dc0 fix(GODT-2277): Fix keychains initialisation in vault-editor (for write as well). 2023-11-13 15:37:32 +01:00
159e1cee7d fix(GODT-2277): Fix keychains initialisation in vault-editor. 2023-11-13 13:58:03 +00:00
4394ad0e9b feat(GODT-3053): use smaller bridge window on small screens. 2023-11-10 14:23:41 +00:00
856bdd1321 fix(GODT-3116): Panic on closed channel
If sync finishes during shutdown, check if there is a context error in
the deferred go routine before rewinding the event.
2023-11-10 14:47:03 +01:00
ff288145df fix(GODT-1623): Throttle SMTP failed requests
If a SMPT client keeps hammering bridge and triggers multiple successive
errors in quick succession, force that client to wait 20 seconds before
trying again.
2023-11-10 12:54:38 +00:00
83bbdbd63e feat(GODT-3113): Only force UTF-8 charset for HTML part when needed. 2023-11-10 12:50:15 +00:00
fa430ee0fb fix(GODT-3047): fixed 'disk full' error message. 2023-11-10 08:57:53 +00:00
0303ba38e8 feat(GODT-3113): Do not render HTML for attachment. 2023-11-10 08:36:46 +00:00
2a78b5c144 feat(GODT-3112): replaced error message when bridge exists prematurely. Added a link to support form. 2023-11-09 12:52:31 +00:00
a00b3cdb92 fix(GODT-3054): Delete draft create from reply
If an IMAP client creates a new message as a reply/forward from an
existing draft, that draft will be deleted once the message has been
sent.

Other than not being the correct behavior, the original reason for which
this line of code was added (carried over from v2), seems to be no longer
necessary as in all tests, the message is correctly removed from the
drafts folder after sent.
2023-11-09 13:24:38 +01:00
8d3e04679f feat(GODT-3010): Do not log error when no MimeType provided to lower the noise. 2023-11-09 09:45:40 +00:00
21ff7b4b97 feat(GODT-2947): Remove 'blame it on the weather' error part from go-smtp. 2023-11-09 09:45:02 +00:00
4ea161f7ad chore(GODT-3010): Log MimeType parsing issue. 2023-11-08 16:21:19 +00:00
dc584ea29b feat(GODT-3104): added log entry for cert install status on startup on macOS. 2023-11-08 16:30:50 +01:00
4a01c46aed fix(GODT-3048): WKD Policy behavior
Ensure Bridge respects the no encrypt setting on a contact which has a
WKD key.
2023-11-08 14:23:36 +01:00
e8d9534b9c feat(GODT-2277): Move Keychain helpers creation in main. 2023-11-08 13:05:57 +00:00
96904b160f test(GODT-2740): Sending Plain text messages to internal recipient 2023-11-07 10:02:26 +00:00
b535be72f8 test(GODT-2892): Create fake log file 2023-11-07 07:21:26 +00:00
40f2d8b30f chore: Wakato Bridge 3.7.0 changelog. 2023-11-06 14:51:20 +01:00
95a1acec0d fix(GODT-3097): Warn about PGPInline encryption scheme which will be deprecated. 2023-11-06 14:29:31 +01:00
5ff074cc49 fix(GODT-3106): Broken import route
https://github.com/ProtonMail/go-proton-api/pull/123
2023-11-06 10:36:27 +01:00
4f0660bb8c chore: Wakato Bridge 3.7.0 changelog. 2023-11-03 17:51:46 +01:00
708184439e chore: update changelog for previous versions. 2023-11-03 17:32:23 +01:00
b8a33b9618 fix(GODT-3041): Fix Invalid Or Missing message signature during send
If we update the address after determining the sender address is
different, we also need to refresh the identity state in order to use
the right encryption keys.
2023-11-03 10:35:36 +01:00
1c385d5c9b fix(GODT-3087): Exclude attachment content-disposition part when determining... 2023-11-03 08:55:01 +00:00
96773f3225 fix(GODT-2887): Inline images with Apple Mail
Fix sending of inline images with Apple Mail when not using rich text.
2023-11-02 14:18:28 +00:00
0f320dbd80 fix(GODT-3100): fix issue where a fatal error that bubble up to cli.Run() is not written in the log file. 2023-11-02 13:36:01 +00:00
6cb233473a fix(GODT-3094): Clean up old update files on bridge startup. 2023-11-02 10:43:55 +01:00
1ac4e70115 test(GODT-1224): Add testing around package creation. 2023-11-02 07:24:43 +00:00
07f93d276b fix(GODT-3012): Fix multipart request retries
Multipart request were failing due a bug in resty, which would cause
retries of the same request to end up with no data passed to the server.

https://github.com/ProtonMail/go-proton-api/pull/120
2023-10-30 15:27:39 +01:00
d29571fb01 fix(GODT-3095): Update GOpenPGP 2023-10-30 10:14:52 +01:00
d6000d025e fix(GODT-2935): Do not allow parentID into drafts
When sending a message ensure that if a ParentID matches a proton
message, it is not a draft. This is not supported by the Proton API.
2023-10-25 16:29:39 +02:00
09ef3b20db fix(GODT-2935): Correct error message when draft fails to create 2023-10-25 15:54:46 +02:00
405331d59b fix(GODT-2970): Correctly handle rename of Inbox
https://github.com/ProtonMail/gluon/pull/398
https://github.com/ProtonMail/gluon/pull/399
2023-10-25 15:29:33 +02:00
eff7df2136 chore: Add debug_assemble binary
Attempt to reassemble messages produced by the mailbox state debug tool.
Unfortunately, most of it will only work if the messages have been fully
decrypted. To handle encrypted messages we need to have access to the
user's keyring, which is not available.
2023-10-25 11:43:39 +00:00
5823e3a99f test(GODT-2723): Add importing a message with remote content 2023-10-25 11:39:16 +00:00
26d866bbbd test(GODT-2737): Sending HTML messages to internal 2023-10-25 09:54:17 +00:00
d3f7be059d test(GODT-3036): Keep inline attachment order on GPA Fake Server. 2023-10-24 08:22:22 +00:00
b52706a3ca feat(GODT-3015): Add simple algorithm to deal with multiple attachment for bug report. 2023-10-20 10:14:20 +00:00
aebe7baed0 fix(GODT-2969): Prevent duration corruption for config status event. 2023-10-19 15:43:44 +02:00
ef31e2917c test: make message structure check more verbose. 2023-10-19 14:22:46 +02:00
9eea26459a fix(GODT-3033): Unable to receive new mail
If the IMAP service happened to finish syncing and wanted to reset the
user event service at a time the latter was publishing an event a
deadlock would occur and the user would not receive any new messages.

This change puts the request to revert the event id in a separate
go-routine to avoid this situation from re-occurring. The operational
flow remains unchanged as the event service will only process this
request once the current set of events have been published.
2023-10-18 14:29:27 +02:00
5747b85543 test: Add test around account settings. 2023-10-18 07:45:08 +00:00
ff78a23084 chore: update changelog 2023-10-17 11:58:18 +02:00
2a95e1ab41 test: Support multiple users when waiting for sync event. 2023-10-17 08:17:17 +00:00
ab76cab533 test: Update fake server with defautl draft content-type and test it. 2023-10-17 08:16:39 +00:00
dda2a5d01a chore: fixed type in QA installer CI job name. 2023-10-13 08:50:46 +00:00
c2afb42fd4 fix(GODT-3019): fix title of main window when no account is connected. 2023-10-13 09:12:02 +02:00
1d53044803 feat(GODT-3004): update gopenpgp and dependencies. 2023-10-11 13:12:37 +00:00
d3f8297eb4 fix(GODT-3013): IMAP service getting "stuck"
* Ensure IMAP service sync cancel request waits until the sync has
  completely cancelled rather than just signaling. It's possible that
  due the context reset on `group.Cancel` that something may have not
  have been bookmarked correctly in subsequent sync restarts.

* Handle connection lost/restored events in the services. Removes the
  need to lock bridge users. Which could conflict with other ongoing
  lock operations. Additionally, it ensure that if one service is
  blocked it doesn't block the entire bridge.

* Revise access to bridge user locks.
2023-10-11 11:20:53 +01:00
b02203e3d3 chore: Umshiang Bridge 3.5.2 changelog. 2023-10-10 11:21:31 +02:00
5c7e4e04f9 fix(GODT-2966): Allow permissive parsing of MediaType parameters for import. 2023-10-09 15:14:51 +00:00
d7dadd7578 test: be less aggressive while checking for message structure. 2023-10-09 10:32:51 +00:00
ab9a758d63 fix(GODT-3003): Ensure IMAP State is reset after vault corruption
After we detect that the user has suffered the GODT-3003 bug due the
vault corruption not ensuring that a previous sync state would be
erased, we patch the gluon db directly and then reset the sync state.

After the account is added, the sync is automatically triggered and the
account state fixes itself.
2023-10-09 10:23:58 +01:00
cb0935be96 fix(GODT-3001): Only create system labels during system label sync 2023-10-06 10:09:10 +01:00
441b388f62 fix(GODT-2966): Add more test regarding quoted/unquoted filename in attachment. 2023-10-05 12:27:43 +00:00
cdbcd30d15 fix(GODT-2490): fix sync progress not being reset when toggling split mode. 2023-10-05 11:37:01 +02:00
acc7ca8d4a feat(GODT-2996): set password fields to hidden when resetting the login form. 2023-10-04 15:57:36 +02:00
42e1dd4c41 chore: Vasco da Gama Bridge 3.6.0 changelog. 2023-10-03 16:44:24 +02:00
4cbd3ca832 feat(GODT-2990): change runner tags 2023-10-03 13:49:45 +00:00
de0b6c0737 feat(GODT-2835): Bump GPA adding support for AsyncAttachments for BugReport +... 2023-10-03 13:43:16 +00:00
1c344211d1 fix(GODT-2992): fix link in 'no account view' in main window after 2FA or TOTP are cancelled. 2023-10-03 10:49:24 +02:00
c11a87c16a fix(GODT-2515): customized notification of unavailable keychain on macOS. 2023-10-02 17:02:39 +02:00
3bf4282037 feat(GODT-2940): allow 3 attempts for mailbox password. 2023-10-02 16:50:07 +02:00
199 changed files with 11014 additions and 2595 deletions

View File

@ -18,6 +18,10 @@
---
image: gitlab.protontech.ch:4567/go/bridge-internal:test-go1.20
default:
tags:
- shared-small
variables:
GOPRIVATE: gitlab.protontech.ch
GOMAXPROCS: $(( ${CI_TAG_CPU} / 2 ))
@ -30,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:
- large
# Stage: TEST
lint:
stage: test
extends:
- .rules-branch-manual-MR-and-devel-always
script:
- make lint
tags:
- medium
bug-report-preview:
stage: test
extends:
- .rules-branch-and-MR-manual
script:
- make lint-bug-report-preview
tags:
- 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:
- large
fuzz-linux:
stage: test
extends:
- .rules-branch-manual-MR-and-devel-always
script:
- make fuzz
tags:
- 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:
- 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"
trigeer-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...

View File

@ -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)
@ -133,5 +134,7 @@ Proton Mail Bridge includes the following 3rd party software:
* [yaml](https://gopkg.in/yaml.v3) available under [license](https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE)
* [docker-credential-helpers](https://github.com/ProtonMail/docker-credential-helpers) available under [license](https://github.com/ProtonMail/docker-credential-helpers/blob/master/LICENSE)
* [go-message](https://github.com/ProtonMail/go-message) available under [license](https://github.com/ProtonMail/go-message/blob/master/LICENSE)
* [go-smtp](https://github.com/ProtonMail/go-smtp) available under [license](https://github.com/ProtonMail/go-smtp/blob/master/LICENSE)
* [resty](https://github.com/LBeernaertProton/resty/v2) available under [license](https://github.com/LBeernaertProton/resty/v2/blob/master/LICENSE)
* [go-keychain](https://github.com/cuthix/go-keychain) available under [license](https://github.com/cuthix/go-keychain/blob/master/LICENSE)
<!-- END AUTOGEN -->

View File

@ -3,6 +3,130 @@
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
* Test(GODT-2740): Sending Plain text messages to internal recipient.
* Test(GODT-2892): Create fake log file.
* GODT-3122: Added test, changed interface for accessing display name.
### Changed
* Remove debug prints.
* GODT-2576: Forward and $Forward Flag Support.
* GODT-3053: Use smaller bridge window on small screens.
* GODT-3113: Only force UTF-8 charset for HTML part when needed.
* GODT-3113: Do not render HTML for attachment.
* GODT-3112: Replaced error message when bridge exists prematurely. Added a link to support form.
* GODT-2947: Remove 'blame it on the weather' error part from go-smtp.
* GODT-3010: Log MimeType parsing issue.
* GODT-3104: Added log entry for cert install status on startup on macOS.
* GODT-2277: Move Keychain helpers creation in main.
### Fixed
* GODT-3054: Only delete drafts after message has been Sent.
* GODT-2576: Correctly handle Forwarded messages from Thunderbird.
* GODT-3122: Use display name as 'Email Account Name' in macOS profile.
* GODT-3125: Heartbeat crash on exit.
* GODT-2617: Validate user can send from the SMTP sender address.
* GODT-3123: Trigger bad event on empty EventID on existing accounts.
* GODT-3118: Do not reset EventID when migrating sync settings.
* GODT-3116: Panic on closed channel.
* GODT-1623: Throttle SMTP failed requests.
* GODT-3047: Fixed 'disk full' error message.
* GODT-3054: Delete draft create from reply.
* GODT-3048: WKD Policy behavior.
## Wakato Bridge 3.7.0
### Added
* Test(GODT-1224): Add testing around package creation.
* Add debug_assemble binary.
* Test(GODT-2723): Add importing a message with remote content.
* Test(GODT-2737): Sending HTML messages to internal.
* Test(GODT-3036): Keep inline attachment order on GPA Fake Server.
* GODT-3015: Add simple algorithm to deal with multiple attachment for bug report.
* Test: make message structure check more verbose.
* Test: Add test around account settings.
### Changed
* GODT-3097: Warn about PGPInline encryption scheme which will be deprecated.
* Test: Support multiple users when waiting for sync event.
* Test: Update fake server with defautl draft content-type and test it.
* Test: be less aggressive while checking for message structure.
* GODT-2996: Set password fields to hidden when resetting the login form.
* GODT-2990: Change runner tags.
* GODT-2835: Bump GPA adding support for AsyncAttachments for BugReport +...
* GODT-2940: Allow 3 attempts for mailbox password.
* GODT-3095: Update GOpenPGP.
### Fixed
* GODT-3106: Broken import route.
* GODT-3041: Fix Invalid Or Missing message signature during send.
* GODT-3087: Exclude attachment content-disposition part when determining...
* GODT-2887: Inline images with Apple Mail.
* GODT-3100: Fix issue where a fatal error that bubble up to cli.Run() is not written in the log file.
* GODT-3094: Clean up old update files on bridge startup.
* GODT-3012: Fix multipart request retries.
* GODT-2935: Do not allow parentID into drafts.
* GODT-2935: Correct error message when draft fails to create.
* GODT-2970: Correctly handle rename of Inbox.
* GODT-2969: Prevent duration corruption for config status event.
* Fixed type in QA installer CI job name.
* GODT-3019: Fix title of main window when no account is connected.
* GODT-3013: IMAP service getting "stuck".
* GODT-2966: Allow permissive parsing of MediaType parameters for import.
* GODT-2966: Add more test regarding quoted/unquoted filename in attachment.
* GODT-2490: Fix sync progress not being reset when toggling split mode.
* GODT-2515: Customized notification of unavailable keychain on macOS.
## Vasco da Gama Bridge 3.6.1
### Fixed
* GODT-3033: Unable to receive new mail.
## Umshiang Bridge 3.5.4
### Fixed
* GODT-3033: Unable to receive new mail.
## Vasco da Gama Bridge 3.6.0
### Added

View File

@ -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.6.0+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
View 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
View 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
View 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
View 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

View File

@ -23,7 +23,6 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/app"
"github.com/bradenaw/juniper/xslices"
"github.com/sirupsen/logrus"
)
/*
@ -44,7 +43,5 @@ import (
*/
func main() {
if err := app.New().Run(xslices.Filter(os.Args, func(arg string) bool { return !strings.Contains(arg, "-psn_") })); err != nil {
logrus.Fatal(err)
}
_ = app.New().Run(xslices.Filter(os.Args, func(arg string) bool { return !strings.Contains(arg, "-psn_") }))
}

25
go.mod
View File

@ -5,10 +5,10 @@ go 1.20
require (
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
github.com/Masterminds/semver/v3 v3.2.0
github.com/ProtonMail/gluon v0.17.1-0.20231009084701-3af0474b0b3c
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.20231011132529-24b5b817ee1f
github.com/ProtonMail/gopenpgp/v2 v2.7.3-proton
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
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37
@ -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
@ -43,10 +44,10 @@ require (
github.com/vmihailenco/msgpack/v5 v5.3.5
go.uber.org/goleak v1.2.1
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
golang.org/x/net v0.10.0
golang.org/x/sys v0.8.0
golang.org/x/text v0.9.0
google.golang.org/grpc v1.53.0
golang.org/x/net v0.17.0
golang.org/x/sys v0.13.0
golang.org/x/text v0.13.0
google.golang.org/grpc v1.56.3
google.golang.org/protobuf v1.30.0
howett.net/plist v1.0.0
)
@ -79,7 +80,7 @@ require (
github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/gofrs/uuid v4.3.0+incompatible // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
@ -110,16 +111,18 @@ require (
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.9.0 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/mod v0.8.0 // indirect
golang.org/x/sync v0.2.0 // indirect
golang.org/x/tools v0.6.0 // indirect
google.golang.org/genproto v0.0.0-20230221151758-ace64dc21148 // indirect
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
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-20231129100320-dddf8030d93a
github.com/keybase/go-keychain => github.com/cuthix/go-keychain v0.0.0-20230517073537-fc1740a83768
)

97
go.sum
View File

@ -11,20 +11,25 @@ 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/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=
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
github.com/ProtonMail/gluon v0.17.1-0.20231009084701-3af0474b0b3c h1:gUDu4pOswgbou0QczfreNiXQFrmvVlpSh8Q+vft/JvI=
github.com/ProtonMail/gluon v0.17.1-0.20231009084701-3af0474b0b3c/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo=
github.com/ProtonMail/gluon v0.17.1-0.20231114153341-2ecbdd2739f7 h1:w+VoSAq9FQvKMm3DlH1MIEZ1KGe7LJ+81EJFVwSV4VU=
github.com/ProtonMail/gluon v0.17.1-0.20231114153341-2ecbdd2739f7/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo=
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
@ -34,18 +39,21 @@ 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.20231011132529-24b5b817ee1f h1:n0oBMAz2dJhn5+1WA6NrjkWqkZN+22FQMkPlRwNGhpU=
github.com/ProtonMail/go-proton-api v0.4.1-0.20231011132529-24b5b817ee1f/go.mod h1:ZmvQMA8hanLiD1tFsvu9+qGBcuxbIRfch/4z/nqBhXA=
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=
github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
github.com/ProtonMail/gopenpgp/v2 v2.7.3-proton h1:wuAxBUU9qF2wyDVJprn/2xPDx000eol5gwlKbOUYY88=
github.com/ProtonMail/gopenpgp/v2 v2.7.3-proton/go.mod h1:omVkSsfPAhmptzPF/piMXb16wKIWUvVhZbVW7sJKh0A=
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton h1:8tqHYM6IGsdEc6Vxf1TWiwpHNj8yIEQNACPhxsDagrk=
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton/go.mod h1:omVkSsfPAhmptzPF/piMXb16wKIWUvVhZbVW7sJKh0A=
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw=
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=
@ -109,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=
@ -120,8 +132,6 @@ github.com/emersion/go-imap-id v0.0.0-20190926060100-f94a56b9ecde/go.mod h1:sPwp
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead h1:fI1Jck0vUrXT8bnphprS1EoVRe2Q5CKCX8iDlpqjQ/Y=
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-smtp v0.15.1-0.20221021114529-49b17434419d h1:hFRM6zCBSc+Xa0rBOqSlG6Qe9dKC/2vLhGAuZlWxTsc=
github.com/emersion/go-smtp v0.15.1-0.20221021114529-49b17434419d/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3 h1:hQ1wTMaKcGfobYRT88RM8NFNyX+IQHvagkm/tqViU98=
@ -134,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=
@ -144,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=
@ -155,18 +170,19 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
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=
@ -178,8 +194,8 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@ -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=
@ -421,8 +450,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -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,19 +492,21 @@ 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=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -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=
@ -521,28 +559,35 @@ golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
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=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -556,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=
@ -588,13 +635,13 @@ google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20230221151758-ace64dc21148 h1:muK+gVBJBfFb4SejshDBlN2/UgxCCOKH9Y34ljqEGOc=
google.golang.org/genproto v0.0.0-20230221151758-ace64dc21148/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc=
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc=
google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
@ -602,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=
@ -613,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=

View File

@ -41,6 +41,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/sentry"
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
"github.com/ProtonMail/proton-bridge/v3/pkg/restarter"
"github.com/pkg/profile"
"github.com/sirupsen/logrus"
@ -204,7 +205,7 @@ func run(c *cli.Context) error {
}()
// Restart the app if requested.
return withRestarter(exe, func(restarter *restarter.Restarter) error {
err = withRestarter(exe, func(restarter *restarter.Restarter) error {
// Handle crashes with various actions.
return withCrashHandler(restarter, reporter, func(crashHandler *crash.Handler, quitCh <-chan struct{}) error {
migrationErr := migrateOldVersions()
@ -234,53 +235,56 @@ func run(c *cli.Context) error {
}
return withSingleInstance(settings, locations.GetLockFile(), version, func() error {
// Unlock the encrypted vault.
return WithVault(locations, crashHandler, func(v *vault.Vault, insecure, corrupt bool) error {
if !v.Migrated() {
// Migrate old settings into the vault.
if err := migrateOldSettings(v); err != nil {
logrus.WithError(err).Error("Failed to migrate old settings")
}
// Migrate old accounts into the vault.
if err := migrateOldAccounts(locations, v); err != nil {
logrus.WithError(err).Error("Failed to migrate old accounts")
}
// The vault has been migrated.
if err := v.SetMigrated(); err != nil {
logrus.WithError(err).Error("Failed to mark vault as migrated")
}
}
logrus.WithFields(logrus.Fields{
"lastVersion": v.GetLastVersion().String(),
"showAllMail": v.GetShowAllMail(),
"updateCh": v.GetUpdateChannel(),
"autoUpdate": v.GetAutoUpdate(),
"rollout": v.GetUpdateRollout(),
"DoH": v.GetProxyAllowed(),
}).Info("Vault loaded")
// Load the cookies from the vault.
return withCookieJar(v, func(cookieJar http.CookieJar) error {
// Create a new bridge instance.
return withBridge(c, exe, locations, version, identifier, crashHandler, reporter, v, cookieJar, func(b *bridge.Bridge, eventCh <-chan events.Event) error {
if insecure {
logrus.Warn("The vault key could not be retrieved; the vault will not be encrypted")
b.PushError(bridge.ErrVaultInsecure)
// Look for available keychains
return WithKeychainList(func(keychains *keychain.List) error {
// Unlock the encrypted vault.
return WithVault(locations, keychains, crashHandler, func(v *vault.Vault, insecure, corrupt bool) error {
if !v.Migrated() {
// Migrate old settings into the vault.
if err := migrateOldSettings(v); err != nil {
logrus.WithError(err).Error("Failed to migrate old settings")
}
if corrupt {
logrus.Warn("The vault is corrupt and has been wiped")
b.PushError(bridge.ErrVaultCorrupt)
// Migrate old accounts into the vault.
if err := migrateOldAccounts(locations, keychains, v); err != nil {
logrus.WithError(err).Error("Failed to migrate old accounts")
}
// Start telemetry heartbeat process
b.StartHeartbeat(b)
// The vault has been migrated.
if err := v.SetMigrated(); err != nil {
logrus.WithError(err).Error("Failed to mark vault as migrated")
}
}
// Run the frontend.
return runFrontend(c, crashHandler, restarter, locations, b, eventCh, quitCh, c.Int(flagParentPID))
logrus.WithFields(logrus.Fields{
"lastVersion": v.GetLastVersion().String(),
"showAllMail": v.GetShowAllMail(),
"updateCh": v.GetUpdateChannel(),
"autoUpdate": v.GetAutoUpdate(),
"rollout": v.GetUpdateRollout(),
"DoH": v.GetProxyAllowed(),
}).Info("Vault loaded")
// Load the cookies from the vault.
return withCookieJar(v, func(cookieJar http.CookieJar) error {
// Create a new bridge instance.
return withBridge(c, exe, locations, version, identifier, crashHandler, reporter, v, cookieJar, keychains, func(b *bridge.Bridge, eventCh <-chan events.Event) error {
if insecure {
logrus.Warn("The vault key could not be retrieved; the vault will not be encrypted")
b.PushError(bridge.ErrVaultInsecure)
}
if corrupt {
logrus.Warn("The vault is corrupt and has been wiped")
b.PushError(bridge.ErrVaultCorrupt)
}
// Remove old updates files
b.RemoveOldUpdates()
// Run the frontend.
return runFrontend(c, crashHandler, restarter, locations, b, eventCh, quitCh, c.Int(flagParentPID))
})
})
})
})
@ -290,6 +294,13 @@ func run(c *cli.Context) error {
})
})
})
// if an error occurs, it must be logged now because we're about to close the log file.
if err != nil {
logrus.Fatal(err)
}
return err
}
// If there's another instance already running, try to raise it and exit.
@ -470,6 +481,13 @@ func withCookieJar(vault *vault.Vault, fn func(http.CookieJar) error) error {
return fn(persister)
}
// WithKeychainList init the list of usable keychains.
func WithKeychainList(fn func(*keychain.List) error) error {
logrus.Debug("Creating keychain list")
defer logrus.Debug("Keychain list stop")
return fn(keychain.NewList())
}
func setDeviceCookies(jar *cookies.Jar) error {
url, err := url.Parse(constants.APIHost)
if err != nil {

View File

@ -37,6 +37,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/internal/versioner"
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
@ -55,6 +56,7 @@ func withBridge(
reporter *sentry.Reporter,
vault *vault.Vault,
cookieJar http.CookieJar,
keychains *keychain.List,
fn func(*bridge.Bridge, <-chan events.Event) error,
) error {
logrus.Debug("Creating bridge")
@ -97,6 +99,7 @@ func withBridge(
autostarter,
updater,
version,
keychains,
// The API stuff.
constants.APIHost,
@ -110,6 +113,7 @@ func withBridge(
crashHandler,
reporter,
imap.DefaultEpochUIDValidityGenerator(),
nil,
// The logging stuff.
c.String(flagLogIMAP) == "client" || c.String(flagLogIMAP) == "all",
@ -155,7 +159,7 @@ func newUpdater(locations *locations.Locations) (*updater.Updater, error) {
}
return updater.NewUpdater(
updater.NewInstaller(versioner.New(updatesDir)),
versioner.New(updatesDir),
verifier,
constants.UpdateName,
runtime.GOOS,

View File

@ -122,7 +122,7 @@ func migrateOldSettingsWithDir(configDir string, v *vault.Vault) error {
return v.SetBridgeTLSCertKey(certPEM, keyPEM)
}
func migrateOldAccounts(locations *locations.Locations, v *vault.Vault) error {
func migrateOldAccounts(locations *locations.Locations, keychains *keychain.List, v *vault.Vault) error {
logrus.Info("Migrating accounts")
settings, err := locations.ProvideSettingsPath()
@ -134,8 +134,7 @@ func migrateOldAccounts(locations *locations.Locations, v *vault.Vault) error {
if err != nil {
return fmt.Errorf("failed to get helper: %w", err)
}
keychain, err := keychain.NewKeychain(helper, "bridge")
keychain, err := keychain.NewKeychain(helper, "bridge", keychains.GetHelpers(), keychains.GetDefaultHelper())
if err != nil {
return fmt.Errorf("failed to create keychain: %w", err)
}

View File

@ -35,7 +35,6 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/pkg/algo"
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
dockerCredentials "github.com/docker/docker-credential-helpers/credentials"
"github.com/stretchr/testify/require"
)
@ -43,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")
@ -64,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")
@ -133,11 +132,9 @@ func TestKeychainMigration(t *testing.T) {
}
func TestUserMigration(t *testing.T) {
keychainHelper := keychain.NewTestHelper()
kcl := keychain.NewTestKeychainsList()
keychain.Helpers["mock"] = func(string) (dockerCredentials.Helper, error) { return keychainHelper, nil }
kc, err := keychain.NewKeychain("mock", "bridge")
kc, err := keychain.NewKeychain("mock", "bridge", kcl.GetHelpers(), kcl.GetDefaultHelper())
require.NoError(t, err)
require.NoError(t, kc.Put("brokenID", "broken"))
@ -176,9 +173,9 @@ 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, v))
require.NoError(t, migrateOldAccounts(locations, kcl, v))
require.Equal(t, []string{wantCredentials.UserID}, v.GetUserIDs())
require.NoError(t, v.GetUser(wantCredentials.UserID, func(u *vault.User) {

View File

@ -22,6 +22,7 @@ import (
"path"
"github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/proton-bridge/v3/internal/certs"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
@ -29,30 +30,37 @@ import (
"github.com/sirupsen/logrus"
)
func WithVault(locations *locations.Locations, panicHandler async.PanicHandler, fn func(*vault.Vault, bool, bool) error) error {
func WithVault(locations *locations.Locations, keychains *keychain.List, panicHandler async.PanicHandler, fn func(*vault.Vault, bool, bool) error) error {
logrus.Debug("Creating vault")
defer logrus.Debug("Vault stopped")
// Create the encVault.
encVault, insecure, corrupt, err := newVault(locations, panicHandler)
encVault, insecure, corrupt, err := newVault(locations, keychains, panicHandler)
if err != nil {
return fmt.Errorf("could not create vault: %w", err)
}
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, 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")
@ -62,7 +70,7 @@ func newVault(locations *locations.Locations, panicHandler async.PanicHandler) (
insecure bool
)
if key, err := loadVaultKey(vaultDir); err != nil {
if key, err := loadVaultKey(vaultDir, keychains); err != nil {
logrus.WithError(err).Error("Could not load/create vault key")
insecure = true
@ -74,24 +82,24 @@ func newVault(locations *locations.Locations, panicHandler async.PanicHandler) (
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
}
func loadVaultKey(vaultDir string) ([]byte, error) {
func loadVaultKey(vaultDir string, keychains *keychain.List) ([]byte, error) {
helper, err := vault.GetHelper(vaultDir)
if err != nil {
return nil, fmt.Errorf("could not get keychain helper: %w", err)
}
kc, err := keychain.NewKeychain(helper, constants.KeyChainName)
kc, err := keychain.NewKeychain(helper, constants.KeyChainName, keychains.GetHelpers(), keychains.GetDefaultHelper())
if err != nil {
return nil, fmt.Errorf("could not create keychain: %w", err)
}

View File

@ -45,6 +45,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/telemetry"
"github.com/ProtonMail/proton-bridge/v3/internal/user"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
"github.com/bradenaw/juniper/xslices"
"github.com/go-resty/resty/v2"
"github.com/sirupsen/logrus"
@ -74,7 +75,7 @@ type Bridge struct {
installCh chan installJob
// heartbeat is the telemetry heartbeat for metrics.
heartbeat telemetry.Heartbeat
heartbeat *heartBeatState
// curVersion is the current version of the bridge,
// newVersion is the version that was installed by the updater.
@ -82,6 +83,9 @@ type Bridge struct {
newVersion *semver.Version
newVersionLock safe.RWMutex
// keychains is the utils that own usable keychains found in the OS.
keychains *keychain.List
// focusService is used to raise the bridge window when needed.
focusService *focus.Service
@ -124,9 +128,6 @@ type Bridge struct {
// goUpdate triggers a check/install of updates.
goUpdate func()
// goHeartbeat triggers a check/sending if heartbeat is needed.
goHeartbeat func()
serverManager *imapsmtpserver.Service
syncService *syncservice.Service
}
@ -138,6 +139,7 @@ func New(
autostarter Autostarter, // the autostarter to manage autostart settings
updater Updater, // the updater to fetch and install updates
curVersion *semver.Version, // the current version of the bridge
keychains *keychain.List, // usable keychains
apiURL string, // the URL of the API to use
cookieJar http.CookieJar, // the cookie jar to use
@ -148,6 +150,7 @@ func New(
panicHandler async.PanicHandler,
reporter reporter.Reporter,
uidValidityGenerator imap.UIDValidityGenerator,
heartBeatManager telemetry.HeartbeatManager,
logIMAPClient, logIMAPServer bool, // whether to log IMAP client/server activity
logSMTP bool, // whether to log SMTP activity
@ -163,6 +166,7 @@ func New(
// bridge is the bridge.
bridge, err := newBridge(
context.Background(),
tasks,
imapEventCh,
@ -171,6 +175,7 @@ func New(
autostarter,
updater,
curVersion,
keychains,
panicHandler,
reporter,
@ -178,6 +183,7 @@ func New(
identifier,
proxyCtl,
uidValidityGenerator,
heartBeatManager,
logIMAPClient, logIMAPServer, logSMTP,
)
if err != nil {
@ -196,6 +202,7 @@ func New(
}
func newBridge(
ctx context.Context,
tasks *async.Group,
imapEventCh chan imapEvents.Event,
@ -204,6 +211,7 @@ func newBridge(
autostarter Autostarter,
updater Updater,
curVersion *semver.Version,
keychains *keychain.List,
panicHandler async.PanicHandler,
reporter reporter.Reporter,
@ -211,6 +219,7 @@ func newBridge(
identifier identifier.Identifier,
proxyCtl ProxyController,
uidValidityGenerator imap.UIDValidityGenerator,
heartbeatManager telemetry.HeartbeatManager,
logIMAPClient, logIMAPServer, logSMTP bool,
) (*Bridge, error) {
@ -256,9 +265,13 @@ func newBridge(
newVersion: curVersion,
newVersionLock: safe.NewRWMutex(),
keychains: keychains,
panicHandler: panicHandler,
reporter: reporter,
heartbeat: newHeartBeatState(ctx, panicHandler),
focusService: focusService,
autostarter: autostarter,
locator: locator,
@ -288,7 +301,13 @@ func newBridge(
return nil, err
}
bridge.syncService.Run(bridge.tasks)
if heartbeatManager == nil {
bridge.heartbeat.init(bridge, bridge)
} else {
bridge.heartbeat.init(bridge, heartbeatManager)
}
bridge.syncService.Run()
return bridge, nil
}
@ -417,6 +436,9 @@ func (bridge *Bridge) GetErrors() []error {
func (bridge *Bridge) Close(ctx context.Context) {
logrus.Info("Closing bridge")
// Stop heart beat before closing users.
bridge.heartbeat.stop()
// Close all users.
safe.Lock(func() {
for _, user := range bridge.users {
@ -429,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()
@ -487,27 +511,15 @@ func (bridge *Bridge) remWatcher(watcher *watcher.Watcher[events.Event]) {
watcher.Close()
}
func (bridge *Bridge) onStatusUp(ctx context.Context) {
func (bridge *Bridge) onStatusUp(_ context.Context) {
logrus.Info("Handling API status up")
safe.RLock(func() {
for _, user := range bridge.users {
user.OnStatusUp(ctx)
}
}, bridge.usersLock)
bridge.goLoad()
}
func (bridge *Bridge) onStatusDown(ctx context.Context) {
logrus.Info("Handling API status down")
safe.RLock(func() {
for _, user := range bridge.users {
user.OnStatusDown(ctx)
}
}, bridge.usersLock)
for backoff := time.Second; ; backoff = min(backoff*2, 30*time.Second) {
select {
case <-ctx.Done():

View File

@ -49,6 +49,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/user"
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
"github.com/ProtonMail/proton-bridge/v3/tests"
"github.com/bradenaw/juniper/xslices"
imapid "github.com/emersion/go-imap-id"
@ -950,6 +951,7 @@ func withBridgeNoMocks(
mocks.Autostarter,
mocks.Updater,
v2_3_0,
keychain.NewTestKeychainsList(),
// The API stuff.
apiURL,
@ -961,6 +963,7 @@ func withBridgeNoMocks(
mocks.CrashHandler,
mocks.Reporter,
testUIDValidityGenerator,
mocks.Heartbeat,
// The logging stuff.
os.Getenv("BRIDGE_LOG_IMAP_CLIENT") == "1",
@ -970,9 +973,6 @@ func withBridgeNoMocks(
require.NoError(t, err)
require.Empty(t, bridge.GetErrors())
// Start the Heartbeat process.
bridge.StartHeartbeat(mocks.Heartbeat)
// Wait for bridge to finish loading users.
waitForEvent(t, eventCh, events.AllUsersLoaded{})

View File

@ -19,6 +19,7 @@ package bridge
import (
"context"
"errors"
"io"
"github.com/ProtonMail/go-proton-api"
@ -33,63 +34,133 @@ const (
DefaultMaxSessionCountForBugReport = 10
)
func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, title, description, username, email, client string, attachLogs bool) error {
var account = username
type ReportBugReq struct {
OSType string
OSVersion string
Title string
Description string
Username string
Email string
EmailClient string
IncludeLogs bool
}
if info, err := bridge.QueryUserInfo(username); err == nil {
account = info.Username
func (bridge *Bridge) ReportBug(ctx context.Context, report *ReportBugReq) error {
if info, err := bridge.QueryUserInfo(report.Username); err == nil {
report.Username = info.Username
} else if userIDs := bridge.GetUserIDs(); len(userIDs) > 0 {
if err := bridge.vault.GetUser(userIDs[0], func(user *vault.User) {
account = user.Username()
report.Username = user.Username()
}); err != nil {
return err
}
}
var attachment []proton.ReportBugAttachment
if attachLogs {
logsPath, err := bridge.locator.ProvideLogsPath()
var attachments []proton.ReportBugAttachment
if report.IncludeLogs {
logs, err := bridge.CollectLogs()
if err != nil {
return err
}
buffer, err := logging.ZipLogsForBugReport(logsPath, DefaultMaxSessionCountForBugReport, DefaultMaxBugReportZipSize)
if err != nil {
return err
}
body, err := io.ReadAll(buffer)
if err != nil {
return err
}
attachment = append(attachment, proton.ReportBugAttachment{
Name: "logs.zip",
Filename: "logs.zip",
MIMEType: "application/zip",
Body: body,
})
attachments = append(attachments, logs)
}
safe.Lock(func() {
var firstAtt proton.ReportBugAttachment
if len(attachments) > 0 && report.IncludeLogs {
firstAtt = attachments[0]
}
attachmentType := proton.AttachmentTypeSync
if len(attachments) > 1 {
attachmentType = proton.AttachmentTypeAsync
}
token, err := bridge.createTicket(ctx, report, attachmentType, firstAtt)
if err != nil || token == "" {
return err
}
safe.RLock(func() {
for _, user := range bridge.users {
user.ReportBugSent()
}
}, bridge.usersLock)
return bridge.api.ReportBug(ctx, proton.ReportBugReq{
OS: osType,
OSVersion: osVersion,
// if we have a token we can append more attachment to the bugReport
for i, att := range attachments {
if i == 0 && report.IncludeLogs {
continue
}
err := bridge.appendComment(ctx, token, att)
if err != nil {
return err
}
}
return err
}
Title: "[Bridge] Bug - " + title,
Description: description,
func (bridge *Bridge) CollectLogs() (proton.ReportBugAttachment, error) {
logsPath, err := bridge.locator.ProvideLogsPath()
if err != nil {
return proton.ReportBugAttachment{}, err
}
Client: client,
buffer, err := logging.ZipLogsForBugReport(logsPath, DefaultMaxSessionCountForBugReport, DefaultMaxBugReportZipSize)
if err != nil {
return proton.ReportBugAttachment{}, err
}
body, err := io.ReadAll(buffer)
if err != nil {
return proton.ReportBugAttachment{}, err
}
return proton.ReportBugAttachment{
Name: "logs.zip",
Filename: "logs.zip",
MIMEType: "application/zip",
Body: body,
}, nil
}
func (bridge *Bridge) createTicket(ctx context.Context, report *ReportBugReq,
asyncAttach proton.AttachmentType, att proton.ReportBugAttachment) (string, error) {
var attachments []proton.ReportBugAttachment
attachments = append(attachments, att)
res, err := bridge.api.ReportBug(ctx, proton.ReportBugReq{
OS: report.OSType,
OSVersion: report.OSVersion,
Title: "[Bridge] Bug - " + report.Title,
Description: report.Description,
Client: report.EmailClient,
ClientType: proton.ClientTypeEmail,
ClientVersion: constants.AppVersion(bridge.curVersion.Original()),
Username: account,
Email: email,
}, attachment...)
Username: report.Username,
Email: report.Email,
AsyncAttachments: asyncAttach,
}, attachments...)
if err != nil || asyncAttach != proton.AttachmentTypeAsync {
return "", err
}
if asyncAttach == proton.AttachmentTypeAsync && res.Token == nil {
return "", errors.New("no token returns for AsyncAttachments")
}
return *res.Token, nil
}
func (bridge *Bridge) appendComment(ctx context.Context, token string, att proton.ReportBugAttachment) error {
var attachments []proton.ReportBugAttachment
attachments = append(attachments, att)
return bridge.api.ReportBugAttachement(ctx, proton.ReportBugAttachmentReq{
Product: proton.ClientTypeEmail,
Body: "Comment adding attachment: " + att.Filename,
Token: token,
}, attachments...)
}

View File

@ -22,7 +22,7 @@ import (
)
func (bridge *Bridge) ReportBugClicked() {
safe.Lock(func() {
safe.RLock(func() {
for _, user := range bridge.users {
user.ReportBugClicked()
}
@ -30,17 +30,17 @@ func (bridge *Bridge) ReportBugClicked() {
}
func (bridge *Bridge) AutoconfigUsed(client string) {
safe.Lock(func() {
safe.RLock(func() {
for _, user := range bridge.users {
user.AutoconfigUsed(client)
}
}, bridge.usersLock)
}
func (bridge *Bridge) KBArticleOpened(article string) {
safe.Lock(func() {
func (bridge *Bridge) ExternalLinkClicked(article string) {
safe.RLock(func() {
for _, user := range bridge.users {
user.KBArticleOpened(article)
user.ExternalLinkClicked(article)
}
}, bridge.usersLock)
}

View File

@ -19,6 +19,7 @@ package bridge
import (
"context"
"errors"
"strings"
"github.com/ProtonMail/proton-bridge/v3/internal/clientconfig"
@ -30,8 +31,8 @@ import (
"github.com/sirupsen/logrus"
)
// ConfigureAppleMail configures apple mail for the given userID and address.
// If configuring apple mail for Catalina or newer, it ensures Bridge is using SSL.
// ConfigureAppleMail configures Apple Mail for the given userID and address.
// If configuring Apple Mail for Catalina or newer, it ensures Bridge is using SSL.
func (bridge *Bridge) ConfigureAppleMail(ctx context.Context, userID, address string) error {
logrus.WithFields(logrus.Fields{
"userID": userID,
@ -44,16 +45,28 @@ func (bridge *Bridge) ConfigureAppleMail(ctx context.Context, userID, address st
return ErrNoSuchUser
}
if address == "" {
address = user.Emails()[0]
emails := user.Emails()
displayNames := user.DisplayNames()
if (len(emails) == 0) || (len(displayNames) == 0) {
return errors.New("could not retrieve user address info")
}
username := address
addresses := address
if address == "" {
address = emails[0]
}
var username, displayName, addresses string
if user.GetAddressMode() == vault.CombinedMode {
username = user.Emails()[0]
addresses = strings.Join(user.Emails(), ",")
username = address
displayName = displayNames[username]
addresses = strings.Join(emails, ",")
} else {
username = address
addresses = address
displayName = displayNames[address]
if len(displayName) == 0 {
displayName = address
}
}
if useragent.IsCatalinaOrNewer() && !bridge.vault.GetSMTPSSL() {
@ -69,6 +82,7 @@ func (bridge *Bridge) ConfigureAppleMail(ctx context.Context, userID, address st
bridge.vault.GetIMAPSSL(),
bridge.vault.GetSMTPSSL(),
username,
displayName,
addresses,
user.BridgePass(),
)

View File

@ -20,18 +20,100 @@ package bridge
import (
"context"
"encoding/json"
"sync"
"time"
"github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/telemetry"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
"github.com/sirupsen/logrus"
)
const HeartbeatCheckInterval = time.Hour
type heartBeatState struct {
task *async.Group
telemetry.Heartbeat
taskLock sync.Mutex
taskStarted bool
taskInterval time.Duration
}
func newHeartBeatState(ctx context.Context, panicHandler async.PanicHandler) *heartBeatState {
return &heartBeatState{
task: async.NewGroup(ctx, panicHandler),
}
}
func (h *heartBeatState) init(bridge *Bridge, manager telemetry.HeartbeatManager) {
h.Heartbeat = telemetry.NewHeartbeat(manager, 1143, 1025, bridge.GetGluonCacheDir(), bridge.keychains.GetDefaultHelper())
h.taskInterval = manager.GetHeartbeatPeriodicInterval()
h.SetRollout(bridge.GetUpdateRollout())
h.SetAutoStart(bridge.GetAutostart())
h.SetAutoUpdate(bridge.GetAutoUpdate())
h.SetBeta(bridge.GetUpdateChannel())
h.SetDoh(bridge.GetProxyAllowed())
h.SetShowAllMail(bridge.GetShowAllMail())
h.SetIMAPConnectionMode(bridge.GetIMAPSSL())
h.SetSMTPConnectionMode(bridge.GetSMTPSSL())
h.SetIMAPPort(bridge.GetIMAPPort())
h.SetSMTPPort(bridge.GetSMTPPort())
h.SetCacheLocation(bridge.GetGluonCacheDir())
if val, err := bridge.GetKeychainApp(); err != nil {
h.SetKeyChainPref(val)
} else {
h.SetKeyChainPref(bridge.keychains.GetDefaultHelper())
}
h.SetPrevVersion(bridge.GetLastVersion().String())
safe.RLock(func() {
var splitMode = false
for _, user := range bridge.users {
if user.GetAddressMode() == vault.SplitMode {
splitMode = true
break
}
}
var nbAccount = len(bridge.users)
h.SetNbAccount(nbAccount)
h.SetSplitMode(splitMode)
// Do not try to send if there is no user yet.
if nbAccount > 0 {
defer h.start()
}
}, bridge.usersLock)
}
func (h *heartBeatState) start() {
h.taskLock.Lock()
defer h.taskLock.Unlock()
if h.taskStarted {
return
}
h.taskStarted = true
h.task.PeriodicOrTrigger(h.taskInterval, 0, func(ctx context.Context) {
logrus.Debug("Checking for heartbeat")
h.TrySending(ctx)
})
}
func (h *heartBeatState) stop() {
h.taskLock.Lock()
defer h.taskLock.Unlock()
if !h.taskStarted {
return
}
h.task.CancelAndWait()
h.taskStarted = false
}
func (bridge *Bridge) IsTelemetryAvailable(ctx context.Context) bool {
var flag = true
if bridge.GetTelemetryDisabled() {
@ -80,49 +162,6 @@ func (bridge *Bridge) SetLastHeartbeatSent(timestamp time.Time) error {
return bridge.vault.SetLastHeartbeatSent(timestamp)
}
func (bridge *Bridge) StartHeartbeat(manager telemetry.HeartbeatManager) {
bridge.heartbeat = telemetry.NewHeartbeat(manager, 1143, 1025, bridge.GetGluonCacheDir(), keychain.DefaultHelper)
// Check for heartbeat when triggered.
bridge.goHeartbeat = bridge.tasks.PeriodicOrTrigger(HeartbeatCheckInterval, 0, func(ctx context.Context) {
logrus.Debug("Checking for heartbeat")
bridge.heartbeat.TrySending(ctx)
})
bridge.heartbeat.SetRollout(bridge.GetUpdateRollout())
bridge.heartbeat.SetAutoStart(bridge.GetAutostart())
bridge.heartbeat.SetAutoUpdate(bridge.GetAutoUpdate())
bridge.heartbeat.SetBeta(bridge.GetUpdateChannel())
bridge.heartbeat.SetDoh(bridge.GetProxyAllowed())
bridge.heartbeat.SetShowAllMail(bridge.GetShowAllMail())
bridge.heartbeat.SetIMAPConnectionMode(bridge.GetIMAPSSL())
bridge.heartbeat.SetSMTPConnectionMode(bridge.GetSMTPSSL())
bridge.heartbeat.SetIMAPPort(bridge.GetIMAPPort())
bridge.heartbeat.SetSMTPPort(bridge.GetSMTPPort())
bridge.heartbeat.SetCacheLocation(bridge.GetGluonCacheDir())
if val, err := bridge.GetKeychainApp(); err != nil {
bridge.heartbeat.SetKeyChainPref(val)
} else {
bridge.heartbeat.SetKeyChainPref(keychain.DefaultHelper)
}
bridge.heartbeat.SetPrevVersion(bridge.GetLastVersion().String())
safe.RLock(func() {
var splitMode = false
for _, user := range bridge.users {
if user.GetAddressMode() == vault.SplitMode {
splitMode = true
break
}
}
var nbAccount = len(bridge.users)
bridge.heartbeat.SetNbAccount(nbAccount)
bridge.heartbeat.SetSplitMode(splitMode)
// Do not try to send if there is no user yet.
if nbAccount > 0 {
defer bridge.goHeartbeat()
}
}, bridge.usersLock)
func (bridge *Bridge) GetHeartbeatPeriodicInterval() time.Duration {
return HeartbeatCheckInterval
}

View File

@ -0,0 +1,24 @@
// 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/>.
package bridge
import "golang.org/x/exp/maps"
func (bridge *Bridge) GetHelpersNames() []string {
return maps.Keys(bridge.keychains.GetHelpers())
}

View File

@ -7,6 +7,7 @@ import (
"os"
"sync"
"testing"
"time"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/proton-bridge/v3/internal/bridge/mocks"
@ -51,6 +52,7 @@ func NewMocks(tb testing.TB, version, minAuto *semver.Version) *Mocks {
// this is called at start of heartbeat process.
mocks.Heartbeat.EXPECT().IsTelemetryAvailable(gomock.Any()).AnyTimes()
mocks.Heartbeat.EXPECT().GetHeartbeatPeriodicInterval().AnyTimes().Return(500 * time.Millisecond)
return mocks
}
@ -154,3 +156,7 @@ func (testUpdater *TestUpdater) GetVersionInfo(_ context.Context, _ updater.Down
func (testUpdater *TestUpdater) InstallUpdate(_ context.Context, _ updater.Downloader, _ updater.VersionInfo) error {
return nil
}
func (testUpdater *TestUpdater) RemoveOldUpdates() error {
return nil
}

View File

@ -36,6 +36,20 @@ func (m *MockHeartbeatManager) EXPECT() *MockHeartbeatManagerMockRecorder {
return m.recorder
}
// GetHeartbeatPeriodicInterval mocks base method.
func (m *MockHeartbeatManager) GetHeartbeatPeriodicInterval() time.Duration {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetHeartbeatPeriodicInterval")
ret0, _ := ret[0].(time.Duration)
return ret0
}
// GetHeartbeatPeriodicInterval indicates an expected call of GetHeartbeatPeriodicInterval.
func (mr *MockHeartbeatManagerMockRecorder) GetHeartbeatPeriodicInterval() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHeartbeatPeriodicInterval", reflect.TypeOf((*MockHeartbeatManager)(nil).GetHeartbeatPeriodicInterval))
}
// GetLastHeartbeatSent mocks base method.
func (m *MockHeartbeatManager) GetLastHeartbeatSent() time.Time {
m.ctrl.T.Helper()

View File

@ -33,6 +33,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
smtpservice "github.com/ProtonMail/proton-bridge/v3/internal/services/smtp"
"github.com/emersion/go-imap"
"github.com/emersion/go-sasl"
"github.com/emersion/go-smtp"
@ -336,6 +337,9 @@ func TestBridge_SendInvite(t *testing.T) {
}
func TestBridge_SendAddTextBodyPartIfNotExists(t *testing.T) {
// NOTE: Prior to GODT-2887, these tests had inline images, however after the implementation to support
// inline images new parts are injected to reference inline images without content-id set. The images
// in this test have been changed to regular attachments to keep the original checks in place.
const messageMultipartWithoutText = `Content-Type: multipart/mixed;
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
Subject: A new message
@ -343,7 +347,7 @@ Date: Mon, 13 Mar 2023 16:06:16 +0100
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
Content-Disposition: inline;
Content-Disposition: attachment;
filename=Cat_August_2010-4.jpeg
Content-Type: image/jpeg;
name="Cat_August_2010-4.jpeg"
@ -360,7 +364,7 @@ Subject: A new message Part2
Date: Mon, 13 Mar 2023 16:06:16 +0100
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
Content-Disposition: inline;
Content-Disposition: attachment;
filename=Cat_August_2010-4.jpeg
Content-Type: image/jpeg;
name="Cat_August_2010-4.jpeg"
@ -520,3 +524,234 @@ SGVsbG8gd29ybGQK
})
})
}
func TestBridge_SendInlineImage(t *testing.T) {
const messageInlineImageOnly = `Content-Type: multipart/mixed;
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
Subject: A new message
Date: Mon, 13 Mar 2023 16:06:16 +0100
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
Content-Disposition: inline;
filename=Cat_August_2010-4.jpeg
Content-Type: image/jpeg;
name="Cat_August_2010-4.jpeg"
Content-Transfer-Encoding: base64
SGVsbG8gd29ybGQ=
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84--
`
const messageInlineImageWithHTML = `Content-Type: multipart/mixed;
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
Subject: A new message Part2
Date: Mon, 13 Mar 2023 16:06:16 +0100
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
Content-Type: text/html;charset=utf8
Content-Transfer-Encoding: quoted-printable
Hello world
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
Content-Disposition: inline;
filename=Cat_August_2010-4.jpeg
Content-Type: image/jpeg;
name="Cat_August_2010-4.jpeg"
Content-Transfer-Encoding: base64
SGVsbG8gd29ybGQ=
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84--
`
const messageInlineImageWithText = `Content-Type: multipart/mixed;
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
Subject: A new message Part3
Date: Mon, 13 Mar 2023 16:06:16 +0100
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
Content-Type: text/plain;charset=utf8
Content-Transfer-Encoding: quoted-printable
Hello world
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
Content-Disposition: inline;
filename=Cat_August_2010-4.jpeg
Content-Type: image/jpeg;
name="Cat_August_2010-4.jpeg"
Content-Transfer-Encoding: base64
SGVsbG8gd29ybGQ=
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84--
`
const messageInlineImageFollowedByText = `Content-Type: multipart/mixed;
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
Subject: A new message Part4
Date: Mon, 13 Mar 2023 16:06:16 +0100
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
Content-Disposition: inline;
filename=Cat_August_2010-4.jpeg
Content-Type: image/jpeg;
name="Cat_August_2010-4.jpeg"
Content-Transfer-Encoding: base64
SGVsbG8gd29ybGQ=
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
Content-Type: text/plain;charset=utf8
Content-Transfer-Encoding: quoted-printable
Hello world
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84--
`
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
_, _, err := s.CreateUser("recipient", password)
require.NoError(t, err)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
smtpWaiter := waitForSMTPServerReady(bridge)
defer smtpWaiter.Done()
senderUserID, err := bridge.LoginFull(ctx, username, password, nil, nil)
require.NoError(t, err)
recipientUserID, err := bridge.LoginFull(ctx, "recipient", password, nil, nil)
require.NoError(t, err)
senderInfo, err := bridge.GetUserInfo(senderUserID)
require.NoError(t, err)
recipientInfo, err := bridge.GetUserInfo(recipientUserID)
require.NoError(t, err)
messages := []string{
messageInlineImageOnly,
messageInlineImageWithHTML,
messageInlineImageWithText,
messageInlineImageFollowedByText,
}
smtpWaiter.Wait()
for _, m := range messages {
// Dial the server.
client, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
require.NoError(t, err)
defer client.Close() //nolint:errcheck
// Upgrade to TLS.
require.NoError(t, client.StartTLS(&tls.Config{InsecureSkipVerify: true}))
// Authorize with SASL LOGIN.
require.NoError(t, client.Auth(sasl.NewLoginClient(
senderInfo.Addresses[0],
string(senderInfo.BridgePass)),
))
// Send the message.
require.NoError(t, client.SendMail(
senderInfo.Addresses[0],
[]string{recipientInfo.Addresses[0]},
strings.NewReader(m),
))
}
// Connect the sender IMAP client.
senderIMAPClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
require.NoError(t, err)
require.NoError(t, senderIMAPClient.Login(senderInfo.Addresses[0], string(senderInfo.BridgePass)))
defer senderIMAPClient.Logout() //nolint:errcheck
// Connect the recipient IMAP client.
recipientIMAPClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
require.NoError(t, err)
require.NoError(t, recipientIMAPClient.Login(recipientInfo.Addresses[0], string(recipientInfo.BridgePass)))
defer recipientIMAPClient.Logout() //nolint:errcheck
require.Eventually(t, func() bool {
messages, err := clientFetch(senderIMAPClient, `Sent`, imap.FetchBodyStructure)
require.NoError(t, err)
if len(messages) != 4 {
return false
}
// messages may not be in order
for _, message := range messages {
require.Equal(t, 1, len(message.BodyStructure.Parts))
require.Equal(t, "multipart", message.BodyStructure.MIMEType)
require.Equal(t, "mixed", message.BodyStructure.MIMESubType)
require.Equal(t, "multipart", message.BodyStructure.Parts[0].MIMEType)
require.Equal(t, "related", message.BodyStructure.Parts[0].MIMESubType)
require.Len(t, message.BodyStructure.Parts[0].Parts, 2)
require.Equal(t, "text", message.BodyStructure.Parts[0].Parts[0].MIMEType)
require.Equal(t, "html", message.BodyStructure.Parts[0].Parts[0].MIMESubType)
require.Equal(t, "image", message.BodyStructure.Parts[0].Parts[1].MIMEType)
require.Equal(t, "jpeg", message.BodyStructure.Parts[0].Parts[1].MIMESubType)
}
return true
}, 10*time.Second, 100*time.Millisecond)
})
})
}
func TestBridge_SendAddressDisabled(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
recipientUserID, _, err := s.CreateUser("recipient", password)
require.NoError(t, err)
senderUserID, addrID, err := s.CreateUser("sender", password)
require.NoError(t, err)
require.NoError(t, s.ChangeAddressAllowSend(senderUserID, addrID, false))
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
smtpWaiter := waitForSMTPServerReady(bridge)
defer smtpWaiter.Done()
senderUserID, err := bridge.LoginFull(ctx, "sender", password, nil, nil)
require.NoError(t, err)
_, err = bridge.LoginFull(ctx, "recipient", password, nil, nil)
require.NoError(t, err)
smtpWaiter.Wait()
recipientInfo, err := bridge.GetUserInfo(recipientUserID)
require.NoError(t, err)
senderInfo, err := bridge.GetUserInfo(senderUserID)
require.NoError(t, err)
// Dial the server.
client, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
require.NoError(t, err)
defer client.Close() //nolint:errcheck
// Upgrade to TLS.
require.NoError(t, client.StartTLS(&tls.Config{InsecureSkipVerify: true}))
require.NoError(t, client.Auth(sasl.NewLoginClient(
senderInfo.Addresses[0],
string(senderInfo.BridgePass)),
))
// Send the message.
err = client.SendMail(
senderInfo.Addresses[0],
[]string{recipientInfo.Addresses[0]},
strings.NewReader("Subject: Test 1\r\n\r\nHello world!"),
)
smtpErr := smtpservice.NewErrCanNotSendOnAddress(senderInfo.Addresses[0])
require.Equal(t, fmt.Sprintf("Error: %v", smtpErr.Error()), err.Error())
})
})
}

View File

@ -261,9 +261,12 @@ func (bridge *Bridge) SetTelemetryDisabled(isDisabled bool) error {
return err
}
// If telemetry is re-enabled locally, try to send the heartbeat.
if !isDisabled {
defer bridge.goHeartbeat()
if isDisabled {
bridge.heartbeat.stop()
} else {
bridge.heartbeat.start()
}
return nil
}

View File

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

View File

@ -53,4 +53,5 @@ type Autostarter interface {
type Updater interface {
GetVersionInfo(context.Context, updater.Downloader, updater.Channel) (updater.VersionInfo, error)
InstallUpdate(context.Context, updater.Downloader, updater.VersionInfo) error
RemoveOldUpdates() error
}

View File

@ -139,3 +139,9 @@ func (bridge *Bridge) installUpdate(ctx context.Context, job installJob) {
}
}, bridge.newVersionLock)
}
func (bridge *Bridge) RemoveOldUpdates() {
if err := bridge.updater.RemoveOldUpdates(); err != nil {
logrus.WithError(err).Error("Remove old updates fails")
}
}

View File

@ -46,6 +46,8 @@ const (
Connected
)
var ErrFailedToUnlock = errors.New("failed to unlock user keys")
type UserInfo struct {
// UserID is the user's API ID.
UserID string
@ -66,10 +68,10 @@ type UserInfo struct {
BridgePass []byte
// UsedSpace is the amount of space used by the user.
UsedSpace int
UsedSpace uint64
// MaxSpace is the total amount of space available to the user.
MaxSpace int
MaxSpace uint64
}
// GetUserIDs returns the IDs of all known users (authorized or not).
@ -157,11 +159,15 @@ func (bridge *Bridge) LoginUser(
func() (string, error) {
return bridge.loginUser(ctx, client, auth.UID, auth.RefreshToken, keyPass)
},
func() error {
return client.AuthDelete(ctx)
},
)
if err != nil {
// Failure to unlock will allow retries, so we do not delete auth.
if !errors.Is(err, ErrFailedToUnlock) {
if deleteErr := client.AuthDelete(ctx); deleteErr != nil {
logrus.WithError(deleteErr).Error("Failed to delete auth")
}
}
return "", fmt.Errorf("failed to login user: %w", err)
}
@ -217,7 +223,16 @@ func (bridge *Bridge) LoginFull(
keyPass = password
}
return bridge.LoginUser(ctx, client, auth, keyPass)
userID, err := bridge.LoginUser(ctx, client, auth, keyPass)
if err != nil {
if deleteErr := client.AuthDelete(ctx); deleteErr != nil {
logrus.WithError(err).Error("Failed to delete auth")
}
return "", err
}
return userID, nil
}
// LogoutUser logs out the given user.
@ -314,7 +329,7 @@ func (bridge *Bridge) SetAddressMode(ctx context.Context, userID string, mode va
func (bridge *Bridge) SendBadEventUserFeedback(_ context.Context, userID string, doResync bool) error {
logrus.WithField("userID", userID).WithField("doResync", doResync).Info("Passing bad event feedback to user")
return safe.LockRet(func() error {
return safe.RLockRet(func() error {
ctx := context.Background()
user, ok := bridge.users[userID]
@ -374,9 +389,9 @@ func (bridge *Bridge) loginUser(ctx context.Context, client *proton.Client, auth
}
if userKR, err := apiUser.Keys.Unlock(saltedKeyPass, nil); err != nil {
return "", fmt.Errorf("failed to unlock user keys: %w", err)
return "", fmt.Errorf("%w: %w", ErrFailedToUnlock, err)
} else if userKR.CountDecryptionEntities() == 0 {
return "", fmt.Errorf("failed to unlock user keys")
return "", ErrFailedToUnlock
}
if err := bridge.addUser(ctx, client, apiUser, authUID, authRef, saltedKeyPass, true); err != nil {
@ -479,7 +494,7 @@ func (bridge *Bridge) addUser(
return fmt.Errorf("failed to add vault user: %w", err)
}
if err := bridge.addUserWithVault(ctx, client, apiUser, vaultUser); err != nil {
if err := bridge.addUserWithVault(ctx, client, apiUser, vaultUser, isNew); err != nil {
if _, ok := err.(*resty.ResponseError); ok || isLogin {
logrus.WithError(err).Error("Failed to add user, clearing its secrets from vault")
@ -514,6 +529,7 @@ func (bridge *Bridge) addUserWithVault(
client *proton.Client,
apiUser proton.User,
vault *vault.User,
isNew bool,
) error {
statsPath, err := bridge.locator.ProvideStatsPath()
if err != nil {
@ -541,6 +557,7 @@ func (bridge *Bridge) addUserWithVault(
&bridgeEventSubscription{b: bridge},
bridge.syncService,
syncSettingsPath,
isNew,
)
if err != nil {
return fmt.Errorf("failed to create user: %w", err)
@ -577,7 +594,7 @@ func (bridge *Bridge) addUserWithVault(
}, bridge.usersLock)
// As we need at least one user to send heartbeat, try to send it.
defer bridge.goHeartbeat()
bridge.heartbeat.start()
return nil
}

View File

@ -49,7 +49,7 @@ func (bridge *Bridge) handleUserDeauth(ctx context.Context, user *user.User) {
}
func (bridge *Bridge) handleUserBadEvent(ctx context.Context, user *user.User, event events.UserBadEvent) {
safe.Lock(func() {
safe.RLock(func() {
if rerr := bridge.reporter.ReportMessageWithContext("Failed to handle event", reporter.Context{
"user_id": user.ID(),
"old_event_id": event.OldEventID,

View File

@ -356,6 +356,10 @@ func removeCertTrustCGo(buffer *C.char, size C.ulonglong) error {
}
}
func osSupportCertInstall() bool {
return true
}
// installCert installs a certificate in the keychain. The certificate is added to the keychain and it is set as trusted.
// This function will trigger a security prompt from the system, unless the certificate is already trusted in the user keychain.
func installCert(certPEM []byte) error {

View File

@ -28,6 +28,7 @@ import (
func TestCertInKeychain(t *testing.T) {
// no trust settings change is performed, so this test will not trigger an OS security prompt.
certPEM := generatePEMCertificate(t)
require.True(t, osSupportCertInstall())
require.False(t, isCertInKeychain(certPEM))
require.NoError(t, addCertToKeychain(certPEM))
require.True(t, isCertInKeychain(certPEM))

View File

@ -17,6 +17,10 @@
package certs
func osSupportCertInstall() bool {
return false
}
func installCert([]byte) error {
return nil // Linux doesn't have a root cert store.
}

View File

@ -17,6 +17,10 @@
package certs
func osSupportCertInstall() bool {
return false
}
func installCert([]byte) error {
return nil // NOTE(GODT-986): Install certs to root cert store?
}

View File

@ -37,6 +37,10 @@ func NewInstaller() *Installer {
}
}
func (installer *Installer) OSSupportCertInstall() bool {
return osSupportCertInstall()
}
func (installer *Installer) InstallCert(certPEM []byte) error {
installer.log.Info("Installing the Bridge TLS certificate in the OS keychain")
@ -64,3 +68,15 @@ func (installer *Installer) UninstallCert(certPEM []byte) error {
func (installer *Installer) IsCertInstalled(certPEM []byte) bool {
return isCertInstalled(certPEM)
}
// LogCertInstallStatus reports the current status of the certificate installation in the log.
// If certificate installation is not supported on the platform, this function does nothing.
func (installer *Installer) LogCertInstallStatus(certPEM []byte) {
if installer.OSSupportCertInstall() {
if installer.IsCertInstalled(certPEM) {
installer.log.Info("The Bridge TLS certificate is installed in the OS keychain")
} else {
installer.log.Info("The Bridge TLS certificate is not installed in the OS keychain")
}
}
}

View File

@ -39,10 +39,10 @@ func (c *AppleMail) Configure(
hostname string,
imapPort, smtpPort int,
imapSSL, smtpSSL bool,
username, addresses string,
username, displayName, addresses string,
password []byte,
) error {
mc := prepareMobileConfig(hostname, imapPort, smtpPort, imapSSL, smtpSSL, username, addresses, password)
mc := prepareMobileConfig(hostname, imapPort, smtpPort, imapSSL, smtpSSL, username, displayName, addresses, password)
confPath, err := saveConfigTemporarily(mc)
if err != nil {
@ -66,13 +66,13 @@ func prepareMobileConfig(
hostname string,
imapPort, smtpPort int,
imapSSL, smtpSSL bool,
username, addresses string,
username, displayName, addresses string,
password []byte,
) *mobileconfig.Config {
return &mobileconfig.Config{
DisplayName: username,
EmailAddress: addresses,
AccountName: username,
AccountName: displayName,
AccountDescription: username,
Identifier: "protonmail " + username + strconv.FormatInt(time.Now().Unix(), 10),
IMAP: &mobileconfig.IMAP{

View File

@ -95,6 +95,13 @@ func (status *ConfigurationStatus) IsPending() bool {
return !status.Data.DataV1.PendingSince.IsZero()
}
func (status *ConfigurationStatus) isPendingSinceMin() int {
if min := int(time.Since(status.Data.DataV1.PendingSince).Minutes()); min > 0 {
return min
}
return 0
}
func (status *ConfigurationStatus) IsFromFailure() bool {
status.DataLock.RLock()
defer status.DataLock.RUnlock()

View File

@ -19,7 +19,6 @@ package configstatus
import (
"strconv"
"time"
)
type ConfigAbortValues struct {
@ -41,17 +40,20 @@ type ConfigAbortData struct {
type ConfigAbortBuilder struct{}
func (*ConfigAbortBuilder) New(data *ConfigurationStatusData) ConfigAbortData {
func (*ConfigAbortBuilder) New(config *ConfigurationStatus) ConfigAbortData {
config.DataLock.RLock()
defer config.DataLock.RUnlock()
return ConfigAbortData{
MeasurementGroup: "bridge.any.configuration",
Event: "bridge_config_abort",
Values: ConfigSuccessValues{
Duration: int(time.Since(data.DataV1.PendingSince).Minutes()),
Duration: config.isPendingSinceMin(),
},
Dimensions: ConfigSuccessDimensions{
ReportClick: strconv.FormatBool(data.DataV1.ReportClick),
ReportSent: strconv.FormatBool(data.DataV1.ReportSent),
ClickedLink: data.clickedLinkToString(),
ReportClick: strconv.FormatBool(config.Data.DataV1.ReportClick),
ReportSent: strconv.FormatBool(config.Data.DataV1.ReportSent),
ClickedLink: config.Data.clickedLinkToString(),
},
}
}

View File

@ -33,7 +33,7 @@ func TestConfigurationAbort_default(t *testing.T) {
require.NoError(t, err)
var builder = configstatus.ConfigAbortBuilder{}
req := builder.New(config.Data)
req := builder.New(config)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_abort", req.Event)
@ -64,7 +64,7 @@ func TestConfigurationAbort_fed(t *testing.T) {
require.NoError(t, err)
var builder = configstatus.ConfigAbortBuilder{}
req := builder.New(config.Data)
req := builder.New(config)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_abort", req.Event)

View File

@ -33,13 +33,16 @@ type ConfigProgressData struct {
type ConfigProgressBuilder struct{}
func (*ConfigProgressBuilder) New(data *ConfigurationStatusData) ConfigProgressData {
func (*ConfigProgressBuilder) New(config *ConfigurationStatus) ConfigProgressData {
config.DataLock.RLock()
defer config.DataLock.RUnlock()
return ConfigProgressData{
MeasurementGroup: "bridge.any.configuration",
Event: "bridge_config_progress",
Values: ConfigProgressValues{
NbDay: numberOfDay(time.Now(), data.DataV1.PendingSince),
NbDaySinceLast: numberOfDay(time.Now(), data.DataV1.LastProgress),
NbDay: numberOfDay(time.Now(), config.Data.DataV1.PendingSince),
NbDaySinceLast: numberOfDay(time.Now(), config.Data.DataV1.LastProgress),
},
}
}

View File

@ -33,7 +33,7 @@ func TestConfigurationProgress_default(t *testing.T) {
require.NoError(t, err)
var builder = configstatus.ConfigProgressBuilder{}
req := builder.New(config.Data)
req := builder.New(config)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_progress", req.Event)
@ -62,7 +62,7 @@ func TestConfigurationProgress_fed(t *testing.T) {
require.NoError(t, err)
var builder = configstatus.ConfigProgressBuilder{}
req := builder.New(config.Data)
req := builder.New(config)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_progress", req.Event)

View File

@ -19,7 +19,6 @@ package configstatus
import (
"strconv"
"time"
)
type ConfigRecoveryValues struct {
@ -43,19 +42,22 @@ type ConfigRecoveryData struct {
type ConfigRecoveryBuilder struct{}
func (*ConfigRecoveryBuilder) New(data *ConfigurationStatusData) ConfigRecoveryData {
func (*ConfigRecoveryBuilder) New(config *ConfigurationStatus) ConfigRecoveryData {
config.DataLock.RLock()
defer config.DataLock.RUnlock()
return ConfigRecoveryData{
MeasurementGroup: "bridge.any.configuration",
Event: "bridge_config_recovery",
Values: ConfigRecoveryValues{
Duration: int(time.Since(data.DataV1.PendingSince).Minutes()),
Duration: config.isPendingSinceMin(),
},
Dimensions: ConfigRecoveryDimensions{
Autoconf: data.DataV1.Autoconf,
ReportClick: strconv.FormatBool(data.DataV1.ReportClick),
ReportSent: strconv.FormatBool(data.DataV1.ReportSent),
ClickedLink: data.clickedLinkToString(),
FailureDetails: data.DataV1.FailureDetails,
Autoconf: config.Data.DataV1.Autoconf,
ReportClick: strconv.FormatBool(config.Data.DataV1.ReportClick),
ReportSent: strconv.FormatBool(config.Data.DataV1.ReportSent),
ClickedLink: config.Data.clickedLinkToString(),
FailureDetails: config.Data.DataV1.FailureDetails,
},
}
}

View File

@ -33,7 +33,7 @@ func TestConfigurationRecovery_default(t *testing.T) {
require.NoError(t, err)
var builder = configstatus.ConfigRecoveryBuilder{}
req := builder.New(config.Data)
req := builder.New(config)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_recovery", req.Event)
@ -66,7 +66,7 @@ func TestConfigurationRecovery_fed(t *testing.T) {
require.NoError(t, err)
var builder = configstatus.ConfigRecoveryBuilder{}
req := builder.New(config.Data)
req := builder.New(config)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_recovery", req.Event)

View File

@ -19,7 +19,6 @@ package configstatus
import (
"strconv"
"time"
)
type ConfigSuccessValues struct {
@ -42,18 +41,21 @@ type ConfigSuccessData struct {
type ConfigSuccessBuilder struct{}
func (*ConfigSuccessBuilder) New(data *ConfigurationStatusData) ConfigSuccessData {
func (*ConfigSuccessBuilder) New(config *ConfigurationStatus) ConfigSuccessData {
config.DataLock.RLock()
defer config.DataLock.RUnlock()
return ConfigSuccessData{
MeasurementGroup: "bridge.any.configuration",
Event: "bridge_config_success",
Values: ConfigSuccessValues{
Duration: int(time.Since(data.DataV1.PendingSince).Minutes()),
Duration: config.isPendingSinceMin(),
},
Dimensions: ConfigSuccessDimensions{
Autoconf: data.DataV1.Autoconf,
ReportClick: strconv.FormatBool(data.DataV1.ReportClick),
ReportSent: strconv.FormatBool(data.DataV1.ReportSent),
ClickedLink: data.clickedLinkToString(),
Autoconf: config.Data.DataV1.Autoconf,
ReportClick: strconv.FormatBool(config.Data.DataV1.ReportClick),
ReportSent: strconv.FormatBool(config.Data.DataV1.ReportSent),
ClickedLink: config.Data.clickedLinkToString(),
},
}
}

View File

@ -33,7 +33,7 @@ func TestConfigurationSuccess_default(t *testing.T) {
require.NoError(t, err)
var builder = configstatus.ConfigSuccessBuilder{}
req := builder.New(config.Data)
req := builder.New(config)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_success", req.Event)
@ -65,7 +65,7 @@ func TestConfigurationSuccess_fed(t *testing.T) {
require.NoError(t, err)
var builder = configstatus.ConfigSuccessBuilder{}
req := builder.New(config.Data)
req := builder.New(config)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_success", req.Event)

View File

@ -175,7 +175,7 @@ type UsedSpaceChanged struct {
UserID string
UsedSpace int
UsedSpace uint64
}
func (event UsedSpaceChanged) String() string {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 &amp;&amp; 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 &amp;&amp; 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/>

View File

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

View File

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

View File

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

View File

@ -415,7 +415,11 @@ int main(int argc, char *argv[]) {
}
catch (Exception const &e) {
sentry_uuid_s const uuid = reportSentryException("Exception occurred during main", e);
QMessageBox::critical(nullptr, "Error", e.qwhat());
QString message = e.qwhat();
if (e.showSupportLink()) {
message += R"(<br/><br/>If the issue persists, please contact our <a href="https://proton.me/support/contact">customer support</a>.)";
}
QMessageBox::critical(nullptr, "Error", message);
QTextStream(stderr) << "reportID: " << QByteArray(uuid.bytes, 16).toHex() << " Captured exception :" << e.detailedWhat() << "\n";
return EXIT_FAILURE;
}

View File

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

View File

@ -34,9 +34,6 @@ QtObject {
function onColorSchemeNameChanged(scheme) {
root.setColorScheme();
}
function onDiskCacheUnavailable() {
mainWindow.showAndRise();
}
function onHideMainWindow() {
mainWindow.hide();
}

View File

@ -113,7 +113,7 @@ SettingsView {
secondary: true
text: qsTr("View logs")
onClicked: Qt.openUrlExternally(Backend.logsPath)
onClicked: Backend.openExternalLink(Backend.logsPath)
}
}
TextEdit {

View File

@ -368,7 +368,7 @@ Item {
currentIndex: hasAccount() ? 1 : 0
NoAccountView {
colorScheme: root.colorScheme
onLinkClicked: function() {
onStartSetup: {
root.showLogin("")
}
}

View File

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

View File

@ -69,8 +69,8 @@ ApplicationWindow {
}
colorScheme: ProtonStyle.currentStyle
height: ProtonStyle.window_default_height
minimumHeight:ProtonStyle.window_minimum_height
height: screen.height < ProtonStyle.window_default_height + 100 ? ProtonStyle.window_minimum_height : ProtonStyle.window_default_height
minimumHeight: ProtonStyle.window_minimum_height
minimumWidth: ProtonStyle.window_minimum_width
visible: true
width: ProtonStyle.window_default_width

View File

@ -23,7 +23,7 @@ Rectangle {
color: root.colorScheme.background_norm
signal linkClicked()
signal startSetup()
ColumnLayout {
anchors.fill: parent
@ -38,8 +38,10 @@ Rectangle {
wizard: setupWizard
Component.onCompleted: {
showOnboarding();
link1.setCallback(root.linkClicked, "Start setup", false)
showNoAccount();
}
onStartSetup: {
root.startSetup();
}
}
Image {

View File

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

View File

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

View File

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

View File

@ -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("Cant 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 disable the local cache (not recommended).")
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;
@ -728,10 +624,14 @@ QtObject {
}
property Notification noKeychain: Notification {
brief: title
description: 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.")
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 a supported password manager and restart the application.")
group: Notifications.Group.Dialogs | Notifications.Group.Configuration
icon: "./icons/ic-exclamation-circle-filled.svg"
title: qsTr("No keychain available")
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
action: [
@ -773,7 +673,7 @@ QtObject {
text: qsTr("Upgrade")
onTriggered: {
Qt.openUrlExternally(root.onlyPaidUsers.pricingLink);
Backend.openExternalLink(root.onlyPaidUsers.pricingLink);
root.onlyPaidUsers.active = false;
}
}
@ -800,7 +700,7 @@ QtObject {
text: qsTr("Open the support page")
onTriggered: {
Backend.openKBArticle();
Backend.openExternalLink();
Backend.quit();
}
}
@ -891,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 {
@ -906,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 {
@ -937,7 +841,7 @@ QtObject {
text: qsTr("Update manually")
onTriggered: {
Qt.openUrlExternally(Backend.landingPageLink);
Backend.openExternalLink(Backend.landingPageLink);
root.updateForce.active = false;
}
},
@ -967,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
@ -975,7 +881,7 @@ QtObject {
text: qsTr("Update manually")
onTriggered: {
Qt.openUrlExternally(Backend.landingPageLink);
Backend.openExternalLink(Backend.landingPageLink);
root.updateForceError.active = false;
}
},
@ -1025,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 couldnt update")
type: Notification.NotificationType.Warning
@ -1033,7 +941,7 @@ QtObject {
text: qsTr("Update manually")
onTriggered: {
Qt.openUrlExternally(Backend.landingPageLink);
Backend.openExternalLink(Backend.landingPageLink);
root.updateManualError.active = false;
Backend.quit();
}
@ -1083,7 +991,7 @@ QtObject {
text: qsTr("Update manually")
onTriggered: {
Qt.openUrlExternally(Backend.landingPageLink);
Backend.openExternalLink(Backend.landingPageLink);
root.updateManualReady.active = false;
}
},
@ -1136,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;
}
}
@ -1186,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

View File

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

View File

@ -114,6 +114,9 @@ FocusScope {
function getText(start, end) {
control.getText(start, end);
}
function hidePassword() {
eyeButton.checked = false;
}
function insert(position, text) {
control.insert(position, text);
}
@ -147,6 +150,9 @@ FocusScope {
function selectWord() {
control.selectWord();
}
function showPassword() {
eyeButton.checked = true;
}
function undo() {
control.undo();
}

View File

@ -79,7 +79,7 @@ Rectangle {
text: qsTr("Open guide")
onClicked: function () {
Backend.openKBArticle(wizard.setupGuideLink());
Backend.openExternalLink(wizard.setupGuideLink());
}
}
}

View File

@ -49,7 +49,7 @@ Button {
text: qsTr("Get help")
onClicked: {
Backend.openKBArticle();
Backend.openExternalLink();
}
}
MenuItem {

View File

@ -18,19 +18,26 @@ import QtQuick.Controls
Item {
id: root
readonly property string addAccountTitle: qsTr("Add a Proton Mail account")
readonly property string welcomeDescription: qsTr("Bridge is the gateway between your Proton account and your email client. It runs in the background and encrypts and decrypts your messages seamlessly. ");
readonly property string welcomeTitle: qsTr("Welcome to\nProton Mail Bridge")
readonly property string welcomeImage: "/qml/icons/img-welcome.svg"
readonly property int welcomeImageHeight: 148;
readonly property int welcomeImageWidth: 265;
property int iconHeight
property string iconSource
property int iconWidth
property var wizard
property ColorScheme colorScheme
property var _colorScheme: wizard ? wizard.colorScheme : colorScheme
property var link1: linkLabel1
property var link2: linkLabel2
signal startSetup()
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() {
@ -44,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) {
@ -65,14 +72,25 @@ Item {
function showLoginMailboxPassword() {
showOnboarding();
}
function showOnboarding() {
titleLabel.text = (Backend.users.count === 0) ? qsTr("Welcome to\nProton Mail Bridge") : qsTr("Add a Proton Mail account");
descriptionLabel.text = qsTr("Bridge is the gateway between your Proton account and your email client. It runs in the background and encrypts and decrypts your messages seamlessly. ");
linkLabel1.setCallback(function() { Backend.openKBArticle("https://proton.me/support/why-you-need-bridge"); }, qsTr("Why do I need Bridge?"), true);
function showNoAccount() {
titleLabel.text = welcomeTitle;
descriptionLabel.text = welcomeDescription;
linkLabel1.setCallback(startSetup, "Start setup", false);
linkLabel2.clear();
root.iconSource = "/qml/icons/img-welcome.svg";
root.iconHeight = 148;
root.iconWidth = 265;
root.iconSource = welcomeImage;
root.iconHeight = welcomeImageHeight;
root.iconWidth = welcomeImageWidth;
}
function showOnboarding() {
titleLabel.text = (Backend.users.count === 0) ? welcomeTitle : addAccountTitle;
descriptionLabel.text = welcomeDescription
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;
root.iconWidth = welcomeImageWidth;
}
ColumnLayout {

View File

@ -44,6 +44,8 @@ FocusScope {
} else {
passwordTextField.forceActiveFocus();
}
passwordTextField.hidePassword();
secondPasswordTextField.hidePassword();
}
StackLayout {

View File

@ -162,7 +162,7 @@ Dialog {
wrapMode: Text.WordWrap
onLinkActivated: function (link) {
Qt.openUrlExternally(link);
Backend.openExternalLink(link);
}
}
}

View File

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

View File

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

View File

@ -26,14 +26,16 @@ namespace bridgepp {
/// \param[in] what A description of the exception.
/// \param[in] details The optional details for the exception.
/// \param[in] function The name of the calling function.
/// \param[in] showSupportLink Should a link to the support web form be included in GUI message.
//****************************************************************************************************************************************************
Exception::Exception(QString qwhat, QString details, QString function, QByteArray attachment) noexcept
Exception::Exception(QString qwhat, QString details, QString function, QByteArray attachment, bool showSupportLink) noexcept
: std::exception()
, qwhat_(std::move(qwhat))
, what_(qwhat_.toLocal8Bit())
, details_(std::move(details))
, function_(std::move(function))
, attachment_(std::move(attachment)) {
, attachment_(std::move(attachment))
, showSupportLink_(showSupportLink) {
}
@ -46,7 +48,8 @@ Exception::Exception(Exception const &ref) noexcept
, what_(ref.what_)
, details_(ref.details_)
, function_(ref.function_)
, attachment_(ref.attachment_) {
, attachment_(ref.attachment_)
, showSupportLink_(ref.showSupportLink_) {
}
@ -59,7 +62,8 @@ Exception::Exception(Exception &&ref) noexcept
, what_(ref.what_)
, details_(ref.details_)
, function_(ref.function_)
, attachment_(ref.attachment_) {
, attachment_(ref.attachment_)
, showSupportLink_(ref.showSupportLink_) {
}
@ -118,4 +122,12 @@ QString Exception::detailedWhat() const {
}
//****************************************************************************************************************************************************
/// \return true iff A link to the support page should shown in the GUI message box.
//****************************************************************************************************************************************************
bool Exception::showSupportLink() const {
return showSupportLink_;
}
} // namespace bridgepp

View File

@ -33,7 +33,7 @@ namespace bridgepp {
class Exception : public std::exception {
public: // member functions
explicit Exception(QString qwhat = QString(), QString details = QString(), QString function = QString(),
QByteArray attachment = QByteArray()) noexcept; ///< Constructor
QByteArray attachment = QByteArray(), bool showSupportLink = false) noexcept; ///< Constructor
Exception(Exception const &ref) noexcept; ///< copy constructor
Exception(Exception &&ref) noexcept; ///< copy constructor
Exception &operator=(Exception const &) = delete; ///< Disabled assignment operator
@ -45,6 +45,7 @@ public: // member functions
QString function() const noexcept; ///< Return the function that threw the exception.
QByteArray attachment() const noexcept; ///< Return the attachment for the exception.
QString detailedWhat() const; ///< Return the detailed description of the message (i.e. including the function name and the details).
bool showSupportLink() const; ///< Return the value for the 'Show support link' option.
public: // static data members
static qsizetype const attachmentMaxLength {25 * 1024}; ///< The maximum length text attachment sent in Sentry reports, in bytes.
@ -55,6 +56,7 @@ private: // data members
QString const details_; ///< The optional details for the exception.
QString const function_; ///< The name of the function that created the exception.
QByteArray const attachment_; ///< The attachment to add to the exception.
bool const showSupportLink_; ///< Should the GUI feedback include a link to support.
};

View File

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

View File

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

View File

@ -72,8 +72,8 @@ GRPCConfig GRPCClient::waitAndRetrieveServiceConfig(QString const & sessionID, Q
bool found = false;
while (true) {
if (serverProcess && serverProcess->getStatus().ended) {
throw Exception("Bridge application exited before providing a gRPC service configuration file.", QString(), __FUNCTION__,
tailOfLatestBridgeLog(sessionID));
throw Exception("Bridge failed to start.", "Bridge application exited before providing a gRPC service configuration file", __FUNCTION__,
tailOfLatestBridgeLog(sessionID), true);
}
if (file.exists()) {
@ -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__);
}

View File

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

View File

@ -309,6 +309,8 @@ void User::setIsSyncing(bool syncing) {
}
isSyncing_ = syncing;
syncProgress_ = 0;
emit isSyncingChanged(syncing);
}

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -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}}})
}
@ -193,7 +189,7 @@ func NewUserBadEvent(userID string, errorMessage string) *StreamEvent {
return userEvent(&UserEvent{Event: &UserEvent_UserBadEvent{UserBadEvent: &UserBadEvent{UserID: userID, ErrorMessage: errorMessage}}})
}
func NewUsedBytesChangedEvent(userID string, usedBytes int) *StreamEvent {
func NewUsedBytesChangedEvent(userID string, usedBytes uint64) *StreamEvent {
return userEvent(&UserEvent{Event: &UserEvent_UsedBytesChangedEvent{UsedBytesChangedEvent: &UsedBytesChangedEvent{UserID: userID, UsedBytes: int64(usedBytes)}}})
}

View File

@ -54,8 +54,9 @@ import (
)
const (
serverConfigFileName = "grpcServerConfig.json"
serverTokenMetadataKey = "server-token"
serverConfigFileName = "grpcServerConfig.json"
serverTokenMetadataKey = "server-token"
twoPasswordsMaxAttemptCount = 3 // The number of attempts allowed for the mailbox password.
)
// Service is the RPC service struct.
@ -82,9 +83,10 @@ type Service struct { // nolint:structcheck
target updater.VersionInfo
targetLock safe.RWMutex
authClient *proton.Client
auth proton.Auth
password []byte
authClient *proton.Client
auth proton.Auth
password []byte
twoPasswordAttemptCount int
log *logrus.Entry
initializing sync.WaitGroup
@ -338,6 +340,11 @@ func (s *Service) watchEvents() {
case events.SyncFinished:
_ = s.SendEvent(NewSyncFinishedEvent(event.UserID))
case events.SyncFailed:
if errors.Is(event.Error, context.Canceled) {
_ = s.SendEvent(NewSyncFinishedEvent(event.UserID))
}
case events.SyncProgress:
_ = s.SendEvent(NewSyncProgressEvent(event.UserID, event.Progress, event.Elapsed.Milliseconds(), event.Remaining.Milliseconds()))
@ -408,7 +415,12 @@ func (s *Service) loginClean() {
}
func (s *Service) finishLogin() {
defer s.loginClean()
performCleanup := true
defer func() {
if performCleanup {
s.loginClean()
}
}()
wasSignedOut := s.bridge.HasUser(s.auth.UserID)
@ -426,10 +438,24 @@ func (s *Service) finishLogin() {
eventCh, done := s.bridge.GetEvents(events.UserLoggedIn{})
defer done()
userID, err := s.bridge.LoginUser(context.Background(), s.authClient, s.auth, s.password)
ctx := context.Background()
userID, err := s.bridge.LoginUser(ctx, s.authClient, s.auth, s.password)
if err != nil {
s.log.WithError(err).Errorf("Finish login failed")
_ = s.SendEvent(NewLoginError(LoginErrorType_TWO_PASSWORDS_ABORT, err.Error()))
s.twoPasswordAttemptCount++
errType := LoginErrorType_TWO_PASSWORDS_ABORT
if errors.Is(err, bridge.ErrFailedToUnlock) {
if s.twoPasswordAttemptCount < twoPasswordsMaxAttemptCount {
performCleanup = false
errType = LoginErrorType_TWO_PASSWORDS_ERROR
} else {
if deleteErr := s.authClient.AuthDelete(ctx); deleteErr != nil {
s.log.WithError(deleteErr).Error("Failed to delete auth")
}
}
}
_ = s.SendEvent(NewLoginError(errType, err.Error()))
return
}

View File

@ -33,10 +33,8 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/service"
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
"github.com/ProtonMail/proton-bridge/v3/pkg/ports"
"github.com/sirupsen/logrus"
"golang.org/x/exp/maps"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/runtime/protoimpl"
@ -339,18 +337,17 @@ func (s *Service) ReportBug(_ context.Context, report *ReportBugRequest) (*empty
defer async.HandlePanic(s.panicHandler)
defer func() { _ = s.SendEvent(NewReportBugFinishedEvent()) }()
if err := s.bridge.ReportBug(
context.Background(),
report.OsType,
report.OsVersion,
report.Title,
report.Description,
report.Address,
report.Address,
report.EmailClient,
report.IncludeLogs,
); err != nil {
reportReq := bridge.ReportBugReq{
OSType: report.OsType,
OSVersion: report.OsVersion,
Title: report.Title,
Description: report.Description,
Username: report.Address,
Email: report.Address,
EmailClient: report.EmailClient,
IncludeLogs: report.IncludeLogs,
}
if err := s.bridge.ReportBug(context.Background(), &reportReq); err != nil {
s.log.WithError(err).Error("Failed to report bug")
_ = s.SendEvent(NewReportBugErrorEvent())
return
@ -384,6 +381,7 @@ func (s *Service) Login(_ context.Context, login *LoginRequest) (*emptypb.Empty,
go func() {
defer async.HandlePanic(s.panicHandler)
s.twoPasswordAttemptCount = 0
password, err := base64Decode(login.Password)
if err != nil {
s.log.WithError(err).Error("Cannot decode password")
@ -712,7 +710,7 @@ func (s *Service) IsPortFree(_ context.Context, port *wrapperspb.Int32Value) (*w
func (s *Service) AvailableKeychains(_ context.Context, _ *emptypb.Empty) (*AvailableKeychainsResponse, error) {
s.log.Debug("AvailableKeychains")
return &AvailableKeychainsResponse{Keychains: maps.Keys(keychain.Helpers)}, nil
return &AvailableKeychainsResponse{Keychains: s.bridge.GetHelpersNames()}, nil
}
func (s *Service) SetCurrentKeychain(ctx context.Context, keychain *wrapperspb.StringValue) (*emptypb.Empty, error) {

View File

@ -166,7 +166,6 @@ func (s *Service) StartEventTest() error {
NewKeychainRebuildKeychainEvent(),
// mail
NewMailNoActiveKeyForRecipientEvent(dummyAddress),
NewMailAddressChangeEvent(dummyAddress),
NewMailAddressChangeLogoutEvent(dummyAddress),
NewMailApiCertIssue(),

View File

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

View File

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

View File

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

View File

@ -48,4 +48,6 @@ type APIClient interface {
DeleteMessage(ctx context.Context, messageIDs ...string) error
MarkMessagesRead(ctx context.Context, messageIDs ...string) error
MarkMessagesUnread(ctx context.Context, messageIDs ...string) error
MarkMessagesForwarded(ctx context.Context, messageIDs ...string) error
MarkMessagesUnForwarded(ctx context.Context, messageIDs ...string) error
}

View File

@ -84,9 +84,9 @@ func NewConnector(
identityState: identityState,
addrID: addrID,
showAllMail: b32(showAllMail),
flags: defaultFlags,
permFlags: defaultPermanentFlags,
attrs: defaultAttributes,
flags: defaultMailboxFlags(),
permFlags: defaultMailboxPermanentFlags(),
attrs: defaultMailboxAttributes(),
client: apiClient,
telemetry: telemetry,
@ -144,6 +144,18 @@ func (s *Connector) Init(ctx context.Context, cache connector.IMAPState) error {
}
}
}
// Retroactively apply the forwarded flags to existing mailboxes so that the IMAP clients can recognize
// that they can store these flags now.
if err := write.AddFlagsToAllMailboxes(ctx, imap.ForwardFlagList...); err != nil {
return fmt.Errorf("failed to add \\Forward flag to all mailboxes:%w", err)
}
// Add forwarded flag as perm flags to all mailboxes.
if err := write.AddPermFlagsToAllMailboxes(ctx, imap.ForwardFlagList...); err != nil {
return fmt.Errorf("failed to add \\Forward permanent flag to all mailboxes:%w", err)
}
return nil
})
}
@ -487,6 +499,14 @@ func (s *Connector) MarkMessagesFlagged(ctx context.Context, _ connector.IMAPSta
return s.client.UnlabelMessages(ctx, usertypes.MapTo[imap.MessageID, string](messageIDs), proton.StarredLabel)
}
func (s *Connector) MarkMessagesForwarded(ctx context.Context, _ connector.IMAPStateWrite, messageIDs []imap.MessageID, flagged bool) error {
if flagged {
return s.client.MarkMessagesForwarded(ctx, usertypes.MapTo[imap.MessageID, string](messageIDs)...)
}
return s.client.MarkMessagesUnForwarded(ctx, usertypes.MapTo[imap.MessageID, string](messageIDs)...)
}
func (s *Connector) GetUpdates() <-chan imap.Update {
return s.updateCh.GetChannel()
}
@ -501,12 +521,6 @@ func (s *Connector) ShowAllMail(v bool) {
atomic.StoreUint32(&s.showAllMail, b32(v))
}
var (
defaultFlags = imap.NewFlagSet(imap.FlagSeen, imap.FlagFlagged, imap.FlagDeleted) // nolint:gochecknoglobals
defaultPermanentFlags = imap.NewFlagSet(imap.FlagSeen, imap.FlagFlagged, imap.FlagDeleted) // nolint:gochecknoglobals
defaultAttributes = imap.NewFlagSet() // nolint:gochecknoglobals
)
const (
folderPrefix = "Folders"
labelPrefix = "Labels"
@ -664,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,
@ -714,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)
@ -812,3 +830,18 @@ func fixGODT3003Labels(
return applied, nil
}
func defaultMailboxFlags() imap.FlagSet {
f := imap.NewFlagSet(imap.FlagSeen, imap.FlagFlagged, imap.FlagDeleted)
f.AddToSelf(imap.ForwardFlagList...)
return f
}
func defaultMailboxPermanentFlags() imap.FlagSet {
return defaultMailboxFlags()
}
func defaultMailboxAttributes() imap.FlagSet {
return imap.NewFlagSet()
}

View File

@ -68,6 +68,10 @@ func BuildFlagSetFromMessageMetadata(message proton.MessageMetadata) imap.FlagSe
flags.AddToSelf(imap.FlagAnswered)
}
if message.IsForwarded {
flags.AddToSelf(imap.ForwardFlagList...)
}
return flags
}

View File

@ -32,8 +32,8 @@ func newSystemMailboxCreatedUpdate(labelID imap.MailboxID, labelName string) *im
}
attrs := imap.NewFlagSet(imap.AttrNoInferiors)
permanentFlags := defaultPermanentFlags
flags := defaultFlags
permanentFlags := defaultMailboxPermanentFlags()
flags := defaultMailboxFlags()
switch labelID {
case proton.TrashLabel:
@ -86,8 +86,8 @@ func newPlaceHolderMailboxCreatedUpdate(labelName string) *imap.MailboxCreated {
return imap.NewMailboxCreated(imap.Mailbox{
ID: imap.MailboxID(labelName),
Name: []string{labelName},
Flags: defaultFlags,
PermanentFlags: defaultPermanentFlags,
Flags: defaultMailboxFlags(),
PermanentFlags: defaultMailboxPermanentFlags(),
Attributes: imap.NewFlagSet(imap.AttrNoSelect),
})
}
@ -96,8 +96,8 @@ func newMailboxCreatedUpdate(labelID imap.MailboxID, labelName []string) *imap.M
return imap.NewMailboxCreated(imap.Mailbox{
ID: labelID,
Name: labelName,
Flags: defaultFlags,
PermanentFlags: defaultPermanentFlags,
Flags: defaultMailboxFlags(),
PermanentFlags: defaultMailboxPermanentFlags(),
Attributes: imap.NewFlagSet(),
})
}

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