Compare commits

..

199 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
0c212fbef4 chore: Vasco da Gama Bridge 3.6.0 changelog. 2023-10-02 16:31:07 +02:00
48d1ca1e72 fix(GODT-2989): allow to send bug report when no account connected. 2023-10-02 13:34:40 +00:00
52addb2582 feat(GODT-2960): replaced the account list with a button and label when no account is configured. 2023-09-29 17:36:23 +02:00
742d9eeef3 feat(GODT-2960): added content in empty view when there is no account. 2023-09-29 17:36:23 +02:00
55a9d4973c fix(GODT-2988): fix setup wizard KB links. 2023-09-29 15:25:30 +02:00
8402657108 fix(GODT-2968): use proper base64 encoded string even for bad password test. 2023-09-29 08:35:41 +00:00
8a6f96f9f2 fix(GODT-2965): fix multipart/mixed testdata + structure parsing steps related to this. 2023-09-29 07:08:10 +00:00
56c53e9188 fix(GODT-2932): fix syncing not being reported in GUI. 2023-09-28 12:39:24 +02:00
bb67d95669 fix(GODT-2967): tray menu entries close the setup wizard when needed. 2023-09-27 18:23:02 +02:00
50acc0dcfb feat(GODT-2725): Implement receive message step with expected structure exposed. 2023-09-27 14:17:51 +00:00
e9c73c2d0d chore: Umshiang Bridge 3.5.1 changelog. 2023-09-27 15:34:50 +02:00
07c03c6920 fix(GODT-2963): Use multi error to report file removal errors
Do not abort removing files on first error. Collect errors and try to
remove as many as possible. This would cause some state files to not be
removed on windows.
2023-09-27 11:30:46 +02:00
f4958b9b53 fix(GODT-2956): Restore old deletion rules
When unlabeling a message from trash we have to check if this message is
present in another folder before perma-deleting.
2023-09-26 13:47:09 +02:00
76f2e7fdb9 fix(GODT-2951): Negative WaitGroup Counter
Do not defer call to `wg.Done()` in `job.onJobFinished`. If there is an
error it will also call `wg.Done()`.
2023-09-26 09:45:27 +02:00
c0992e8801 fix(GODT-2590): Fix send on closed channel
Ensure periodic user tasks are terminated before the other user
services. The panic triggered due to the fact that the telemetry service
was shutdown before this periodic task.
2023-09-26 09:20:01 +02:00
cf3abaa96f fix(GODT-2949): Fix close of close channel in event service
This issue is triggered due to the `Service.Close()` call after the
go-routine for the event service exists. It is possible that during this
period a recently added subscriber with `pendingOpAdd` gets cancelled
and closed.

However, the subscriber later also enqueues a `pendingOpRemove` which
gets processed again with a call in `user.eventService.Close()` leading
to the double close panic.

This patch simply removes the `s.Close()` from the service, and leaves
the cleanup to called externally from user.Close() or user.Logout().
2023-09-26 09:08:25 +02:00
e422b28bc3 fix(GODT-2212): Preserver Header order in message building
https://github.com/ProtonMail/go-proton-api/pull/100
2023-09-25 15:05:21 +02:00
a1a5ffba5d chore: Vasco da Gama Bridge 3.6.0 changelog. 2023-09-25 12:00:10 +02:00
f8b86a76dd feat(GODT-2772): fixed missing space in error message. 2023-09-19 07:58:19 +02:00
ab1281ceee feat(GODT-2772): added final link to knowledge base articles. 2023-09-19 07:58:19 +02:00
0ab0f2f4ff feat(GODT-2772): setup wizard report knowledge base article opening event. 2023-09-19 07:58:19 +02:00
09d87023f1 feat(GODT-2772): removed web engine from deploy.
This partly reverts commit c89d206a9576499c3df29139c8df9099a053a839.
2023-09-19 07:58:19 +02:00
139ad75394 feat(GODT-2772): removed web frame. 2023-09-19 07:58:19 +02:00
c8cf90abfe feat(GODT-2772): use os browser instead of integrated one for external links (for now). 2023-09-19 07:57:59 +02:00
5d4f8f7d40 feat(GODT-2772): implemented internal help links. 2023-09-19 07:57:59 +02:00
ea26dc0e97 feat(GODT-2772): external links have an icon. 2023-09-19 07:57:59 +02:00
8d346ea511 feat(GODT-2772): removed useless extra space in button with icons. 2023-09-19 07:57:59 +02:00
44df3cfd4a feat(GODT-2772): configure email client button is highlighted
Misc minor tweaks & fixes.
2023-09-19 07:57:59 +02:00
683458e264 feat(GODT-2772): use new Thunderbird logo.
The logo is a raster image inside a SVG file, as the pure vector version does not render properly in QML or Affinity Designer.
2023-09-19 07:57:59 +02:00
36651698cb feat(GODT-2772): new illustration for client selector. 2023-09-19 07:57:59 +02:00
0c7e17701f feat(GODT-2772): HTML placeholder is not loaded from resources anymore. 2023-09-19 07:57:59 +02:00
86cd2437aa feat(GODT-2772): misc tweaks.
- Step description box tweaks and text color changes.
- Factored out some constants (margins and dimensions.
- Removed the ProtonStyle.px scaling which was useless as it was not applied everywhere.
2023-09-19 07:57:59 +02:00
53f5f9aa43 feat(GODT-2772): client selector left pane tweaks. 2023-09-19 07:57:59 +02:00
c849762445 feat(GODT-2772): placeholder for missing help content. 2023-09-19 07:57:59 +02:00
32f2c72575 feat(GODT-2772): use WebEngineView instead of WebView 2023-09-19 07:57:59 +02:00
958e1280d7 feat(GODT-2772): error handling for Apple Mail auto config. 2023-09-19 07:57:59 +02:00
df09d6d221 feat(GODT-2772): back button. 2023-09-19 07:57:59 +02:00
e0875dc928 feat(GODT-2772): placement of error message on login pages. 2023-09-19 07:57:59 +02:00
b3a5270bdc feat(GODT-2772): marked strings as translatable. 2023-09-19 07:57:59 +02:00
f617a44d28 feat(GODT-2772): link for Apple Mail manual configuration. 2023-09-19 07:57:59 +02:00
75ed3ca660 feat(GODT-2772): QML import cleanup. 2023-09-19 07:57:59 +02:00
69f3029430 feat(GODT-2772): Apple Mail profile install page. 2023-09-19 07:57:59 +02:00
1203709ab9 feat(GODT-2772): Apple Mail cert install page. 2023-09-19 07:57:59 +02:00
15c18189d3 feat(GODT-2772): client config success screen. 2023-09-19 07:57:59 +02:00
a9e95f618b feat(GODT-2772): tweaked client parameter screen. 2023-09-19 07:57:59 +02:00
272f9cf59b feat(GODT-2772): new client selector design. 2023-09-19 07:57:59 +02:00
6e86c95640 feat(GODT-2772): new login layout. 2023-09-19 07:57:59 +02:00
81afc5fb1f feat(GODT-2772): new onboarding layout. 2023-09-19 07:57:59 +02:00
53ea5e9adc feat(GODT-2772): fix aliasing in protonmail wordmark on Windows. 2023-09-19 07:57:59 +02:00
6f420f9098 feat(GODT-2772): converted setup wizard help link to button with context menu. 2023-09-19 07:57:59 +02:00
65846ff40f feat(GODT-2772): removed warning and outlook selector setup wizard pages. 2023-09-19 07:57:59 +02:00
43f7a989be feat(GODT-2771): added CLI commands for cert install/uninstall/status check on macOS. 2023-09-19 07:57:59 +02:00
452d3068f0 feat(GODT-2771): removed cert check and install on app startup on macOS. 2023-09-19 07:57:59 +02:00
69190daf3f feat(GODT-2771): macOS cert install support in bridge-gui-test + placeholder QML. 2023-09-19 07:57:59 +02:00
f57a40677e feat(GODT-2771): gRPC calls for TLS certificates. 2023-09-19 07:57:59 +02:00
2d6f42e0b5 feat(GODT-2771): improved macOS cert installation tools. 2023-09-19 07:57:59 +02:00
bccf31501d feat(GODT-2769): moved LinkLabel QML component to Proton custom component folder. 2023-09-19 07:57:59 +02:00
9b546b5412 feat(GODT-2762): adjust mac and windows qt deploy
* do not remove web engine frameworks from macos bundle
* add libs, QML files, resources, translations needed for WebView
* ship QWebEngineProcess in linux and windows builds
2023-09-19 07:57:59 +02:00
f48a60d58c feat(GODT-2762): bump version Go 1.20 Qt 6.4.3. 2023-09-19 07:57:59 +02:00
0a51c7a6b0 feat(GODT-2769): Setup Wizard QML foundations. 2023-09-19 07:57:59 +02:00
7355c7dfd6 feat(GODT-2767): unified colorScheme management. [skip-ci] 2023-09-19 07:57:59 +02:00
bb5a91ee6d feat(GODT-2767): wired bug report link + use enum for wizard stack layout. 2023-09-19 07:57:58 +02:00
ca5f7ce9f6 feat(GODT-2767): connected existing entrypoints to wizard, and moved it to a stack layout. [skip-ci] 2023-09-19 07:57:58 +02:00
ad31e6a9c5 feat(GODT-2767): pass user and username to setup wizard. 2023-09-19 07:57:58 +02:00
9ef7d133c0 feat(GODT-2767): client config page. [skip-ci] 2023-09-19 07:57:58 +02:00
83b842b19d feat(GODT-2767): per client configuration left pane + refactoring. [skip-ci] 2023-09-19 07:57:58 +02:00
df02e39fe1 feat(GODT-2767): Outlook version selector and warning screen. 2023-09-19 07:57:58 +02:00
a35c8424a3 chore: fix after rebase. 2023-09-19 07:57:58 +02:00
5d207810bd feat(GODT-2767): client selection. [skip-ci] 2023-09-19 07:57:58 +02:00
6c9d96d5e1 chore: fixed missing GoOs gRPC call in bridge-gui-tester. 2023-09-19 07:57:58 +02:00
0fc41d1966 feat(GODT-2767): unified left pane + client config left pane. [skip-ci] 2023-09-19 07:57:58 +02:00
dd5e745e37 feat(GODT-2767): login right pane. [skip-ci] 2023-09-19 07:57:58 +02:00
c8f0d7f32a feat(GODT-2767): login right pane, WIP. [skip-ci] 2023-09-19 07:57:58 +02:00
bd986901c3 feat(GODT-2767): login left pane. [skip-ci] 2023-09-19 07:57:58 +02:00
cdc19492ee feat(GODT-2762): onboarding right pane. 2023-09-19 07:57:58 +02:00
635b2a4891 feat(GODT-2762): setup wizard: onboarding left pane. 2023-09-19 07:57:58 +02:00
e5bac33a04 feat(GODT-2767): setup wizard frame. WIP [skip-cli] 2023-09-19 07:57:58 +02:00
7b96a07cf5 feat(GODT-2770): proof of concept for web view as a tool window. 2023-09-19 07:57:58 +02:00
87e79fdcba feat(GODT-2770): proof of concept for web view as overlay. 2023-09-19 07:57:58 +02:00
03c3404044 chore(GODT-2916): Split Decryption from Message Building
This helps the export tool to deal with problems arising from message
assembly after everything has been successfully encrypted.

The original behavior is still available under `DecryptAndBuildRFC822`.
2023-09-18 14:40:07 +02:00
fa794a982b feat(GODT-2597): Implement contact specific settings in integration tests. 2023-09-15 10:53:58 +00:00
cab32d5d5a chore: update changelog. 2023-09-13 10:26:24 +02:00
8e5a892c45 feat(GODT-2664): trigger QA installer. 2023-09-12 08:45:02 +00:00
50dc5c4085 chore: Umshiang Bridge 3.5.0 changelog. 2023-09-12 08:00:12 +02:00
3b58078595 fix(GODT-2929): Message dedup with different text transfer encoding
https://github.com/ProtonMail/gluon/pull/396
2023-09-11 15:44:11 +02:00
271 changed files with 17277 additions and 5357 deletions

View File

@ -18,6 +18,10 @@
--- ---
image: gitlab.protontech.ch:4567/go/bridge-internal:test-go1.20 image: gitlab.protontech.ch:4567/go/bridge-internal:test-go1.20
default:
tags:
- shared-small
variables: variables:
GOPRIVATE: gitlab.protontech.ch GOPRIVATE: gitlab.protontech.ch
GOMAXPROCS: $(( ${CI_TAG_CPU} / 2 )) GOMAXPROCS: $(( ${CI_TAG_CPU} / 2 ))
@ -30,256 +34,9 @@ stages:
- test - test
- build - build
.rules-branch-and-MR-manual: include:
rules: - local: ci/rules.yml
- if: $CI_COMMIT_BRANCH || $CI_PIPELINE_SOURCE == "merge_request_event" - local: ci/env.yml
when: manual - local: ci/test.yml
allow_failure: true - local: ci/build.yml
- 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-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.3.2/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.3.2/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.3.2
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"
# TODO: PUT BACK ALL THE JOBS! JUST DID THIS FOR NOW TO GET CI WORKING AGAIN...

View File

@ -10,7 +10,7 @@
* Windres (Windows) * Windres (Windows)
* libglvnd and libsecret development files (Linux) * libglvnd and libsecret development files (Linux)
* pkg-config (Linux) * pkg-config (Linux)
* cmake, ninja-build and Qt 6 are required to build the graphical user interface. On Linux, * cmake, ninja-build and Qt 6.4.3 are required to build the graphical user interface. On Linux,
the Mesa OpenGL development files are also needed. the Mesa OpenGL development files are also needed.
To enable the sending of crash reports using Sentry please set the To enable the sending of crash reports using Sentry please set the
@ -19,7 +19,7 @@ Otherwise, the sending of crash reports will be disabled.
## Build ## Build
In order to build Bridge app with Qt interface we are using In order to build Bridge app with Qt interface we are using
[Qt 6.3](https://doc.qt.io/qt-6/gettingstarted.html). [Qt 6.4.3](https://doc.qt.io/qt-6/gettingstarted.html).
Please note that qmake path must be in your `PATH` to ensure Qt to be found. Please note that qmake path must be in your `PATH` to ensure Qt to be found.
Also, before you start build **on Windows**, please unset the `MSYSTEM` variable Also, before you start build **on Windows**, please unset the `MSYSTEM` variable

View File

@ -40,6 +40,7 @@ Proton Mail Bridge includes the following 3rd party software:
* [go-message](https://github.com/emersion/go-message) available under [license](https://github.com/emersion/go-message/blob/master/LICENSE) * [go-message](https://github.com/emersion/go-message) available under [license](https://github.com/emersion/go-message/blob/master/LICENSE)
* [go-sasl](https://github.com/emersion/go-sasl) available under [license](https://github.com/emersion/go-sasl/blob/master/LICENSE) * [go-sasl](https://github.com/emersion/go-sasl) available under [license](https://github.com/emersion/go-sasl/blob/master/LICENSE)
* [go-smtp](https://github.com/emersion/go-smtp) available under [license](https://github.com/emersion/go-smtp/blob/master/LICENSE) * [go-smtp](https://github.com/emersion/go-smtp) available under [license](https://github.com/emersion/go-smtp/blob/master/LICENSE)
* [go-vcard](https://github.com/emersion/go-vcard) available under [license](https://github.com/emersion/go-vcard/blob/master/LICENSE)
* [color](https://github.com/fatih/color) available under [license](https://github.com/fatih/color/blob/master/LICENSE) * [color](https://github.com/fatih/color) available under [license](https://github.com/fatih/color/blob/master/LICENSE)
* [sentry-go](https://github.com/getsentry/sentry-go) available under [license](https://github.com/getsentry/sentry-go/blob/master/LICENSE) * [sentry-go](https://github.com/getsentry/sentry-go) available under [license](https://github.com/getsentry/sentry-go/blob/master/LICENSE)
* [resty](https://github.com/go-resty/resty/v2) available under [license](https://github.com/go-resty/resty/v2/blob/master/LICENSE) * [resty](https://github.com/go-resty/resty/v2) available under [license](https://github.com/go-resty/resty/v2/blob/master/LICENSE)
@ -49,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) * [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) * [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) * [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) * [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) * [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) * [memory](https://github.com/pbnjay/memory) available under [license](https://github.com/pbnjay/memory/blob/master/LICENSE)
@ -83,7 +85,6 @@ Proton Mail Bridge includes the following 3rd party software:
* [go-spew](https://github.com/davecgh/go-spew) available under [license](https://github.com/davecgh/go-spew/blob/master/LICENSE) * [go-spew](https://github.com/davecgh/go-spew) available under [license](https://github.com/davecgh/go-spew/blob/master/LICENSE)
* [go-windows](https://github.com/elastic/go-windows) available under [license](https://github.com/elastic/go-windows/blob/master/LICENSE) * [go-windows](https://github.com/elastic/go-windows) available under [license](https://github.com/elastic/go-windows/blob/master/LICENSE)
* [go-textwrapper](https://github.com/emersion/go-textwrapper) available under [license](https://github.com/emersion/go-textwrapper/blob/master/LICENSE) * [go-textwrapper](https://github.com/emersion/go-textwrapper) available under [license](https://github.com/emersion/go-textwrapper/blob/master/LICENSE)
* [go-vcard](https://github.com/emersion/go-vcard) available under [license](https://github.com/emersion/go-vcard/blob/master/LICENSE)
* [fgprof](https://github.com/felixge/fgprof) available under [license](https://github.com/felixge/fgprof/blob/master/LICENSE) * [fgprof](https://github.com/felixge/fgprof) available under [license](https://github.com/felixge/fgprof/blob/master/LICENSE)
* [go-shlex](https://github.com/flynn-archive/go-shlex) available under [license](https://github.com/flynn-archive/go-shlex/blob/master/LICENSE) * [go-shlex](https://github.com/flynn-archive/go-shlex) available under [license](https://github.com/flynn-archive/go-shlex/blob/master/LICENSE)
* [mimetype](https://github.com/gabriel-vasile/mimetype) available under [license](https://github.com/gabriel-vasile/mimetype/blob/master/LICENSE) * [mimetype](https://github.com/gabriel-vasile/mimetype) available under [license](https://github.com/gabriel-vasile/mimetype/blob/master/LICENSE)
@ -123,6 +124,7 @@ Proton Mail Bridge includes the following 3rd party software:
* [codec](https://github.com/ugorji/go/codec) available under [license](https://github.com/ugorji/go/codec/blob/master/LICENSE) * [codec](https://github.com/ugorji/go/codec) available under [license](https://github.com/ugorji/go/codec/blob/master/LICENSE)
* [tagparser](https://github.com/vmihailenco/tagparser/v2) available under [license](https://github.com/vmihailenco/tagparser/v2/blob/master/LICENSE) * [tagparser](https://github.com/vmihailenco/tagparser/v2) available under [license](https://github.com/vmihailenco/tagparser/v2/blob/master/LICENSE)
* [smetrics](https://github.com/xrash/smetrics) available under [license](https://github.com/xrash/smetrics/blob/master/LICENSE) * [smetrics](https://github.com/xrash/smetrics) available under [license](https://github.com/xrash/smetrics/blob/master/LICENSE)
* [go-ordered-json](https://gitlab.com/c0b/go-ordered-json)
* [arch](https://golang.org/x/arch) available under [license](https://cs.opensource.google/go/x/arch/+/master:LICENSE) * [arch](https://golang.org/x/arch) available under [license](https://cs.opensource.google/go/x/arch/+/master:LICENSE)
* [crypto](https://golang.org/x/crypto) available under [license](https://cs.opensource.google/go/x/crypto/+/master:LICENSE) * [crypto](https://golang.org/x/crypto) available under [license](https://cs.opensource.google/go/x/crypto/+/master:LICENSE)
* [mod](https://golang.org/x/mod) available under [license](https://cs.opensource.google/go/x/mod/+/master:LICENSE) * [mod](https://golang.org/x/mod) available under [license](https://cs.opensource.google/go/x/mod/+/master:LICENSE)
@ -132,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) * [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) * [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-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) * [go-keychain](https://github.com/cuthix/go-keychain) available under [license](https://github.com/cuthix/go-keychain/blob/master/LICENSE)
<!-- END AUTOGEN --> <!-- END AUTOGEN -->

View File

@ -3,6 +3,183 @@
Changelog [format](http://keepachangelog.com/en/1.0.0/) 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
* GODT-2762: Setup wizard.
* GODT-2772: Setup wizard content.
* GODT-2769: Setup Wizard architecture.
* GODT-2767: Setup Wizard foundations.
* GODT-2725: Implement receive message step with expected structure exposed.
### Changed
* GODT-2960: Added content in empty view when there is no account.
* GODT-2771: Cert related tools for macOS.
* GODT-2770: Proof of concept for web view as a tool window and overlay (not used).
* GODT-2916: Split Decryption from Message Building.
* GODT-2597: Implement contact specific settings in integration tests.
* GODT-2664: Trigger QA installer.
### Fixed
* GODT-2992: Fix link in 'no account view' in main window after 2FA or TOTP are cancelled.
* GODT-2989: Allow to send bug report when no account connected.
* GODT-2988: Fix setup wizard KB links.
* GODT-2968: Use proper base64 encoded string even for bad password test.
* GODT-2965: Fix multipart/mixed testdata + structure parsing steps related to this.
* GODT-2932: Fix syncing not being reported in GUI.
* GODT-2967: Tray menu entries close the setup wizard when needed.
* GODT-2212: Preserver Header order in message building.
* Fixed missing GoOs gRPC call in bridge-gui-tester.
* GODT-2929: Message dedup with different text transfer encoding.
## Umshiang Bridge 3.5.3
### Changed
* GODT-3004: Update gopenpgp and dependencies.
## Umshiang Bridge 3.5.2
### Fixed
* GODT-3003: Ensure IMAP State is reset after vault corruption.
* GODT-3001: Only create system labels during system label sync.
## Umshiang Bridge 3.5.1
### Fixed
* GODT-2963: Use multi error to report file removal errors.
* GODT-2956: Restore old deletion rules.
* GODT-2951: Negative WaitGroup Counter.
* GODT-2590: Fix send on closed channel.
* GODT-2949: Fix close of close channel in event service.
## Umshiang Bridge 3.5.0 ## Umshiang Bridge 3.5.0
### Added ### Added
@ -75,6 +252,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* GODT-2780: Fix 'QSystemTrayIcon::setVisible: No Icon set' warning in bridge-gui log on startup. * GODT-2780: Fix 'QSystemTrayIcon::setVisible: No Icon set' warning in bridge-gui log on startup.
* GODT-2778: Fix login screen being disabled after an 'already logged in' error. * GODT-2778: Fix login screen being disabled after an 'already logged in' error.
* Fix typos found by codespell. * Fix typos found by codespell.
* GODT-2577: Answered flag should only be applied to replied messages.
## Trift Bridge 3.4.1 ## Trift Bridge 3.4.1

View File

@ -11,7 +11,7 @@ ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
.PHONY: build build-gui build-nogui build-launcher versioner hasher .PHONY: build build-gui build-nogui build-launcher versioner hasher
# Keep version hardcoded so app build works also without Git repository. # Keep version hardcoded so app build works also without Git repository.
BRIDGE_APP_VERSION?=3.5.0+git BRIDGE_APP_VERSION?=3.8.0+git
APP_VERSION:=${BRIDGE_APP_VERSION} APP_VERSION:=${BRIDGE_APP_VERSION}
APP_FULL_NAME:=Proton Mail Bridge APP_FULL_NAME:=Proton Mail Bridge
APP_VENDOR:=Proton AG APP_VENDOR:=Proton AG
@ -304,6 +304,7 @@ ApplyStageInput,BuildStageInput,BuildStageOutput,DownloadStageInput,DownloadStag
StateProvider,Regulator,UpdateApplier,MessageBuilder,APIClient,Reporter,DownloadRateModifier \ StateProvider,Regulator,UpdateApplier,MessageBuilder,APIClient,Reporter,DownloadRateModifier \
> tmp > tmp
mv tmp internal/services/syncservice/mocks_test.go mv tmp internal/services/syncservice/mocks_test.go
mockgen --package mocks github.com/ProtonMail/gluon/connector IMAPStateWrite > internal/services/imapservice/mocks/mocks.go
lint: gofiles lint-golang lint-license lint-dependencies lint-changelog lint-bug-report lint: gofiles lint-golang lint-license lint-dependencies lint-changelog lint-bug-report

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

30
go.mod
View File

@ -5,10 +5,10 @@ go 1.20
require ( require (
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557 github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
github.com/Masterminds/semver/v3 v3.2.0 github.com/Masterminds/semver/v3 v3.2.0
github.com/ProtonMail/gluon v0.17.1-0.20230829112217-5d5c25c504b5 github.com/ProtonMail/gluon v0.17.1-0.20231114153341-2ecbdd2739f7
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
github.com/ProtonMail/go-proton-api v0.4.1-0.20230831064234-0e3a549b3f36 github.com/ProtonMail/go-proton-api v0.4.1-0.20231130083229-e8aa47d7a366
github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton
github.com/PuerkitoBio/goquery v1.8.1 github.com/PuerkitoBio/goquery v1.8.1
github.com/abiosoft/ishell v2.0.0+incompatible github.com/abiosoft/ishell v2.0.0+incompatible
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37 github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37
@ -16,12 +16,13 @@ require (
github.com/cucumber/godog v0.12.5 github.com/cucumber/godog v0.12.5
github.com/cucumber/messages-go/v16 v16.0.1 github.com/cucumber/messages-go/v16 v16.0.1
github.com/docker/docker-credential-helpers v0.6.3 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 v1.2.1
github.com/emersion/go-imap-id v0.0.0-20190926060100-f94a56b9ecde github.com/emersion/go-imap-id v0.0.0-20190926060100-f94a56b9ecde
github.com/emersion/go-message v0.16.0 github.com/emersion/go-message v0.16.0
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead
github.com/emersion/go-smtp v0.15.1-0.20221021114529-49b17434419d github.com/emersion/go-smtp v0.15.1-0.20221021114529-49b17434419d
github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3
github.com/fatih/color v1.13.0 github.com/fatih/color v1.13.0
github.com/getsentry/sentry-go v0.15.0 github.com/getsentry/sentry-go v0.15.0
github.com/go-resty/resty/v2 v2.7.0 github.com/go-resty/resty/v2 v2.7.0
@ -31,6 +32,7 @@ require (
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.0
github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-multierror v1.1.1
github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba 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/keybase/go-keychain v0.0.0
github.com/miekg/dns v1.1.50 github.com/miekg/dns v1.1.50
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58
@ -42,17 +44,17 @@ require (
github.com/vmihailenco/msgpack/v5 v5.3.5 github.com/vmihailenco/msgpack/v5 v5.3.5
go.uber.org/goleak v1.2.1 go.uber.org/goleak v1.2.1
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
golang.org/x/net v0.10.0 golang.org/x/net v0.17.0
golang.org/x/sys v0.8.0 golang.org/x/sys v0.13.0
golang.org/x/text v0.9.0 golang.org/x/text v0.13.0
google.golang.org/grpc v1.53.0 google.golang.org/grpc v1.56.3
google.golang.org/protobuf v1.30.0 google.golang.org/protobuf v1.30.0
howett.net/plist v1.0.0 howett.net/plist v1.0.0
) )
require ( require (
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // indirect github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 // indirect github.com/ProtonMail/go-crypto v0.0.0-20230717121622-edf196117233 // indirect
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect
github.com/ProtonMail/go-srp v0.0.7 // indirect github.com/ProtonMail/go-srp v0.0.7 // indirect
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect
@ -68,7 +70,6 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/elastic/go-windows v1.0.1 // indirect github.com/elastic/go-windows v1.0.1 // indirect
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect
github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3 // indirect
github.com/felixge/fgprof v0.9.3 // indirect github.com/felixge/fgprof v0.9.3 // indirect
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect
@ -79,7 +80,7 @@ require (
github.com/go-playground/validator/v10 v10.14.0 // indirect github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect github.com/goccy/go-json v0.10.2 // indirect
github.com/gofrs/uuid v4.3.0+incompatible // 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/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
@ -108,17 +109,20 @@ require (
github.com/ugorji/go/codec v1.2.11 // indirect github.com/ugorji/go/codec v1.2.11 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a // indirect
golang.org/x/arch v0.3.0 // indirect golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.9.0 // indirect golang.org/x/crypto v0.14.0 // indirect
golang.org/x/mod v0.8.0 // indirect golang.org/x/mod v0.8.0 // indirect
golang.org/x/sync v0.2.0 // indirect golang.org/x/sync v0.2.0 // indirect
golang.org/x/tools v0.6.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 gopkg.in/yaml.v3 v3.0.1 // indirect
) )
replace ( replace (
github.com/docker/docker-credential-helpers => github.com/ProtonMail/docker-credential-helpers v1.1.0 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-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 github.com/keybase/go-keychain => github.com/cuthix/go-keychain v0.0.0-20230517073537-fc1740a83768
) )

111
go.sum
View File

@ -11,42 +11,49 @@ 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/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 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= 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 h1:l6surSnJ3RP4qA1qmKJ+hQn3UjytosdoG27WGjrDlVs=
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557/go.mod h1:sTrmvD/TxuypdOERsDOS7SndZg0rzzcCi1b6wQMXUYM= 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/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/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 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= 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/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-20210511135022-227b4adcab57/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug= github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug=
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo= github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk= 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/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
github.com/ProtonMail/gluon v0.17.1-0.20230829112217-5d5c25c504b5 h1:C/8P5NHAKi2yCKez+OZ5rSR8SsL7k8si4pK4SE2QtV8= github.com/ProtonMail/gluon v0.17.1-0.20231114153341-2ecbdd2739f7 h1:w+VoSAq9FQvKMm3DlH1MIEZ1KGe7LJ+81EJFVwSV4VU=
github.com/ProtonMail/gluon v0.17.1-0.20230829112217-5d5c25c504b5/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo= 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 h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4= github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE= github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
github.com/ProtonMail/go-crypto v0.0.0-20230322105811-d73448b7e800/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE= github.com/ProtonMail/go-crypto v0.0.0-20230717121622-edf196117233 h1:bdoKdh0f66/lrgVfYlxw0aqISY/KOqXmFJyGt7rGmnc=
github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 h1:ZK3C5DtzV2nVAQTx5S5jQvMeDqWtD1By5mOoyY/xJek= github.com/ProtonMail/go-crypto v0.0.0-20230717121622-edf196117233/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7 h1:+j+Kd/DyZ/qGfMT9htAT7HxqIEbZHsatsx+m8AoV6fc= github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7 h1:+j+Kd/DyZ/qGfMT9htAT7HxqIEbZHsatsx+m8AoV6fc=
github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4= github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k= github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw= github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
github.com/ProtonMail/go-proton-api v0.4.1-0.20230831064234-0e3a549b3f36 h1:JVMK2w90bCWayUCXJIb3wkQ5+j2P/NbnrX3BrDoLzsc= 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.20230831064234-0e3a549b3f36/go.mod h1:nS8hMGjJLgC0Iej0JMYbsI388LesEkM1Hj/jCCxQeaQ= 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 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk= github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton h1:YS6M20yvjCJPR1r4ADW5TPn6rahs4iAyZaACei86bEc= github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton h1:8tqHYM6IGsdEc6Vxf1TWiwpHNj8yIEQNACPhxsDagrk=
github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton/go.mod h1:S1lYsaGHykYpxxh2SnJL6ypcAlANKj5NRSY6HxKryKQ= 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 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ= 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 h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw=
github.com/abiosoft/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzgHf/aS6+zVXRj14cVk9qg= 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 h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8=
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db/go.mod h1:rB3B4rKii8V21ydCbIzH5hZiCQE7f5E9SzUb/ZZx530= 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/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/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= github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37 h1:28uU3TtuvQ6KRndxg9TrC868jBWmSKgh0GTXkACCXmA=
@ -64,6 +71,7 @@ github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJm
github.com/bradenaw/juniper v0.12.0 h1:Q/7icpPQD1nH/La5DobQfNEtwyrBSiSu47jOQx7lJEM= github.com/bradenaw/juniper v0.12.0 h1:Q/7icpPQD1nH/La5DobQfNEtwyrBSiSu47jOQx7lJEM=
github.com/bradenaw/juniper v0.12.0/go.mod h1:Z2B7aJlQ7xbfWsnMLROj5t/5FQ94/MkIdKC30J4WvzI= github.com/bradenaw/juniper v0.12.0/go.mod h1:Z2B7aJlQ7xbfWsnMLROj5t/5FQ94/MkIdKC30J4WvzI=
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
@ -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/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/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/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/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
github.com/elastic/go-sysinfo v1.8.1/go.mod h1:JfllUnzoQV/JRYymbH3dO1yggI3mV2oTKSXsDHM+uIM= 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 h1:AlYZOldA+UJ0/2nBuqWdo90GFCgG9xuyw9SYzGUtJm0=
github.com/elastic/go-windows v1.0.1/go.mod h1:FoVvqWSun28vaDQPbj2Elfc0JahhPB7WQEGa3c814Ss= github.com/elastic/go-windows v1.0.1/go.mod h1:FoVvqWSun28vaDQPbj2Elfc0JahhPB7WQEGa3c814Ss=
github.com/emersion/go-imap v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA= 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-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 h1:fI1Jck0vUrXT8bnphprS1EoVRe2Q5CKCX8iDlpqjQ/Y=
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= 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 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= 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= 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 h1:BMXYYRWTLOJKlh+lOBt6nUQgXAfB7oVIQt5cNreqSLI=
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:rZfgFAXFS/z/lEd6LJmf9HVZ1LkgYiHx5pHhV5DR16M= 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.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 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/getsentry/sentry-go v0.15.0 h1:CP9bmA7pralrVUedYZsmIHWpq/pBtXTSew7xvVpfLaA= 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 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= 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-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 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-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.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 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/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 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= 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/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 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 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 h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4=
github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= 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.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 h1:CaSVZxm5B+7o45rtab4jC2G37WGYX1zQfuU2i6DSvnc=
github.com/gofrs/uuid v4.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 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.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.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/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/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= 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.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.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.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 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 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/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 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/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/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/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 h1:QFQpJdgbON7I0jr2hYW7Bs+XV0qjc3d5tZoDnRFnqTg=
github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk= 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/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 h1:rp+c0RAYOWj8l6qbCUTSiRLG/iKnW3K3/QfPPuSsBt4=
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak= 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/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.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 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/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 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= 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/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.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
@ -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 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 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/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/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 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 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/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 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= 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 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 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/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 h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM= 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= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@ -397,8 +425,11 @@ 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/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 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= 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.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 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=
gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a/go.mod h1:NREvu3a57BaK0R1+ztrEzHWiZAihohNLQ6trPxlIqZI=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
@ -417,9 +448,10 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= 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-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -429,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/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-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-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-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-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -440,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/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.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.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.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.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
@ -458,18 +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-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-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-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-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-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-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-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.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.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.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -478,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-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-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-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-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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -497,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-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-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-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-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-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-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-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -507,34 +548,46 @@ 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-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-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-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-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-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.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.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.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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 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.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= 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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.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.5-0.20201125200606-c27b9fd57aec/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 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.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.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.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-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/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-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-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -548,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-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-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-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-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-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-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-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-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.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.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
@ -580,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-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-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-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-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
google.golang.org/genproto v0.0.0-20230221151758-ace64dc21148/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= 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.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 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.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc= google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc=
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
@ -594,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/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 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= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
@ -605,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.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.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.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-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.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 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/sentry"
"github.com/ProtonMail/proton-bridge/v3/internal/useragent" "github.com/ProtonMail/proton-bridge/v3/internal/useragent"
"github.com/ProtonMail/proton-bridge/v3/internal/vault" "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/ProtonMail/proton-bridge/v3/pkg/restarter"
"github.com/pkg/profile" "github.com/pkg/profile"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -204,7 +205,7 @@ func run(c *cli.Context) error {
}() }()
// Restart the app if requested. // 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. // Handle crashes with various actions.
return withCrashHandler(restarter, reporter, func(crashHandler *crash.Handler, quitCh <-chan struct{}) error { return withCrashHandler(restarter, reporter, func(crashHandler *crash.Handler, quitCh <-chan struct{}) error {
migrationErr := migrateOldVersions() migrationErr := migrateOldVersions()
@ -234,53 +235,56 @@ func run(c *cli.Context) error {
} }
return withSingleInstance(settings, locations.GetLockFile(), version, func() error { return withSingleInstance(settings, locations.GetLockFile(), version, func() error {
// Unlock the encrypted vault. // Look for available keychains
return WithVault(locations, crashHandler, func(v *vault.Vault, insecure, corrupt bool) error { return WithKeychainList(func(keychains *keychain.List) error {
if !v.Migrated() { // Unlock the encrypted vault.
// Migrate old settings into the vault. return WithVault(locations, keychains, crashHandler, func(v *vault.Vault, insecure, corrupt bool) error {
if err := migrateOldSettings(v); err != nil { if !v.Migrated() {
logrus.WithError(err).Error("Failed to migrate old settings") // 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)
} }
if corrupt { // Migrate old accounts into the vault.
logrus.Warn("The vault is corrupt and has been wiped") if err := migrateOldAccounts(locations, keychains, v); err != nil {
b.PushError(bridge.ErrVaultCorrupt) logrus.WithError(err).Error("Failed to migrate old accounts")
} }
// Start telemetry heartbeat process // The vault has been migrated.
b.StartHeartbeat(b) if err := v.SetMigrated(); err != nil {
logrus.WithError(err).Error("Failed to mark vault as migrated")
}
}
// Run the frontend. logrus.WithFields(logrus.Fields{
return runFrontend(c, crashHandler, restarter, locations, b, eventCh, quitCh, c.Int(flagParentPID)) "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. // 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) 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 { func setDeviceCookies(jar *cookies.Jar) error {
url, err := url.Parse(constants.APIHost) url, err := url.Parse(constants.APIHost)
if err != nil { 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/useragent"
"github.com/ProtonMail/proton-bridge/v3/internal/vault" "github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/internal/versioner" "github.com/ProtonMail/proton-bridge/v3/internal/versioner"
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -55,6 +56,7 @@ func withBridge(
reporter *sentry.Reporter, reporter *sentry.Reporter,
vault *vault.Vault, vault *vault.Vault,
cookieJar http.CookieJar, cookieJar http.CookieJar,
keychains *keychain.List,
fn func(*bridge.Bridge, <-chan events.Event) error, fn func(*bridge.Bridge, <-chan events.Event) error,
) error { ) error {
logrus.Debug("Creating bridge") logrus.Debug("Creating bridge")
@ -97,6 +99,7 @@ func withBridge(
autostarter, autostarter,
updater, updater,
version, version,
keychains,
// The API stuff. // The API stuff.
constants.APIHost, constants.APIHost,
@ -110,6 +113,7 @@ func withBridge(
crashHandler, crashHandler,
reporter, reporter,
imap.DefaultEpochUIDValidityGenerator(), imap.DefaultEpochUIDValidityGenerator(),
nil,
// The logging stuff. // The logging stuff.
c.String(flagLogIMAP) == "client" || c.String(flagLogIMAP) == "all", c.String(flagLogIMAP) == "client" || c.String(flagLogIMAP) == "all",
@ -155,7 +159,7 @@ func newUpdater(locations *locations.Locations) (*updater.Updater, error) {
} }
return updater.NewUpdater( return updater.NewUpdater(
updater.NewInstaller(versioner.New(updatesDir)), versioner.New(updatesDir),
verifier, verifier,
constants.UpdateName, constants.UpdateName,
runtime.GOOS, runtime.GOOS,

View File

@ -122,7 +122,7 @@ func migrateOldSettingsWithDir(configDir string, v *vault.Vault) error {
return v.SetBridgeTLSCertKey(certPEM, keyPEM) 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") logrus.Info("Migrating accounts")
settings, err := locations.ProvideSettingsPath() settings, err := locations.ProvideSettingsPath()
@ -134,8 +134,7 @@ func migrateOldAccounts(locations *locations.Locations, v *vault.Vault) error {
if err != nil { if err != nil {
return fmt.Errorf("failed to get helper: %w", err) return fmt.Errorf("failed to get helper: %w", err)
} }
keychain, err := keychain.NewKeychain(helper, "bridge", keychains.GetHelpers(), keychains.GetDefaultHelper())
keychain, err := keychain.NewKeychain(helper, "bridge")
if err != nil { if err != nil {
return fmt.Errorf("failed to create keychain: %w", err) 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/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/pkg/algo" "github.com/ProtonMail/proton-bridge/v3/pkg/algo"
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain" "github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
dockerCredentials "github.com/docker/docker-credential-helpers/credentials"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -43,7 +42,7 @@ func TestMigratePrefsToVaultWithKeys(t *testing.T) {
// Create a new vault. // Create a new vault.
vault, corrupt, err := vault.New(t.TempDir(), t.TempDir(), []byte("my secret key"), async.NoopPanicHandler{}) vault, corrupt, err := vault.New(t.TempDir(), t.TempDir(), []byte("my secret key"), async.NoopPanicHandler{})
require.NoError(t, err) require.NoError(t, err)
require.False(t, corrupt) require.NoError(t, corrupt)
// load the old prefs file. // load the old prefs file.
configDir := filepath.Join("testdata", "with_keys") configDir := filepath.Join("testdata", "with_keys")
@ -64,7 +63,7 @@ func TestMigratePrefsToVaultWithoutKeys(t *testing.T) {
// Create a new vault. // Create a new vault.
vault, corrupt, err := vault.New(t.TempDir(), t.TempDir(), []byte("my secret key"), async.NoopPanicHandler{}) vault, corrupt, err := vault.New(t.TempDir(), t.TempDir(), []byte("my secret key"), async.NoopPanicHandler{})
require.NoError(t, err) require.NoError(t, err)
require.False(t, corrupt) require.NoError(t, corrupt)
// load the old prefs file. // load the old prefs file.
configDir := filepath.Join("testdata", "without_keys") configDir := filepath.Join("testdata", "without_keys")
@ -133,11 +132,9 @@ func TestKeychainMigration(t *testing.T) {
} }
func TestUserMigration(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", kcl.GetHelpers(), kcl.GetDefaultHelper())
kc, err := keychain.NewKeychain("mock", "bridge")
require.NoError(t, err) require.NoError(t, err)
require.NoError(t, kc.Put("brokenID", "broken")) 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{}) v, corrupt, err := vault.New(settingsFolder, settingsFolder, token, async.NoopPanicHandler{})
require.NoError(t, err) 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.Equal(t, []string{wantCredentials.UserID}, v.GetUserIDs())
require.NoError(t, v.GetUser(wantCredentials.UserID, func(u *vault.User) { require.NoError(t, v.GetUser(wantCredentials.UserID, func(u *vault.User) {

View File

@ -30,47 +30,37 @@ import (
"github.com/sirupsen/logrus" "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") logrus.Debug("Creating vault")
defer logrus.Debug("Vault stopped") defer logrus.Debug("Vault stopped")
// Create the encVault. // Create the encVault.
encVault, insecure, corrupt, err := newVault(locations, panicHandler) encVault, insecure, corrupt, err := newVault(locations, keychains, panicHandler)
if err != nil { if err != nil {
return fmt.Errorf("could not create vault: %w", err) return fmt.Errorf("could not create vault: %w", err)
} }
logrus.WithFields(logrus.Fields{ logrus.WithFields(logrus.Fields{
"insecure": insecure, "insecure": insecure,
"corrupt": corrupt, "corrupt": corrupt != nil,
}).Debug("Vault created") }).Debug("Vault created")
// Install the certificates if needed. if corrupt != nil {
if installed := encVault.GetCertsInstalled(); !installed { logrus.WithError(corrupt).Warn("Failed to load existing vault, vault has been reset")
logrus.Debug("Installing certificates")
certPEM, _ := encVault.GetBridgeTLSCert()
if err := certs.NewInstaller().InstallCert(certPEM); err != nil {
return fmt.Errorf("failed to install certs: %w", err)
}
if err := encVault.SetCertsInstalled(true); err != nil {
return fmt.Errorf("failed to set certs installed: %w", err)
}
logrus.Debug("Certificates successfully installed")
} }
cert, _ := encVault.GetBridgeTLSCert()
certs.NewInstaller().LogCertInstallStatus(cert)
// GODT-1950: Add teardown actions (e.g. to close the vault). // 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() vaultDir, err := locations.ProvideSettingsPath()
if err != nil { 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") logrus.WithField("vaultDir", vaultDir).Debug("Loading vault from directory")
@ -80,7 +70,7 @@ func newVault(locations *locations.Locations, panicHandler async.PanicHandler) (
insecure bool 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") logrus.WithError(err).Error("Could not load/create vault key")
insecure = true insecure = true
@ -92,24 +82,24 @@ func newVault(locations *locations.Locations, panicHandler async.PanicHandler) (
gluonCacheDir, err := locations.ProvideGluonCachePath() gluonCacheDir, err := locations.ProvideGluonCachePath()
if err != nil { 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) vault, corrupt, err := vault.New(vaultDir, gluonCacheDir, vaultKey, panicHandler)
if err != nil { 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 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) helper, err := vault.GetHelper(vaultDir)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not get keychain helper: %w", err) 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 { if err != nil {
return nil, fmt.Errorf("could not create keychain: %w", err) 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/telemetry"
"github.com/ProtonMail/proton-bridge/v3/internal/user" "github.com/ProtonMail/proton-bridge/v3/internal/user"
"github.com/ProtonMail/proton-bridge/v3/internal/vault" "github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
"github.com/bradenaw/juniper/xslices" "github.com/bradenaw/juniper/xslices"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -74,7 +75,7 @@ type Bridge struct {
installCh chan installJob installCh chan installJob
// heartbeat is the telemetry heartbeat for metrics. // heartbeat is the telemetry heartbeat for metrics.
heartbeat telemetry.Heartbeat heartbeat *heartBeatState
// curVersion is the current version of the bridge, // curVersion is the current version of the bridge,
// newVersion is the version that was installed by the updater. // newVersion is the version that was installed by the updater.
@ -82,6 +83,9 @@ type Bridge struct {
newVersion *semver.Version newVersion *semver.Version
newVersionLock safe.RWMutex 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 is used to raise the bridge window when needed.
focusService *focus.Service focusService *focus.Service
@ -124,9 +128,6 @@ type Bridge struct {
// goUpdate triggers a check/install of updates. // goUpdate triggers a check/install of updates.
goUpdate func() goUpdate func()
// goHeartbeat triggers a check/sending if heartbeat is needed.
goHeartbeat func()
serverManager *imapsmtpserver.Service serverManager *imapsmtpserver.Service
syncService *syncservice.Service syncService *syncservice.Service
} }
@ -138,6 +139,7 @@ func New(
autostarter Autostarter, // the autostarter to manage autostart settings autostarter Autostarter, // the autostarter to manage autostart settings
updater Updater, // the updater to fetch and install updates updater Updater, // the updater to fetch and install updates
curVersion *semver.Version, // the current version of the bridge curVersion *semver.Version, // the current version of the bridge
keychains *keychain.List, // usable keychains
apiURL string, // the URL of the API to use apiURL string, // the URL of the API to use
cookieJar http.CookieJar, // the cookie jar to use cookieJar http.CookieJar, // the cookie jar to use
@ -148,6 +150,7 @@ func New(
panicHandler async.PanicHandler, panicHandler async.PanicHandler,
reporter reporter.Reporter, reporter reporter.Reporter,
uidValidityGenerator imap.UIDValidityGenerator, uidValidityGenerator imap.UIDValidityGenerator,
heartBeatManager telemetry.HeartbeatManager,
logIMAPClient, logIMAPServer bool, // whether to log IMAP client/server activity logIMAPClient, logIMAPServer bool, // whether to log IMAP client/server activity
logSMTP bool, // whether to log SMTP activity logSMTP bool, // whether to log SMTP activity
@ -163,6 +166,7 @@ func New(
// bridge is the bridge. // bridge is the bridge.
bridge, err := newBridge( bridge, err := newBridge(
context.Background(),
tasks, tasks,
imapEventCh, imapEventCh,
@ -171,6 +175,7 @@ func New(
autostarter, autostarter,
updater, updater,
curVersion, curVersion,
keychains,
panicHandler, panicHandler,
reporter, reporter,
@ -178,6 +183,7 @@ func New(
identifier, identifier,
proxyCtl, proxyCtl,
uidValidityGenerator, uidValidityGenerator,
heartBeatManager,
logIMAPClient, logIMAPServer, logSMTP, logIMAPClient, logIMAPServer, logSMTP,
) )
if err != nil { if err != nil {
@ -196,6 +202,7 @@ func New(
} }
func newBridge( func newBridge(
ctx context.Context,
tasks *async.Group, tasks *async.Group,
imapEventCh chan imapEvents.Event, imapEventCh chan imapEvents.Event,
@ -204,6 +211,7 @@ func newBridge(
autostarter Autostarter, autostarter Autostarter,
updater Updater, updater Updater,
curVersion *semver.Version, curVersion *semver.Version,
keychains *keychain.List,
panicHandler async.PanicHandler, panicHandler async.PanicHandler,
reporter reporter.Reporter, reporter reporter.Reporter,
@ -211,6 +219,7 @@ func newBridge(
identifier identifier.Identifier, identifier identifier.Identifier,
proxyCtl ProxyController, proxyCtl ProxyController,
uidValidityGenerator imap.UIDValidityGenerator, uidValidityGenerator imap.UIDValidityGenerator,
heartbeatManager telemetry.HeartbeatManager,
logIMAPClient, logIMAPServer, logSMTP bool, logIMAPClient, logIMAPServer, logSMTP bool,
) (*Bridge, error) { ) (*Bridge, error) {
@ -256,9 +265,13 @@ func newBridge(
newVersion: curVersion, newVersion: curVersion,
newVersionLock: safe.NewRWMutex(), newVersionLock: safe.NewRWMutex(),
keychains: keychains,
panicHandler: panicHandler, panicHandler: panicHandler,
reporter: reporter, reporter: reporter,
heartbeat: newHeartBeatState(ctx, panicHandler),
focusService: focusService, focusService: focusService,
autostarter: autostarter, autostarter: autostarter,
locator: locator, locator: locator,
@ -288,7 +301,13 @@ func newBridge(
return nil, err 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 return bridge, nil
} }
@ -417,6 +436,9 @@ func (bridge *Bridge) GetErrors() []error {
func (bridge *Bridge) Close(ctx context.Context) { func (bridge *Bridge) Close(ctx context.Context) {
logrus.Info("Closing bridge") logrus.Info("Closing bridge")
// Stop heart beat before closing users.
bridge.heartbeat.stop()
// Close all users. // Close all users.
safe.Lock(func() { safe.Lock(func() {
for _, user := range bridge.users { for _, user := range bridge.users {
@ -429,6 +451,8 @@ func (bridge *Bridge) Close(ctx context.Context) {
logrus.WithError(err).Error("Failed to close servers") logrus.WithError(err).Error("Failed to close servers")
} }
bridge.syncService.Close()
// Stop all ongoing tasks. // Stop all ongoing tasks.
bridge.tasks.CancelAndWait() bridge.tasks.CancelAndWait()
@ -487,27 +511,15 @@ func (bridge *Bridge) remWatcher(watcher *watcher.Watcher[events.Event]) {
watcher.Close() watcher.Close()
} }
func (bridge *Bridge) onStatusUp(ctx context.Context) { func (bridge *Bridge) onStatusUp(_ context.Context) {
logrus.Info("Handling API status up") logrus.Info("Handling API status up")
safe.RLock(func() {
for _, user := range bridge.users {
user.OnStatusUp(ctx)
}
}, bridge.usersLock)
bridge.goLoad() bridge.goLoad()
} }
func (bridge *Bridge) onStatusDown(ctx context.Context) { func (bridge *Bridge) onStatusDown(ctx context.Context) {
logrus.Info("Handling API status down") 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) { for backoff := time.Second; ; backoff = min(backoff*2, 30*time.Second) {
select { select {
case <-ctx.Done(): 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/user"
"github.com/ProtonMail/proton-bridge/v3/internal/useragent" "github.com/ProtonMail/proton-bridge/v3/internal/useragent"
"github.com/ProtonMail/proton-bridge/v3/internal/vault" "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/ProtonMail/proton-bridge/v3/tests"
"github.com/bradenaw/juniper/xslices" "github.com/bradenaw/juniper/xslices"
imapid "github.com/emersion/go-imap-id" imapid "github.com/emersion/go-imap-id"
@ -585,7 +586,7 @@ func TestBridge_MissingGluonStore(t *testing.T) {
require.NoError(t, os.RemoveAll(gluonDir)) require.NoError(t, os.RemoveAll(gluonDir))
// Bridge starts but can't find the gluon store dir; there should be no error. // Bridge starts but can't find the gluon store dir; there should be no error.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { withBridgeWaitForServers(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
// ... // ...
}) })
}) })
@ -950,6 +951,7 @@ func withBridgeNoMocks(
mocks.Autostarter, mocks.Autostarter,
mocks.Updater, mocks.Updater,
v2_3_0, v2_3_0,
keychain.NewTestKeychainsList(),
// The API stuff. // The API stuff.
apiURL, apiURL,
@ -961,6 +963,7 @@ func withBridgeNoMocks(
mocks.CrashHandler, mocks.CrashHandler,
mocks.Reporter, mocks.Reporter,
testUIDValidityGenerator, testUIDValidityGenerator,
mocks.Heartbeat,
// The logging stuff. // The logging stuff.
os.Getenv("BRIDGE_LOG_IMAP_CLIENT") == "1", os.Getenv("BRIDGE_LOG_IMAP_CLIENT") == "1",
@ -970,9 +973,6 @@ func withBridgeNoMocks(
require.NoError(t, err) require.NoError(t, err)
require.Empty(t, bridge.GetErrors()) require.Empty(t, bridge.GetErrors())
// Start the Heartbeat process.
bridge.StartHeartbeat(mocks.Heartbeat)
// Wait for bridge to finish loading users. // Wait for bridge to finish loading users.
waitForEvent(t, eventCh, events.AllUsersLoaded{}) waitForEvent(t, eventCh, events.AllUsersLoaded{})

View File

@ -19,6 +19,7 @@ package bridge
import ( import (
"context" "context"
"errors"
"io" "io"
"github.com/ProtonMail/go-proton-api" "github.com/ProtonMail/go-proton-api"
@ -33,63 +34,133 @@ const (
DefaultMaxSessionCountForBugReport = 10 DefaultMaxSessionCountForBugReport = 10
) )
func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, title, description, username, email, client string, attachLogs bool) error { type ReportBugReq struct {
var account string OSType string
OSVersion string
Title string
Description string
Username string
Email string
EmailClient string
IncludeLogs bool
}
if info, err := bridge.QueryUserInfo(username); err == nil { func (bridge *Bridge) ReportBug(ctx context.Context, report *ReportBugReq) error {
account = info.Username if info, err := bridge.QueryUserInfo(report.Username); err == nil {
report.Username = info.Username
} else if userIDs := bridge.GetUserIDs(); len(userIDs) > 0 { } else if userIDs := bridge.GetUserIDs(); len(userIDs) > 0 {
if err := bridge.vault.GetUser(userIDs[0], func(user *vault.User) { if err := bridge.vault.GetUser(userIDs[0], func(user *vault.User) {
account = user.Username() report.Username = user.Username()
}); err != nil { }); err != nil {
return err return err
} }
} }
var attachment []proton.ReportBugAttachment var attachments []proton.ReportBugAttachment
if report.IncludeLogs {
if attachLogs { logs, err := bridge.CollectLogs()
logsPath, err := bridge.locator.ProvideLogsPath()
if err != nil { if err != nil {
return err return err
} }
attachments = append(attachments, logs)
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,
})
} }
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 { for _, user := range bridge.users {
user.ReportBugSent() user.ReportBugSent()
} }
}, bridge.usersLock) }, bridge.usersLock)
return bridge.api.ReportBug(ctx, proton.ReportBugReq{ // if we have a token we can append more attachment to the bugReport
OS: osType, for i, att := range attachments {
OSVersion: osVersion, if i == 0 && report.IncludeLogs {
continue
}
err := bridge.appendComment(ctx, token, att)
if err != nil {
return err
}
}
return err
}
Title: "[Bridge] Bug - " + title, func (bridge *Bridge) CollectLogs() (proton.ReportBugAttachment, error) {
Description: description, 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, ClientType: proton.ClientTypeEmail,
ClientVersion: constants.AppVersion(bridge.curVersion.Original()), ClientVersion: constants.AppVersion(bridge.curVersion.Original()),
Username: account, Username: report.Username,
Email: email, Email: report.Email,
}, attachment...)
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() { func (bridge *Bridge) ReportBugClicked() {
safe.Lock(func() { safe.RLock(func() {
for _, user := range bridge.users { for _, user := range bridge.users {
user.ReportBugClicked() user.ReportBugClicked()
} }
@ -30,17 +30,17 @@ func (bridge *Bridge) ReportBugClicked() {
} }
func (bridge *Bridge) AutoconfigUsed(client string) { func (bridge *Bridge) AutoconfigUsed(client string) {
safe.Lock(func() { safe.RLock(func() {
for _, user := range bridge.users { for _, user := range bridge.users {
user.AutoconfigUsed(client) user.AutoconfigUsed(client)
} }
}, bridge.usersLock) }, bridge.usersLock)
} }
func (bridge *Bridge) KBArticleOpened(article string) { func (bridge *Bridge) ExternalLinkClicked(article string) {
safe.Lock(func() { safe.RLock(func() {
for _, user := range bridge.users { for _, user := range bridge.users {
user.KBArticleOpened(article) user.ExternalLinkClicked(article)
} }
}, bridge.usersLock) }, bridge.usersLock)
} }

View File

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

View File

@ -20,18 +20,100 @@ package bridge
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"sync"
"time" "time"
"github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/gluon/reporter" "github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/proton-bridge/v3/internal/safe" "github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/telemetry" "github.com/ProtonMail/proton-bridge/v3/internal/telemetry"
"github.com/ProtonMail/proton-bridge/v3/internal/vault" "github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
const HeartbeatCheckInterval = time.Hour 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 { func (bridge *Bridge) IsTelemetryAvailable(ctx context.Context) bool {
var flag = true var flag = true
if bridge.GetTelemetryDisabled() { if bridge.GetTelemetryDisabled() {
@ -80,49 +162,6 @@ func (bridge *Bridge) SetLastHeartbeatSent(timestamp time.Time) error {
return bridge.vault.SetLastHeartbeatSent(timestamp) return bridge.vault.SetLastHeartbeatSent(timestamp)
} }
func (bridge *Bridge) StartHeartbeat(manager telemetry.HeartbeatManager) { func (bridge *Bridge) GetHeartbeatPeriodicInterval() time.Duration {
bridge.heartbeat = telemetry.NewHeartbeat(manager, 1143, 1025, bridge.GetGluonCacheDir(), keychain.DefaultHelper) return HeartbeatCheckInterval
// 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)
} }

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" "os"
"sync" "sync"
"testing" "testing"
"time"
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
"github.com/ProtonMail/proton-bridge/v3/internal/bridge/mocks" "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. // this is called at start of heartbeat process.
mocks.Heartbeat.EXPECT().IsTelemetryAvailable(gomock.Any()).AnyTimes() mocks.Heartbeat.EXPECT().IsTelemetryAvailable(gomock.Any()).AnyTimes()
mocks.Heartbeat.EXPECT().GetHeartbeatPeriodicInterval().AnyTimes().Return(500 * time.Millisecond)
return mocks 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 { func (testUpdater *TestUpdater) InstallUpdate(_ context.Context, _ updater.Downloader, _ updater.VersionInfo) error {
return nil return nil
} }
func (testUpdater *TestUpdater) RemoveOldUpdates() error {
return nil
}

View File

@ -36,6 +36,20 @@ func (m *MockHeartbeatManager) EXPECT() *MockHeartbeatManagerMockRecorder {
return m.recorder 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. // GetLastHeartbeatSent mocks base method.
func (m *MockHeartbeatManager) GetLastHeartbeatSent() time.Time { func (m *MockHeartbeatManager) GetLastHeartbeatSent() time.Time {
m.ctrl.T.Helper() 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/bridge"
"github.com/ProtonMail/proton-bridge/v3/internal/constants" "github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/events" "github.com/ProtonMail/proton-bridge/v3/internal/events"
smtpservice "github.com/ProtonMail/proton-bridge/v3/internal/services/smtp"
"github.com/emersion/go-imap" "github.com/emersion/go-imap"
"github.com/emersion/go-sasl" "github.com/emersion/go-sasl"
"github.com/emersion/go-smtp" "github.com/emersion/go-smtp"
@ -336,6 +337,9 @@ func TestBridge_SendInvite(t *testing.T) {
} }
func TestBridge_SendAddTextBodyPartIfNotExists(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; const messageMultipartWithoutText = `Content-Type: multipart/mixed;
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84" boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
Subject: A new message Subject: A new message
@ -343,7 +347,7 @@ Date: Mon, 13 Mar 2023 16:06:16 +0100
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84 --Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
Content-Disposition: inline; Content-Disposition: attachment;
filename=Cat_August_2010-4.jpeg filename=Cat_August_2010-4.jpeg
Content-Type: image/jpeg; Content-Type: image/jpeg;
name="Cat_August_2010-4.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 Date: Mon, 13 Mar 2023 16:06:16 +0100
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84 --Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
Content-Disposition: inline; Content-Disposition: attachment;
filename=Cat_August_2010-4.jpeg filename=Cat_August_2010-4.jpeg
Content-Type: image/jpeg; Content-Type: image/jpeg;
name="Cat_August_2010-4.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 return err
} }
// If telemetry is re-enabled locally, try to send the heartbeat. // If telemetry is re-enabled locally, try to send the heartbeat.
if !isDisabled { if isDisabled {
defer bridge.goHeartbeat() bridge.heartbeat.stop()
} else {
bridge.heartbeat.start()
} }
return nil return nil
} }

View File

@ -37,6 +37,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/bridge" "github.com/ProtonMail/proton-bridge/v3/internal/bridge"
"github.com/ProtonMail/proton-bridge/v3/internal/constants" "github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/events" "github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice"
"github.com/bradenaw/juniper/iterator" "github.com/bradenaw/juniper/iterator"
"github.com/bradenaw/juniper/stream" "github.com/bradenaw/juniper/stream"
"github.com/bradenaw/juniper/xslices" "github.com/bradenaw/juniper/xslices"
@ -579,6 +580,116 @@ func TestBridge_MessageCreateDuringSync(t *testing.T) {
}, server.WithTLS(false)) }, server.WithTLS(false))
} }
func TestBridge_CorruptedVaultClearsPreviousIMAPSyncState(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
userID, addrID, err := s.CreateUser("imap", password)
require.NoError(t, err)
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
require.NoError(t, err)
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
createNumMessages(ctx, t, c, addrID, labelID, 100)
})
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
defer done()
var err error
userID, err = bridge.LoginFull(context.Background(), "imap", password, nil, nil)
require.NoError(t, err)
// Wait for sync to finish
require.Equal(t, userID, (<-syncCh).UserID)
})
settingsPath, err := locator.ProvideSettingsPath()
require.NoError(t, err)
syncConfigPath, err := locator.ProvideIMAPSyncConfigPath()
require.NoError(t, err)
syncStatePath := imapservice.GetSyncConfigPath(syncConfigPath, userID)
// Check sync state is complete
{
state, err := imapservice.NewSyncState(syncStatePath)
require.NoError(t, err)
syncStatus, err := state.GetSyncStatus(context.Background())
require.NoError(t, err)
require.True(t, syncStatus.IsComplete())
}
// corrupt the vault
require.NoError(t, os.WriteFile(filepath.Join(settingsPath, "vault.enc"), []byte("Trash!"), 0o600))
// Bridge starts but can't find the gluon database dir; there should be no error.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
_, err := bridge.LoginFull(context.Background(), "imap", password, nil, nil)
require.NoError(t, err)
})
// Check sync state is reset.
{
state, err := imapservice.NewSyncState(syncStatePath)
require.NoError(t, err)
syncStatus, err := state.GetSyncStatus(context.Background())
require.NoError(t, err)
require.False(t, syncStatus.IsComplete())
}
})
}
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 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( m := proton.New(
proton.WithHostURL(s.GetHostURL()), proton.WithHostURL(s.GetHostURL()),

View File

@ -53,4 +53,5 @@ type Autostarter interface {
type Updater interface { type Updater interface {
GetVersionInfo(context.Context, updater.Downloader, updater.Channel) (updater.VersionInfo, error) GetVersionInfo(context.Context, updater.Downloader, updater.Channel) (updater.VersionInfo, error)
InstallUpdate(context.Context, updater.Downloader, 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) }, 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 Connected
) )
var ErrFailedToUnlock = errors.New("failed to unlock user keys")
type UserInfo struct { type UserInfo struct {
// UserID is the user's API ID. // UserID is the user's API ID.
UserID string UserID string
@ -66,10 +68,10 @@ type UserInfo struct {
BridgePass []byte BridgePass []byte
// UsedSpace is the amount of space used by the user. // 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 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). // GetUserIDs returns the IDs of all known users (authorized or not).
@ -157,11 +159,15 @@ func (bridge *Bridge) LoginUser(
func() (string, error) { func() (string, error) {
return bridge.loginUser(ctx, client, auth.UID, auth.RefreshToken, keyPass) return bridge.loginUser(ctx, client, auth.UID, auth.RefreshToken, keyPass)
}, },
func() error {
return client.AuthDelete(ctx)
},
) )
if err != nil { 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) return "", fmt.Errorf("failed to login user: %w", err)
} }
@ -217,7 +223,16 @@ func (bridge *Bridge) LoginFull(
keyPass = password 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. // 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 { 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") 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() ctx := context.Background()
user, ok := bridge.users[userID] 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 { 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 { } 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 { 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) 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 { if _, ok := err.(*resty.ResponseError); ok || isLogin {
logrus.WithError(err).Error("Failed to add user, clearing its secrets from vault") logrus.WithError(err).Error("Failed to add user, clearing its secrets from vault")
@ -514,6 +529,7 @@ func (bridge *Bridge) addUserWithVault(
client *proton.Client, client *proton.Client,
apiUser proton.User, apiUser proton.User,
vault *vault.User, vault *vault.User,
isNew bool,
) error { ) error {
statsPath, err := bridge.locator.ProvideStatsPath() statsPath, err := bridge.locator.ProvideStatsPath()
if err != nil { if err != nil {
@ -541,6 +557,7 @@ func (bridge *Bridge) addUserWithVault(
&bridgeEventSubscription{b: bridge}, &bridgeEventSubscription{b: bridge},
bridge.syncService, bridge.syncService,
syncSettingsPath, syncSettingsPath,
isNew,
) )
if err != nil { if err != nil {
return fmt.Errorf("failed to create user: %w", err) return fmt.Errorf("failed to create user: %w", err)
@ -577,7 +594,7 @@ func (bridge *Bridge) addUserWithVault(
}, bridge.usersLock) }, bridge.usersLock)
// As we need at least one user to send heartbeat, try to send it. // As we need at least one user to send heartbeat, try to send it.
defer bridge.goHeartbeat() bridge.heartbeat.start()
return nil 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) { 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{ if rerr := bridge.reporter.ReportMessageWithContext("Failed to handle event", reporter.Context{
"user_id": user.ID(), "user_id": user.ID(),
"old_event_id": event.OldEventID, "old_event_id": event.OldEventID,

View File

@ -23,71 +23,200 @@ package certs
#import <Foundation/Foundation.h> #import <Foundation/Foundation.h>
#import <Security/Security.h> #import <Security/Security.h>
// Memory management rules:
// Foundation object (Objective-C prefixed with `NS`) get ARC (Automatic Reference Counting), and do not need to be released manually.
// Core Foundation objects (C), prefixed with need to be released manually using CFRelease() unless:
// - They're obtained using a CF method containing the word Get (a.k.a. the Get Rule).
// - They're obtained using toll-free bridging from a Foundation Object (using the __bridge keyword).
int installTrustedCert(char const *bytes, unsigned long long length) { //****************************************************************************************************************************************************
if (length == 0) { /// \brief Create a certificate object from DER-encoded data.
return errSecInvalidData; ///
} /// \return The certifcation. The caller is responsible for releasing the object using CFRelease.
/// \return NULL if data is not a valid DER-encoded certificate.
NSData *der = [NSData dataWithBytes:bytes length:length]; //****************************************************************************************************************************************************
SecCertificateRef certFromData(char const* data, uint64_t length) {
// Step 1. Import the certificate in the keychain. NSData *der = [NSData dataWithBytes:data length:length];
SecCertificateRef cert = SecCertificateCreateWithData(NULL, (CFDataRef) der); return SecCertificateCreateWithData(NULL, (__bridge CFDataRef)der);
NSDictionary* addQuery = @{
(id)kSecValueRef: (__bridge id) cert,
(id)kSecClass: (id)kSecClassCertificate,
};
OSStatus status = SecItemAdd((__bridge CFDictionaryRef) addQuery, NULL);
if ((errSecSuccess != status) && (errSecDuplicateItem != status)) {
CFRelease(cert);
return status;
}
// Step 2. Set the trust for the certificate.
SecPolicyRef policy = SecPolicyCreateSSL(true, NULL); // we limit our trust to SSL
NSDictionary *trustSettings = @{
(id)kSecTrustSettingsResult: [NSNumber numberWithInt:kSecTrustSettingsResultTrustRoot],
(id)kSecTrustSettingsPolicy: (__bridge id) policy,
};
status = SecTrustSettingsSetTrustSettings(cert, kSecTrustSettingsDomainUser, (__bridge CFTypeRef)(trustSettings));
CFRelease(policy);
CFRelease(cert);
return status;
} }
int removeTrustedCert(char const *bytes, unsigned long long length) { //****************************************************************************************************************************************************
if (0 == length) { /// \brief Check if a certificate is in the user's keychain.
return errSecInvalidData; ///
} /// \param[in] cert The certificate.
/// \return true iff the certificate is in the user's keychain.
//****************************************************************************************************************************************************
bool _isCertificateInKeychain(SecCertificateRef const cert) {
NSDictionary *attrs = @{
(id)kSecMatchItemList: @[(__bridge id)cert],
(id)kSecClass: (id)kSecClassCertificate,
(id)kSecReturnData: @YES
};
return errSecSuccess == SecItemCopyMatching((__bridge CFDictionaryRef)attrs, NULL);
}
NSData *der = [NSData dataWithBytes: bytes length: length]; //****************************************************************************************************************************************************
SecCertificateRef cert = SecCertificateCreateWithData(NULL, (CFDataRef) der); /// \brief Check if a certificate is in the user's keychain.
///
/// \param[in] certData The certificate data in DER encoded format.
/// \param[in] certSize The size of the certData in bytes.
/// \return true iff the certificate is in the user's keychain.
//****************************************************************************************************************************************************
bool isCertificateInKeychain(char const* certData, uint64_t certSize) {
return _isCertificateInKeychain(certFromData(certData, certSize));
}
// Step 1. Unset the trust for the certificate.
SecPolicyRef policy = SecPolicyCreateSSL(true, NULL);
NSDictionary * trustSettings = @{
(id)kSecTrustSettingsResult: [NSNumber numberWithInt:kSecTrustSettingsResultUnspecified],
(id)kSecTrustSettingsPolicy: (__bridge id) policy,
};
OSStatus status = SecTrustSettingsSetTrustSettings(cert, kSecTrustSettingsDomainUser, (__bridge CFTypeRef)(trustSettings));
CFRelease(policy);
if (errSecSuccess != status) {
CFRelease(cert);
return status;
}
// Step 2. Remove the certificate from the keychain. //****************************************************************************************************************************************************
NSDictionary *query = @{ (id)kSecClass: (id)kSecClassCertificate, /// \brief Add a certificate to the user's keychain.
(id)kSecMatchItemList: @[(__bridge id)cert], ///
(id)kSecMatchLimit: (id)kSecMatchLimitOne, /// \param[in] cert The certificate.
}; /// \return The status for the operation.
status = SecItemDelete((__bridge CFDictionaryRef) query); //****************************************************************************************************************************************************
OSStatus _addCertificateToKeychain(SecCertificateRef const cert) {
NSDictionary* addQuery = @{
(id)kSecValueRef: (__bridge id) cert,
(id)kSecClass: (id)kSecClassCertificate,
};
return SecItemAdd((__bridge CFDictionaryRef) addQuery, NULL);
}
CFRelease(cert); //****************************************************************************************************************************************************
return status; /// \brief Add a certificate to the user's keychain.
///
/// \param[in] certData The certificate data in DER encoded format.
/// \param[in] certSize The size of the certData in bytes.
/// \return The status for the operation.
//****************************************************************************************************************************************************
OSStatus addCertificateToKeychain(char const* certData, uint64_t certSize) {
return _addCertificateToKeychain(certFromData(certData, certSize));
}
//****************************************************************************************************************************************************
/// \brief Add a certificate to the user's keychain.
///
/// \param[in] cert The certificate.
/// \return The status for the operation.
//****************************************************************************************************************************************************
OSStatus _removeCertificateFromKeychain(SecCertificateRef const cert) {
NSDictionary *query = @{ (id)kSecClass: (id)kSecClassCertificate,
(id)kSecMatchItemList: @[(__bridge id)cert],
(id)kSecMatchLimit: (id)kSecMatchLimitOne,
};
return SecItemDelete((__bridge CFDictionaryRef) query);
}
//****************************************************************************************************************************************************
/// \brief Add a certificate to the user's keychain.
///
/// \param[in] certData The certificate data in DER encoded format.
/// \param[in] certSize The size of the certData in bytes.
/// \return The status for the operation.
//****************************************************************************************************************************************************
OSStatus removeCertificateFromKeychain(char const* certData, uint64_t certSize) {
return _removeCertificateFromKeychain(certFromData(certData, certSize));
}
//****************************************************************************************************************************************************
/// \brief Check if a certificate is trusted in the user's keychain.
///
/// \param[in] cert The certificate.
/// \return true iff the certificate is trusted in the user's keychain.
//****************************************************************************************************************************************************
bool _isCertificateTrusted(SecCertificateRef const cert) {
CFArrayRef trustSettings = NULL;
OSStatus status = SecTrustSettingsCopyTrustSettings(cert, kSecTrustSettingsDomainUser, &trustSettings);
if (status != errSecSuccess) {
return false;
}
CFIndex count = CFArrayGetCount(trustSettings);
bool result = false;
for (CFIndex index = 0; index < count; ++index) {
CFDictionaryRef dict = (CFDictionaryRef)CFArrayGetValueAtIndex(trustSettings, index);
if (!dict) {
continue;
}
CFNumberRef num = (CFNumberRef)CFDictionaryGetValue(dict, kSecTrustSettingsResult);
int value;
if (num && CFNumberGetValue(num, kCFNumberSInt32Type, &value) && (value == kSecTrustSettingsResultTrustRoot)) {
result = true;
break;
}
}
CFRelease(trustSettings);
return result;
}
//****************************************************************************************************************************************************
/// \brief Check if a certificate is trusted in the user's keychain.
///
/// \param[in] certData The certificate data in DER encoded format.
/// \param[in] certSize The size of the certData in bytes.
/// \return true iff the certificate is trusted in the user's keychain.
//****************************************************************************************************************************************************
bool isCertificateTrusted(char const* certData, uint64_t certSize) {
return _isCertificateTrusted(certFromData(certData, certSize));
}
//****************************************************************************************************************************************************
/// \brief Set the trust level for a certificate in the user's keychain. This call will trigger a security prompt.
///
/// \param[in] cert The certificate.
/// \param[in] trustLevel The trust level.
/// \return The status for the operation.
//****************************************************************************************************************************************************
OSStatus _setCertificateTrustLevel(SecCertificateRef const cert, int trustLevel) {
SecPolicyRef policy = SecPolicyCreateSSL(true, NULL); // we limit our trust to SSL
NSDictionary *trustSettings = @{
(id)kSecTrustSettingsResult: [NSNumber numberWithInt:trustLevel],
(id)kSecTrustSettingsPolicy: (__bridge id) policy,
};
OSStatus status = SecTrustSettingsSetTrustSettings(cert, kSecTrustSettingsDomainUser, (__bridge CFTypeRef)(trustSettings));
CFRelease(policy);
return status;
}
//****************************************************************************************************************************************************
/// \brief Set a certificate as trusted in the user's keychain. This call will trigger a security prompt.
///
/// \param[in] cert The certificate.
/// \return The status for the operation.
//****************************************************************************************************************************************************
OSStatus _setCertificateTrusted(SecCertificateRef cert) {
return _setCertificateTrustLevel(cert, kSecTrustSettingsResultTrustRoot);
}
//****************************************************************************************************************************************************
/// \brief Set a certificate as trusted in the user's keychain. This call will trigger a security prompt.
///
/// \param[in] certData The certificate data in DER encoded format.
/// \param[in] certSize The size of the certData in bytes.
/// \return The status for the operation.
//****************************************************************************************************************************************************
OSStatus setCertificateTrusted(char const* certData, uint64_t certSize) {
return _setCertificateTrusted(certFromData(certData, certSize));
}
//****************************************************************************************************************************************************
/// \brief Remove the trust level of a certificate in the user's keychain.
///
/// \param[in] cert The certificate.
/// \return The status for the operation.
//****************************************************************************************************************************************************
OSStatus _removeCertificateTrust(SecCertificateRef cert) {
return _setCertificateTrustLevel(cert, kSecTrustSettingsResultUnspecified);
}
//****************************************************************************************************************************************************
/// \brief Remove the trust level of a certificate in the user's keychain.
///
/// \param[in] certData The certificate data in DER encoded format.
/// \param[in] certSize The size of the certData in bytes.
/// \return The status for the operation.
//****************************************************************************************************************************************************
OSStatus removeCertificateTrust(char const* certData, uint64_t certSize) {
return _removeCertificateTrust(certFromData(certData, certSize));
} }
*/ */
import "C" import "C"
@ -119,6 +248,120 @@ func certPEMToDER(certPEM []byte) ([]byte, error) {
return block.Bytes, nil return block.Bytes, nil
} }
// wrapCGoCertCallReturningBool wrap call to a CGo function returning a bool.
// if the certificate is invalid the call will return false.
func wrapCGoCertCallReturningBool(certPEM []byte, fn func(*C.char, C.ulonglong) bool) bool {
certDER, err := certPEMToDER(certPEM)
if err != nil {
return false // error are ignored
}
buffer := C.CBytes(certDER)
defer C.free(unsafe.Pointer(buffer)) //nolint:unconvert
return fn((*C.char)(buffer), C.ulonglong(len(certDER)))
}
// wrapCGoCertCallReturningBool wrap call to a CGo function returning an error
func wrapCGoCertCallReturningError(certPEM []byte, fn func(*C.char, C.ulonglong) error) error {
certDER, err := certPEMToDER(certPEM)
if err != nil {
return err
}
buffer := C.CBytes(certDER)
defer C.free(unsafe.Pointer(buffer)) //nolint:unconvert
return fn((*C.char)(buffer), C.ulonglong(len(certDER)))
}
// isCertInKeychain returns true if the given certificate is stored in the user's keychain.
func isCertInKeychain(certPEM []byte) bool {
return wrapCGoCertCallReturningBool(certPEM, isCertInKeychainCGo)
}
func isCertInKeychainCGo(buffer *C.char, size C.ulonglong) bool {
return bool(C.isCertificateInKeychain(buffer, size))
}
// addCertToKeychain adds a certificate to the user's keychain.
// Trying to add a certificate that is already in the keychain will result in an error.
func addCertToKeychain(certPEM []byte) error {
return wrapCGoCertCallReturningError(certPEM, addCertToKeychainCGo)
}
func addCertToKeychainCGo(buffer *C.char, size C.ulonglong) error {
if errCode := C.addCertificateToKeychain(buffer, size); errCode != errSecSuccess {
return fmt.Errorf("could not add certificate to keychain (error %v)", errCode)
}
return nil
}
// removeCertFromKeychain removes a certificate from the user's keychain.
// Trying to remove a certificate that is not in the keychain will result in an error.
func removeCertFromKeychain(certPEM []byte) error {
return wrapCGoCertCallReturningError(certPEM, removeCertFromKeychainCGo)
}
func removeCertFromKeychainCGo(buffer *C.char, size C.ulonglong) error {
if errCode := C.removeCertificateFromKeychain(buffer, size); errCode != errSecSuccess {
return fmt.Errorf("could not remove certificate from keychain (error %v)", errCode)
}
return nil
}
// isCertTrusted check if a certificate is trusted in the user's keychain.
func isCertTrusted(certPEM []byte) bool {
return wrapCGoCertCallReturningBool(certPEM, isCertTrustedCGo)
}
func isCertTrustedCGo(buffer *C.char, size C.ulonglong) bool {
return bool(C.isCertificateTrusted(buffer, size))
}
// setCertTrusted sets a certificate as trusted in the user's keychain.
// This function will trigger a security prompt from the system.
func setCertTrusted(certPEM []byte) error {
return wrapCGoCertCallReturningError(certPEM, setCertTrustedCGo)
}
func setCertTrustedCGo(buffer *C.char, size C.ulonglong) error {
errCode := C.setCertificateTrusted(buffer, size)
switch errCode {
case errSecSuccess:
return nil
case errAuthorizationCanceled:
return ErrUserCanceledCertificateInstall
default:
return fmt.Errorf("could not set certificate trust in keychain (error %v)", errCode)
}
}
// removeCertTrust remove the trust level of the certificated from the user's keychain.
// This function will trigger a security prompt from the system.
func removeCertTrust(certPEM []byte) error {
return wrapCGoCertCallReturningError(certPEM, removeCertTrustCGo)
}
func removeCertTrustCGo(buffer *C.char, size C.ulonglong) error {
errCode := C.removeCertificateTrust(buffer, size)
switch errCode {
case errSecSuccess:
return nil
case errAuthorizationCanceled:
return ErrUserCanceledCertificateInstall
default:
return fmt.Errorf("could not set certificate trust in keychain (error %v)", errCode)
}
}
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 { func installCert(certPEM []byte) error {
certDER, err := certPEMToDER(certPEM) certDER, err := certPEMToDER(certPEM)
if err != nil { if err != nil {
@ -127,18 +370,24 @@ func installCert(certPEM []byte) error {
p := C.CBytes(certDER) p := C.CBytes(certDER)
defer C.free(unsafe.Pointer(p)) //nolint:unconvert defer C.free(unsafe.Pointer(p)) //nolint:unconvert
buffer := (*C.char)(p)
size := C.ulonglong(len(certDER))
errCode := C.installTrustedCert((*C.char)(p), (C.ulonglong)(len(certDER))) if !isCertInKeychainCGo(buffer, size) {
switch errCode { if err := addCertToKeychainCGo(buffer, size); err != nil {
case errSecSuccess: return err
return nil }
case errAuthorizationCanceled:
return fmt.Errorf("the user cancelled the authorization dialog")
default:
return fmt.Errorf("could not install certification into keychain (error %v)", errCode)
} }
if !isCertTrustedCGo(buffer, size) {
return setCertTrustedCGo(buffer, size)
}
return nil
} }
// uninstallCert uninstalls a certificate in the keychain. The certificate trust is removed and the certificated is deleted from the keychain.
// This function will trigger a security prompt from the system, unless the certificate is not trusted in the user keychain.
func uninstallCert(certPEM []byte) error { func uninstallCert(certPEM []byte) error {
certDER, err := certPEMToDER(certPEM) certDER, err := certPEMToDER(certPEM)
if err != nil { if err != nil {
@ -147,10 +396,32 @@ func uninstallCert(certPEM []byte) error {
p := C.CBytes(certDER) p := C.CBytes(certDER)
defer C.free(unsafe.Pointer(p)) //nolint:unconvert defer C.free(unsafe.Pointer(p)) //nolint:unconvert
buffer := (*C.char)(p)
size := C.ulonglong(len(certDER))
if errCode := C.removeTrustedCert((*C.char)(p), (C.ulonglong)(len(certDER))); errCode != 0 { if isCertTrustedCGo(buffer, size) {
return fmt.Errorf("could not install certificate from keychain (error %v)", errCode) if err := removeCertTrustCGo(buffer, size); err != nil {
return err
}
}
if isCertInKeychainCGo(buffer, size) {
return removeCertFromKeychainCGo(buffer, size)
} }
return nil return nil
} }
func isCertInstalled(certPEM []byte) bool {
certDER, err := certPEMToDER(certPEM)
if err != nil {
return false
}
p := C.CBytes(certDER)
defer C.free(unsafe.Pointer(p)) //nolint:unconvert
buffer := (*C.char)(p)
size := C.ulonglong(len(certDER))
return isCertInKeychainCGo(buffer, size) && isCertTrustedCGo(buffer, size)
}

View File

@ -25,20 +25,74 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
// This test implies human interactions to enter password and is disabled by default. func TestCertInKeychain(t *testing.T) {
func _TestTrustedCertsDarwin(t *testing.T) { //nolint:unused // 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))
require.Error(t, addCertToKeychain(certPEM))
require.True(t, isCertInKeychain(certPEM))
require.NoError(t, removeCertFromKeychain(certPEM))
require.False(t, isCertInKeychain(certPEM))
require.Error(t, removeCertFromKeychain(certPEM))
require.False(t, isCertInKeychain(certPEM))
}
// This test require human interaction (macOS security prompts), and is disabled by default.
func _TestCertificateTrust(t *testing.T) { //nolint:unused
certPEM := generatePEMCertificate(t)
require.False(t, isCertTrusted(certPEM))
require.NoError(t, addCertToKeychain(certPEM))
require.NoError(t, setCertTrusted(certPEM))
require.True(t, isCertTrusted(certPEM))
require.NoError(t, removeCertTrust(certPEM))
require.False(t, isCertTrusted(certPEM))
require.NoError(t, removeCertFromKeychain(certPEM))
}
// This test require human interaction (macOS security prompts), and is disabled by default.
func _TestInstallAndRemove(t *testing.T) { //nolint:unused
certPEM := generatePEMCertificate(t)
// fresh install
require.False(t, isCertInstalled(certPEM))
require.NoError(t, installCert(certPEM))
require.True(t, isCertInKeychain(certPEM))
require.True(t, isCertTrusted(certPEM))
require.True(t, isCertInstalled(certPEM))
require.NoError(t, uninstallCert(certPEM))
require.False(t, isCertInKeychain(certPEM))
require.False(t, isCertTrusted(certPEM))
require.False(t, isCertInstalled(certPEM))
// Install where certificate is already in Keychain, but not trusted.
require.NoError(t, addCertToKeychain(certPEM))
require.False(t, isCertInstalled(certPEM))
require.NoError(t, installCert(certPEM))
require.True(t, isCertInstalled(certPEM))
// Install where certificate is already installed
require.NoError(t, installCert(certPEM))
// Remove when certificate is not trusted.
require.NoError(t, removeCertTrust(certPEM))
require.NoError(t, uninstallCert(certPEM))
require.False(t, isCertInstalled(certPEM))
// Remove when certificate has already been removed.
require.NoError(t, uninstallCert(certPEM))
require.False(t, isCertTrusted(certPEM))
require.False(t, isCertInKeychain(certPEM))
}
func generatePEMCertificate(t *testing.T) []byte {
template, err := NewTLSTemplate() template, err := NewTLSTemplate()
require.NoError(t, err) require.NoError(t, err)
certPEM, _, err := GenerateCert(template) certPEM, _, err := GenerateCert(template)
require.NoError(t, err) require.NoError(t, err)
require.Error(t, installCert([]byte{0})) // Cannot install an invalid cert. return certPEM
require.Error(t, uninstallCert(certPEM)) // Cannot uninstall a cert that is not installed.
require.NoError(t, installCert(certPEM)) // Can install a valid cert.
require.NoError(t, installCert(certPEM)) // Can install an already installed cert.
require.NoError(t, uninstallCert(certPEM)) // Can uninstall an installed cert.
require.Error(t, uninstallCert(certPEM)) // Cannot uninstall an already uninstalled cert.
require.NoError(t, installCert(certPEM)) // Can reinstall an uninstalled cert.
require.NoError(t, uninstallCert(certPEM)) // Can uninstall a reinstalled cert.
} }

View File

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

View File

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

View File

@ -17,16 +17,66 @@
package certs package certs
type Installer struct{} import (
"errors"
"github.com/sirupsen/logrus"
)
var (
ErrUserCanceledCertificateInstall = errors.New("the user cancelled the authorization dialog")
)
type Installer struct {
log *logrus.Entry
}
func NewInstaller() *Installer { func NewInstaller() *Installer {
return &Installer{} return &Installer{
log: logrus.WithField("pkg", "certs"),
}
}
func (installer *Installer) OSSupportCertInstall() bool {
return osSupportCertInstall()
} }
func (installer *Installer) InstallCert(certPEM []byte) error { func (installer *Installer) InstallCert(certPEM []byte) error {
return installCert(certPEM) installer.log.Info("Installing the Bridge TLS certificate in the OS keychain")
if err := installCert(certPEM); err != nil {
installer.log.WithError(err).Error("The Bridge TLS certificate could not be installed in the OS keychain")
return err
}
installer.log.Info("The Bridge TLS certificate was successfully installed in the OS keychain")
return nil
} }
func (installer *Installer) UninstallCert(certPEM []byte) error { func (installer *Installer) UninstallCert(certPEM []byte) error {
return uninstallCert(certPEM) installer.log.Info("Uninstalling the Bridge TLS certificate from the OS keychain")
if err := uninstallCert(certPEM); err != nil {
installer.log.WithError(err).Error("The Bridge TLS certificate could not be uninstalled from the OS keychain")
return err
}
installer.log.Info("The Bridge TLS certificate was successfully uninstalled from the OS keychain")
return nil
}
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, hostname string,
imapPort, smtpPort int, imapPort, smtpPort int,
imapSSL, smtpSSL bool, imapSSL, smtpSSL bool,
username, addresses string, username, displayName, addresses string,
password []byte, password []byte,
) error { ) 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) confPath, err := saveConfigTemporarily(mc)
if err != nil { if err != nil {
@ -66,13 +66,13 @@ func prepareMobileConfig(
hostname string, hostname string,
imapPort, smtpPort int, imapPort, smtpPort int,
imapSSL, smtpSSL bool, imapSSL, smtpSSL bool,
username, addresses string, username, displayName, addresses string,
password []byte, password []byte,
) *mobileconfig.Config { ) *mobileconfig.Config {
return &mobileconfig.Config{ return &mobileconfig.Config{
DisplayName: username, DisplayName: username,
EmailAddress: addresses, EmailAddress: addresses,
AccountName: username, AccountName: displayName,
AccountDescription: username, AccountDescription: username,
Identifier: "protonmail " + username + strconv.FormatInt(time.Now().Unix(), 10), Identifier: "protonmail " + username + strconv.FormatInt(time.Now().Unix(), 10),
IMAP: &mobileconfig.IMAP{ IMAP: &mobileconfig.IMAP{

View File

@ -95,6 +95,13 @@ func (status *ConfigurationStatus) IsPending() bool {
return !status.Data.DataV1.PendingSince.IsZero() 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 { func (status *ConfigurationStatus) IsFromFailure() bool {
status.DataLock.RLock() status.DataLock.RLock()
defer status.DataLock.RUnlock() defer status.DataLock.RUnlock()

View File

@ -19,7 +19,6 @@ package configstatus
import ( import (
"strconv" "strconv"
"time"
) )
type ConfigAbortValues struct { type ConfigAbortValues struct {
@ -41,17 +40,20 @@ type ConfigAbortData struct {
type ConfigAbortBuilder 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{ return ConfigAbortData{
MeasurementGroup: "bridge.any.configuration", MeasurementGroup: "bridge.any.configuration",
Event: "bridge_config_abort", Event: "bridge_config_abort",
Values: ConfigSuccessValues{ Values: ConfigSuccessValues{
Duration: int(time.Since(data.DataV1.PendingSince).Minutes()), Duration: config.isPendingSinceMin(),
}, },
Dimensions: ConfigSuccessDimensions{ Dimensions: ConfigSuccessDimensions{
ReportClick: strconv.FormatBool(data.DataV1.ReportClick), ReportClick: strconv.FormatBool(config.Data.DataV1.ReportClick),
ReportSent: strconv.FormatBool(data.DataV1.ReportSent), ReportSent: strconv.FormatBool(config.Data.DataV1.ReportSent),
ClickedLink: data.clickedLinkToString(), ClickedLink: config.Data.clickedLinkToString(),
}, },
} }
} }

View File

@ -33,7 +33,7 @@ func TestConfigurationAbort_default(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
var builder = configstatus.ConfigAbortBuilder{} 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.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_abort", req.Event) require.Equal(t, "bridge_config_abort", req.Event)
@ -64,7 +64,7 @@ func TestConfigurationAbort_fed(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
var builder = configstatus.ConfigAbortBuilder{} 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.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_abort", req.Event) require.Equal(t, "bridge_config_abort", req.Event)

View File

@ -33,13 +33,16 @@ type ConfigProgressData struct {
type ConfigProgressBuilder 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{ return ConfigProgressData{
MeasurementGroup: "bridge.any.configuration", MeasurementGroup: "bridge.any.configuration",
Event: "bridge_config_progress", Event: "bridge_config_progress",
Values: ConfigProgressValues{ Values: ConfigProgressValues{
NbDay: numberOfDay(time.Now(), data.DataV1.PendingSince), NbDay: numberOfDay(time.Now(), config.Data.DataV1.PendingSince),
NbDaySinceLast: numberOfDay(time.Now(), data.DataV1.LastProgress), NbDaySinceLast: numberOfDay(time.Now(), config.Data.DataV1.LastProgress),
}, },
} }
} }

View File

@ -33,7 +33,7 @@ func TestConfigurationProgress_default(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
var builder = configstatus.ConfigProgressBuilder{} 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.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_progress", req.Event) require.Equal(t, "bridge_config_progress", req.Event)
@ -62,7 +62,7 @@ func TestConfigurationProgress_fed(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
var builder = configstatus.ConfigProgressBuilder{} 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.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_progress", req.Event) require.Equal(t, "bridge_config_progress", req.Event)

View File

@ -19,7 +19,6 @@ package configstatus
import ( import (
"strconv" "strconv"
"time"
) )
type ConfigRecoveryValues struct { type ConfigRecoveryValues struct {
@ -43,19 +42,22 @@ type ConfigRecoveryData struct {
type ConfigRecoveryBuilder 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{ return ConfigRecoveryData{
MeasurementGroup: "bridge.any.configuration", MeasurementGroup: "bridge.any.configuration",
Event: "bridge_config_recovery", Event: "bridge_config_recovery",
Values: ConfigRecoveryValues{ Values: ConfigRecoveryValues{
Duration: int(time.Since(data.DataV1.PendingSince).Minutes()), Duration: config.isPendingSinceMin(),
}, },
Dimensions: ConfigRecoveryDimensions{ Dimensions: ConfigRecoveryDimensions{
Autoconf: data.DataV1.Autoconf, Autoconf: config.Data.DataV1.Autoconf,
ReportClick: strconv.FormatBool(data.DataV1.ReportClick), ReportClick: strconv.FormatBool(config.Data.DataV1.ReportClick),
ReportSent: strconv.FormatBool(data.DataV1.ReportSent), ReportSent: strconv.FormatBool(config.Data.DataV1.ReportSent),
ClickedLink: data.clickedLinkToString(), ClickedLink: config.Data.clickedLinkToString(),
FailureDetails: data.DataV1.FailureDetails, FailureDetails: config.Data.DataV1.FailureDetails,
}, },
} }
} }

View File

@ -33,7 +33,7 @@ func TestConfigurationRecovery_default(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
var builder = configstatus.ConfigRecoveryBuilder{} 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.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_recovery", req.Event) require.Equal(t, "bridge_config_recovery", req.Event)
@ -66,7 +66,7 @@ func TestConfigurationRecovery_fed(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
var builder = configstatus.ConfigRecoveryBuilder{} 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.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_recovery", req.Event) require.Equal(t, "bridge_config_recovery", req.Event)

View File

@ -19,7 +19,6 @@ package configstatus
import ( import (
"strconv" "strconv"
"time"
) )
type ConfigSuccessValues struct { type ConfigSuccessValues struct {
@ -42,18 +41,21 @@ type ConfigSuccessData struct {
type ConfigSuccessBuilder 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{ return ConfigSuccessData{
MeasurementGroup: "bridge.any.configuration", MeasurementGroup: "bridge.any.configuration",
Event: "bridge_config_success", Event: "bridge_config_success",
Values: ConfigSuccessValues{ Values: ConfigSuccessValues{
Duration: int(time.Since(data.DataV1.PendingSince).Minutes()), Duration: config.isPendingSinceMin(),
}, },
Dimensions: ConfigSuccessDimensions{ Dimensions: ConfigSuccessDimensions{
Autoconf: data.DataV1.Autoconf, Autoconf: config.Data.DataV1.Autoconf,
ReportClick: strconv.FormatBool(data.DataV1.ReportClick), ReportClick: strconv.FormatBool(config.Data.DataV1.ReportClick),
ReportSent: strconv.FormatBool(data.DataV1.ReportSent), ReportSent: strconv.FormatBool(config.Data.DataV1.ReportSent),
ClickedLink: data.clickedLinkToString(), ClickedLink: config.Data.clickedLinkToString(),
}, },
} }
} }

View File

@ -33,7 +33,7 @@ func TestConfigurationSuccess_default(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
var builder = configstatus.ConfigSuccessBuilder{} 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.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_success", req.Event) require.Equal(t, "bridge_config_success", req.Event)
@ -65,7 +65,7 @@ func TestConfigurationSuccess_fed(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
var builder = configstatus.ConfigSuccessBuilder{} 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.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_success", req.Event) require.Equal(t, "bridge_config_success", req.Event)

View File

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

View File

@ -17,7 +17,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.28.0 // protoc-gen-go v1.31.0
// protoc v3.21.12 // protoc v3.21.12
// source: focus.proto // 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. // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions: // versions:
// - protoc-gen-go-grpc v1.2.0 // - protoc-gen-go-grpc v1.3.0
// - protoc v3.21.12 // - protoc v3.21.12
// source: focus.proto // source: focus.proto
@ -20,6 +37,11 @@ import (
// Requires gRPC-Go v1.32.0 or later. // Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7 const _ = grpc.SupportPackageIsVersion7
const (
Focus_Raise_FullMethodName = "/focus.Focus/Raise"
Focus_Version_FullMethodName = "/focus.Focus/Version"
)
// FocusClient is the client API for Focus service. // 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. // 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) { func (c *focusClient) Raise(ctx context.Context, in *wrapperspb.StringValue, opts ...grpc.CallOption) (*emptypb.Empty, error) {
out := new(emptypb.Empty) 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 { if err != nil {
return nil, err 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) { func (c *focusClient) Version(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*VersionResponse, error) {
out := new(VersionResponse) 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 { if err != nil {
return nil, err return nil, err
} }
@ -96,7 +118,7 @@ func _Focus_Raise_Handler(srv interface{}, ctx context.Context, dec func(interfa
} }
info := &grpc.UnaryServerInfo{ info := &grpc.UnaryServerInfo{
Server: srv, Server: srv,
FullMethod: "/focus.Focus/Raise", FullMethod: Focus_Raise_FullMethodName,
} }
handler := func(ctx context.Context, req interface{}) (interface{}, error) { handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(FocusServer).Raise(ctx, req.(*wrapperspb.StringValue)) 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{ info := &grpc.UnaryServerInfo{
Server: srv, Server: srv,
FullMethod: "/focus.Focus/Version", FullMethod: Focus_Version_FullMethodName,
} }
handler := func(ctx context.Context, req interface{}) (interface{}, error) { handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(FocusServer).Version(ctx, req.(*emptypb.Empty)) return srv.(FocusServer).Version(ctx, req.(*emptypb.Empty))

View File

@ -42,6 +42,7 @@ void GRPCQtProxy::connectSignals() {
connect(this, &GRPCQtProxy::setIsTelemetryDisabledReceived, &settingsTab, &SettingsTab::setIsTelemetryDisabled); connect(this, &GRPCQtProxy::setIsTelemetryDisabledReceived, &settingsTab, &SettingsTab::setIsTelemetryDisabled);
connect(this, &GRPCQtProxy::setColorSchemeNameReceived, &settingsTab, &SettingsTab::setColorSchemeName); connect(this, &GRPCQtProxy::setColorSchemeNameReceived, &settingsTab, &SettingsTab::setColorSchemeName);
connect(this, &GRPCQtProxy::reportBugReceived, &settingsTab, &SettingsTab::setBugReport); connect(this, &GRPCQtProxy::reportBugReceived, &settingsTab, &SettingsTab::setBugReport);
connect(this, &GRPCQtProxy::installTLSCertificateReceived, &settingsTab, &SettingsTab::installTLSCertificate);
connect(this, &GRPCQtProxy::exportTLSCertificatesReceived, &settingsTab, &SettingsTab::exportTLSCertificates); connect(this, &GRPCQtProxy::exportTLSCertificatesReceived, &settingsTab, &SettingsTab::exportTLSCertificates);
connect(this, &GRPCQtProxy::setIsStreamingReceived, &settingsTab, &SettingsTab::setIsStreaming); connect(this, &GRPCQtProxy::setIsStreamingReceived, &settingsTab, &SettingsTab::setIsStreaming);
connect(this, &GRPCQtProxy::setClientPlatformReceived, &settingsTab, &SettingsTab::setClientPlatform); connect(this, &GRPCQtProxy::setClientPlatformReceived, &settingsTab, &SettingsTab::setClientPlatform);
@ -119,6 +120,13 @@ void GRPCQtProxy::reportBug(QString const &osType, QString const &osVersion, QSt
} }
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void GRPCQtProxy::installTLSCertificate() {
emit installTLSCertificateReceived();
}
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \param[in] folderPath The folder path. /// \param[in] folderPath The folder path.
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************

View File

@ -45,6 +45,7 @@ public: // member functions.
void setColorSchemeName(QString const &name); ///< Forward a SetColorSchemeName call via a Qt Signal void setColorSchemeName(QString const &name); ///< Forward a SetColorSchemeName call via a Qt Signal
void reportBug(QString const &osType, QString const &osVersion, QString const &emailClient, QString const &address, void reportBug(QString const &osType, QString const &osVersion, QString const &emailClient, QString const &address,
QString const &description, bool includeLogs); ///< Forwards a ReportBug call via a Qt signal. QString const &description, bool includeLogs); ///< Forwards a ReportBug call via a Qt signal.
void installTLSCertificate(); ///< Forwards a InstallTLScertificate call via a Qt signal.
void exportTLSCertificates(QString const &folderPath); //< Forward an 'ExportTLSCertificates' call via a Qt signal. void exportTLSCertificates(QString const &folderPath); //< Forward an 'ExportTLSCertificates' call via a Qt signal.
void setIsStreaming(bool isStreaming); ///< Forward a isStreaming internal messages via a Qt signal. void setIsStreaming(bool isStreaming); ///< Forward a isStreaming internal messages via a Qt signal.
void setClientPlatform(QString const &clientPlatform); ///< Forward a setClientPlatform call via a Qt signal. void setClientPlatform(QString const &clientPlatform); ///< Forward a setClientPlatform call via a Qt signal.
@ -67,6 +68,7 @@ signals:
void setColorSchemeNameReceived(QString const &name); ///< Forward a SetColorScheme call via a Qt Signal void setColorSchemeNameReceived(QString const &name); ///< Forward a SetColorScheme call via a Qt Signal
void reportBugReceived(QString const &osType, QString const &osVersion, QString const &emailClient, QString const &address, void reportBugReceived(QString const &osType, QString const &osVersion, QString const &emailClient, QString const &address,
QString const &description, bool includeLogs); ///< Signal for the ReportBug gRPC call QString const &description, bool includeLogs); ///< Signal for the ReportBug gRPC call
void installTLSCertificateReceived(); ///< Signal for the InstallTLSCertificate gRPC call.
void exportTLSCertificatesReceived(QString const &folderPath); ///< Signal for the ExportTLSCertificates gRPC call. void exportTLSCertificatesReceived(QString const &folderPath); ///< Signal for the ExportTLSCertificates gRPC call.
void setIsStreamingReceived(bool isStreaming); ///< Signal for the IsStreaming internal message. void setIsStreamingReceived(bool isStreaming); ///< Signal for the IsStreaming internal message.
void setClientPlatformReceived(QString const &clientPlatform); ///< Signal for the SetClientPlatform gRPC call. void setClientPlatformReceived(QString const &clientPlatform); ///< Signal for the SetClientPlatform gRPC call.

View File

@ -214,6 +214,16 @@ grpc::Status GRPCService::IsTelemetryDisabled(::grpc::ServerContext *, ::google:
} }
//****************************************************************************************************************************************************
/// \param[out] response The response.
/// \return The status for the call.
//****************************************************************************************************************************************************
Status GRPCService::GoOs(ServerContext *, Empty const*, StringValue *response) {
response->set_value(app().mainWindow().settingsTab().os().toStdString());
return Status::OK;
}
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \return The status for the call. /// \return The status for the call.
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
@ -354,29 +364,25 @@ Status GRPCService::ReportBug(ServerContext *, ReportBugRequest const *request,
qtProxy_.reportBug(QString::fromStdString(request->ostype()), QString::fromStdString(request->osversion()), qtProxy_.reportBug(QString::fromStdString(request->ostype()), QString::fromStdString(request->osversion()),
QString::fromStdString(request->emailclient()), QString::fromStdString(request->address()), QString::fromStdString(request->description()), QString::fromStdString(request->emailclient()), QString::fromStdString(request->address()), QString::fromStdString(request->description()),
request->includelogs()); 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()); qtProxy_.sendDelayedEvent(newReportBugFinishedEvent());
return Status::OK; return Status::OK;
} }
//****************************************************************************************************************************************************
/// \param[in] request The request
//****************************************************************************************************************************************************
Status GRPCService::ExportTLSCertificates(ServerContext *, StringValue const *request, Empty *response) {
SettingsTab &tab = app().mainWindow().settingsTab();
if (!tab.nextTLSCertExportWillSucceed()) {
qtProxy_.sendDelayedEvent(newGenericErrorEvent(grpc::TLS_CERT_EXPORT_ERROR));
}
if (!tab.nextTLSKeyExportWillSucceed()) {
qtProxy_.sendDelayedEvent(newGenericErrorEvent(grpc::TLS_KEY_EXPORT_ERROR));
}
qtProxy_.exportTLSCertificates(QString::fromStdString(request->value()));
return Status::OK;
}
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \param[in] request The request. /// \param[in] request The request.
/// \return The status for the call. /// \return The status for the call.
@ -406,7 +412,7 @@ Status GRPCService::Login(ServerContext *, LoginRequest const *request, Empty *)
return Status::OK; return Status::OK;
} }
if (usersTab.nextUserTwoPasswordsRequired()) { if (usersTab.nextUserTwoPasswordsRequired()) {
qtProxy_.sendDelayedEvent(newLoginTwoPasswordsRequestedEvent()); qtProxy_.sendDelayedEvent(newLoginTwoPasswordsRequestedEvent(loginUsername_));
return Status::OK; return Status::OK;
} }
@ -431,7 +437,7 @@ Status GRPCService::Login2FA(ServerContext *, LoginRequest const *request, Empty
return Status::OK; return Status::OK;
} }
if (usersTab.nextUserTwoPasswordsRequired()) { if (usersTab.nextUserTwoPasswordsRequired()) {
qtProxy_.sendDelayedEvent(newLoginTwoPasswordsRequestedEvent()); qtProxy_.sendDelayedEvent(newLoginTwoPasswordsRequestedEvent(loginUsername_));
return Status::OK; return Status::OK;
} }
@ -541,7 +547,7 @@ Status GRPCService::SetDiskCachePath(ServerContext *, StringValue const *path, E
// we mimic the behaviour of Bridge // we mimic the behaviour of Bridge
if (!tab.nextCacheChangeWillSucceed()) { if (!tab.nextCacheChangeWillSucceed()) {
qtProxy_.sendDelayedEvent(newDiskCacheErrorEvent(grpc::DiskCacheErrorType(tab.cacheError()))); qtProxy_.sendDelayedEvent(newDiskCacheErrorEvent(grpc::DiskCacheErrorType(CANT_MOVE_DISK_CACHE_ERROR)));
} else { } else {
qtProxy_.setDiskCachePath(qPath); qtProxy_.setDiskCachePath(qPath);
qtProxy_.sendDelayedEvent(newDiskCachePathChangedEvent(qPath)); qtProxy_.sendDelayedEvent(newDiskCachePathChangedEvent(qPath));
@ -758,9 +764,86 @@ Status GRPCService::ConfigureUserAppleMail(ServerContext *, ConfigureAppleMailRe
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \param[in] request The request /// \param[in] request The request
/// \param[in] writer The writer
/// \return The status for the call. /// \return The status for the call.
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
Status GRPCService::ExportTLSCertificates(ServerContext *, StringValue const *request, Empty *response) {
app().log().debug(__FUNCTION__);
SettingsTab &tab = app().mainWindow().settingsTab();
if (!tab.nextTLSCertExportWillSucceed()) {
qtProxy_.sendDelayedEvent(newGenericErrorEvent(grpc::TLS_CERT_EXPORT_ERROR));
}
if (!tab.nextTLSKeyExportWillSucceed()) {
qtProxy_.sendDelayedEvent(newGenericErrorEvent(grpc::TLS_KEY_EXPORT_ERROR));
}
qtProxy_.exportTLSCertificates(QString::fromStdString(request->value()));
return Status::OK;
}
//****************************************************************************************************************************************************
/// \param[in] response The reponse.
/// \return The status for the call.
//****************************************************************************************************************************************************
Status GRPCService::IsTLSCertificateInstalled(ServerContext *, const Empty *request, BoolValue *response) {
app().log().debug(__FUNCTION__);
response->set_value(app().mainWindow().settingsTab().isTLSCertificateInstalled());
return Status::OK;
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
Status GRPCService::InstallTLSCertificate(ServerContext *, Empty const *, Empty *) {
app().log().debug(__FUNCTION__);
SPStreamEvent event;
qtProxy_.installTLSCertificate();
switch (app().mainWindow().settingsTab().nextTLSCertInstallResult()) {
case SettingsTab::TLSCertInstallResult::Success:
event = newCertificateInstallSuccessEvent();
break;
case SettingsTab::TLSCertInstallResult::Canceled:
event = newCertificateInstallCanceledEvent();
break;
default:
event = newCertificateInstallFailedEvent();
break;
}
qtProxy_.sendDelayedEvent(event);
return Status::OK;
}
//****************************************************************************************************************************************************
/// \param[in] request The request.
//****************************************************************************************************************************************************
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;
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
Status GRPCService::ReportBugClicked(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::Empty *) {
app().log().debug(__FUNCTION__);
return Status::OK;
}
//****************************************************************************************************************************************************
/// \param[in] request The request.
//****************************************************************************************************************************************************
Status GRPCService::AutoconfigClicked(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *response) {
app().log().debug(QString("%1 - Client = %2").arg(__FUNCTION__, QString::fromStdString(request->value())));
return Status::OK;
}
//****************************************************************************************************************************************************
/// \param[in] request The request
/// \param[in] writer The writer
//****************************************************************************************************************************************************
Status GRPCService::RunEventStream(ServerContext *ctx, EventStreamRequest const *request, ServerWriter<StreamEvent> *writer) { Status GRPCService::RunEventStream(ServerContext *ctx, EventStreamRequest const *request, ServerWriter<StreamEvent> *writer) {
app().log().debug(__FUNCTION__); app().log().debug(__FUNCTION__);
{ {
@ -850,4 +933,3 @@ void GRPCService::finishLogin() {
qtProxy_.sendDelayedEvent(newLoginFinishedEvent(user->id(), alreadyExist)); qtProxy_.sendDelayedEvent(newLoginFinishedEvent(user->id(), alreadyExist));
} }

View File

@ -53,6 +53,7 @@ public: // member functions.
grpc::Status IsAllMailVisible(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::BoolValue *response) override; grpc::Status IsAllMailVisible(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::BoolValue *response) override;
grpc::Status SetIsTelemetryDisabled(::grpc::ServerContext *, ::google::protobuf::BoolValue const *request, ::google::protobuf::Empty *response) override; grpc::Status SetIsTelemetryDisabled(::grpc::ServerContext *, ::google::protobuf::BoolValue const *request, ::google::protobuf::Empty *response) override;
grpc::Status IsTelemetryDisabled(::grpc::ServerContext *, ::google::protobuf::Empty const *request, ::google::protobuf::BoolValue *response) override; grpc::Status IsTelemetryDisabled(::grpc::ServerContext *, ::google::protobuf::Empty const *request, ::google::protobuf::BoolValue *response) override;
grpc::Status GoOs(::grpc::ServerContext *, ::google::protobuf::Empty const*, ::google::protobuf::StringValue *response) override;
grpc::Status TriggerReset(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::Empty *) override; grpc::Status TriggerReset(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::Empty *) override;
grpc::Status Version(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::StringValue *response) override; grpc::Status Version(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::StringValue *response) override;
grpc::Status LogsPath(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::StringValue *response) override; grpc::Status LogsPath(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::StringValue *response) override;
@ -64,7 +65,6 @@ public: // member functions.
grpc::Status ColorSchemeName(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::StringValue *response) override; grpc::Status ColorSchemeName(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::StringValue *response) override;
grpc::Status CurrentEmailClient(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::StringValue *response) override; grpc::Status CurrentEmailClient(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::StringValue *response) override;
grpc::Status ReportBug(::grpc::ServerContext *, ::grpc::ReportBugRequest const *request, ::google::protobuf::Empty *) override; grpc::Status ReportBug(::grpc::ServerContext *, ::grpc::ReportBugRequest const *request, ::google::protobuf::Empty *) override;
grpc::Status ExportTLSCertificates(::grpc::ServerContext *context, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *response) override;
grpc::Status ForceLauncher(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override; grpc::Status ForceLauncher(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
grpc::Status SetMainExecutable(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override; grpc::Status SetMainExecutable(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
grpc::Status Login(::grpc::ServerContext *, ::grpc::LoginRequest const *request, ::google::protobuf::Empty *) override; grpc::Status Login(::grpc::ServerContext *, ::grpc::LoginRequest const *request, ::google::protobuf::Empty *) override;
@ -93,6 +93,12 @@ public: // member functions.
grpc::Status LogoutUser(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override; grpc::Status LogoutUser(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
grpc::Status RemoveUser(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override; grpc::Status RemoveUser(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
grpc::Status ConfigureUserAppleMail(::grpc::ServerContext *, ::grpc::ConfigureAppleMailRequest const *request, ::google::protobuf::Empty *) override; grpc::Status ConfigureUserAppleMail(::grpc::ServerContext *, ::grpc::ConfigureAppleMailRequest const *request, ::google::protobuf::Empty *) override;
grpc::Status IsTLSCertificateInstalled(::grpc::ServerContext *, ::google::protobuf::Empty const*, ::google::protobuf::BoolValue *response) override;
grpc::Status InstallTLSCertificate(::grpc::ServerContext *, ::google::protobuf::Empty const*, ::google::protobuf::Empty *) override;
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 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 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; 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. 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. /// \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_.buttonInternetOn, &QPushButton::clicked, []() { app().grpc().sendEvent(newInternetStatusEvent(true)); });
connect(ui_.buttonInternetOff, &QPushButton::clicked, []() { app().grpc().sendEvent(newInternetStatusEvent(false)); }); connect(ui_.buttonInternetOff, &QPushButton::clicked, []() { app().grpc().sendEvent(newInternetStatusEvent(false)); });
connect(ui_.buttonShowMainWindow, &QPushButton::clicked, []() { app().grpc().sendEvent(newShowMainWindowEvent()); }); 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_.buttonAPICertIssue, &QPushButton::clicked, []() { app().grpc().sendEvent(newApiCertIssueEvent()); });
connect(ui_.buttonDiskCacheUnavailable, &QPushButton::clicked, []() { connectAddressError(ui_.buttonAddressChanged, ui_.editAddressErrors, newAddressChangedEvent);
app().grpc().sendEvent( connectAddressError(ui_.buttonAddressChangedLogout, ui_.editAddressErrors, newAddressChangedLogoutEvent);
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()));
});
connect(ui_.checkNextCacheChangeWillSucceed, &QCheckBox::toggled, this, &SettingsTab::updateGUIState); 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->resetUI();
this->updateGUIState(); this->updateGUIState();
} }
@ -68,7 +86,6 @@ void SettingsTab::updateGUIState() {
for (QWidget *widget: { ui_.groupVersion, ui_.groupGeneral, ui_.groupMail, ui_.groupPaths, ui_.groupCache }) { for (QWidget *widget: { ui_.groupVersion, ui_.groupGeneral, ui_.groupMail, ui_.groupPaths, ui_.groupCache }) {
widget->setEnabled(!connected); 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 { bool SettingsTab::isAutostartOn() const {
return ui_.checkAutostart->isChecked(); return ui_.checkAutostart->isChecked();
@ -285,21 +302,44 @@ 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->nextTLSCertInstallResult() == TLSCertInstallResult::Success);
}
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \param[in] folderPath The folder path. /// \param[in] folderPath The folder path.
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
void SettingsTab::exportTLSCertificates(QString const &folderPath) { void SettingsTab::exportTLSCertificates(QString const &folderPath) {
ui_.labeLastTLSCertsExport->setText(QString("%1 Export to %2") ui_.labeLastTLSCertExport->setText(QString("%1 Export to %2").arg(QDateTime::currentDateTime().toString(Qt::ISODateWithMs),folderPath));
.arg(QDateTime::currentDateTime().toString(Qt::ISODateWithMs))
.arg(folderPath));
} }
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \return The state of the check box. /// \return The state of the check box.
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
bool SettingsTab::nextBugReportWillSucceed() const { SettingsTab::BugReportResult SettingsTab::nextBugReportResult() const {
return ui_.checkNextBugReportWillSucceed->isChecked(); return BugReportResult(ui_.comboBugReportResult->currentIndex());
}
//****************************************************************************************************************************************************
/// \return the state of the 'TLS Certificate is installed' check box.
//****************************************************************************************************************************************************
bool SettingsTab::isTLSCertificateInstalled() const {
return ui_.checkTLSCertIsInstalled->isChecked();
}
//****************************************************************************************************************************************************
/// \return The value for the 'Next TLS cert install result'.
//****************************************************************************************************************************************************
SettingsTab::TLSCertInstallResult SettingsTab::nextTLSCertInstallResult() const {
return TLSCertInstallResult(ui_.comboNextTLSCertInstallResult->currentIndex());
} }
@ -421,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. /// \return the value for the 'Automatic Update' check.
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
@ -489,7 +521,7 @@ void SettingsTab::resetUI() {
ui_.editAddress->setText(QString()); ui_.editAddress->setText(QString());
ui_.editDescription->setPlainText(QString()); ui_.editDescription->setPlainText(QString());
ui_.labelIncludeLogsValue->setText(QString()); ui_.labelIncludeLogsValue->setText(QString());
ui_.checkNextBugReportWillSucceed->setChecked(true); ui_.comboBugReportResult->setCurrentIndex(0);
ui_.editHostname->setText("localhost"); ui_.editHostname->setText("localhost");
ui_.spinPortIMAP->setValue(1143); ui_.spinPortIMAP->setValue(1143);
@ -502,7 +534,13 @@ void SettingsTab::resetUI() {
QDir().mkpath(cacheDir); QDir().mkpath(cacheDir);
ui_.editDiskCachePath->setText(QDir::toNativeSeparators(cacheDir)); ui_.editDiskCachePath->setText(QDir::toNativeSeparators(cacheDir));
ui_.checkNextCacheChangeWillSucceed->setChecked(true); ui_.checkNextCacheChangeWillSucceed->setChecked(true);
ui_.comboCacheError->setCurrentIndex(0);
ui_.checkAutomaticUpdate->setChecked(true); ui_.checkAutomaticUpdate->setChecked(true);
ui_.checkTLSCertIsInstalled->setChecked(false);
ui_.comboNextTLSCertInstallResult->setCurrentIndex(0);
ui_.checkTLSCertExportWillSucceed->setChecked(true);
ui_.checkTLSKeyExportWillSucceed->setChecked(true);
ui_.labeLastTLSCertExport->setText("Last export: never");
ui_.labelLastTLSCertInstall->setText("Last install: never");
} }

View File

@ -28,11 +28,24 @@
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
class SettingsTab : public QWidget { class SettingsTab : public QWidget {
Q_OBJECT Q_OBJECT
public: // data types.
enum class TLSCertInstallResult {
Success = 0,
Canceled = 1,
Failure = 2
}; ///< 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. public: // member functions.
explicit SettingsTab(QWidget *parent = nullptr); ///< Default constructor. explicit SettingsTab(QWidget *parent = nullptr); ///< Default constructor.
SettingsTab(SettingsTab const &) = delete; ///< Disabled copy-constructor. SettingsTab(SettingsTab const &) = delete; ///< Disabled copy-constructor.
SettingsTab(SettingsTab &&) = delete; ///< Disabled assignment 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 const &) = delete; ///< Disabled assignment operator.
SettingsTab &operator=(SettingsTab &&) = delete; ///< Disabled move assignment operator. SettingsTab &operator=(SettingsTab &&) = delete; ///< Disabled move assignment operator.
@ -53,7 +66,9 @@ public: // member functions.
QString releaseNotesPageLink() const; ///< Get the content of the 'Release Notes Page Link' edit. 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 dependencyLicenseLink() const; ///< Get the content of the 'Dependency License Link' edit.
QString landingPageLink() const; ///< Get the content of the 'Landing Page 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 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 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. 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. QString hostname() const; ///< Get the value of the 'Hostname' edit.
@ -65,7 +80,6 @@ public: // member functions.
bool isPortFree() const; ///< Get the value for the "Is Port Free" check box. 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. 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. 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. bool isAutomaticUpdateOn() const; ///<Get the value for the 'Automatic Update' check box.
public slots: public slots:
@ -79,6 +93,7 @@ public slots:
void setColorSchemeName(QString const &name); ///< Set the value for the 'Use Dark Theme' check box. void setColorSchemeName(QString const &name); ///< Set the value for the 'Use Dark Theme' check box.
void setBugReport(QString const &osType, QString const &osVersion, QString const &emailClient, QString const &address, QString const &description, void setBugReport(QString const &osType, QString const &osVersion, QString const &emailClient, QString const &address, QString const &description,
bool includeLogs); ///< Set the content of the bug report box. bool includeLogs); ///< Set the content of the bug report box.
void installTLSCertificate(); ///< Install the TLS certificate.
void exportTLSCertificates(QString const &folderPath); ///< Export the TLS certificates. void exportTLSCertificates(QString const &folderPath); ///< Export the TLS certificates.
void setMailServerSettings(qint32 imapPort, qint32 smtpPort, bool useSSLForIMAP, bool useSSLForSMTP); ///< Change the mail server settings. void setMailServerSettings(qint32 imapPort, qint32 smtpPort, bool useSSLForIMAP, bool useSSLForSMTP); ///< Change the mail server settings.
void setIsDoHEnabled(bool enabled); ///< Set the value for the 'DoH Enabled' check box. void setIsDoHEnabled(bool enabled); ///< Set the value for the 'DoH Enabled' check box.
@ -89,7 +104,7 @@ private: // member functions.
void resetUI(); ///< Reset the widget. void resetUI(); ///< Reset the widget.
private: // data members. 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> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>1127</width> <width>1160</width>
<height>808</height> <height>777</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
@ -18,6 +18,9 @@
<layout class="QHBoxLayout" name="horizontalLayout_5"> <layout class="QHBoxLayout" name="horizontalLayout_5">
<item> <item>
<layout class="QVBoxLayout" name="verticalLayout"> <layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>4</number>
</property>
<item> <item>
<widget class="QGroupBox" name="groupVersion"> <widget class="QGroupBox" name="groupVersion">
<property name="minimumSize"> <property name="minimumSize">
@ -103,6 +106,9 @@
<string>General Settings</string> <string>General Settings</string>
</property> </property>
<layout class="QGridLayout" name="gridLayout_2"> <layout class="QGridLayout" name="gridLayout_2">
<property name="verticalSpacing">
<number>4</number>
</property>
<item row="0" column="0"> <item row="0" column="0">
<widget class="QCheckBox" name="checkShowOnStartup"> <widget class="QCheckBox" name="checkShowOnStartup">
<property name="text"> <property name="text">
@ -186,6 +192,9 @@
<string>Mail</string> <string>Mail</string>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_6"> <layout class="QVBoxLayout" name="verticalLayout_6">
<property name="spacing">
<number>4</number>
</property>
<item> <item>
<layout class="QHBoxLayout" name="horizontalLayout_6" stretch="0,0"> <layout class="QHBoxLayout" name="horizontalLayout_6" stretch="0,0">
<item> <item>
@ -287,6 +296,9 @@
<string>Paths &amp;&amp; Links</string> <string>Paths &amp;&amp; Links</string>
</property> </property>
<layout class="QGridLayout" name="gridLayout"> <layout class="QGridLayout" name="gridLayout">
<property name="verticalSpacing">
<number>4</number>
</property>
<item row="0" column="0"> <item row="0" column="0">
<widget class="QLabel" name="labelLogsPath"> <widget class="QLabel" name="labelLogsPath">
<property name="text"> <property name="text">
@ -370,7 +382,7 @@
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QGroupBox" name="groupCache_2"> <widget class="QGroupBox" name="groupCert">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>0</width> <width>0</width>
@ -380,34 +392,84 @@
<property name="title"> <property name="title">
<string>TLS Certficates</string> <string>TLS Certficates</string>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_9"> <layout class="QGridLayout" name="gridLayout_4" columnstretch="1,1">
<item> <property name="verticalSpacing">
<widget class="QLabel" name="labeLastTLSCertsExport"> <number>4</number>
</property>
<item row="0" column="0">
<widget class="QCheckBox" name="checkTLSCertIsInstalled">
<property name="text"> <property name="text">
<string>Last Export: Never</string> <string>Certificate is installed</string>
</property>
<property name="checked">
<bool>false</bool>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item row="0" column="1">
<widget class="QCheckBox" name="checkTLSCertExportWillSucceed"> <widget class="QCheckBox" name="checkTLSCertExportWillSucceed">
<property name="text"> <property name="text">
<string>TLS certificate export will succeed</string> <string>Certificate export will succeed</string>
</property> </property>
<property name="checked"> <property name="checked">
<bool>true</bool> <bool>true</bool>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item row="1" column="1">
<widget class="QCheckBox" name="checkTLSKeyExportWillSucceed"> <widget class="QCheckBox" name="checkTLSKeyExportWillSucceed">
<property name="text"> <property name="text">
<string>TLS private key export will succeed</string> <string>Key export will succeed</string>
</property> </property>
<property name="checked"> <property name="checked">
<bool>true</bool> <bool>true</bool>
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="1">
<widget class="QLabel" name="labeLastTLSCertExport">
<property name="text">
<string>Last Export: never</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="labelLastTLSCertInstall">
<property name="text">
<string>Last install: never</string>
</property>
</widget>
</item>
<item row="1" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_14" stretch="0,1">
<item>
<widget class="QLabel" name="labelNextInstall">
<property name="text">
<string>Next install will</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboNextTLSCertInstallResult">
<item>
<property name="text">
<string>Succeed</string>
</property>
</item>
<item>
<property name="text">
<string>Be Canceled</string>
</property>
</item>
<item>
<property name="text">
<string>Fail</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>
@ -440,6 +502,9 @@
<string>Status</string> <string>Status</string>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_3"> <layout class="QVBoxLayout" name="verticalLayout_3">
<property name="spacing">
<number>5</number>
</property>
<item> <item>
<layout class="QHBoxLayout" name="horizontalLayout_2"> <layout class="QHBoxLayout" name="horizontalLayout_2">
<item> <item>
@ -549,8 +614,8 @@
<string>Bug Report</string> <string>Bug Report</string>
</property> </property>
<layout class="QGridLayout" name="gridLayout_3"> <layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="3"> <item row="1" column="1">
<widget class="QLineEdit" name="editOSVersion"> <widget class="QLineEdit" name="editEmailClient">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>0</width> <width>0</width>
@ -563,6 +628,37 @@
<height>0</height> <height>0</height>
</size> </size>
</property> </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"> <property name="readOnly">
<bool>true</bool> <bool>true</bool>
</property> </property>
@ -581,13 +677,6 @@
</property> </property>
</widget> </widget>
</item> </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"> <item row="3" column="0">
<widget class="QLabel" name="labelIncludeLogs"> <widget class="QLabel" name="labelIncludeLogs">
<property name="text"> <property name="text">
@ -595,22 +684,18 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="2"> <item row="2" column="0">
<widget class="QLabel" name="labelAddress"> <widget class="QLabel" name="labelDescription">
<property name="text"> <property name="text">
<string>Address</string> <string>Description</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="2"> <item row="0" column="3">
<widget class="QLabel" name="labelOSVersion"> <widget class="QLineEdit" name="editOSVersion">
<property name="text">
<string>OS Version</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="editOSType">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>0</width> <width>0</width>
@ -635,43 +720,29 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="0"> <item row="0" column="1">
<widget class="QLabel" name="labelDescription"> <widget class="QLineEdit" name="editOSType">
<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">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>0</width> <width>0</width>
<height>0</height> <height>0</height>
</size> </size>
</property> </property>
<property name="text"> <property name="baseSize">
<string/> <size>
<width>250</width>
<height>0</height>
</size>
</property> </property>
<property name="readOnly"> <property name="readOnly">
<bool>true</bool> <bool>true</bool>
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="0"> <item row="0" column="2">
<widget class="QLabel" name="labelEmailClient"> <widget class="QLabel" name="labelOSVersion">
<property name="text"> <property name="text">
<string>Email Cient</string> <string>OS Version</string>
</property>
</widget>
</item>
<item row="2" column="1" colspan="4">
<widget class="QPlainTextEdit" name="editDescription">
<property name="readOnly">
<bool>true</bool>
</property> </property>
</widget> </widget>
</item> </item>
@ -690,6 +761,9 @@
<string>Events &amp;&amp; Errors</string> <string>Events &amp;&amp; Errors</string>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_8"> <layout class="QVBoxLayout" name="verticalLayout_8">
<property name="spacing">
<number>4</number>
</property>
<item> <item>
<layout class="QHBoxLayout" name="horizontalLayout_11"> <layout class="QHBoxLayout" name="horizontalLayout_11">
<item> <item>
@ -707,6 +781,9 @@
<property name="suffix"> <property name="suffix">
<string> ms</string> <string> ms</string>
</property> </property>
<property name="minimum">
<number>-1</number>
</property>
<property name="maximum"> <property name="maximum">
<number>3600000</number> <number>3600000</number>
</property> </property>
@ -714,7 +791,7 @@
<number>100</number> <number>100</number>
</property> </property>
<property name="value"> <property name="value">
<number>1000</number> <number>0</number>
</property> </property>
</widget> </widget>
</item> </item>
@ -734,51 +811,141 @@
</layout> </layout>
</item> </item>
<item> <item>
<layout class="QHBoxLayout" name="horizontalLayout_9"> <layout class="QGridLayout" name="gridLayout_5">
<item> <property name="horizontalSpacing">
<widget class="QPushButton" name="buttonInternetOff"> <number>-1</number>
<property name="text"> </property>
<string>Internet Off</string> <property name="verticalSpacing">
</property> <number>8</number>
</widget> </property>
</item> <item row="0" column="1">
<item>
<widget class="QPushButton" name="buttonInternetOn"> <widget class="QPushButton" name="buttonInternetOn">
<property name="text"> <property name="text">
<string>Internet On</string> <string>Internet On</string>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item row="0" column="2">
<widget class="QPushButton" name="buttonShowMainWindow"> <widget class="QPushButton" name="buttonShowMainWindow">
<property name="text"> <property name="text">
<string>Show Main Window</string> <string>Show Main Window</string>
</property> </property>
</widget> </widget>
</item> </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"> <widget class="QPushButton" name="buttonAPICertIssue">
<property name="text"> <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> </property>
</widget> </widget>
</item> </item>
</layout> </layout>
</item> </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> <item>
<layout class="QHBoxLayout" name="horizontalLayout_13"> <layout class="QHBoxLayout" name="horizontalLayout_13">
<item> <item>
<widget class="QPushButton" name="buttonDiskCacheUnavailable"> <widget class="QLabel" name="labelNextBugReportResult">
<property name="text"> <property name="text">
<string>Disk Cache Unavailable</string> <string>Next bug report result</string>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QPushButton" name="buttonDiskFull"> <widget class="QComboBox" name="comboBugReportResult">
<property name="text"> <item>
<string>Disk Full</string> <property name="text">
</property> <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> </widget>
</item> </item>
<item> <item>
@ -797,52 +964,96 @@
</layout> </layout>
</item> </item>
<item> <item>
<layout class="QHBoxLayout" name="horizontalLayout_12"> <layout class="QGridLayout" name="gridLayout_6">
<item> <property name="verticalSpacing">
<widget class="QPushButton" name="buttonNoActiveKeyForRecipient"> <number>8</number>
</property>
<item row="0" column="0">
<widget class="QPushButton" name="buttonUpdateManualReady">
<property name="text"> <property name="text">
<string>No Active Key For Recipient</string> <string>Update Manual Ready</string>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item row="0" column="2">
<widget class="QLineEdit" name="editNoActiveKeyForRecipient"> <widget class="QLineEdit" name="editUpdateVersion">
<property name="text"> <property name="text">
<string>dummy.user@proton.me</string> <string>4.0</string>
</property> </property>
</widget> </widget>
</item> </item>
</layout> <item row="4" column="0">
</item> <widget class="QPushButton" name="buttonUpdateVersionChanged">
<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">
<property name="text"> <property name="text">
<string>Next Cache Change will succeed</string> <string>Update version changed</string>
</property> </property>
</widget> </widget>
</item> </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"> <spacer name="horizontalSpacer_6">
<property name="orientation"> <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Horizontal</enum>
@ -855,34 +1066,8 @@
</property> </property>
</spacer> </spacer>
</item> </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> </layout>
</item> </item>
<item>
<widget class="QCheckBox" name="checkNextBugReportWillSucceed">
<property name="text">
<string>Next Bug Report Will Succeed</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>
@ -909,19 +1094,10 @@
<tabstop>editDependencyLicenseLink</tabstop> <tabstop>editDependencyLicenseLink</tabstop>
<tabstop>editLandingPageLink</tabstop> <tabstop>editLandingPageLink</tabstop>
<tabstop>editDiskCachePath</tabstop> <tabstop>editDiskCachePath</tabstop>
<tabstop>editOSType</tabstop>
<tabstop>editOSVersion</tabstop> <tabstop>editOSVersion</tabstop>
<tabstop>editEmailClient</tabstop> <tabstop>editEmailClient</tabstop>
<tabstop>editAddress</tabstop>
<tabstop>editDescription</tabstop>
<tabstop>spinEventDelay</tabstop> <tabstop>spinEventDelay</tabstop>
<tabstop>buttonInternetOff</tabstop>
<tabstop>buttonInternetOn</tabstop>
<tabstop>buttonShowMainWindow</tabstop>
<tabstop>checkIsPortFree</tabstop> <tabstop>checkIsPortFree</tabstop>
<tabstop>checkNextCacheChangeWillSucceed</tabstop>
<tabstop>comboCacheError</tabstop>
<tabstop>checkNextBugReportWillSucceed</tabstop>
</tabstops> </tabstops>
<resources/> <resources/>
<connections/> <connections/>

View File

@ -58,7 +58,7 @@ UsersTab::UsersTab(QWidget *parent)
connect(ui_.checkSync, &QCheckBox::toggled, this, &UsersTab::onCheckSyncToggled); connect(ui_.checkSync, &QCheckBox::toggled, this, &UsersTab::onCheckSyncToggled);
connect(ui_.sliderSync, &QSlider::valueChanged, this, &UsersTab::onSliderSyncValueChanged); connect(ui_.sliderSync, &QSlider::valueChanged, this, &UsersTab::onSliderSyncValueChanged);
users_.append(randomUser()); users_.append(defaultUser());
this->updateGUIState(); this->updateGUIState();
} }

View File

@ -40,7 +40,8 @@ using namespace bridgepp;
namespace { namespace {
QString const bugReportFile = ":qml/Resources/bug_report_flow.json"; QString const bugReportFile = ":qml/Resources/bug_report_flow.json";
QString const bridgeKBUrl = "https://proton.me/support/bridge"; ///< The URL for the root of the bridge knowledge base.
} }
@ -278,6 +279,30 @@ void QMLBackend::clearAnswers() {
} }
//****************************************************************************************************************************************************
/// \return true iff the Bridge TLS certificate is installed.
//****************************************************************************************************************************************************
bool QMLBackend::isTLSCertificateInstalled() {
HANDLE_EXCEPTION_RETURN_BOOL(
bool v = false;
app().grpc().isTLSCertificateInstalled(v);
return v;
)
}
//****************************************************************************************************************************************************
/// \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::openExternalLink(QString const &url) {
HANDLE_EXCEPTION(
QString const u = url.isEmpty() ? bridgeKBUrl : url;
QDesktopServices::openUrl(u);
emit notifyExternalLinkClicked(u);
)
}
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \return The value for the 'showOnStartup' property. /// \return The value for the 'showOnStartup' property.
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
@ -941,6 +966,15 @@ void QMLBackend::reportBug(QString const &category, QString const &description,
} }
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void QMLBackend::installTLSCertificate() {
HANDLE_EXCEPTION(
app().grpc().installTLSCertificate();
)
}
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
// //
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
@ -1028,9 +1062,9 @@ void QMLBackend::notifyAutoconfigClicked(QString const &client) const {
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \param[in] article The url of the KB article. /// \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( HANDLE_EXCEPTION(
app().grpc().KBArticleClicked(article); app().grpc().externalLinkClicked(article);
) )
} }
@ -1267,12 +1301,13 @@ void QMLBackend::connectGrpcEvents() {
connect(client, &GRPCClient::reportBugSuccess, this, &QMLBackend::bugReportSendSuccess); connect(client, &GRPCClient::reportBugSuccess, this, &QMLBackend::bugReportSendSuccess);
connect(client, &GRPCClient::reportBugFallback, this, &QMLBackend::bugReportSendFallback); connect(client, &GRPCClient::reportBugFallback, this, &QMLBackend::bugReportSendFallback);
connect(client, &GRPCClient::reportBugError, this, &QMLBackend::bugReportSendError); connect(client, &GRPCClient::reportBugError, this, &QMLBackend::bugReportSendError);
connect(client, &GRPCClient::certificateInstallSuccess, this, &QMLBackend::certificateInstallSuccess);
connect(client, &GRPCClient::certificateInstallCanceled, this, &QMLBackend::certificateInstallCanceled);
connect(client, &GRPCClient::certificateInstallFailed, this, &QMLBackend::certificateInstallFailed);
connect(client, &GRPCClient::showMainWindow, [&]() { this->showMainWindow("gRPC showMainWindow event"); }); connect(client, &GRPCClient::showMainWindow, [&]() { this->showMainWindow("gRPC showMainWindow event"); });
// cache events // cache events
connect(client, &GRPCClient::diskCacheUnavailable, this, &QMLBackend::diskCacheUnavailable);
connect(client, &GRPCClient::cantMoveDiskCache, this, &QMLBackend::cantMoveDiskCache); connect(client, &GRPCClient::cantMoveDiskCache, this, &QMLBackend::cantMoveDiskCache);
connect(client, &GRPCClient::diskFull, this, &QMLBackend::diskFull);
connect(client, &GRPCClient::diskCachePathChanged, this, &QMLBackend::diskCachePathChanged); connect(client, &GRPCClient::diskCachePathChanged, this, &QMLBackend::diskCachePathChanged);
connect(client, &GRPCClient::diskCachePathChangeFinished, this, &QMLBackend::diskCachePathChangeFinished); connect(client, &GRPCClient::diskCachePathChangeFinished, this, &QMLBackend::diskCachePathChangeFinished);
@ -1317,7 +1352,6 @@ void QMLBackend::connectGrpcEvents() {
connect(client, &GRPCClient::rebuildKeychain, this, &QMLBackend::notifyRebuildKeychain); connect(client, &GRPCClient::rebuildKeychain, this, &QMLBackend::notifyRebuildKeychain);
// mail events // mail events
connect(client, &GRPCClient::noActiveKeyForRecipient, this, &QMLBackend::noActiveKeyForRecipient);
connect(client, &GRPCClient::addressChanged, this, &QMLBackend::addressChanged); connect(client, &GRPCClient::addressChanged, this, &QMLBackend::addressChanged);
connect(client, &GRPCClient::addressChangedLogout, this, &QMLBackend::addressChangedLogout); connect(client, &GRPCClient::addressChangedLogout, this, &QMLBackend::addressChangedLogout);
connect(client, &GRPCClient::apiCertIssue, this, &QMLBackend::apiCertIssue); connect(client, &GRPCClient::apiCertIssue, this, &QMLBackend::apiCertIssue);

View File

@ -64,6 +64,8 @@ public: // member functions.
Q_INVOKABLE QString getQuestionAnswer(quint8 questionId) const; ///< Get the answer for a given question. Q_INVOKABLE QString getQuestionAnswer(quint8 questionId) const; ///< Get the answer for a given question.
Q_INVOKABLE QString collectAnswers(quint8 categoryId) const; ///< Collect answer for a given set of questions. 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 void clearAnswers(); ///< Clear all collected answers.
Q_INVOKABLE bool isTLSCertificateInstalled(); ///< Check if the bridge certificate is installed in the OS keychain.
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) 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) Q_PROPERTY(bool showOnStartup READ showOnStartup NOTIFY showOnStartupChanged)
@ -195,6 +197,7 @@ public slots: // slot for signals received from QML -> To be forwarded to Bridge
void installUpdate() const; ///< Slot for the update install. void installUpdate() const; ///< Slot for the update install.
void triggerReset() const; ///< Slot for the triggering of reset. void triggerReset() const; ///< Slot for the triggering of reset.
void reportBug(QString const &category, QString const &description, QString const &address, QString const &emailClient, bool includeLogs) const; ///< Slot for the bug report. void reportBug(QString const &category, QString const &description, QString const &address, QString const &emailClient, bool includeLogs) const; ///< Slot for the bug report.
void installTLSCertificate(); ///< Installs the Bridge TLS certificate in the Keychain.
void exportTLSCertificates() const; ///< Slot for the export of the TLS certificates. void exportTLSCertificates() const; ///< Slot for the export of the TLS certificates.
void onResetFinished(); ///< Slot for the reset finish signal. void onResetFinished(); ///< Slot for the reset finish signal.
void onVersionChanged(); ///< Slot for the version change signal. void onVersionChanged(); ///< Slot for the version change signal.
@ -202,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 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 notifyReportBugClicked() const; ///< Slot for the ReportBugClicked gRPC event.
void notifyAutoconfigClicked(QString const &client) const; ///< Slot for gAutoconfigClicked gRPC event. void notifyAutoconfigClicked(QString const &client) const; ///< Slot for gAutoconfigClicked gRPC event.
void 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. public slots: // slots for functions that need to be processed locally.
void setNormalTrayIcon(); ///< Set the tray icon to normal. void setNormalTrayIcon(); ///< Set the tray icon to normal.
@ -221,17 +224,15 @@ public slots: // slot for signals received from gRPC that need transformation in
signals: // Signals received from the Go backend, to be forwarded to QML signals: // Signals received from the Go backend, to be forwarded to QML
void toggleAutostartFinished(); ///< Signal for the 'toggleAutostartFinished' gRPC stream event. 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 cantMoveDiskCache(); ///< Signal for the 'cantMoveDiskCache' gRPC stream event.
void diskCachePathChangeFinished(); ///< Signal for the 'diskCachePathChangeFinished' 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 loginUsernamePasswordError(QString const &errorMsg); ///< Signal for the 'loginUsernamePasswordError' gRPC stream event.
void loginFreeUserError(); ///< Signal for the 'loginFreeUserError' gRPC stream event. void loginFreeUserError(); ///< Signal for the 'loginFreeUserError' gRPC stream event.
void loginConnectionError(QString const &errorMsg); ///< Signal for the 'loginConnectionError' gRPC stream event. void loginConnectionError(QString const &errorMsg); ///< Signal for the 'loginConnectionError' gRPC stream event.
void login2FARequested(QString const &username); ///< Signal for the 'login2FARequested' gRPC stream event. void login2FARequested(QString const &username); ///< Signal for the 'login2FARequested' gRPC stream event.
void login2FAError(QString const &errorMsg); ///< Signal for the 'login2FAError' gRPC stream event. void login2FAError(QString const &errorMsg); ///< Signal for the 'login2FAError' gRPC stream event.
void login2FAErrorAbort(QString const &errorMsg); ///< Signal for the 'login2FAErrorAbort' gRPC stream event. void login2FAErrorAbort(QString const &errorMsg); ///< Signal for the 'login2FAErrorAbort' gRPC stream event.
void login2PasswordRequested(); ///< Signal for the 'login2PasswordRequested' gRPC stream event. void login2PasswordRequested(QString const &username); ///< Signal for the 'login2PasswordRequested' gRPC stream event.
void login2PasswordError(QString const &errorMsg); ///< Signal for the 'login2PasswordError' gRPC stream event. void login2PasswordError(QString const &errorMsg); ///< Signal for the 'login2PasswordError' gRPC stream event.
void login2PasswordErrorAbort(QString const &errorMsg); ///< Signal for the 'login2PasswordErrorAbort' gRPC stream event. void login2PasswordErrorAbort(QString const &errorMsg); ///< Signal for the 'login2PasswordErrorAbort' gRPC stream event.
void loginFinished(int index, bool wasSignedOut); ///< Signal for the 'loginFinished' gRPC stream event. void loginFinished(int index, bool wasSignedOut); ///< Signal for the 'loginFinished' gRPC stream event.
@ -255,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 changeKeychainFinished(); ///< Signal for the 'changeKeychainFinished' gRPC stream event.
void notifyHasNoKeychain(); ///< Signal for the 'notifyHasNoKeychain' gRPC stream event. void notifyHasNoKeychain(); ///< Signal for the 'notifyHasNoKeychain' gRPC stream event.
void notifyRebuildKeychain(); ///< Signal for the 'notifyRebuildKeychain' 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 addressChanged(QString const &address); ///< Signal for the 'addressChanged' gRPC stream event.
void addressChangedLogout(QString const &address); ///< Signal for the 'addressChangedLogout' gRPC stream event. void addressChangedLogout(QString const &address); ///< Signal for the 'addressChangedLogout' gRPC stream event.
void apiCertIssue(); ///< Signal for the 'apiCertIssue' gRPC stream event. void apiCertIssue(); ///< Signal for the 'apiCertIssue' gRPC stream event.
@ -268,10 +268,13 @@ signals: // Signals received from the Go backend, to be forwarded to QML
void bugReportSendSuccess(); ///< Signal for the 'bugReportSendSuccess' gRPC stream event. void bugReportSendSuccess(); ///< Signal for the 'bugReportSendSuccess' gRPC stream event.
void bugReportSendFallback(); ///< Signal for the 'bugReportSendFallback' gRPC stream event. void bugReportSendFallback(); ///< Signal for the 'bugReportSendFallback' gRPC stream event.
void bugReportSendError(); ///< Signal for the 'bugReportSendError' gRPC stream event. void bugReportSendError(); ///< Signal for the 'bugReportSendError' gRPC stream event.
void certificateInstallSuccess(); ///< Signal for the 'certificateInstallSuccess' gRPC stream event.
void certificateInstallCanceled(); ///< Signal for the 'certificateInstallCanceled' gRPC stream event.
void certificateInstallFailed(); /// Signal for the 'certificateInstallFailed' gRPC stream event.
void showMainWindow(); ///< Signal for the 'showMainWindow' gRPC stream event. void showMainWindow(); ///< Signal for the 'showMainWindow' gRPC stream event.
void hideMainWindow(); ///< Signal for the 'hideMainWindow' gRPC stream event. void hideMainWindow(); ///< Signal for the 'hideMainWindow' gRPC stream event.
void showHelp(); ///< Signal for the 'showHelp' event (from the context menu). void showHelp(); ///< Signal for the 'showHelp' event (from the context menu).
void showSettings(); ///< Signal for the 'showHelp' event (from the context menu). void showSettings(); ///< Signal for the 'showSettings' event (from the context menu).
void selectUser(QString const& userID, bool forceShowWindow); ///< Signal emitted in order to selected a user with a given ID in the list. void selectUser(QString const& userID, bool forceShowWindow); ///< Signal emitted in order to selected a user with a given ID in the list.
void genericError(QString const &title, QString const &description); ///< Signal for the 'genericError' gRPC stream event. void genericError(QString const &title, QString const &description); ///< Signal for the 'genericError' gRPC stream event.
void imapLoginWhileSignedOut(QString const& username); ///< Signal for the notification of IMAP login attempt on a signed out account. void imapLoginWhileSignedOut(QString const& username); ///< Signal for the notification of IMAP login attempt on a signed out account.

View File

@ -5,7 +5,6 @@
<file>qml/AccountView.qml</file> <file>qml/AccountView.qml</file>
<file>qml/Banner.qml</file> <file>qml/Banner.qml</file>
<file>qml/Bridge.qml</file> <file>qml/Bridge.qml</file>
<file>qml/bridgeqml.qmlproject</file>
<file>qml/BugCategoryView.qml</file> <file>qml/BugCategoryView.qml</file>
<file>qml/BugQuestionView.qml</file> <file>qml/BugQuestionView.qml</file>
<file>qml/BugReportFlow.qml</file> <file>qml/BugReportFlow.qml</file>
@ -20,9 +19,11 @@
<file>qml/icons/ic-alert.svg</file> <file>qml/icons/ic-alert.svg</file>
<file>qml/icons/ic-apple-mail.svg</file> <file>qml/icons/ic-apple-mail.svg</file>
<file>qml/icons/ic-arrow-left.svg</file> <file>qml/icons/ic-arrow-left.svg</file>
<file>qml/icons/ic-bridge.svg</file>
<file>qml/icons/ic-card-identity.svg</file> <file>qml/icons/ic-card-identity.svg</file>
<file>qml/icons/ic-check.svg</file> <file>qml/icons/ic-check.svg</file>
<file>qml/icons/ic-chevron-down.svg</file> <file>qml/icons/ic-chevron-down.svg</file>
<file>qml/icons/ic-chevron-left.svg</file>
<file>qml/icons/ic-chevron-right.svg</file> <file>qml/icons/ic-chevron-right.svg</file>
<file>qml/icons/ic-chevron-up.svg</file> <file>qml/icons/ic-chevron-up.svg</file>
<file>qml/icons/ic-cog-wheel.svg</file> <file>qml/icons/ic-cog-wheel.svg</file>
@ -49,13 +50,18 @@
<file>qml/icons/ic-success.svg</file> <file>qml/icons/ic-success.svg</file>
<file>qml/icons/ic-three-dots-vertical.svg</file> <file>qml/icons/ic-three-dots-vertical.svg</file>
<file>qml/icons/ic-trash.svg</file> <file>qml/icons/ic-trash.svg</file>
<file>qml/icons/ic-warning-orange.svg</file>
<file>qml/icons/img-client-config-selector.svg</file>
<file>qml/icons/img-client-config-success.svg</file>
<file>qml/icons/img-macos-cert-screenshot.png</file>
<file>qml/icons/img-macos-profile-screenshot.png</file>
<file>qml/icons/img-mail-clients.svg</file>
<file>qml/icons/img-mail-logo-wordmark-dark.svg</file>
<file>qml/icons/img-mail-logo-wordmark.svg</file>
<file>qml/icons/img-proton-logos.png</file> <file>qml/icons/img-proton-logos.png</file>
<file>qml/icons/img-proton-logos.svg</file> <file>qml/icons/img-proton-logos.svg</file>
<file>qml/icons/img-splash.png</file> <file>qml/icons/img-splash.png</file>
<file>qml/icons/img-splash.svg</file> <file>qml/icons/img-splash.svg</file>
<file>qml/icons/img-welcome-dark.png</file>
<file>qml/icons/img-welcome-dark.svg</file>
<file>qml/icons/img-welcome.png</file>
<file>qml/icons/img-welcome.svg</file> <file>qml/icons/img-welcome.svg</file>
<file>qml/icons/Loader_16.svg</file> <file>qml/icons/Loader_16.svg</file>
<file>qml/icons/Loader_48.svg</file> <file>qml/icons/Loader_48.svg</file>
@ -75,6 +81,7 @@
<file>qml/KeychainSettings.qml</file> <file>qml/KeychainSettings.qml</file>
<file>qml/LocalCacheSettings.qml</file> <file>qml/LocalCacheSettings.qml</file>
<file>qml/MainWindow.qml</file> <file>qml/MainWindow.qml</file>
<file>qml/NoAccountView.qml</file>
<file>qml/NotificationDialog.qml</file> <file>qml/NotificationDialog.qml</file>
<file>qml/NotificationPopups.qml</file> <file>qml/NotificationPopups.qml</file>
<file>qml/Notifications/Notification.qml</file> <file>qml/Notifications/Notification.qml</file>
@ -90,6 +97,7 @@
<file>qml/Proton/ComboBox.qml</file> <file>qml/Proton/ComboBox.qml</file>
<file>qml/Proton/Dialog.qml</file> <file>qml/Proton/Dialog.qml</file>
<file>qml/Proton/Label.qml</file> <file>qml/Proton/Label.qml</file>
<file>qml/Proton/LinkLabel.qml</file>
<file>qml/Proton/Menu.qml</file> <file>qml/Proton/Menu.qml</file>
<file>qml/Proton/MenuItem.qml</file> <file>qml/Proton/MenuItem.qml</file>
<file>qml/Proton/Popup.qml</file> <file>qml/Proton/Popup.qml</file>
@ -101,14 +109,26 @@
<file>qml/Proton/TextField.qml</file> <file>qml/Proton/TextField.qml</file>
<file>qml/Proton/Toggle.qml</file> <file>qml/Proton/Toggle.qml</file>
<file>qml/QuestionItem.qml</file> <file>qml/QuestionItem.qml</file>
<file>qml/Resources/bug_report_flow.json</file> <file>qml/Resources/bug_report_flow.json</file>
<file>qml/Resources/Help/Template.html</file>
<file>qml/Resources/Help/WhyBridge.html</file>
<file>qml/Resources/Help/WhyCertificate.html</file>
<file>qml/Resources/Help/WhyProfileWarning.html</file>
<file>qml/SettingsItem.qml</file> <file>qml/SettingsItem.qml</file>
<file>qml/SettingsView.qml</file> <file>qml/SettingsView.qml</file>
<file>qml/SetupGuide.qml</file> <file>qml/SetupWizard/ClientListItem.qml</file>
<file>qml/SignIn.qml</file> <file>qml/SetupWizard/LeftPane.qml</file>
<file>qml/SetupWizard/ClientConfigAppleMail.qml</file>
<file>qml/SetupWizard/ClientConfigEnd.qml</file>
<file>qml/SetupWizard/ClientConfigParameters.qml</file>
<file>qml/SetupWizard/ClientConfigSelector.qml</file>
<file>qml/SetupWizard/HelpButton.qml</file>
<file>qml/SetupWizard/SetupWizard.qml</file>
<file>qml/SetupWizard/Login.qml</file>
<file>qml/SetupWizard/Onboarding.qml</file>
<file>qml/SetupWizard/StepDescriptionBox.qml</file>
<file>qml/ConnectionModeSettings.qml</file> <file>qml/ConnectionModeSettings.qml</file>
<file>qml/SplashScreen.qml</file> <file>qml/SplashScreen.qml</file>
<file>qml/Status.qml</file> <file>qml/Status.qml</file>
<file>qml/WelcomeGuide.qml</file>
</qresource> </qresource>
</RCC> </RCC>

View File

@ -262,7 +262,7 @@ void UserList::onUsedBytesChanged(QString const &userID, qint64 usedBytes) {
void UserList::onSyncStarted(QString const &userID) { void UserList::onSyncStarted(QString const &userID) {
int const index = this->rowOfUserID(userID); int const index = this->rowOfUserID(userID);
if (index < 0) { if (index < 0) {
app().log().error(QString("Received onSyncStarted event for unknown userID %1").arg(userID)); app().log().error(QString("Received syncStarted event for unknown userID %1").arg(userID));
return; return;
} }
users_[index]->setIsSyncing(true); users_[index]->setIsSyncing(true);
@ -275,7 +275,7 @@ void UserList::onSyncStarted(QString const &userID) {
void UserList::onSyncFinished(QString const &userID) { void UserList::onSyncFinished(QString const &userID) {
int const index = this->rowOfUserID(userID); int const index = this->rowOfUserID(userID);
if (index < 0) { if (index < 0) {
app().log().error(QString("Received onSyncFinished event for unknown userID %1").arg(userID)); app().log().error(QString("Received syncFinished event for unknown userID %1").arg(userID));
return; return;
} }
users_[index]->setIsSyncing(false); users_[index]->setIsSyncing(false);
@ -293,7 +293,7 @@ void UserList::onSyncProgress(QString const &userID, double progress, float elap
Q_UNUSED(remainingMs) Q_UNUSED(remainingMs)
int const index = this->rowOfUserID(userID); int const index = this->rowOfUserID(userID);
if (index < 0) { if (index < 0) {
app().log().error(QString("Received onSyncFinished event for unknown userID %1").arg(userID)); app().log().error(QString("Received syncProgress event for unknown userID %1").arg(userID));
return; return;
} }
users_[index]->setSyncProgress(progress); users_[index]->setSyncProgress(progress);

View File

@ -415,7 +415,11 @@ int main(int argc, char *argv[]) {
} }
catch (Exception const &e) { catch (Exception const &e) {
sentry_uuid_s const uuid = reportSentryException("Exception occurred during main", e); sentry_uuid_s const uuid = reportSentryException("Exception occurred during main", e);
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"; QTextStream(stderr) << "reportID: " << QByteArray(uuid.bytes, 16).toHex() << " Captured exception :" << e.detailedWhat() << "\n";
return EXIT_FAILURE; return EXIT_FAILURE;
} }

View File

@ -22,7 +22,7 @@ Item {
LargeView LargeView
} }
property var _spacing: 12 * ProtonStyle.px property var _spacing: 12
property ColorScheme colorScheme property ColorScheme colorScheme
property color progressColor: { property color progressColor: {
if (!root.enabled) if (!root.enabled)
@ -154,7 +154,7 @@ Item {
} }
} }
Item { Item {
implicitHeight: root.type === AccountDelegate.LargeView ? 6 * ProtonStyle.px : 0 implicitHeight: root.type === AccountDelegate.LargeView ? 6 : 0
} }
RowLayout { RowLayout {
spacing: 0 spacing: 0
@ -222,15 +222,15 @@ Item {
} }
} }
Item { Item {
implicitHeight: root.type === AccountDelegate.LargeView ? 3 * ProtonStyle.px : 0 implicitHeight: root.type === AccountDelegate.LargeView ? 3 : 0
} }
Rectangle { Rectangle {
id: progress_bar id: progress_bar
color: root.colorScheme.border_weak color: root.colorScheme.border_weak
height: 4 * ProtonStyle.px height: 4
radius: ProtonStyle.progress_bar_radius radius: ProtonStyle.progress_bar_radius
visible: root.user ? root.type === AccountDelegate.LargeView : false visible: root.user ? root.type === AccountDelegate.LargeView : false
width: 140 * ProtonStyle.px width: 140
Rectangle { Rectangle {
id: progress_bar_filled id: progress_bar_filled

View File

@ -23,13 +23,14 @@ Item {
property int _detailsMargin: 25 property int _detailsMargin: 25
property int _lineThickness: 1 property int _lineThickness: 1
property int _spacing: 20 property int _spacing: 20
property int _buttonSpacing: 8
property int _topMargin: 32 property int _topMargin: 32
property ColorScheme colorScheme property ColorScheme colorScheme
property var notifications property var notifications
property var user property var user
signal showSetupGuide(var user, string address) signal showClientConfigurator(var user, string address, bool justLoggedIn)
signal showSignIn signal showLogin(var username)
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
@ -63,7 +64,7 @@ Item {
// account delegate with action buttons // account delegate with action buttons
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: _topMargin Layout.topMargin: _topMargin
spacing: _buttonSpacing
AccountDelegate { AccountDelegate {
Layout.fillWidth: true Layout.fillWidth: true
colorScheme: root.colorScheme colorScheme: root.colorScheme
@ -92,9 +93,9 @@ Item {
visible: root.user ? (root.user.state === EUserState.SignedOut) : false visible: root.user ? (root.user.state === EUserState.SignedOut) : false
onClicked: { onClicked: {
if (!root.user) if (user) {
return; root.showLogin(user.primaryEmailOrUsername());
root.showSignIn(); }
} }
} }
Button { Button {
@ -118,18 +119,18 @@ Item {
} }
SettingsItem { SettingsItem {
Layout.fillWidth: true Layout.fillWidth: true
actionText: qsTr("Configure") actionText: qsTr("Configure email client")
colorScheme: root.colorScheme colorScheme: root.colorScheme
description: qsTr("Using the mailbox details below (re)configure your client.") description: qsTr("Using the mailbox details below (re)configure your client.")
showSeparator: splitMode.visible showSeparator: splitMode.visible
text: qsTr("Email clients") text: qsTr("Email clients")
type: SettingsItem.Button type: SettingsItem.PrimaryButton
visible: _connected && (!root.user.splitMode) || (root.user.addresses.length === 1) visible: _connected && ((!root.user.splitMode) || (root.user.addresses.length === 1))
onClicked: { onClicked: {
if (!root.user) if (!root.user)
return; return;
root.showSetupGuide(root.user, user.addresses[0]); root.showClientConfigurator(root.user, user.addresses[0], false);
} }
} }
SettingsItem { SettingsItem {
@ -165,13 +166,13 @@ Item {
} }
Button { Button {
colorScheme: root.colorScheme colorScheme: root.colorScheme
secondary: true secondary: false
text: qsTr("Configure") text: qsTr("Configure email client")
onClicked: { onClicked: {
if (!root.user) if (!root.user)
return; return;
root.showSetupGuide(root.user, addressSelector.displayText); root.showClientConfigurator(root.user, addressSelector.displayText, false);
} }
} }
} }

View File

@ -84,6 +84,7 @@ Popup {
anchors.topMargin: 14 anchors.topMargin: 14
spacing: 8 spacing: 8
ColorImage { ColorImage {
Layout.preferredHeight: 24 Layout.preferredHeight: 24
Layout.preferredWidth: 24 Layout.preferredWidth: 24
@ -108,14 +109,29 @@ Popup {
sourceSize.width: 24 sourceSize.width: 24
width: 24 width: 24
} }
Label { ColumnLayout {
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignTop
Layout.fillWidth: true Layout.fillWidth: true
Layout.leftMargin: 16 Layout.leftMargin: 16
color: root.colorScheme.text_invert Label {
colorScheme: root.colorScheme id: messageLabel
text: root.notification ? root.notification.description : "" Layout.alignment: Qt.AlignTop
wrapMode: Text.WordWrap 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,13 +34,9 @@ QtObject {
function onColorSchemeNameChanged(scheme) { function onColorSchemeNameChanged(scheme) {
root.setColorScheme(); root.setColorScheme();
} }
function onDiskCacheUnavailable() {
mainWindow.showAndRise();
}
function onHideMainWindow() { function onHideMainWindow() {
mainWindow.hide(); mainWindow.hide();
} }
target: Backend target: Backend
} }
} }

View File

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

View File

@ -21,6 +21,7 @@ Rectangle {
property int _margin: 24 property int _margin: 24
property ColorScheme colorScheme property ColorScheme colorScheme
property bool highlightPassword
property string hostname property string hostname
property string password property string password
property string port property string port
@ -68,7 +69,8 @@ Rectangle {
} }
ConfigurationItem { ConfigurationItem {
colorScheme: root.colorScheme colorScheme: root.colorScheme
label: qsTr("Password") label: highlightPassword ? qsTr("Use this password") : qsTr("Password")
labelColor: highlightPassword ? colorScheme.signal_warning_active : colorScheme.text_norm
value: root.password value: root.password
} }
ConfigurationItem { ConfigurationItem {

View File

@ -21,6 +21,7 @@ Item {
property var colorScheme property var colorScheme
property string label property string label
property string labelColor: root.colorScheme.text_norm
property string value property string value
Layout.fillWidth: true Layout.fillWidth: true
@ -35,9 +36,10 @@ Item {
ColumnLayout { ColumnLayout {
Label { Label {
color: labelColor
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: root.label text: root.label
type: Label.Body type: Label.Body_semibold
} }
TextEdit { TextEdit {
id: valueText id: valueText

View File

@ -24,7 +24,8 @@ Item {
signal closeWindow signal closeWindow
signal quitBridge signal quitBridge
signal showSetupGuide(var user, string address) signal showClientConfigurator(var user, string address, bool justLoggedIn)
signal showLogin(var username)
function selectUser(userID) { function selectUser(userID) {
const users = Backend.users; const users = Backend.users;
@ -35,11 +36,14 @@ Item {
} }
accounts.currentIndex = i; accounts.currentIndex = i;
if (user.state === EUserState.SignedOut) if (user.state === EUserState.SignedOut)
showSignIn(user.primaryEmailOrUsername()); showLogin(user.primaryEmailOrUsername());
return; return;
} }
console.error("User with ID ", userID, " was not found in the account list"); console.error("User with ID ", userID, " was not found in the account list");
} }
function showBugReport() {
rightContent.showBugReport();
}
function showHelp() { function showHelp() {
rightContent.showHelpView(); rightContent.showHelpView();
} }
@ -49,9 +53,9 @@ Item {
function showSettings() { function showSettings() {
rightContent.showGeneralSettings(); rightContent.showGeneralSettings();
} }
function showSignIn(username) {
signIn.username = username; function hasAccount() {
rightContent.showSignIn(); return Backend.users.count > 0
} }
RowLayout { RowLayout {
@ -190,6 +194,41 @@ Item {
Layout.minimumHeight: 1 Layout.minimumHeight: 1
color: leftBar.colorScheme.border_weak color: leftBar.colorScheme.border_weak
} }
Item {
id: noAccountBox
Layout.fillHeight: true
Layout.fillWidth: true
Layout.topMargin: 24
visible: !hasAccount()
ColumnLayout {
anchors.fill: parent
spacing: 8
Label {
colorScheme: leftBar.colorScheme
color: colorScheme.text_weak
Layout.alignment: Qt.AlignHCenter
text: qsTr("No accounts")
}
Button {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
colorScheme: leftBar.colorScheme
text: qsTr("Add an account")
secondary: true
onClicked: root.showLogin("")
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
}
}
}
ListView { ListView {
id: accounts id: accounts
@ -206,7 +245,7 @@ Item {
clip: true clip: true
model: Backend.users model: Backend.users
spacing: 12 spacing: 12
visible: hasAccount()
delegate: Item { delegate: Item {
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
@ -233,8 +272,7 @@ Item {
if (user.state !== EUserState.SignedOut) { if (user.state !== EUserState.SignedOut) {
rightContent.showAccount(); rightContent.showAccount();
} else { } else {
signIn.username = user.primaryEmailOrUsername(); showLogin(user.primaryEmailOrUsername());
rightContent.showSignIn();
} }
} }
} }
@ -282,8 +320,7 @@ Item {
width: 36 width: 36
onClicked: { onClicked: {
signIn.username = ""; root.showLogin("");
rightContent.showSignIn();
} }
} }
} }
@ -323,65 +360,42 @@ Item {
function showPortSettings() { function showPortSettings() {
rightContent.currentIndex = 4; rightContent.currentIndex = 4;
} }
function showSignIn() {
rightContent.currentIndex = 1;
signIn.focus = true;
}
anchors.fill: parent anchors.fill: parent
AccountView { StackLayout {
// 0 // 0
colorScheme: root.colorScheme currentIndex: hasAccount() ? 1 : 0
notifications: root.notifications NoAccountView {
user: {
if (accounts.currentIndex < 0)
return undefined;
if (Backend.users.count === 0)
return undefined;
return Backend.users.get(accounts.currentIndex);
}
onShowSetupGuide: function (user, address) {
root.showSetupGuide(user, address);
}
onShowSignIn: {
const user = this.user;
signIn.username = user ? user.primaryEmailOrUsername() : "";
rightContent.showSignIn();
}
}
GridLayout {
// 1 Sign In
columns: 2
Button {
id: backButton
Layout.alignment: Qt.AlignTop
Layout.leftMargin: 18
Layout.topMargin: 10
colorScheme: root.colorScheme colorScheme: root.colorScheme
horizontalPadding: 8 onStartSetup: {
icon.source: "/qml/icons/ic-arrow-left.svg" root.showLogin("")
secondary: true
onClicked: {
signIn.abort();
rightContent.showAccount();
} }
} }
SignIn { AccountView {
id: signIn
Layout.bottomMargin: 68
Layout.fillHeight: true
Layout.fillWidth: true
Layout.leftMargin: 80 - backButton.width - 18
Layout.preferredWidth: 320
Layout.rightMargin: 80
Layout.topMargin: 68
colorScheme: root.colorScheme colorScheme: root.colorScheme
notifications: root.notifications
user: {
if (accounts.currentIndex < 0)
return undefined;
if (Backend.users.count === 0)
return undefined;
return Backend.users.get(accounts.currentIndex);
}
onShowClientConfigurator: function (user, address, justLoggedIn) {
root.showClientConfigurator(user, address, justLoggedIn);
}
onShowLogin: function (username) {
root.showLogin(username);
}
} }
} }
Rectangle {
Layout.fillHeight: true
Layout.fillWidth: true
color: "#ff9900"
}
GeneralSettings { GeneralSettings {
// 2 // 2
colorScheme: root.colorScheme colorScheme: root.colorScheme

View File

@ -36,8 +36,7 @@ SettingsView {
type: SettingsItem.PrimaryButton type: SettingsItem.PrimaryButton
onClicked: { onClicked: {
Backend.notifyKBArticleClicked("https://proton.me/support/bridge"); Backend.openExternalLink();
Qt.openUrlExternally("https://proton.me/support/bridge");
} }
} }
SettingsItem { SettingsItem {
@ -71,7 +70,7 @@ SettingsView {
text: qsTr("Logs") text: qsTr("Logs")
type: SettingsItem.Button type: SettingsItem.Button
onClicked: Qt.openUrlExternally(Backend.logsPath) onClicked: Backend.openExternalLink(Backend.logsPath)
} }
SettingsItem { SettingsItem {
id: reportBug id: reportBug
@ -104,7 +103,7 @@ SettingsView {
type: Label.Caption type: Label.Caption
onLinkActivated: function (link) { onLinkActivated: function (link) {
Qt.openUrlExternally(link); Backend.openExternalLink(link)
} }
} }
} }

View File

@ -17,14 +17,29 @@ import QtQuick.Layouts
import QtQuick.Controls import QtQuick.Controls
import Proton import Proton
import Notifications import Notifications
import "SetupWizard"
ApplicationWindow { ApplicationWindow {
id: root id: root
property int _defaultHeight: 780
property int _defaultWidth: 1080
property var notifications property var notifications
function layoutForUserCount(userCount) {
if (userCount === 0) {
contentLayout.currentIndex = 1;
setupWizard.showOnboarding();
return;
}
const u = Backend.users.get(0);
if (!u) {
console.trace();
return;
}
if ((userCount === 1) && (u.state === EUserState.SignedOut)) {
contentLayout.currentIndex = 1;
setupWizard.showLogin(u.primaryEmailOrUsername());
}
}
function selectUser(userID) { function selectUser(userID) {
contentWrapper.selectUser(userID); contentWrapper.selectUser(userID);
} }
@ -35,42 +50,42 @@ ApplicationWindow {
root.requestActivate(); root.requestActivate();
} }
} }
function showClientConfigurator(user, address, justLoggedIn) {
contentLayout.currentIndex = 1;
setupWizard.showClientConfig(user, address, justLoggedIn);
}
function showHelp() { function showHelp() {
contentWrapper.showHelp(); contentWrapper.showHelp();
} }
function showLocalCacheSettings() { function showLocalCacheSettings() {
contentWrapper.showLocalCacheSettings(); contentWrapper.showLocalCacheSettings();
} }
function showLogin(username = "") {
contentLayout.currentIndex = 1;
setupWizard.showLogin(username);
}
function showSettings() { function showSettings() {
contentWrapper.showSettings(); contentWrapper.showSettings();
} }
function showSetup(user, address) {
setupGuide.user = user;
setupGuide.address = address;
setupGuide.reset();
contentLayout._showSetup = !!setupGuide.user;
}
function showSignIn(username) {
if (contentLayout.currentIndex === 1)
return;
contentWrapper.showSignIn(username);
}
colorScheme: ProtonStyle.currentStyle colorScheme: ProtonStyle.currentStyle
height: _defaultHeight height: screen.height < ProtonStyle.window_default_height + 100 ? ProtonStyle.window_minimum_height : ProtonStyle.window_default_height
minimumWidth: _defaultWidth minimumHeight: ProtonStyle.window_minimum_height
minimumWidth: ProtonStyle.window_minimum_width
visible: true visible: true
width: _defaultWidth width: ProtonStyle.window_default_width
Component.onCompleted: {
layoutForUserCount(Backend.users.count);
}
// show Setup Guide on every new user // show Setup Guide on every new user
Connections { Connections {
function onRowsAboutToBeRemoved(parent, first, last) { function onRowsAboutToBeRemoved(parent, first, last) {
for (let i = first; i <= last; i++) { for (let i = first; i <= last; i++) {
const user = Backend.users.get(i); const user = Backend.users.get(i);
if (setupGuide.user === user) { if (setupWizard.user === user) {
setupGuide.user = null; setupWizard.closeWizard();
contentLayout._showSetup = false;
return;
} }
} }
} }
@ -83,65 +98,53 @@ ApplicationWindow {
if (user.setupGuideSeen) { if (user.setupGuideSeen) {
return; return;
} }
root.showSetup(user, user.addresses[0]); root.showClientConfigurator(user, user.addresses[0], false);
} }
target: Backend.users target: Backend.users
} }
Connections { Connections {
function onLoginFinished(index, wasSignedOut) {
const user = Backend.users.get(index);
if (user && !wasSignedOut) {
root.showSetup(user, user.addresses[0]);
}
console.debug("Login finished", index);
}
function onSelectUser(userID, forceShowWindow) { function onSelectUser(userID, forceShowWindow) {
contentWrapper.selectUser(userID); contentWrapper.selectUser(userID);
if (setupWizard.visible) {
setupWizard.closeWizard()
}
if (forceShowWindow) { if (forceShowWindow) {
root.showAndRise(); root.showAndRise();
} }
} }
function onShowHelp() { function onShowHelp() {
root.showHelp(); root.showHelp();
if (setupWizard.visible) {
setupWizard.closeWizard()
}
root.showAndRise(); root.showAndRise();
} }
function onShowMainWindow() { function onShowMainWindow() {
root.showAndRise(); root.showAndRise();
} }
function onShowSettings() { function onShowSettings() {
if (setupWizard.visible) {
setupWizard.closeWizard()
}
root.showSettings(); root.showSettings();
root.showAndRise(); root.showAndRise();
} }
target: Backend target: Backend
} }
Connections {
function onCountChanged(count) {
layoutForUserCount(count);
}
target: Backend.users
}
StackLayout { StackLayout {
id: contentLayout id: contentLayout
property bool _showSetup: false
anchors.fill: parent anchors.fill: parent
currentIndex: { currentIndex: 0
// show welcome when there are no users
if (Backend.users.count === 0) {
return 1;
}
const u = Backend.users.get(0);
if (!u) {
console.trace();
console.log("empty user");
return 1;
}
if ((Backend.users.count === 1) && (u.state === EUserState.SignedOut)) {
showSignIn(u.primaryEmailOrUsername());
return 0;
}
if (contentLayout._showSetup) {
return 2;
}
return 0;
}
ContentWrapper { ContentWrapper {
// 0 // 0
@ -160,30 +163,24 @@ ApplicationWindow {
root.close(); root.close();
Backend.quit(); Backend.quit();
} }
onShowSetupGuide: function (user, address) { onShowClientConfigurator: function (user, address, justLoggedIn) {
root.showSetup(user, address); root.showClientConfigurator(user, address, justLoggedIn);
}
onShowLogin: function (username) {
root.showLogin(username);
} }
} }
WelcomeGuide { SetupWizard {
Layout.fillHeight: true id: setupWizard
Layout.fillWidth: true // 1
colorScheme: root.colorScheme
}
SetupGuide {
// 2
id: setupGuide
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
colorScheme: root.colorScheme colorScheme: root.colorScheme
onDismissed: { onBugReportRequested: {
root.showSetup(null, ""); contentWrapper.showBugReport();
} }
onFinished: { onWizardEnded: {
// TODO: Do not close window. Trigger Backend to check that contentLayout.currentIndex = 0;
// there is a successfully connected client. Then Backend
// should send another signal to close the setup guide.
root.showSetup(null, "");
} }
} }
} }

View File

@ -0,0 +1,58 @@
// 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/>.
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Proton
import "SetupWizard"
Rectangle {
id: root
property ColorScheme colorScheme
color: root.colorScheme.background_norm
signal startSetup()
ColumnLayout {
anchors.fill: parent
spacing: 0
// we use the setup wizard left pane (onboarding version)
LeftPane {
Layout.alignment: Qt.AlignHCenter
Layout.fillHeight: true
Layout.preferredWidth: ProtonStyle.wizard_pane_width
colorScheme: root.colorScheme
wizard: setupWizard
Component.onCompleted: {
showNoAccount();
}
onStartSetup: {
root.startSetup();
}
}
Image {
id: mailLogoWithWordmark
Layout.alignment: Qt.AlignHCenter
Layout.bottomMargin: ProtonStyle.wizard_window_margin
height: sourceSize.height
source: root.colorScheme.mail_logo_with_wordmark
sourceSize.height: 36
sourceSize.width: 134
width: sourceSize.width
}
}
}

View File

@ -71,7 +71,7 @@ Dialog {
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
onLinkActivated: function (link) { onLinkActivated: function (link) {
Qt.openUrlExternally(link); Backend.openExternalLink(link);
} }
} }
Item { Item {
@ -82,6 +82,17 @@ Dialog {
implicitWidth: additionalChildrenContainer.childrenRect.width implicitWidth: additionalChildrenContainer.childrenRect.width
visible: children.length > 0 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 { ColumnLayout {
spacing: 8 spacing: 8

View File

@ -61,18 +61,10 @@ Item {
colorScheme: root.colorScheme colorScheme: root.colorScheme
notification: root.notifications.enableBeta notification: root.notifications.enableBeta
} }
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.cacheUnavailable
}
NotificationDialog { NotificationDialog {
colorScheme: root.colorScheme colorScheme: root.colorScheme
notification: root.notifications.cacheCantMove notification: root.notifications.cacheCantMove
} }
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.diskFull
}
NotificationDialog { NotificationDialog {
colorScheme: root.colorScheme colorScheme: root.colorScheme
notification: root.notifications.enableSplitMode notification: root.notifications.enableSplitMode
@ -101,10 +93,6 @@ Item {
colorScheme: root.colorScheme colorScheme: root.colorScheme
notification: root.notifications.apiCertIssue notification: root.notifications.apiCertIssue
} }
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.noActiveKeyForRecipient
}
NotificationDialog { NotificationDialog {
colorScheme: root.colorScheme colorScheme: root.colorScheme
notification: root.notifications.userBadEvent notification: root.notifications.userBadEvent

View File

@ -24,19 +24,17 @@ QtObject {
property list<Action> action property list<Action> action
property bool active: false property bool active: false
// brief is used in status view only property string brief // brief is used in status view only
property string brief
default property var children default property var children
property var data property var data
// description is used in banners and in dialogs as description property string description // description is used in banners and in dialogs as description
property string description
property bool dismissed: false property bool dismissed: false
property int group property int group
property string icon property string icon
property string linkUrl: ""
property string linkText: ""
readonly property var occurred: active ? new Date() : undefined readonly property var occurred: active ? new Date() : undefined
property string title // title is used in dialogs only
// title is used in dialogs only
property string title
property int type property int type
onActiveChanged: { onActiveChanged: {

View File

@ -28,31 +28,15 @@ QtObject {
Dialogs = 64 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 { property Notification addressChanged: Notification {
brief: title brief: title
description: qsTr("The address list for your account has changed. You might need to reconfigure your email client.") description: qsTr("The address list for your account has changed. You might need to reconfigure your email client.")
group: Notifications.Group.Configuration group: Notifications.Group.Configuration
icon: "./icons/ic-exclamation-circle-filled.svg" 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") title: qsTr("Address list changes")
type: Notification.NotificationType.Warning type: Notification.NotificationType.Warning
action: [ action: [
Action { Action {
text: qsTr("OK") text: qsTr("OK")
@ -76,7 +60,7 @@ QtObject {
target: Backend target: Backend
} }
} }
property var all: [root.noInternet, root.imapPortStartupError, root.smtpPortStartupError, root.imapPortChangeError, root.smtpPortChangeError, root.imapConnectionModeChangeError, root.smtpConnectionModeChangeError, root.updateManualReady, root.updateManualRestartNeeded, root.updateManualError, root.updateForce, root.updateForceError, root.updateSilentRestartNeeded, root.updateSilentError, root.updateIsLatestVersion, root.loginConnectionError, root.onlyPaidUsers, root.alreadyLoggedIn, root.enableBeta, root.bugReportSendSuccess, root.bugReportSendError, root.bugReportSendFallback, root.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 { property Notification alreadyLoggedIn: Notification {
brief: qsTr("Already signed in") brief: qsTr("Already signed in")
description: qsTr("This account is already signed in.") description: qsTr("This account is already signed in.")
@ -104,9 +88,11 @@ QtObject {
} }
property Notification apiCertIssue: Notification { property Notification apiCertIssue: Notification {
brief: qsTr("Cannot establish secure connection") 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 group: Notifications.Group.Dialogs | Notifications.Group.Connection
icon: "./icons/ic-exclamation-circle-filled.svg" 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") title: qsTr("Unable to establish a \nsecure connection to \nProton servers")
type: Notification.NotificationType.Danger 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.") 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 group: Notifications.Group.Configuration | Notifications.Group.Dialogs
icon: "./icons/ic-exclamation-circle-filled.svg" 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") title: qsTr("Cant move cache")
type: Notification.NotificationType.Warning type: Notification.NotificationType.Warning
@ -263,43 +251,6 @@ QtObject {
target: Backend 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 Notification changeAllMailVisibility: Notification {
property var isVisibleNow property var isVisibleNow
@ -378,41 +329,6 @@ QtObject {
target: root 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 { property Notification enableBeta: Notification {
brief: title brief: title
description: qsTr("Be the first to get new updates and use new features. Bridge will update to the latest beta version.") description: qsTr("Be the first to get new updates and use new features. Bridge will update to the latest beta version.")
@ -453,7 +369,9 @@ QtObject {
brief: title brief: title
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.") 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 group: Notifications.Group.Configuration | Notifications.Group.Dialogs
icon: "/qml/icons/ic-question-circle.svg" 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?") title: qsTr("Enable split mode?")
type: Notification.NotificationType.Warning type: Notification.NotificationType.Warning
@ -600,7 +518,9 @@ QtObject {
description: "#PlaceHolderText" description: "#PlaceHolderText"
group: Notifications.Group.Connection group: Notifications.Group.Connection
icon: "./icons/ic-exclamation-circle-filled.svg" 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 type: Notification.NotificationType.Danger
action: [ action: [
@ -627,6 +547,8 @@ QtObject {
description: qsTr("The IMAP port could not be changed.") description: qsTr("The IMAP port could not be changed.")
group: Notifications.Group.Connection group: Notifications.Group.Connection
icon: "./icons/ic-alert.svg" 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 type: Notification.NotificationType.Danger
Connections { Connections {
@ -642,6 +564,8 @@ QtObject {
description: qsTr("The IMAP server could not be started. Please check or change the IMAP port.") description: qsTr("The IMAP server could not be started. Please check or change the IMAP port.")
group: Notifications.Group.Connection group: Notifications.Group.Connection
icon: "./icons/ic-alert.svg" 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 type: Notification.NotificationType.Danger
Connections { Connections {
@ -679,33 +603,6 @@ QtObject {
target: Backend 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 // Connection
property Notification noInternet: Notification { property Notification noInternet: Notification {
@ -714,7 +611,6 @@ QtObject {
group: Notifications.Group.Connection group: Notifications.Group.Connection
icon: "./icons/ic-no-connection.svg" icon: "./icons/ic-no-connection.svg"
type: Notification.NotificationType.Danger type: Notification.NotificationType.Danger
Connections { Connections {
function onInternetOff() { function onInternetOff() {
root.noInternet.active = true; root.noInternet.active = true;
@ -728,10 +624,14 @@ QtObject {
} }
property Notification noKeychain: Notification { property Notification noKeychain: Notification {
brief: title 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 group: Notifications.Group.Dialogs | Notifications.Group.Configuration
icon: "./icons/ic-exclamation-circle-filled.svg" 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 type: Notification.NotificationType.Danger
action: [ action: [
@ -773,7 +673,7 @@ QtObject {
text: qsTr("Upgrade") text: qsTr("Upgrade")
onTriggered: { onTriggered: {
Qt.openUrlExternally(root.onlyPaidUsers.pricingLink); Backend.openExternalLink(root.onlyPaidUsers.pricingLink);
root.onlyPaidUsers.active = false; root.onlyPaidUsers.active = false;
} }
} }
@ -788,8 +688,6 @@ QtObject {
} }
} }
property Notification rebuildKeychain: Notification { property Notification rebuildKeychain: Notification {
property var supportLink: "https://proton.me/support/bridge"
brief: title brief: title
description: qsTr("Bridge is not able to access your macOS keychain. Please consult the instructions on our support page.") description: qsTr("Bridge is not able to access your macOS keychain. Please consult the instructions on our support page.")
group: Notifications.Group.Dialogs | Notifications.Group.Configuration group: Notifications.Group.Dialogs | Notifications.Group.Configuration
@ -802,8 +700,7 @@ QtObject {
text: qsTr("Open the support page") text: qsTr("Open the support page")
onTriggered: { onTriggered: {
Backend.notifyKBArticleClicked(root.rebuildKeychain.supportLink); Backend.openExternalLink();
Qt.openUrlExternally(root.rebuildKeychain.supportLink);
Backend.quit(); Backend.quit();
} }
} }
@ -894,6 +791,8 @@ QtObject {
description: qsTr("The SMTP port could not be changed.") description: qsTr("The SMTP port could not be changed.")
group: Notifications.Group.Connection group: Notifications.Group.Connection
icon: "./icons/ic-alert.svg" 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 type: Notification.NotificationType.Danger
Connections { Connections {
@ -909,6 +808,8 @@ QtObject {
description: qsTr("The SMTP server could not be started. Please check or change the SMTP port.") description: qsTr("The SMTP server could not be started. Please check or change the SMTP port.")
group: Notifications.Group.Connection group: Notifications.Group.Connection
icon: "./icons/ic-alert.svg" 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 type: Notification.NotificationType.Danger
Connections { Connections {
@ -940,7 +841,7 @@ QtObject {
text: qsTr("Update manually") text: qsTr("Update manually")
onTriggered: { onTriggered: {
Qt.openUrlExternally(Backend.landingPageLink); Backend.openExternalLink(Backend.landingPageLink);
root.updateForce.active = false; root.updateForce.active = false;
} }
}, },
@ -970,6 +871,8 @@ QtObject {
description: qsTr("You must update manually. Go to: https://proton.me/mail/bridge#download") description: qsTr("You must update manually. Go to: https://proton.me/mail/bridge#download")
group: Notifications.Group.Update | Notifications.Group.Dialogs group: Notifications.Group.Update | Notifications.Group.Dialogs
icon: "./icons/ic-exclamation-circle-filled.svg" 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") title: qsTr("Bridge couldn't update")
type: Notification.NotificationType.Danger type: Notification.NotificationType.Danger
@ -978,7 +881,7 @@ QtObject {
text: qsTr("Update manually") text: qsTr("Update manually")
onTriggered: { onTriggered: {
Qt.openUrlExternally(Backend.landingPageLink); Backend.openExternalLink(Backend.landingPageLink);
root.updateForceError.active = false; root.updateForceError.active = false;
} }
}, },
@ -1028,6 +931,8 @@ QtObject {
description: qsTr("Please follow manual installation in order to update Bridge.") description: qsTr("Please follow manual installation in order to update Bridge.")
group: Notifications.Group.Update group: Notifications.Group.Update
icon: "./icons/ic-exclamation-circle-filled.svg" 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") title: qsTr("Bridge couldnt update")
type: Notification.NotificationType.Warning type: Notification.NotificationType.Warning
@ -1036,7 +941,7 @@ QtObject {
text: qsTr("Update manually") text: qsTr("Update manually")
onTriggered: { onTriggered: {
Qt.openUrlExternally(Backend.landingPageLink); Backend.openExternalLink(Backend.landingPageLink);
root.updateManualError.active = false; root.updateManualError.active = false;
Backend.quit(); Backend.quit();
} }
@ -1086,7 +991,7 @@ QtObject {
text: qsTr("Update manually") text: qsTr("Update manually")
onTriggered: { onTriggered: {
Qt.openUrlExternally(Backend.landingPageLink); Backend.openExternalLink(Backend.landingPageLink);
root.updateManualReady.active = false; root.updateManualReady.active = false;
} }
}, },
@ -1139,13 +1044,15 @@ QtObject {
description: qsTr("Bridge couldn't update") description: qsTr("Bridge couldn't update")
group: Notifications.Group.Update group: Notifications.Group.Update
icon: "./icons/ic-exclamation-circle-filled.svg" 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 type: Notification.NotificationType.Warning
action: Action { action: Action {
text: qsTr("Update manually") text: qsTr("Update manually")
onTriggered: { onTriggered: {
Qt.openUrlExternally(Backend.landingPageLink); Backend.openExternalLink(Backend.landingPageLink);
root.updateSilentError.active = false; root.updateSilentError.active = false;
} }
} }
@ -1189,6 +1096,8 @@ QtObject {
description: "#PlaceHolderText" description: "#PlaceHolderText"
group: Notifications.Group.Connection | Notifications.Group.Dialogs group: Notifications.Group.Connection | Notifications.Group.Dialogs
icon: "./icons/ic-exclamation-circle-filled.svg" 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") title: qsTr("Internal error")
type: Notification.NotificationType.Danger type: Notification.NotificationType.Danger

View File

@ -23,11 +23,13 @@ T.Button {
property bool borderless: false property bool borderless: false
property ColorScheme colorScheme property ColorScheme colorScheme
readonly property bool hasTextAndIcon: (control.text !== "") && (iconImage.source.toString().length > 0) readonly property bool hasTextAndIcon: (control.text !== "") && (iconImage.source.toString().length > 0)
property bool iconOnTheLeft: false
readonly property bool isIcon: control.text === "" readonly property bool isIcon: control.text === ""
property int labelType: Proton.Label.LabelType.Body property int labelType: Proton.Label.LabelType.Body
property bool loading: false property bool loading: false
readonly property bool primary: !secondary readonly property bool primary: !secondary
property alias secondary: control.flat property alias secondary: control.flat
property bool secondaryIsOpaque: false
property alias textHorizontalAlignment: label.horizontalAlignment property alias textHorizontalAlignment: label.horizontalAlignment
property alias textVerticalAlignment: label.verticalAlignment property alias textVerticalAlignment: label.verticalAlignment
@ -77,7 +79,7 @@ T.Button {
if (control.loading) { if (control.loading) {
return control.colorScheme.interaction_default_hover; return control.colorScheme.interaction_default_hover;
} }
return control.colorScheme.interaction_default; return secondaryIsOpaque ? control.colorScheme.background_norm : control.colorScheme.interaction_default;
} }
} else { } else {
if (primary) { if (primary) {
@ -103,7 +105,7 @@ T.Button {
if (control.loading) { if (control.loading) {
return control.colorScheme.interaction_default_hover; return control.colorScheme.interaction_default_hover;
} }
return control.colorScheme.interaction_default; return secondaryIsOpaque ? control.colorScheme.background_norm : control.colorScheme.interaction_default;
} }
} }
} }
@ -115,6 +117,7 @@ T.Button {
} }
contentItem: RowLayout { contentItem: RowLayout {
id: _contentItem id: _contentItem
layoutDirection: iconOnTheLeft ? Qt.RightToLeft : Qt.LeftToRight
spacing: control.hasTextAndIcon ? control.spacing : 0 spacing: control.hasTextAndIcon ? control.spacing : 0
Proton.Label { Proton.Label {
@ -128,12 +131,13 @@ T.Button {
return control.colorScheme.text_norm; return control.colorScheme.text_norm;
} }
} }
colorScheme: root.colorScheme colorScheme: control.colorScheme
elide: Text.ElideRight elide: Text.ElideRight
horizontalAlignment: Qt.AlignHCenter horizontalAlignment: Qt.AlignHCenter
opacity: control.enabled || control.loading ? 1.0 : 0.5 opacity: control.enabled || control.loading ? 1.0 : 0.5
text: control.text text: control.text
type: labelType type: labelType
verticalAlignment: Text.AlignVCenter
visible: !control.isIcon visible: !control.isIcon
} }
ColorImage { ColorImage {

View File

@ -48,6 +48,7 @@ QtObject {
property color interaction_weak_active property color interaction_weak_active
property color interaction_weak_hover property color interaction_weak_hover
property string logo_img property string logo_img
property string mail_logo_with_wordmark
// Primary // Primary
property color primary_norm property color primary_norm
@ -82,7 +83,4 @@ QtObject {
// Text // Text
property color text_norm property color text_norm
property color text_weak property color text_weak
// Images
property string welcome_img
} }

View File

@ -0,0 +1,89 @@
// 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/>.
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
RowLayout {
id: root
property var callback: null
property ColorScheme colorScheme
property bool external: false
property string link: "#"
property string text: ""
property color color: colorScheme.interaction_norm
function clear() {
root.callback = null;
root.text = "";
root.link = "";
root.external = false;
}
function link(url, text) {
return label.link(url, text);
}
function setCallback(callback, linkText, external) {
root.callback = callback;
root.text = linkText;
root.link = "#"; // Cannot be empty, otherwise the text is not an hyperlink.
root.external = external;
}
function setLink(linkURL, linkText, external) {
root.callback = null;
root.text = linkText;
root.link = linkURL;
root.external = external;
}
Label {
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)) {
Backend.openExternalLink(link);
}
if (callback) {
callback();
}
}
}
ColorImage {
Layout.alignment: Qt.AlignVCenter
color: label.linkColor
height: sourceSize.height
source: "/qml/icons/ic-external-link.svg"
sourceSize.height: 16
sourceSize.width: 16
visible: external
width: sourceSize.width
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
label.onLinkActivated(root.link);
}
}
}
HoverHandler {
acceptedDevices: PointerDevice.Mouse
cursorShape: Qt.PointingHandCursor
enabled: true
}
}

View File

@ -20,21 +20,21 @@ import "."
QtObject { QtObject {
id: root id: root
property real account_hover_radius: 12 * root.px // px property int account_hover_radius: 12
property real account_row_radius: 12 * root.px // px property int account_row_radius: 12
property real avatar_radius: 8 * root.px // px property int avatar_radius: 8
property real banner_radius: 12 * root.px // px property int banner_radius: 12
property real big_avatar_radius: 12 * root.px // px property int big_avatar_radius: 12
property int body_font_size: 14 property int body_font_size: 14
property real body_letter_spacing: 0.2 * root.px property real body_letter_spacing: 0.2
property int body_line_height: 20 property int body_line_height: 20
property real button_radius: 8 * root.px // px property int button_radius: 8
property int caption_font_size: 12 property int caption_font_size: 12
property real caption_letter_spacing: 0.4 * root.px property real caption_letter_spacing: 0.4
property int caption_line_height: 16 property int caption_line_height: 16
property real card_radius: 12 * root.px // px property int card_radius: 12
property real checkbox_radius: 4 * root.px // px property int checkbox_radius: 4
property real context_item_radius: 8 * root.px // px property int context_item_radius: 8
property ColorScheme currentStyle: lightStyle property ColorScheme currentStyle: lightStyle
property ColorScheme darkProminentStyle: ColorScheme { property ColorScheme darkProminentStyle: ColorScheme {
id: _darkProminentStyle id: _darkProminentStyle
@ -72,6 +72,7 @@ QtObject {
interaction_weak_active: "#6D697D" interaction_weak_active: "#6D697D"
interaction_weak_hover: "#5B576B" interaction_weak_hover: "#5B576B"
logo_img: "/qml/icons/product_logos_dark.svg" logo_img: "/qml/icons/product_logos_dark.svg"
mail_logo_with_wordmark: "/qml/icons/img-mail-logo-wordmark-dark.svg"
// Primary // Primary
primary_norm: "#8A6EFF" primary_norm: "#8A6EFF"
@ -105,9 +106,6 @@ QtObject {
// Text // Text
text_norm: "#FFFFFF" text_norm: "#FFFFFF"
text_weak: "#A7A4B5" text_weak: "#A7A4B5"
// Images
welcome_img: "/qml/icons/img-welcome-dark.png"
} }
property ColorScheme darkStyle: ColorScheme { property ColorScheme darkStyle: ColorScheme {
id: _darkStyle id: _darkStyle
@ -145,6 +143,7 @@ QtObject {
interaction_weak_active: "#6D697D" interaction_weak_active: "#6D697D"
interaction_weak_hover: "#5B576B" interaction_weak_hover: "#5B576B"
logo_img: "/qml/icons/product_logos_dark.svg" logo_img: "/qml/icons/product_logos_dark.svg"
mail_logo_with_wordmark: "/qml/icons/img-mail-logo-wordmark-dark.svg"
// Primary // Primary
primary_norm: "#8A6EFF" primary_norm: "#8A6EFF"
@ -178,11 +177,8 @@ QtObject {
// Text // Text
text_norm: "#FFFFFF" text_norm: "#FFFFFF"
text_weak: "#A7A4B5" text_weak: "#A7A4B5"
// Images
welcome_img: "/qml/icons/img-welcome-dark.png"
} }
property real dialog_radius: 12 * root.px // px property int dialog_radius: 12
property int fontWeight_100: Font.Thin property int fontWeight_100: Font.Thin
property int fontWeight_200: Font.Light property int fontWeight_200: Font.Light
property int fontWeight_300: Font.ExtraLight property int fontWeight_300: Font.ExtraLight
@ -206,7 +202,7 @@ QtObject {
} }
property int heading_font_size: 28 property int heading_font_size: 28
property int heading_line_height: 36 property int heading_line_height: 36
property real input_radius: 8 * root.px // px property int input_radius: 8
property int lead_font_size: 18 property int lead_font_size: 18
property int lead_line_height: 26 property int lead_line_height: 26
property ColorScheme lightProminentStyle: ColorScheme { property ColorScheme lightProminentStyle: ColorScheme {
@ -245,6 +241,7 @@ QtObject {
interaction_weak_active: "#8A6EFF" interaction_weak_active: "#8A6EFF"
interaction_weak_hover: "#6D4AFF" interaction_weak_hover: "#6D4AFF"
logo_img: "/qml/icons/product_logos_dark.svg" logo_img: "/qml/icons/product_logos_dark.svg"
mail_logo_with_wordmark: "/qml/icons/img-mail-logo-wordmark-dark.svg"
// Primary // Primary
primary_norm: "#8A6EFF" primary_norm: "#8A6EFF"
@ -278,9 +275,6 @@ QtObject {
// Text // Text
text_norm: "#FFFFFF" text_norm: "#FFFFFF"
text_weak: "#9282D4" text_weak: "#9282D4"
// Images
welcome_img: "/qml/icons/img-welcome-dark.png"
} }
// TODO: Once we will use Qt >=5.15 this should be refactored with inline components as follows: // TODO: Once we will use Qt >=5.15 this should be refactored with inline components as follows:
// https://doc.qt.io/qt-5/qtqml-documents-definetypes.html#inline-components // https://doc.qt.io/qt-5/qtqml-documents-definetypes.html#inline-components
@ -325,6 +319,7 @@ QtObject {
interaction_weak_active: "#A8A6A3" interaction_weak_active: "#A8A6A3"
interaction_weak_hover: "#C2BFBC" interaction_weak_hover: "#C2BFBC"
logo_img: "/qml/icons/product_logos.svg" logo_img: "/qml/icons/product_logos.svg"
mail_logo_with_wordmark: "/qml/icons/img-mail-logo-wordmark.svg"
// Primary // Primary
primary_norm: "#6D4AFF" primary_norm: "#6D4AFF"
@ -358,13 +353,35 @@ QtObject {
// Text // Text
text_norm: "#0C0C14" text_norm: "#0C0C14"
text_weak: "#706D6B" text_weak: "#706D6B"
// Images
welcome_img: "/qml/icons/img-welcome.png"
} }
property real progress_bar_radius: 3 * root.px // px property int progress_bar_radius: 3
property real px: 1.00 // px
property int title_font_size: 20 property int title_font_size: 20
property int title_line_height: 24 property int title_line_height: 24
property real tooltip_radius: 8 * root.px // px property int tooltip_radius: 8
// WebView overlay styling
property int web_view_button_width: 320
property int web_view_corner_radius: 10
property int web_view_overlay_button_vertical_margin: 10
property int web_view_overlay_horizontal_padding: 10
property int web_view_overlay_horizontal_margin: 250
property int web_view_overlay_vertical_margin: 50
property real web_view_overlay_opacity: 0.6
property int web_view_overlay_vertical_padding: web_view_corner_radius
property int web_view_overley_border_width: 1
property int window_default_height: 780
property int window_default_width: 1080
property int window_minimum_height: 650
property int window_minimum_width: window_default_width
// setup wizard constant
property int wizard_pane_bottomMargin: 92
property int wizard_pane_width: 364
property int wizard_window_margin: 40
property int wizard_spacing_extra_large: 32
property int wizard_spacing_extra_small: 4
property int wizard_spacing_large: 24
property int wizard_spacing_medium: 16
property int wizard_spacing_small: 8
} }

View File

@ -238,12 +238,12 @@ FocusScope {
bottomPadding: 8 bottomPadding: 8
color: { color: {
if (!control.enabled) { if (!control.enabled) {
return root.colorScheme.text_disabled return root.colorScheme.text_disabled;
} }
if (control.readOnly) { if (control.readOnly) {
return root.colorScheme.text_hint return root.colorScheme.text_hint;
} }
return root.colorScheme.text_norm return root.colorScheme.text_norm;
} }
// enforcing default focus here within component // enforcing default focus here within component

View File

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

View File

@ -28,6 +28,7 @@ CheckBox 4.0 CheckBox.qml
ComboBox 4.0 ComboBox.qml ComboBox 4.0 ComboBox.qml
Dialog 4.0 Dialog.qml Dialog 4.0 Dialog.qml
Label 4.0 Label.qml Label 4.0 Label.qml
LinkLabel 4.0 LinkLabel.qml
Menu 4.0 Menu.qml Menu 4.0 Menu.qml
MenuItem 4.0 MenuItem.qml MenuItem 4.0 MenuItem.qml
Popup 4.0 Popup.qml Popup 4.0 Popup.qml
@ -36,3 +37,4 @@ Switch 4.0 Switch.qml
TextArea 4.0 TextArea.qml TextArea 4.0 TextArea.qml
TextField 4.0 TextField.qml TextField 4.0 TextField.qml
Toggle 4.0 Toggle.qml Toggle 4.0 Toggle.qml
WebFrame 4.0 WebFrame.qml

View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
<style>
body {font-family: sans-serif}
h1 { font-size: 1.5em; text-align: center; margin-bottom: 2em;}
p {text-align: justify; margin-bottom: 2em; }
p.standfirst { font-weight: bold;}
</style>
</head>
<body>
%1
</body>
</html>

View File

@ -0,0 +1,19 @@
<h1>Why do I need bridge?</h1>
<p class="standfirst">
Proton does not have access to the content of your messages, so it cannot share your unencrypted messages with your email client from the
Proton servers.
</p>
<p>
Email clients such as Microsoft Outlook, Mozilla Thunderbird and Apple Mail use standard protocols named IMAP and SMTP to receive and send emails.
</p>
<p>
Even though the IMAP and SMTP protocols can use secure channels (using SSL/TLS), they do not offer support for encrypted messages.
Because Proton does not have access to the content of your messages, it is not possible to configure your email client to connect directly to
Proton servers.
</p>
<p>
The key to solving this problem is Bridge. Once installed on your computer and connected to your Proton account, Bridge can access your
encrypted messages stored on the Proton servers. Bridge integrates an IMAP and a SMTP server that run on your computer and are accessible only
to applications executing on your machine. Your email client connects to these local servers and Bridge is responsible for seamlessly encrypting
and decrypting the messages that you send and receive.
</p>

View File

@ -0,0 +1,19 @@
<h1>Why do I need to install a certificate when configuring Apple Mail with Bridge?</h1>
<p class="standfirst">
Apple Mail requires a secure channel for communications with email servers, and the server needs to be acknowledged as trusted.
</p>
<p>
In order to communicate with Bridge, Apple Mail requires secure connections using SSL/TLS. This cryptographic protocol includes an identity
verification system using certificates. For publicly available servers, certificates are normally issued and digitally signed by a certificate
authority, such as Let's Encrypt. This is not possible for Bridge, as the IMAP and SMTP servers are running on your own computer, and are not
accessible from any network (local or internet).
</p>
<p>
The solution is to use a self-signed certificate. When setting up an email account where the server provides a self-signed certificate, most
email clients will issue a warning asking you whether you trust the server or not, because the certificate was not issued by a certificate
authority.
</p>
<p>
Apple Mail requires an extra step. It will simply refuse to connect if the certificate is not set as trusted. Bridge solves this by storing this
certificate in the macOS keychain. This operation requires that you provide your macOS account password.
</p>

View File

@ -0,0 +1,21 @@
<h1>Why is there a warning sign when installing the Bridge profile on macOS?</h1>
<p class="standfirst">
This warning indicates that the certificate used to secure the communication channel between Bridge and your email client is not signed by a
trusted third party.
</p>
<p>
In order to communicate with Bridge, Apple Mail requires secure connections using SSL/TLS. This cryptographic protocol includes an identity
verification system using certificates. For publicly available servers, certificates are normally issued and digitally signed by a certificate
authority, such as Let's Encrypt. This is not possible for Bridge, as the IMAP and SMTP servers are running on your own computer, and are not
accessible from any network (local or internet).
</p>
<p>
The solution is to use a self-signed certificate. When setting up an email account where the server provides a self-signed certificate, most
email clients will issue a warning asking you whether you trust the server or not, because the certificate was not issued by a certificate
authority. The client has no way of verifying that the server is who it pretends to be.
</p>
<p>
You can safely ignore this warning. The check concerns only the communication between your email client and Bridge, which occurs within your
computer. On the other end, the communication between Bridge and the Proton servers uses the HTTPS protocol, and the identity of the remote
server is verified by Bridge.
</p>

View File

@ -90,7 +90,7 @@ Item {
icon.source: root.actionIcon icon.source: root.actionIcon
loading: root.loading loading: root.loading
secondary: root.type !== SettingsItem.PrimaryButton secondary: root.type !== SettingsItem.PrimaryButton
text: root.actionText + (root.actionIcon !== "" ? " " : "") text: root.actionText
visible: root.type === SettingsItem.Button || root.type === SettingsItem.PrimaryButton visible: root.type === SettingsItem.Button || root.type === SettingsItem.PrimaryButton
onClicked: { onClicked: {

View File

@ -1,293 +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/>.
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import QtQuick.Controls.impl
import Proton
Item {
id: root
property string address
property ColorScheme colorScheme
property var user
signal dismissed
signal finished
function reset() {
guidePages.currentIndex = 0;
clientList.currentIndex = -1;
actionList.currentIndex = -1;
}
function setupAction(actionID, clientID) {
if (user) {
user.setupGuideSeen = true;
}
switch (actionID) {
case -1:
root.dismissed();
break; // dismiss
case 0 // automatic
:
if (user) {
switch (clientID) {
case 0:
root.user.configureAppleMail(root.address);
Backend.notifyAutoconfigClicked("AppleMail");
break;
}
}
root.finished();
break;
case 1 // manual
:
let clientObj = clients.get(clientID);
if (clientObj !== undefined && clientObj.link !== "") {
Qt.openUrlExternally(clientObj.link);
Backend.notifyKBArticleClicked(clientObj.link);
} else {
console.log("unexpected client index", actionID, clientID);
}
root.finished();
break;
default:
console.log("unexpected client setup action", actionID, clientID);
}
}
implicitHeight: children[0].implicitHeight
implicitWidth: children[0].implicitWidth
ListModel {
id: clients
property bool haveAutoSetup: true
property string iconSource: "/qml/icons/ic-apple-mail.svg"
property string link: "https://proton.me/support/protonmail-bridge-clients-apple-mail"
property string name: "Apple Mail"
Component.onCompleted: {
if (Backend.goos === "darwin") {
append({
"name": "Apple Mail",
"iconSource": "/qml/icons/ic-apple-mail.svg",
"haveAutoSetup": true,
"link": "https://proton.me/support/protonmail-bridge-clients-apple-mail"
});
append({
"name": "Microsoft Outlook",
"iconSource": "/qml/icons/ic-microsoft-outlook.svg",
"haveAutoSetup": false,
"link": "https://proton.me/support/protonmail-bridge-clients-macos-outlook-2019"
});
}
if (Backend.goos === "windows") {
append({
"name": "Microsoft Outlook",
"iconSource": "/qml/icons/ic-microsoft-outlook.svg",
"haveAutoSetup": false,
"link": "https://proton.me/support/protonmail-bridge-clients-windows-outlook-2019"
});
}
append({
"name": "Mozilla Thunderbird",
"iconSource": "/qml/icons/ic-mozilla-thunderbird.svg",
"haveAutoSetup": false,
"link": "https://proton.me/support/protonmail-bridge-clients-windows-thunderbird"
});
append({
"name": "Other",
"iconSource": "/qml/icons/ic-other-mail-clients.svg",
"haveAutoSetup": false,
"link": "https://proton.me/support/protonmail-bridge-configure-client"
});
}
}
Rectangle {
anchors.fill: root
color: root.colorScheme.background_norm
}
StackLayout {
id: guidePages
anchors.bottomMargin: 70
anchors.fill: parent
anchors.leftMargin: 80
anchors.rightMargin: 80
anchors.topMargin: 30
ColumnLayout {
// 0: Client selection
id: clientView
property int columnWidth: 268
Layout.fillHeight: true
spacing: 8
Label {
colorScheme: root.colorScheme
text: qsTr("Setting up email client")
type: Label.LabelType.Heading
}
Label {
color: root.colorScheme.text_weak
colorScheme: root.colorScheme
text: address
type: Label.LabelType.Lead
}
RowLayout {
Layout.topMargin: 32 - clientView.spacing
spacing: 24
ColumnLayout {
id: clientColumn
Layout.alignment: Qt.AlignTop
Label {
id: labelA
colorScheme: root.colorScheme
text: qsTr("Choose an email client")
type: Label.LabelType.Body_semibold
}
ListView {
id: clientList
Layout.fillHeight: true
model: clients
width: clientView.columnWidth
delegate: Item {
implicitHeight: clientRow.height
implicitWidth: clientRow.width
ColumnLayout {
id: clientRow
width: clientList.width
RowLayout {
Layout.bottomMargin: 12
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 12
ColorImage {
height: 36
source: model.iconSource
sourceSize.height: 36
}
Label {
Layout.leftMargin: 12
colorScheme: root.colorScheme
text: model.name
type: Label.LabelType.Body
}
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1
color: root.colorScheme.border_weak
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
clientList.currentIndex = index;
if (!model.haveAutoSetup) {
root.setupAction(1, index);
}
}
}
}
highlight: Rectangle {
color: root.colorScheme.interaction_default_active
radius: ProtonStyle.context_item_radius
}
}
}
ColumnLayout {
id: actionColumn
Layout.alignment: Qt.AlignTop
visible: clientList.currentIndex >= 0 && clients.get(clientList.currentIndex).haveAutoSetup
Label {
colorScheme: root.colorScheme
text: qsTr("Choose configuration mode")
type: Label.LabelType.Body_semibold
}
ListView {
id: actionList
Layout.fillHeight: true
model: [qsTr("Configure automatically"), qsTr("Configure manually")]
width: clientView.columnWidth
delegate: Item {
implicitHeight: children[0].height
implicitWidth: children[0].width
ColumnLayout {
width: actionList.width
Label {
Layout.bottomMargin: 20
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 20
colorScheme: root.colorScheme
text: modelData
type: Label.LabelType.Body
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1
color: root.colorScheme.border_weak
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
actionList.currentIndex = index;
root.setupAction(index, clientList.currentIndex);
}
}
}
highlight: Rectangle {
color: root.colorScheme.interaction_default_active
radius: ProtonStyle.context_item_radius
}
}
}
}
Item {
Layout.fillHeight: true
}
Button {
colorScheme: root.colorScheme
flat: true
text: qsTr("Set up later")
onClicked: {
root.setupAction(-1, -1);
if (user) {
user.setupGuideSeen = true;
}
root.dismissed();
}
}
}
}
}

View File

@ -0,0 +1,274 @@
// 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/>.
import QtQml
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
Item {
id: root
enum Screen {
CertificateInstall,
ProfileInstall
}
property var wizard
signal appleMailAutoconfigCertificateInstallPageShown
signal appleMailAutoconfigProfileInstallPageShow
function showAutoconfig() {
if (Backend.isTLSCertificateInstalled()) {
showProfileInstall();
} else {
showCertificateInstall();
}
}
function showCertificateInstall() {
certificateInstall.reset();
stack.currentIndex = ClientConfigAppleMail.Screen.CertificateInstall;
appleMailAutoconfigCertificateInstallPageShown();
}
function showProfileInstall() {
profileInstall.reset();
stack.currentIndex = ClientConfigAppleMail.Screen.ProfileInstall;
appleMailAutoconfigProfileInstallPageShow();
}
StackLayout {
id: stack
anchors.fill: parent
// stack index 0
Item {
id: certificateInstall
property string errorString: ""
property bool showBugReportLink: false
property bool waitingForCert: false
function clearError() {
errorString = "";
showBugReportLink = false;
}
function reset() {
waitingForCert = false;
clearError();
}
Layout.fillHeight: true
Layout.fillWidth: true
ColumnLayout {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: ProtonStyle.wizard_spacing_large
Connections {
function onCertificateInstallCanceled() {
certificateInstall.waitingForCert = false;
certificateInstall.errorString = qsTr("Apple Mail cannot be configured if you do not install the certificate. Please retry.");
certificateInstall.showBugReportLink = false;
}
function onCertificateInstallFailed() {
certificateInstall.waitingForCert = false;
certificateInstall.errorString = qsTr("An error occurred while installing the certificate.");
certificateInstall.showBugReportLink = true;
}
function onCertificateInstallSuccess() {
certificateInstall.reset();
root.showAutoconfig();
}
target: Backend
}
ColumnLayout {
Layout.fillWidth: true
spacing: ProtonStyle.wizard_spacing_medium
Label {
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
colorScheme: wizard.colorScheme
horizontalAlignment: Text.AlignHCenter
text: qsTr("Install the bridge certificate")
type: Label.LabelType.Title
wrapMode: Text.WordWrap
}
Label {
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
color: colorScheme.text_weak
colorScheme: wizard.colorScheme
horizontalAlignment: Text.AlignHCenter
text: qsTr("After clicking on the button below, a system pop-up will ask you for your credentials, please enter your macOS user credentials (not your Proton accounts) and validate.")
type: Label.LabelType.Body
wrapMode: Text.WordWrap
}
}
Image {
Layout.alignment: Qt.AlignHCenter
height: 182
opacity: certificateInstall.waitingForCert ? 0.3 : 1.0
source: "/qml/icons/img-macos-cert-screenshot.png"
width: 140
}
ColumnLayout {
Layout.fillWidth: true
spacing: ProtonStyle.wizard_spacing_medium
Button {
Layout.fillWidth: true
colorScheme: wizard.colorScheme
enabled: !certificateInstall.waitingForCert
loading: certificateInstall.waitingForCert
text: qsTr("Install the certificate")
onClicked: {
certificateInstall.clearError();
certificateInstall.waitingForCert = true;
Backend.installTLSCertificate();
}
}
Button {
Layout.fillWidth: true
colorScheme: wizard.colorScheme
enabled: !certificateInstall.waitingForCert
secondary: true
text: qsTr("Cancel")
onClicked: {
wizard.closeWizard();
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: ProtonStyle.wizard_spacing_small
RowLayout {
Layout.fillWidth: true
spacing: ProtonStyle.wizard_spacing_extra_small
ColorImage {
color: wizard.colorScheme.signal_danger
height: errorLabel.lineHeight
source: "/qml/icons/ic-exclamation-circle-filled.svg"
sourceSize.height: errorLabel.lineHeight
visible: certificateInstall.errorString.length > 0
}
Label {
id: errorLabel
Layout.fillWidth: true
color: wizard.colorScheme.signal_danger
colorScheme: wizard.colorScheme
horizontalAlignment: Text.AlignHCenter
text: certificateInstall.errorString
type: Label.LabelType.Body_semibold
wrapMode: Text.WordWrap
}
}
LinkLabel {
Layout.alignment: Qt.AlignHCenter
callback: wizard.showBugReport
colorScheme: wizard.colorScheme
link: "#"
text: qsTr("Report the problem")
visible: certificateInstall.showBugReportLink
}
}
}
}
}
// stack index 1
Item {
id: profileInstall
property bool profilePaneLaunched: false
function reset() {
profilePaneLaunched = false;
}
Layout.fillHeight: true
Layout.fillWidth: true
ColumnLayout {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: ProtonStyle.wizard_spacing_large
ColumnLayout {
Layout.fillWidth: true
spacing: ProtonStyle.wizard_spacing_medium
Label {
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
colorScheme: wizard.colorScheme
horizontalAlignment: Text.AlignHCenter
text: qsTr("Install the profile")
type: Label.LabelType.Title
wrapMode: Text.WordWrap
}
Label {
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
color: colorScheme.text_weak
colorScheme: wizard.colorScheme
horizontalAlignment: Text.AlignHCenter
text: qsTr("A system pop-up will appear. Double click on the entry with your email, and click Install in the dialog that appears.")
type: Label.LabelType.Body
wrapMode: Text.WordWrap
}
}
Image {
Layout.alignment: Qt.AlignHCenter
height: 102
source: "/qml/icons/img-macos-profile-screenshot.png"
width: 364
}
ColumnLayout {
Layout.fillWidth: true
spacing: ProtonStyle.wizard_spacing_medium
Button {
Layout.fillWidth: true
colorScheme: wizard.colorScheme
text: profileInstall.profilePaneLaunched ? qsTr("I have installed the profile") : qsTr("Install the profile")
onClicked: {
if (profileInstall.profilePaneLaunched) {
wizard.showClientConfigEnd();
} else {
wizard.user.configureAppleMail(wizard.address);
profileInstall.profilePaneLaunched = true;
}
}
}
Button {
Layout.fillWidth: true
colorScheme: wizard.colorScheme
secondary: true
text: qsTr("Cancel")
onClicked: {
wizard.closeWizard();
}
}
}
}
}
}
}

View File

@ -0,0 +1,99 @@
// 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/>.
import QtQml
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
Rectangle {
id: root
property ColorScheme colorScheme: wizard.colorScheme
property var wizard
clip: true
color: colorScheme.background_norm
Item {
id: centeredContainer
anchors.bottom: parent.bottom
anchors.bottomMargin: 84
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: 32
clip: true
width: ProtonStyle.wizard_pane_width
ColumnLayout {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: ProtonStyle.wizard_spacing_medium
Image {
Layout.alignment: Qt.AlignHCenter
Layout.preferredHeight: sourceSize.height
Layout.preferredWidth: sourceSize.width
source: "/qml/icons/img-client-config-success.svg"
sourceSize.height: 104
sourceSize.width: 190
}
Label {
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
colorScheme: wizard.colorScheme
horizontalAlignment: Text.AlignHCenter
text: qsTr("Congratulations! You're all setup")
type: Label.LabelType.Title
wrapMode: Text.WordWrap
}
Label {
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
color: colorScheme.text_weak
colorScheme: wizard.colorScheme
horizontalAlignment: Text.AlignHCenter
text: wizard.address
type: Label.LabelType.Body
wrapMode: Text.WordWrap
}
Label {
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
colorScheme: wizard.colorScheme
horizontalAlignment: Text.AlignHCenter
text: qsTr("Your client has been configured. While complete synchronization might take some time, you can already send encrypted emails.")
type: Label.LabelType.Body
wrapMode: Text.WordWrap
}
Button {
Layout.fillWidth: true
colorScheme: root.colorScheme
text: qsTr("Done")
onClicked: wizard.closeWizard()
}
}
}
Image {
id: mailLogoWithWordmark
anchors.bottom: parent.bottom
anchors.bottomMargin: 32
anchors.horizontalCenter: parent.horizontalCenter
height: 36
source: root.colorScheme.mail_logo_with_wordmark
sourceSize.height: height
sourceSize.width: width
width: 134
}
}

View File

@ -0,0 +1,163 @@
// 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/>.
import QtQml
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import ".."
Rectangle {
id: root
property ColorScheme colorScheme: wizard.colorScheme
readonly property bool genericClient: SetupWizard.Client.Generic === wizard.client
property var wizard
clip: true
color: colorScheme.background_weak
Item {
id: centeredContainer
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
width: 640
ColumnLayout {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: ProtonStyle.wizard_spacing_medium
Label {
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
colorScheme: wizard.colorScheme
horizontalAlignment: Text.AlignHCenter
text: qsTr("Configure %1").arg(wizard.clientName())
type: Label.LabelType.Title
wrapMode: Text.WordWrap
}
Rectangle {
Layout.fillWidth: true
border.color: colorScheme.border_norm
border.width: 1
color: "transparent"
height: childrenRect.height + 2 * ProtonStyle.wizard_spacing_medium
radius: 12
RowLayout {
anchors.left: parent.left
anchors.margins: ProtonStyle.wizard_spacing_medium
anchors.right: parent.right
anchors.top: parent.top
spacing: ProtonStyle.wizard_spacing_small
Label {
Layout.fillHeight: true
Layout.fillWidth: true
colorScheme: wizard.colorScheme
horizontalAlignment: Text.AlignLeft
text: (SetupWizard.Client.MicrosoftOutlook === wizard.client) ? qsTr("Are you unsure about your Outlook version or do you need assistance in configuring Outlook?") : qsTr("Do you need assistance in configuring %1?".arg(wizard.clientName()))
type: Label.LabelType.Body
verticalAlignment: Text.AlignVCenter
wrapMode: Text.WordWrap
}
Button {
colorScheme: root.colorScheme
icon.source: "/qml/icons/ic-external-link.svg"
text: qsTr("Open guide")
onClicked: function () {
Backend.openExternalLink(wizard.setupGuideLink());
}
}
}
}
Rectangle {
Layout.fillWidth: true
border.color: colorScheme.signal_warning
border.width: 1
color: "transparent"
height: childrenRect.height + 2 * ProtonStyle.wizard_spacing_medium
radius: ProtonStyle.banner_radius
RowLayout {
anchors.left: parent.left
anchors.margins: ProtonStyle.wizard_spacing_medium
anchors.right: parent.right
anchors.top: parent.top
spacing: ProtonStyle.wizard_spacing_medium
ColorImage {
id: image
height: 36
source: "/qml/icons/ic-warning-orange.svg"
sourceSize.height: height
sourceSize.width: width
width: height
}
Label {
Layout.fillHeight: true
Layout.fillWidth: true
colorScheme: wizard.colorScheme
horizontalAlignment: Text.AlignLeft
text: qsTr("Copy paste the provided configuration parameters. Use the password below (not your Proton password), when adding your Proton account to %1.".arg(wizard.clientName()))
type: Label.LabelType.Body
verticalAlignment: Text.AlignVCenter
wrapMode: Text.WordWrap
}
}
}
RowLayout {
id: configuration
Layout.fillWidth: true
spacing: ProtonStyle.wizard_spacing_extra_large
Configuration {
Layout.fillWidth: true
colorScheme: wizard.colorScheme
highlightPassword: true
hostname: Backend.hostname
password: wizard.user ? wizard.user.password : ""
port: Backend.imapPort.toString()
security: Backend.useSSLForIMAP ? "SSL" : "STARTTLS"
title: "IMAP"
username: wizard.address
}
Configuration {
Layout.fillWidth: true
colorScheme: wizard.colorScheme
highlightPassword: true
hostname: Backend.hostname
password: wizard.user ? wizard.user.password : ""
port: Backend.smtpPort.toString()
security: Backend.useSSLForSMTP ? "SSL" : "STARTTLS"
title: "SMTP"
username: wizard.address
}
}
Button {
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: 304
colorScheme: root.colorScheme
secondary: true
secondaryIsOpaque: true
text: qsTr("Continue")
onClicked: wizard.showClientConfigEnd()
}
}
}
}

View File

@ -0,0 +1,101 @@
// 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/>.
import QtQml
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
Item {
id: root
readonly property bool onMacOS: (Backend.goos === "darwin")
readonly property bool onWindows: (Backend.goos === "windows")
property var wizard
ColumnLayout {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: ProtonStyle.wizard_spacing_medium
Label {
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
Layout.topMargin: ProtonStyle.wizard_spacing_medium
colorScheme: wizard.colorScheme
horizontalAlignment: Qt.AlignHCenter
text: qsTr("Select your email client")
type: Label.LabelType.Title
wrapMode: Text.WordWrap
}
ClientListItem {
Layout.fillWidth: true
colorScheme: wizard.colorScheme
iconSource: "/qml/icons/ic-apple-mail.svg"
text: "Apple Mail"
visible: root.onMacOS
onClicked: {
wizard.client = SetupWizard.Client.AppleMail;
wizard.showAppleMailAutoConfig();
}
}
ClientListItem {
Layout.fillWidth: true
colorScheme: wizard.colorScheme
iconSource: "/qml/icons/ic-microsoft-outlook.svg"
text: "Microsoft Outlook"
visible: root.onMacOS || root.onWindows
onClicked: {
wizard.client = SetupWizard.Client.MicrosoftOutlook;
wizard.showClientParams();
}
}
ClientListItem {
Layout.fillWidth: true
colorScheme: wizard.colorScheme
iconSource: "/qml/icons/ic-mozilla-thunderbird.svg"
text: "Mozilla Thunderbird"
onClicked: {
wizard.client = SetupWizard.Client.MozillaThunderbird;
wizard.showClientParams();
}
}
ClientListItem {
Layout.fillWidth: true
colorScheme: wizard.colorScheme
iconSource: "/qml/icons/ic-other-mail-clients.svg"
text: qsTr("Other")
onClicked: {
wizard.client = SetupWizard.Client.Generic;
wizard.showClientParams();
}
}
Button {
Layout.fillWidth: true
Layout.topMargin: 20
colorScheme: wizard.colorScheme
secondary: true
secondaryIsOpaque: true
text: qsTr("Setup later")
onClicked: {
root.wizard.closeWizard();
}
}
}
}

View File

@ -0,0 +1,70 @@
// 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/>.
import QtQml
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
Rectangle {
id: root
property ColorScheme colorScheme
property string iconSource
property string text
signal clicked
border.color: colorScheme.border_norm
border.width: 1
color: {
if (mouseArea.pressed) {
return colorScheme.interaction_default_active;
}
if (mouseArea.containsMouse) {
return colorScheme.interaction_default_hover;
}
return colorScheme.background_norm;
}
height: 68
radius: ProtonStyle.banner_radius
RowLayout {
anchors.fill: parent
anchors.margins: ProtonStyle.wizard_spacing_medium
ColorImage {
height: sourceSize.height
source: iconSource
sourceSize.height: 36
}
Label {
Layout.fillWidth: true
Layout.leftMargin: 12
colorScheme: root.colorScheme
horizontalAlignment: Text.AlignLeft
text: root.text
type: Label.LabelType.Body
verticalAlignment: Text.AlignVCenter
}
}
MouseArea {
id: mouseArea
acceptedButtons: Qt.LeftButton
anchors.fill: parent
hoverEnabled: true
onClicked: {
root.clicked();
}
}
}

View File

@ -0,0 +1,65 @@
// 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/>.
import QtQml
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
Button {
id: root
property var wizard
readonly property int _iconPadding: 8 // The SVG image we use has internal padding that we need to compensate for alignment.
readonly property int _iconSize: 24
anchors.bottom: parent.bottom
anchors.bottomMargin: ProtonStyle.wizard_window_margin - _iconPadding
anchors.right: parent.right
anchors.rightMargin: ProtonStyle.wizard_window_margin - _iconPadding
colorScheme: wizard.colorScheme
horizontalPadding: 0
icon.color: wizard.colorScheme.text_weak
icon.height: _iconSize
icon.source: "/qml/icons/ic-question-circle.svg"
icon.width: _iconSize
verticalPadding: 0
onClicked: {
menu.popup(-menu.width + root.width, -menu.height);
}
Menu {
id: menu
colorScheme: root.colorScheme
modal: true
MenuItem {
id: getHelpItem
colorScheme: root.colorScheme
text: qsTr("Get help")
onClicked: {
Backend.openExternalLink();
}
}
MenuItem {
id: reportAProblemItem
colorScheme: root.colorScheme
text: qsTr("Report a problem")
onClicked: {
wizard.showBugReport();
}
}
}
}

View File

@ -0,0 +1,145 @@
// 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/>.
import QtQml
import QtQuick
import QtQuick.Layouts
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
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.openExternalLink("https://proton.me/support/apple-mail-certificate"); }, qsTr("Why is this certificate needed?"), true);
linkLabel2.clear();
}
function showAppleMailAutoconfigCommon() {
titleLabel.text = "";
linkLabel1.clear();
linkLabel2.clear();
iconSource = wizard.clientIconSource();
iconHeight = 80;
iconWidth = 80;
}
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.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) {
titleLabel.text = "";
descriptionLabel.text = newAccount ? qsTr("Bridge is now connected to Proton, and has already started downloading your messages. Lets now connect your email client to Bridge.") : qsTr("Lets connect your email client to Bridge.");
linkLabel1.clear();
linkLabel2.clear();
iconSource = "/qml/icons/img-client-config-selector.svg";
iconHeight = 104;
iconWidth = 266;
}
function showLogin() {
showOnboarding();
}
function showLogin2FA() {
showOnboarding();
}
function showLoginMailboxPassword() {
showOnboarding();
}
function showNoAccount() {
titleLabel.text = welcomeTitle;
descriptionLabel.text = welcomeDescription;
linkLabel1.setCallback(startSetup, "Start setup", false);
linkLabel2.clear();
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 {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: ProtonStyle.wizard_spacing_medium
Image {
id: icon
Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
Layout.preferredHeight: root.iconHeight
Layout.preferredWidth: root.iconWidth
source: root.iconSource
sourceSize.height: root.iconHeight
sourceSize.width: root.iconWidth
}
Label {
id: titleLabel
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
colorScheme: _colorScheme
horizontalAlignment: Text.AlignHCenter
text: ""
type: Label.LabelType.Heading
visible: text.length !== 0
wrapMode: Text.WordWrap
}
Label {
id: descriptionLabel
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
colorScheme: _colorScheme
horizontalAlignment: Text.AlignHCenter
text: ""
type: Label.LabelType.Body
wrapMode: Text.WordWrap
}
LinkLabel {
id: linkLabel1
Layout.alignment: Qt.AlignHCenter
colorScheme: _colorScheme
visible: (text !== "")
}
LinkLabel {
id: linkLabel2
Layout.alignment: Qt.AlignHCenter
colorScheme: _colorScheme
visible: (text !== "")
}
}
}

View File

@ -0,0 +1,479 @@
// 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/>.
import QtQml
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
FocusScope {
id: root
enum RootStack {
Login,
TOTP,
MailboxPassword
}
property alias currentIndex: stackLayout.currentIndex
property alias username: usernameTextField.text
property var wizard
signal loginAbort(string username, bool wasSignedOut)
function abort() {
root.reset();
loginAbort(usernameTextField.text, false);
Backend.loginAbort(usernameTextField.text);
}
function reset(clearUsername = false) {
stackLayout.currentIndex = Login.RootStack.Login;
loginLayout.reset(clearUsername);
totpLayout.reset();
mailboxPasswordLayout.reset();
if (username.length === 0) {
usernameTextField.forceActiveFocus();
} else {
passwordTextField.forceActiveFocus();
}
passwordTextField.hidePassword();
secondPasswordTextField.hidePassword();
}
StackLayout {
id: stackLayout
function loginFailed() {
signInButton.loading = false;
usernameTextField.enabled = true;
usernameTextField.error = true;
passwordTextField.enabled = true;
passwordTextField.error = true;
}
anchors.fill: parent
Connections {
function onLogin2FAError(_) {
console.assert(stackLayout.currentIndex === Login.RootStack.TOTP, "Unexpected login2FAError");
twoFAButton.loading = false;
twoFactorPasswordTextField.enabled = true;
twoFactorPasswordTextField.error = true;
twoFactorPasswordTextField.errorString = qsTr("Your code is incorrect");
twoFactorPasswordTextField.focus = true;
}
function onLogin2FAErrorAbort(_) {
console.assert(stackLayout.currentIndex === Login.RootStack.TOTP, "Unexpected login2FAErrorAbort");
root.reset();
errorLabel.text = qsTr("Incorrect login credentials. Please try again.");
}
function onLogin2FARequested(username) {
console.assert(stackLayout.currentIndex === Login.RootStack.Login, "Unexpected login2FARequested");
twoFactorUsernameLabel.text = username;
stackLayout.currentIndex = Login.RootStack.TOTP;
twoFactorPasswordTextField.focus = true;
}
function onLogin2PasswordError(_) {
console.assert(stackLayout.currentIndex === Login.RootStack.MailboxPassword, "Unexpected login2PasswordError");
secondPasswordButton.loading = false;
secondPasswordTextField.enabled = true;
secondPasswordTextField.error = true;
secondPasswordTextField.errorString = qsTr("Your mailbox password is incorrect");
secondPasswordTextField.focus = true;
}
function onLogin2PasswordErrorAbort(_) {
console.assert(stackLayout.currentIndex === Login.RootStack.MailboxPassword, "Unexpected login2PasswordErrorAbort");
root.reset();
errorLabel.text = qsTr("Incorrect login credentials. Please try again.");
}
function onLogin2PasswordRequested(username) {
console.assert(stackLayout.currentIndex === Login.RootStack.Login || stackLayout.currentIndex === Login.RootStack.TOTP, "Unexpected login2PasswordRequested");
stackLayout.currentIndex = Login.RootStack.MailboxPassword;
mailboxPasswordUsernameLabel.text = username;
secondPasswordTextField.focus = true;
}
function onLoginAlreadyLoggedIn(_) {
stackLayout.currentIndex = Login.RootStack.Login;
root.reset();
}
function onLoginConnectionError(_) {
if (stackLayout.currentIndex === Login.RootStack.Login) {
stackLayout.loginFailed();
}
}
function onLoginFinished(_) {
stackLayout.currentIndex = Login.RootStack.Login;
root.reset();
}
function onLoginFreeUserError() {
console.assert(stackLayout.currentIndex === Login.RootStack.Login, "Unexpected loginFreeUserError");
stackLayout.loginFailed();
}
function onLoginUsernamePasswordError(errorMsg) {
console.assert(stackLayout.currentIndex === Login.RootStack.Login, "Unexpected loginUsernamePasswordError");
stackLayout.loginFailed();
if (errorMsg !== "")
errorLabel.text = errorMsg;
else
errorLabel.text = qsTr("Incorrect login credentials");
}
target: Backend
}
Item {
ColumnLayout {
id: loginLayout
function clearErrors() {
usernameTextField.error = false;
usernameTextField.errorString = "";
passwordTextField.error = false;
passwordTextField.errorString = "";
errorLabel.text = "";
}
function reset(clearUsername = false) {
signInButton.loading = false;
errorLabel.text = "";
usernameTextField.enabled = true;
usernameTextField.focus = true;
if (clearUsername) {
usernameTextField.text = "";
}
passwordTextField.enabled = true;
passwordTextField.text = "";
clearErrors();
}
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: ProtonStyle.wizard_spacing_medium
ColumnLayout {
Layout.fillWidth: true
spacing: ProtonStyle.wizard_spacing_small
Label {
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
colorScheme: wizard.colorScheme
horizontalAlignment: Text.AlignHCenter
text: qsTr("Sign in")
type: Label.LabelType.Title
}
Label {
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
color: wizard.colorScheme.text_weak
colorScheme: wizard.colorScheme
horizontalAlignment: Text.AlignHCenter
text: qsTr("Enter your Proton Account details.")
type: Label.LabelType.Body
}
}
RowLayout {
Layout.fillWidth: true
spacing: 0
ColorImage {
color: wizard.colorScheme.signal_danger
height: errorLabel.lineHeight
source: "/qml/icons/ic-exclamation-circle-filled.svg"
sourceSize.height: errorLabel.lineHeight
visible: errorLabel.text.length > 0
}
Label {
id: errorLabel
Layout.fillWidth: true
Layout.leftMargin: 4
color: wizard.colorScheme.signal_danger
colorScheme: wizard.colorScheme
type: Label.LabelType.Caption_semibold
wrapMode: Text.WordWrap
}
}
TextField {
id: usernameTextField
Layout.fillWidth: true
colorScheme: wizard.colorScheme
focus: true
label: qsTr("Email or username")
validateOnEditingFinished: false
validator: function (str) {
if (str.length === 0) {
return qsTr("Enter email or username");
}
}
onAccepted: passwordTextField.forceActiveFocus()
onTextChanged: {
loginLayout.clearErrors();
}
}
TextField {
id: passwordTextField
Layout.fillWidth: true
colorScheme: wizard.colorScheme
echoMode: TextInput.Password
label: qsTr("Password")
validateOnEditingFinished: false
validator: function (str) {
if (str.length === 0) {
return qsTr("Enter password");
}
}
onAccepted: signInButton.checkAndSignIn()
onTextChanged: {
loginLayout.clearErrors();
}
}
Button {
id: signInButton
function checkAndSignIn() {
usernameTextField.validate();
passwordTextField.validate();
if (usernameTextField.error || passwordTextField.error) {
return;
}
usernameTextField.enabled = false;
passwordTextField.enabled = false;
loading = true;
Backend.login(usernameTextField.text, Qt.btoa(passwordTextField.text));
}
Layout.fillWidth: true
colorScheme: wizard.colorScheme
enabled: !loading
text: loading ? qsTr("Signing in") : qsTr("Sign in")
onClicked: {
checkAndSignIn();
}
}
Button {
Layout.fillWidth: true
colorScheme: wizard.colorScheme
enabled: !signInButton.loading
secondary: true
secondaryIsOpaque: true
text: qsTr("Cancel")
onClicked: {
root.abort();
}
}
LinkLabel {
Layout.alignment: Qt.AlignHCenter
colorScheme: wizard.colorScheme
external: true
link: "https://proton.me/mail/pricing"
text: qsTr("Create or upgrade your account")
}
}
}
Item {
ColumnLayout {
id: totpLayout
function reset() {
twoFAButton.loading = false;
twoFactorPasswordTextField.enabled = true;
twoFactorPasswordTextField.error = false;
twoFactorPasswordTextField.errorString = "";
twoFactorPasswordTextField.text = "";
}
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: ProtonStyle.wizard_spacing_medium
ColumnLayout {
Layout.fillWidth: true
spacing: ProtonStyle.wizard_spacing_small
Label {
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
colorScheme: wizard.colorScheme
horizontalAlignment: Text.AlignHCenter
text: qsTr("Two-factor authentication")
type: Label.LabelType.Title
}
Label {
id: twoFactorUsernameLabel
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
color: wizard.colorScheme.text_weak
colorScheme: wizard.colorScheme
horizontalAlignment: Text.AlignHCenter
text: ""
type: Label.LabelType.Body
}
}
Label {
id: descriptionLabel
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
colorScheme: wizard.colorScheme
horizontalAlignment: Text.AlignHCenter
text: qsTr("You have enabled two-factor authentication. Please enter the 6-digit code provided by your authenticator application.")
type: Label.LabelType.Body
wrapMode: Text.WordWrap
}
TextField {
id: twoFactorPasswordTextField
Layout.fillWidth: true
colorScheme: wizard.colorScheme
label: qsTr("Two-factor code")
validateOnEditingFinished: false
validator: function (str) {
if (str.length === 0) {
return qsTr("Enter the 6-digit code");
}
}
onAccepted: {
twoFAButton.onClicked();
}
onTextChanged: {
if (text.length >= 6) {
twoFAButton.onClicked();
}
}
}
Button {
id: twoFAButton
Layout.fillWidth: true
colorScheme: wizard.colorScheme
enabled: !loading
text: loading ? qsTr("Authenticating") : qsTr("Authenticate")
onClicked: {
twoFactorPasswordTextField.validate();
if (twoFactorPasswordTextField.error) {
return;
}
twoFactorPasswordTextField.enabled = false;
loading = true;
Backend.login2FA(usernameTextField.text, Qt.btoa(twoFactorPasswordTextField.text));
}
}
Button {
Layout.fillWidth: true
colorScheme: wizard.colorScheme
enabled: !twoFAButton.loading
secondary: true
secondaryIsOpaque: true
text: qsTr("Cancel")
onClicked: {
root.abort();
}
}
}
}
Item {
ColumnLayout {
id: mailboxPasswordLayout
function reset() {
secondPasswordButton.loading = false;
secondPasswordTextField.enabled = true;
secondPasswordTextField.error = false;
secondPasswordTextField.errorString = "";
secondPasswordTextField.text = "";
}
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: ProtonStyle.wizard_spacing_medium
ColumnLayout {
Layout.fillWidth: true
spacing: ProtonStyle.wizard_spacing_small
Label {
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
colorScheme: wizard.colorScheme
horizontalAlignment: Text.AlignHCenter
text: qsTr("Unlock your mailbox")
type: Label.LabelType.Title
}
Label {
id: mailboxPasswordUsernameLabel
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
color: wizard.colorScheme.text_weak
colorScheme: wizard.colorScheme
horizontalAlignment: Text.AlignHCenter
text: ""
type: Label.LabelType.Body
}
}
Label {
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
colorScheme: wizard.colorScheme
horizontalAlignment: Text.AlignHCenter
text: qsTr("You have secured your account with a separate mailbox password.")
type: Label.LabelType.Body
wrapMode: Text.WordWrap
}
TextField {
id: secondPasswordTextField
Layout.fillWidth: true
colorScheme: wizard.colorScheme
echoMode: TextInput.Password
label: qsTr("Mailbox password")
validateOnEditingFinished: false
validator: function (str) {
if (str.length === 0) {
return qsTr("Enter password");
}
}
onAccepted: {
secondPasswordButton.onClicked();
}
}
Button {
id: secondPasswordButton
Layout.fillWidth: true
colorScheme: wizard.colorScheme
enabled: !loading
text: loading ? qsTr("Unlocking") : qsTr("Unlock")
onClicked: {
secondPasswordTextField.validate();
if (secondPasswordTextField.error) {
return;
}
secondPasswordTextField.enabled = false;
loading = true;
Backend.login2Password(usernameTextField.text, Qt.btoa(secondPasswordTextField.text));
}
}
Button {
Layout.fillWidth: true
colorScheme: wizard.colorScheme
enabled: !secondPasswordButton.loading
secondary: true
secondaryIsOpaque: true
text: qsTr("Cancel")
onClicked: {
root.abort();
}
}
}
}
}
}

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