Compare commits

..

271 Commits

Author SHA1 Message Date
a80fd92018 chore: Trift Bridge 3.4.2 changelog. 2023-09-01 15:12:34 +02:00
71063ac5ee fix(GODT-2902): do not check for changed values. Related to GODT-2857. 2023-09-01 14:44:27 +02:00
0a555bf767 chore: Trift Bridge 3.4.1 changelog. 2023-08-14 08:33:51 +02:00
d72980e443 fix(GODT-2859): Fix scope of the function + rename properly. 2023-08-10 08:32:54 +02:00
b24937b666 fix(GODT-2859): Fix lint. 2023-08-10 08:30:46 +02:00
5ca9ec6674 fix(GODT-2859): Trigger user resync while updating from 3.4.0 to 3.4.1. 2023-08-10 08:28:39 +02:00
e6ab874308 chore: Trift Bridge 3.4.1 changelog. 2023-08-08 15:06:16 +02:00
c35344d6f1 fix(GODT-2833): Fix migration of message flags
Migration of message flags was incomplete, leading to incorrect state
after migration.

https://github.com/ProtonMail/gluon/pull/388
2023-08-08 12:36:19 +02:00
a9865976a3 fix(GODT-2759): Use examine rather than select for fetching
When fetching messages in the debug mailbox state command, use read only
mode to avoid modifying the mailbox state.
2023-08-08 09:51:59 +02:00
84adbbc461 chore: Trift Bridge 3.4.0 changelog. 2023-08-01 07:54:57 +02:00
75811d22e8 fix(GODT-2822): rename funcs 2023-07-31 15:21:53 +02:00
e26c7683d2 fix(GODT-2822): event loop behaviour on 429. 2023-07-31 13:52:39 +02:00
d7b71aceda fix(GODT-2822): retry 429 for metadata and exponential cooldown GODT-2823. 2023-07-28 07:58:05 +02:00
25a787529b fix(GODT-2822): Retry on 429 during Message ID fetch 2023-07-27 17:59:13 +02:00
f82965b825 fix(GODT-2812): Fix rare sync deadlock
Copy data rather than hold onto the locks while sync is ongoing. The
data in question does not change while the sync is ongoing and holding
on to the locks during a very long sync can create a deadlock with
due to some IMAP operation that needs to acquire one of those locks with
write access.
2023-07-27 17:48:55 +02:00
f1cf4ee194 fix(GODT-2822): Sync Cache
When the sync fail, store the previously downloaded data in memory so
that on next retry we don't have to re-download everything.
2023-07-27 17:24:20 +02:00
5136919c36 fix(GODT-2822): Handle 429 during message download
When we run into 429 during a message download, do not cancel the whole
batch and switch to a sequential downloader to avoid API overload.
2023-07-27 16:55:18 +02:00
4f8ecd598f chore: Trift Bridge 3.4.0 changelog. 2023-07-13 08:14:44 +02:00
65365281eb chore: merge release/stone with release/trift. 2023-07-13 07:51:19 +02:00
c05dfb36d3 chore: Stone Bridge 3.3.2 changelog. 2023-07-11 17:02:38 +02:00
b2830b39e0 fix(GODT-2782): Filter all labels when doing perma delete check
Previously we were not filtering out labels we ignored from the
perma-delete check. The introduction of new system label types could
break this check leading to user never being able to perma-delete
messages.
2023-07-11 15:25:46 +02:00
7997ad2b93 fix(GODT-2782): Filter all labels when doing perma delete check
Previously we were not filtering out labels we ignored from the
perma-delete check. The introduction of new system label types could
break this check leading to user never being able to perma-delete
messages.
2023-07-11 15:17:25 +02:00
80d743afec chore: Trift Bridge 3.4.0 changelog. 2023-07-06 16:15:55 +02:00
ac9857a965 chore: Merge remote-tracking branch 'origin/devel' into release/trift 2023-07-06 16:13:25 +02:00
0e9fd46a5c chore: Stone Bridge 3.3.1 changelog. 2023-07-06 15:50:18 +02:00
305d180a5f fix(GODT-2774): only check telemetry availability for the current user. 2023-07-06 15:28:24 +02:00
c4f80103b6 fix(GODT-2774): Add external context to telemetry tasks
This ensures they get cancelled if the parent context becomes invalid
2023-07-06 15:28:05 +02:00
0d57e3645a test: Add require.Eventually to TestBridge_UserAgentFromSMTPClient 2023-07-06 14:40:17 +02:00
0afdc31f96 test: Add smtp-send utility
Small program to simulate external SMTP send to Bridge.
2023-07-06 14:06:28 +02:00
91de6e001e fix(GODT-2763): Missing Answered flag on Sync and Message Create
Ensure we are using the same flag conversion code for all IMAP updates.
2023-07-06 14:02:05 +02:00
908ed3e723 feat(GODT-2759): Check for oprhan messages
Orphan messages are messages which are only in AllMail, AllDrafts or
AllSent.
2023-07-06 14:02:05 +02:00
7411073c08 feat(GODT-2759): Add prompt to download missing messages for analysis
This will download the missing messages into a temporary directory and
decrypt them along with the metadata so we can attempt analyze them once
submitted to see what is going wrong.
2023-07-06 14:02:05 +02:00
7d838375bb feat(GODT-2759): CLI debug commands
Add debug commands to CLI to diagnose potential bride problems.
Currently we only have a command which validates whether the state of
all the mailboxes reported by IMAP matches what is currently available
on the proton servers.
2023-07-06 14:02:05 +02:00
f545f30ec0 fix(GODT-2774): only check telemetry availability for the current user. 2023-07-06 13:38:06 +02:00
6579cdfc7f fix(GODT-2774): Add context to Authorize in gluon.Connector
Backported from: https://github.com/ProtonMail/gluon/pull/379
2023-07-06 13:28:58 +02:00
40c48ba804 fix(GODT-2774): Add external context to telemetry tasks
This ensures they get cancelled if the parent context becomes invalid
2023-07-06 13:09:35 +02:00
1c2cb4f439 chore: changelog fix. 2023-07-06 08:45:35 +02:00
650dac37a7 chore: changelog fix. 2023-07-05 12:14:49 +02:00
55275b23ee chore: Trift Bridge 3.4.0 changelog. 2023-07-05 12:13:20 +02:00
bce69e1a1b chore: Merge remote-tracking branch 'origin/devel' into release/trift 2023-07-05 12:07:37 +02:00
f1917ad0de fix(GODT-2758): Fix panic in SetFlagsOnMessages
https://github.com/ProtonMail/gluon/pull/377
2023-07-05 10:57:53 +02:00
6ea6d54af6 chore: Trift Bridge 3.4.0 changelog. 2023-07-04 13:31:16 +02:00
c1b486a7eb chore: Merge remote-tracking branch 'origin/devel' into release/trift 2023-07-04 13:26:46 +02:00
454c9e1534 chore: Stone Bridge 3.3.1 changelog. 2023-07-04 13:09:51 +02:00
eaa673c4e4 fix(GODT-2708): fix dimensions event format + handling of ReportClicked event. 2023-07-04 13:04:30 +02:00
9c389e3007 fix(GODT-2708): fix dimensions event format + handling of ReportClicked event. 2023-07-04 09:59:43 +00:00
7e9a5934c5 feat(GODT-2707): set bridge-gui default log level to 'debug'.
(cherry picked from commit cc1d0e803b)
2023-07-04 11:14:40 +02:00
cc17366c1c fix(GODT-2578): Refresh literals appended to Sent folder
Whenever a message gets moved to the sent folder we should retrieve the
new literal in order to guarantee that, if another client modifies and
sends the message, we always see the latest version of the message and
not a previous state stored in the Gluon cache.

Includes the following Gluon MRs:
* https://github.com/ProtonMail/gluon/pull/374
* https://github.com/ProtonMail/gluon/pull/376

Includes the followin gpa MR:
https://github.com/ProtonMail/go-proton-api/pull/88
2023-07-04 10:39:07 +02:00
62f6db35db chore: Merge remote-tracking branch 'origin/devel' into release/trift. 2023-07-04 08:26:08 +02:00
be422001e8 fix(GODT-2756): fix for 'Settings' context menu opening the 'Help' page.
(cherry picked from commit b2eb35592f)
2023-07-03 16:36:12 +02:00
b2eb35592f fix(GODT-2756): fix for 'Settings' context menu opening the 'Help' page. 2023-07-03 16:35:42 +02:00
a3b26431ce chore: Trift Bridge 3.4.0 changelog. 2023-07-03 12:03:05 +02:00
f460323cc5 chore: Stone Bridge 3.3.1 changelog. 2023-07-03 10:58:12 +02:00
26694d3bd8 feat(GODT-2674): Add more logs during update failed.
(cherry picked from commit 3afd94c61d)

feat(GODT-2674): add more logs to failed update.

(cherry picked from commit 4902898880)
2023-07-03 10:02:59 +02:00
e96713a998 chore: remove gRPC auto-generated C++ source files.
CMake will regenerate the files when needed, they do not need to be under source control.
2023-07-03 09:47:54 +02:00
6359b8639e feat(GODT-2750): disable raise on main window when a notification is clicked on Linux.
(cherry picked from commit 4e080b59d3)
2023-07-03 07:33:39 +00:00
6c9d5ccd4a fix(GODT-2753): vault test now check that value auto-assigned is first available port. 2023-07-03 09:21:01 +02:00
234554b459 feat(GODT-2709): Remove the config status file when user is removed. 2023-07-01 07:21:27 +02:00
6df5a82364 feat(GODT-2749): manual test-windows again. 2023-06-30 15:10:35 +02:00
ac75410657 feat(GODT-2709): Remove the config status file when user is removed. 2023-06-30 14:48:42 +02:00
238929c3ec feat(GODT-2712): Feed config_status with user action while pending. 2023-06-30 09:43:26 +00:00
1f79e3b0a7 feat(GODT-2715): Add Unitary test for configStatus event. 2023-06-30 09:43:26 +00:00
f5af2afce5 feat(GODT-2715): Add Functional test for configStatus telemetry event. 2023-06-30 09:43:26 +00:00
9482bea8af feat(GODT-2714): Apply PR comments. 2023-06-30 09:43:26 +00:00
a55572e5b3 feat(GODT-2714): Set Configuration Status to Failure and send Recovery event when issue is solved. 2023-06-30 09:43:26 +00:00
098eb7cb7a feat(GODT-2713): Send config_progress event once a day if the configuration is stucked in pending for more than a day. 2023-06-30 09:43:26 +00:00
68334e3bb8 feat(GODT-2711): Send config_abort event on User removal. 2023-06-30 09:43:26 +00:00
124231c3c7 feat(GODT-2710): Send config success on IMAP/SMTP connection.. 2023-06-30 09:43:26 +00:00
f591af2cbd feat(GODT-2716): Make Configuration Statistics persistent. 2023-06-30 09:43:26 +00:00
ff11d20d9c feat(GODT-2709): Init Configuration status. 2023-06-30 09:43:26 +00:00
72911235c5 feat(GODT-2748): log calls that cause main window to show, with reason.
(cherry picked from commit a91d9762db)

# Conflicts:
#	internal/frontend/bridge-gui/bridge-gui/QMLBackend.h
2023-06-30 10:37:33 +02:00
60de00c73f feat(GODT-2705): added log entries for focus service on client and server sides.
(cherry picked from commit ae65385c38)
2023-06-30 08:18:23 +00:00
4e080b59d3 feat(GODT-2750): disable raise on main window when a notification is clicked on Linux. 2023-06-30 10:05:07 +02:00
bac4b90c1d feat(GODT-2712): Feed config_status with user action while pending. 2023-06-30 07:51:53 +00:00
ea47c9aa1c feat(GODT-2728): remove the sentry report for gRPC event stream interruptions in bridge-gui.
(cherry picked from commit 58b45d8458)
2023-06-30 09:26:03 +02:00
a91d9762db feat(GODT-2748): log calls that cause main window to show, with reason. 2023-06-29 16:56:47 +02:00
6fb11d69f9 test: Force all unit test to use minimum sync spec 2023-06-29 13:31:46 +02:00
1eab3296d1 Revert "feat(GODT-2749): manual windows-test."
This reverts commit 650158ea8a.
2023-06-29 13:31:21 +02:00
4352154b84 test: Force sync limits to minimum with env variable
Set `BRIDGE_SYNC_FORCE_MINIMUM_SPEC` as environment variable to force
all the sync limits to minimum spec.

This is enabled for windows builds.
2023-06-29 13:31:03 +02:00
03cf601921 feat(GODT-2715): Add Unitary test for configStatus event. 2023-06-29 13:26:08 +02:00
9f13301613 feat(GODT-2715): Add Functional test for configStatus telemetry event. 2023-06-29 13:15:50 +02:00
650158ea8a feat(GODT-2749): manual windows-test. 2023-06-29 11:37:57 +02:00
552fc2700f chore: Disable windows runner 2023-06-29 11:28:43 +02:00
582afa1451 fix(GODT-2726): Fix Parsing of Details field in GPA error message
https://github.com/ProtonMail/go-proton-api/pull/87
2023-06-29 11:24:55 +02:00
c43739a7ef feat(GODT-2714): Apply PR comments. 2023-06-29 09:54:42 +02:00
720f662afe feat(GODT-2714): Set Configuration Status to Failure and send Recovery event when issue is solved. 2023-06-29 09:10:28 +02:00
e9488d12ee fix(GODT-2522): Handle migration with unreferenced db values
https://github.com/ProtonMail/gluon/pull/373
2023-06-29 06:49:54 +00:00
9a87b155ba fix(GODT-2693): Allow missing whitespace after header field colon
https://github.com/ProtonMail/gluon/pull/372/
2023-06-29 06:49:54 +00:00
f6a1cd9b64 fix(GODT-2726): Fix Parsing of Details field in GPA error message
https://github.com/ProtonMail/go-proton-api/pull/87
2023-06-29 06:49:54 +00:00
7b7c9093ce feat(GODT-2691): close logrus output file on exit. 2023-06-28 16:11:40 +02:00
fa4c0ec823 feat(GODT-2713): Send config_progress event once a day if the configuration is stucked in pending for more than a day. 2023-06-28 14:44:20 +02:00
08af1da966 feat(GODT-2711): Send config_abort event on User removal. 2023-06-28 14:40:51 +02:00
ed8475dacf feat(GODT-2710): Send config success on IMAP/SMTP connection.. 2023-06-28 14:39:49 +02:00
e91cdca6f3 feat(GODT-2716): Make Configuration Statistics persistent. 2023-06-28 10:34:01 +00:00
c267168cb7 feat(GODT-2522): New Gluon database layout
All changes were made in Gluon.
2023-06-28 10:49:13 +02:00
58b45d8458 feat(GODT-2728): remove the sentry report for gRPC event stream interruptions in bridge-gui. 2023-06-27 17:08:09 +02:00
b77512dfd9 feat(GODT-2709): Init Configuration status. 2023-06-27 11:24:28 +02:00
bdc6542970 feat(GODT-2678): When internet is off, do not display status dot icon for the user in the context menu. 2023-06-26 11:03:49 +02:00
c942a44f6a feat(GODT-2686): Change the orientation of the expand/collapse arrow for Advanced settings. 2023-06-23 09:01:40 +00:00
55081fa59b test(GODT-2636): Add step for sending from EML 2023-06-23 06:45:31 +00:00
cc1d0e803b feat(GODT-2707): set bridge-gui default log level to 'debug'. 2023-06-22 11:30:40 +00:00
b7a2371220 chore: Log failed message ids during sync 2023-06-22 11:05:36 +00:00
ae65385c38 feat(GODT-2705): added log entries for focus service on client and server sides. 2023-06-22 10:03:15 +00:00
ab70e85f1c fix(GODT-2653): Only log when err is not nil 2023-06-22 10:12:03 +02:00
ff57eb2b43 feat(GODT-2510): Remove Ent
Update Gluon to use new direct SQLite3 implementation.
2023-06-22 06:13:16 +00:00
e78cb8089b test(GODT-2600): Changing state (read/unread, starred/unstarred) of a message in integration tests 2023-06-22 05:13:53 +00:00
09eef64514 feat(GODT-2703): got rid of account details dialog with Apple Mail autoconf. 2023-06-21 10:21:19 +02:00
a2c2710760 feat(GODT-2685): update to bug report log attachment logic. 2023-06-19 16:30:36 +02:00
38a0cdb4ab feat(GODT-2690): update sentry reporting in GUI for new log file naming. 2023-06-14 14:28:38 +02:00
c587dfc0dc feat(GODT-2668): implemented new log retention policy. 2023-06-14 08:44:37 +02:00
7a090ffcc9 test(GODT-2683): Save Draft without "Date" & "From" in headers 2023-06-08 11:24:14 +00:00
6ab49367ba chore: merge branch release/stone to devel 2023-06-08 08:40:31 +02:00
12f9fb03c3 chore: Log errors on failed message Downloads 2023-06-07 16:38:57 +02:00
419a4427d7 chore: Stone Bridge 3.3.0 changelog. 2023-06-07 15:53:56 +02:00
e434a4fd0c fix(GODT-2683): Only validate messages that are not appended to Drafts
https://github.com/ProtonMail/gluon/pull/364
2023-06-07 12:30:49 +00:00
d68b163c20 chore: Stone Bridge 3.3.0 changelog. 2023-06-07 11:57:08 +02:00
71a559ee4c fix(GODT-2683): Reduce message checks when appending into Drafts
https://github.com/ProtonMail/gluon/pull/362
2023-06-07 10:43:49 +02:00
ac00ef1b64 feat(GODT-2666): feat(GODT-2667): introduce sessionID in bridge. 2023-06-07 09:00:33 +02:00
1e9a77c7b2 fix(GODT-2680): fix for C++ debugger not working on ARM64 because of OpenSSL 3.1 2023-06-05 13:37:41 +00:00
4aaa555c9b chore: Stone Bridge 3.3.0 changelog. 2023-06-05 14:35:34 +02:00
a8dd52800e chore: Fix linter errors 2023-06-05 14:07:39 +02:00
fab063f194 feat(GODT-2653): Log API error details on Message import and send 2023-06-05 14:06:04 +02:00
5c606aee73 chore: Fix linter errors 2023-06-05 13:48:56 +02:00
51315d5d2b feat(GODT-2653): Log API error details on Message import and send 2023-06-05 13:21:04 +02:00
cde9c19f71 feat(GODT-2655): display internal build time tag in log and GUI. 2023-06-05 07:37:10 +02:00
4902898880 feat(GODT-2674): add more logs to failed update. 2023-06-05 05:31:14 +00:00
c46b3245b8 feat(GODT-2660): calculate bridge coverage and refactor CI yaml file. 2023-06-02 11:56:10 +00:00
3afd94c61d feat(GODT-2674): Add more logs during update failed. 2023-06-02 11:21:27 +00:00
ade2fd9403 chore: Add error logs when messages fail to build during sync 2023-06-02 13:17:08 +02:00
89632b7acd chore: Fix dependency_license script to handle dot formated version. 2023-06-02 11:38:36 +02:00
b8cf193911 chore: Add error logs when messages fail to build during sync 2023-06-02 10:22:12 +02:00
fb4d34ef5b fix(GODT-2675): Update GPA to applye togin-gonic/gin patch + update COPYING_NOTES. 2023-06-02 09:48:42 +02:00
7f7e360cd7 feat(GODT-2673): Use NoClient as UserAgent without any client connected and... 2023-06-01 15:27:56 +00:00
802f7dbc67 feat(GODT-2673): Use NoClient as UserAgent without any client connected and... 2023-06-01 15:15:37 +00:00
fc06665d2b feat(GODT-2655): display internal build time tag in log and GUI. 2023-06-01 05:24:51 +00:00
c4dc829e6d fix(GODT-2669): Display sentry ID in bridge init log. 2023-05-31 14:28:26 +02:00
bfaf9765ae fix(GODT-2672): fix context cancelled when IMAP/SMTP parameters change is in progress.
(cherry picked from commit 0eab1c0c2b)
2023-05-31 10:59:01 +00:00
a702e19dff fix(GODT-2669): Display sentry ID in bridge init log. 2023-05-31 08:40:15 +02:00
0eab1c0c2b fix(GODT-2672): fix context cancelled when IMAP/SMTP parameters change is in progress. 2023-05-31 08:10:59 +02:00
11f55b59a9 chore: Stone Bridge 3.3.0 changelog. 2023-05-29 10:25:37 +02:00
172b59c756 feat(GODT-2648): make win build work on AWS machine.
- add git safe directory (most probably the runner user is different?)
2023-05-26 13:16:17 +00:00
8564d3a483 fix(GODT-2650): Fix crash during header serialization
Fix crash in go-message when the header field key was greater than the
`preferredHeaderLen` folding line limit.

b62c999c85
2023-05-26 11:54:03 +02:00
0cef181432 fix(GODT-2437): Fix lint. 2023-05-25 09:25:25 +02:00
262c4c5d95 fix(GODT-2437): Fix test + bump Gluon with silenced report. 2023-05-25 09:12:15 +02:00
e343d1f1af fix(GODT-2437): Silence harmless report to sentry. 2023-05-24 17:24:47 +02:00
84a771d9fe fix(GODT-2437): Silence harmless report to sentry. 2023-05-24 17:18:56 +02:00
f6741a9b58 fix(GODT-2649): Clean up cache files after failed connector create (Gluon)
https://github.com/ProtonMail/gluon/pull/357
2023-05-24 06:38:13 +00:00
bc5de2b884 fix(GODT-2638): Validate messages before import.
Ensure messages have required header fields and adhere to the rfc5322
spec before import.
2023-05-24 06:38:13 +00:00
7d54e6907d chore: disable building of bridgepp-test app in build script.
Also enabled verbose cmake output for easier debugging.
2023-05-23 16:06:08 +02:00
4b52d998db chore: merge release/Rialto into devel 2023-05-23 15:53:29 +02:00
aa72fd641d feat(GODT-2631): Bump go to 1.20. 2023-05-23 13:37:12 +00:00
ebe45d5abe fix(GODT-2646): Bump GPA and Gluon dependecy after CIRCL upgrade. 2023-05-23 11:28:51 +02:00
b6eb5a1b13 fix(GODT-2454): Only Send status update if transaction succeeded
https://github.com/ProtonMail/gluon/pull/354
2023-05-23 09:32:54 +02:00
9c25f56fe6 test: fix flaky tests. 2023-05-22 11:16:56 +00:00
bb99695e68 feat(GODT-2639): Enhance sentry init log. 2023-05-22 09:30:51 +00:00
35f0e081a5 fix(GODT-2628): Attempt to fix closed channel panic on logout
It should not be possible to reach this state on purpose. But due to
scheduling and synchronization variances, it is possible in theory that
a UserDeathEvent can occur at the same time as the bridge is closing,
causing a call to `User.Close()` to be executed 2 times.

To avoid this in the future we just clear the map after all the channels
have been closed.
2023-05-17 11:54:02 +00:00
40dc17aea5 feat(GODT-2161): auto-submit 2FA. 2023-05-17 10:10:42 +00:00
5fee2f707b fix(GODT-2627): Properly handle recording of message with Bcc fields
Ensure the SMTP send recorder properly handles the recording of messages
which may have the same body hash but have different recipients. E.g.:
send the same message twice to 2 different users via Bcc.

The send recorder now maintains a list of send requests and waiting for
a message to be sent is done one the oldest of the messages.
2023-05-17 11:01:30 +02:00
d6304b087a fix(GODT-2627): Fix crash on closed channel
See `TestSendHasher_DualAddDoesNotCauseCrash`'s comment for more
details.
2023-05-17 11:01:30 +02:00
21f833ea11 fix(GODT-2307): removed deprecated macOS security framework function. 2023-05-17 07:52:42 +00:00
900caec09e fix(GODT-2637): Fix address parser error due to trailing separator
https://github.com/ProtonMail/gluon/pull/353
2023-05-16 16:06:49 +02:00
9fc9f5ad9f fix(GODT-2635): Ensure Bridge can be compiled with GCC 13
Requires updating vcpkg to include the port fixes.
2023-05-16 11:38:58 +02:00
d8ccc6c05d fix(GODT-2626): Handle rare crash due to missing address update ch
Ensure that we can handle the rare case that can cause a crash if for
whichever reason we end up with an Address Delete and Message
Create/Update in the same event object.
2023-05-16 10:41:36 +02:00
4e3ad4f7fa fix(GODT-2626): Server Events should not be merged.
d18e5932b28f83b201709a04fb7b8c6f74003574
Includes GPA bump: https://github.com/ProtonMail/go-proton-api/pull/80
2023-05-16 09:53:41 +02:00
8c958cdc2f chore: Bump Gluon for GODT-2595, GODT-2634 and GODT-2619
https://github.com/ProtonMail/gluon/pull/350
https://github.com/ProtonMail/gluon/pull/351
https://github.com/ProtonMail/gluon/pull/352
2023-05-16 09:34:45 +02:00
0ef9c9c9c9 fix(GODT-2606): Improve Vault concurrency scopes
Rewrite the vault to have one RWlock rather then two separate locks for
data and reference counts.

In certain circumstance, it could be possible that that different
requests could end up in undefined states if a user got deleted
successfully while at he same time another goroutine/thread is loading
the given user.

While I have not been able to reproduce this in a test, restricting the
access scope to one lock rather than two, should avoid corner cases
where logic code is executing outside of the lock scope.
2023-05-15 13:28:00 +02:00
73bb0ed03e chore: Bridge Rialto 3.2.0 changelog 2023-05-15 12:03:28 +02:00
faf3780eee test: Fix TestBridge_Report 2023-05-12 15:37:33 +02:00
98031d296e Revert "fix(GODT-2588): Always perma-delete from Drafts/Trash"
This reverts commit f9a0c35daa.
2023-05-12 10:19:56 +02:00
d08b3fcca4 chore: extend the timeout for integration test form 20m to 30. 2023-05-12 07:28:41 +00:00
6adb440b84 fix(GODT-2623): log IMAP/SMTP login failure as error. 2023-05-12 07:28:41 +00:00
a3e07428b5 chore: Improve CPC code
* Remove distinction between values with and without reply.
* Hide types that don't need to be public.
* Don't allow direct access to the request's internal types.
2023-05-12 09:06:54 +02:00
dfd85f7ed3 fix(GODT-2625): Update bridge pubkey and add option to verify in hasher. 2023-05-12 07:19:16 +02:00
4b5edd62d0 feat(GODT-2585): Only Start IMAP/SMTP once one user is loaded
Update ServerManager to follow the new expected behavior. The servers
will only be started when one user is active.

If all users are logged out or removed from the system, the servers will
stop.

If the network goes down, the servers will stop and resume once network
has been restored.
2023-05-11 16:27:54 +02:00
fb4a0e77af feat(GODT-2585): Server Manager
Add a dedicated go-routine whose sole responsibility is to manage the
life time of the IMAP and SMTP servers and their listeners.

The current implementation behaves the same way as the previous state.
The new behavior will be implemented in a follow MR.
2023-05-11 09:48:54 +02:00
edac2419f9 feat(GODT-2585): Add CPC utility
Add Channel based RPC utilities.
2023-05-11 09:48:54 +02:00
6ba8052a1e feat(GODT-2621): display pop up warning when IMAP login fails because user is locked (connecting). 2023-05-10 15:53:44 +00:00
51288791c0 fix(GODT-2527): Cleanup 503 test since handled by GPA. 2023-05-10 16:14:18 +02:00
51aafe3266 chore: Bridge Rialto 3.2.0 changelog 2023-05-10 09:15:22 +02:00
8fd09547fa chore: Bridge Rialto 3.2.0 changelog 2023-05-10 09:00:31 +02:00
aba94a9353 chore: merge release Quebec into Rialto
Fix: GODT-2614
Fix: GODT-2616
2023-05-10 08:50:20 +02:00
e6dfb814fd fix(GODT-2616): Updage gluon: GODT-2620 and GODT-2616
Fix: GODT-2616 Silence out of order UID
Fix: GODT-2620 Always defer wait group done on the goroutine to avoid stalls in case of panics.
2023-05-10 08:38:35 +02:00
bfa53b1619 fix(GODT-2588): revert "Always perma-delete from Drafts/Trash"
This reverts commit f9a0c35daa.
2023-05-10 08:24:28 +02:00
b9c54e9276 fix(GODT-2615): remove keyboard shortcut for tray icon context menu on Windows and Linux. 2023-05-09 16:03:22 +02:00
e606d98664 fix(GODT-2613): install the TLS certificate in the user keychain. 2023-05-09 08:55:50 +00:00
36342299c7 chore: Set default log level to Debug 2023-05-09 08:05:11 +00:00
a05f93debd feat(GODT-2520): Update error message for free users. 2023-05-09 07:20:15 +00:00
c438704648 test: Disable sync open files test
It is os specific and it has a tendency to succeed on CI runners.
2023-05-09 08:31:28 +02:00
0417e495ae fix(GODT-2618): Crash when address does not have unlocked keyring 2023-05-09 08:31:28 +02:00
bda158d6c6 feat(GODT-2346): treat external address as disabled one. 2023-05-09 04:12:12 +00:00
ad02c71ad6 fix(GODT-2616): Silence out of Order UID report
https://github.com/ProtonMail/gluon/pull/348
2023-05-08 11:48:13 +00:00
eee2c73a61 feat(GODT-2610): re-use previous password when removing and adding back account. 2023-05-08 11:43:48 +02:00
01aa19edff fix(GODT-2615): remove keyboard shortcut for tray icon context menu on Windows and Linux. 2023-05-08 07:49:09 +02:00
a0db1645f2 fix(GODT-2614): Handle failed update during sync
The sync process was getting stuck since we never handled the case where
the update to Gluon failed. This caused the flush stage to exist, but
the sync process would continue until it eventually gets stuck due to
lack of progress.
2023-05-05 16:09:23 +02:00
324593596a chore: Update Gluon for async.Group.Do() fix
https://github.com/ProtonMail/gluon/pull/346
2023-05-05 14:50:09 +02:00
b51d85e768 chore: upgraded golangci-lint v1.52.2 and fixed all issues. 2023-05-05 07:57:22 +02:00
bd47303074 feat(GODT-2611): bridge CLI exits on the first SIGINT / Ctrl+C. 2023-05-04 11:48:05 +02:00
fdae8cb729 feat(GODT-2540): make icon loading failure behavior consistent. 2023-05-03 16:40:29 +02:00
333daa05c5 feat(GODT-2540): pop-up notification error icon is loaded on startup. 2023-05-03 15:19:02 +02:00
6a6ead8e6d feat(GODT-2540): notify user of wrong IMAP password. 2023-05-03 13:17:25 +02:00
543c35041d fix(GODT-2464): Filter attachment name from content-type parameter to not send it twice to the API. 2023-05-02 07:13:21 +00:00
50709acc5f chore: Bridge Rialto 3.2.0 changelog 2023-04-28 14:26:01 +02:00
994cec196c chore: merge tmp/Quebec into devel 2023-04-28 14:05:16 +02:00
910060a14c fix(GODT-2598): Map Message Size Error to Gluon Error
Prevents messages with invalid size from ending up in the recovery
mailbox.
2023-04-28 11:44:02 +02:00
38b13d710c fix(GODT-2596): fix bug when trying to generate Sentry report and there is not log. 2023-04-28 10:38:26 +02:00
06f710a9b1 fix(GODT-1374): Fix tray icon DPI change handling. 2023-04-28 09:03:55 +02:00
fbbd0245de feat(GODT-2569): Add functional test to validate community PR. 2023-04-27 12:21:48 +00:00
7002806999 feat(GODT-2569): Support multiple externalID matching if we send one of it when looking for parentID.
Change behavior of `getParentID`: when `getParentID` gets called on an
external ID and more than one message with that ID is found, inspect the
metadata flags and if only one of those messages is `MessageFlagSent`,
then choose that as the parent.
2023-04-27 12:21:48 +00:00
c49b42060e chore(GODT-2576): Connector can send any flags to Gluon
Requires Gluon bump: https://github.com/ProtonMail/gluon/pull/344
2023-04-27 11:52:55 +02:00
21b20ac420 fix(GODT-2410): Keep hardcoded version for ICU libs since Qt is looking to load the exact version it has been bundled with. 2023-04-27 10:07:57 +02:00
d8fa2fb3e3 fix(GODT-2582): Update Gluon for updated GetMessageHash
This patch also update getMessageHash to use the fixed version in Gluon.

https://github.com/ProtonMail/gluon/pull/342
2023-04-26 14:53:38 +02:00
ae7621ed6a chore: Bridge Rialto 2.3.0 changelog 2023-04-26 13:24:03 +02:00
f3fb10fb2d feat(GODT-2410): Do not set hard requierement for ICU dependencies. 2023-04-26 09:43:56 +00:00
9241e5317f feat(GODT-2410): Use cmake to require ICU version >= 56. 2023-04-26 09:43:56 +00:00
3ebd2f53f4 chore: merge release/Quebec into devel 2023-04-26 11:32:56 +02:00
59a944a06c feat(GODT-2496): Bump gopenPGP to 2.7.1-proton. 2023-04-26 08:47:47 +00:00
f9a0c35daa fix(GODT-2588): Always perma-delete from Drafts/Trash 2023-04-25 13:49:05 +02:00
e9629aca47 feat(GODT-2517): replace status window with native tray icon context menu.
feat(GODT-2517): show main windows on left click (Linux & Windows).
feat(GODT-2517): removed old QML status window.
feat(GODT-2517): polishing.
feat(GODT-2517): renaming + removal of dead code (v2 tests).
2023-04-25 11:17:57 +00:00
7383d65cb2 fix(GODT-2582): Dedup recovered messages folder
https://github.com/ProtonMail/gluon/pull/338
https://github.com/ProtonMail/gluon/pull/339
https://github.com/ProtonMail/gluon/pull/340
2023-04-25 10:24:51 +02:00
3ef3ab72ed feat(GODT-2553): Try to send telemetry every hour. 2023-04-24 18:33:50 +00:00
00adb8bc22 feat(GODT-2552): Fix review comment + use only string for Heartbeat Dimension. 2023-04-24 18:33:50 +00:00
d3fc9a50f6 feat(GODT-2556): Add functional test for Heartbeat Init and telemetry availability. 2023-04-24 18:33:50 +00:00
4adc6d60b9 feat(GODT-2552): Fix unit test. 2023-04-24 18:33:50 +00:00
d88bee68c6 feat(GODT-2552): Add functional test. 2023-04-24 18:33:50 +00:00
67b5e7f96a feat(GODT-2552): Add unit test. 2023-04-24 18:33:50 +00:00
b250d49af8 feat(GODT-2552): Send first heartbeat. 2023-04-24 18:33:50 +00:00
0f621d0aad feat(GODT-2552): Init telemetry heartbeat. 2023-04-24 18:33:50 +00:00
6ddaa94bc0 fix(GODT-2589): update BUILDS.md. 2023-04-24 17:07:37 +02:00
fed503501d feat(GODT-2575): Add dev info to cookies. 2023-04-24 12:44:08 +02:00
ce5a559926 feat(GODT-2586): Two-columns layout for account details. 2023-04-24 08:42:08 +02:00
3b297fa37b feat(GODT-2580): updated link to support website in GUI. 2023-04-20 10:40:19 +02:00
95741c6d63 fix(GODT-2581): Update outdated link to bridge homepage in CLI 'manual' command. 2023-04-20 09:01:57 +02:00
ad4e853a8a feat(GODT-2555): fixed minor issue spotted during review. 2023-04-20 07:53:54 +02:00
f4631c4bc9 feat(GODT-2555): add local telemetry settings.
feat(GODT-2555): add 'TelemetryDisabled' settings to vault.
feat(GODT-2555): CLI and GUI implementation.
feat(GODT-2555): implemented setting in bridge-gui-tester.
feat(GODT-2555): added unit tests.
feat(GODT-2555): feature tests.
2023-04-19 16:45:42 +02:00
c000ee8a3c feat(GODT-2239): bridgepp worker/overseer unit tests. 2023-04-17 20:23:29 +02:00
3ddd88e127 feat(GODT-2538): implement smart picking of default IMAP/SMTP ports 2023-04-17 13:26:47 +00:00
54b209f9e1 fix(GODT-2337): filter reply-to on draft. 2023-04-17 10:25:10 +02:00
4e7a669260 chore: Bridge Quebec 3.1.2 changelog. 2023-04-14 10:47:39 +02:00
8093bbf5f6 feat(GODT-2502): Additional info. 2023-04-14 10:27:37 +02:00
7bb925b6d7 feat(GODT-2502): Improve logs. 2023-04-14 10:27:37 +02:00
d6760d6f50 chore(GODT-2551): Store and Recover Last User Agent from Vault 2023-04-14 09:48:39 +02:00
2191dc70dc fix(GODT-2550): Announce IMAP ID Capability
https://github.com/ProtonMail/gluon/pull/337
2023-04-13 15:02:41 +02:00
ff44a3d6df test(GODT-2550): Verify IMAP ID is set properly 2023-04-13 14:28:53 +02:00
b0c3faa228 fix(GODT-2574): Fix label/unlabel of large amounts of messages
Requires update to GPA:
https://github.com/ProtonMail/go-proton-api/pull/75
2023-04-13 11:25:16 +02:00
3928ed08f6 feat(GODT-2554): Compute telemetry availability from API UserSettings. 2023-04-13 08:06:48 +00:00
c7ae239350 fix(GODT-2573): Handle invalid header fields in message
Requires updating Gluon and GPA

https://github.com/ProtonMail/gluon/pull/336
https://github.com/ProtonMail/go-proton-api/pull/74
2023-04-12 10:18:14 +02:00
7d51e9123d chore: merge release/quebec to devel 2023-04-12 08:52:12 +02:00
a8cb0012e1 test: add missing double quotes in test
During integration test training, it was noticed that double-quotes were
missing after a specific regex.
Added them both in the *.feature and bdd_test.go files where applicable.
2023-04-05 12:31:03 +00:00
c84919faae chore: merge release/Quebec into devel 2023-04-05 13:55:22 +02:00
3735d4b327 feat(GODT-2239): unit tests for BridgeUtils.cpp in bridgepp. 2023-04-05 13:19:35 +02:00
1323229362 feat(GODT-2239): introduce GoogleTest unit tests for bridgepp. 2023-04-03 16:52:41 +02:00
0a53dc1da7 feat(GODT-2523): use software QML rendering backend by default on Windows.
(cherry picked from commit 934749b278e95a9d69818ddf6b45ee7bb896af03)
(cherry picked from commit 7448b814e8e6da90335b8db465fd64e3d4f08bdd)
2023-04-03 09:58:53 +02:00
86fd7961a1 fix(GODT-2526): Fix high memory usage with fetch/search
https://github.com/ProtonMail/gluon/pull/333
2023-03-29 14:52:46 +02:00
aa957f3314 test: Disable TestBridge503DuringEventDoesNotCauseBadEvent
Changes to GPA 503 handling is now causing GPA client to treat this as
context cancelled rather than producing 503. To be re-enabled as part of
GODT-2527.
2023-03-28 14:35:13 +02:00
7124e7c9a0 fix(GODT-2514): Apply Retry-After to 503
https://github.com/ProtonMail/go-proton-api/pull/67
2023-03-28 10:52:06 +02:00
421da99cd8 fix(GODT-2524): Preserve old vault values
Keep the record of old vault settings alive to avoid issues when one
downgrades from a 3.1 release to a 3.0.x release.
2023-03-28 10:51:45 +02:00
ef1940d227 fix(GODT-2508): Handle Address Updated for none-existing address
If we receive an address update and the address does not exist, attempt
to create it.
2023-03-28 10:51:26 +02:00
f1fb525dc8 fix(GODT-2504): Fix missing attachments in imported message
https://github.com/ProtonMail/go-proton-api/pull/64
2023-03-23 13:39:20 +01:00
74203e07a4 fix(GODT-2513): Scanner Crash in Gluon
Gluon MR: https://github.com/ProtonMail/gluon/pull/330
2023-03-22 13:20:18 +01:00
58df3312c1 feat(GODT-2509): Migrate TLS cert from v1/v2 location during upgrade to v3 2023-03-22 11:01:51 +01:00
4c94606e20 fix(GODT-2516): log error when the vault key cannot be created/loaded from the keychain.
Backported from release/perth-narrows.
2023-03-22 09:10:26 +01:00
ee5e87fe85 test: Add 503 request test for message create event
Simulate 503 status during a message create event when the message
data is being downloaded.
2023-03-21 13:47:01 +00:00
dbd156a272 fix(GODT-2512): Catch unhandled API errors
Bump GPA https://github.com/ProtonMail/go-proton-api/pull/63
2023-03-21 13:47:01 +00:00
03c3e95b34 feat(GODT-2511): add bridge-gui switches to permanently select the QML rendering backend. 2023-03-21 12:30:50 +01:00
a8250ff65e fix(GODT-2507): Memory consumption bug
Fix was made in Gluon: https://github.com/ProtonMail/gluon/pull/329
2023-03-21 08:47:26 +01:00
d8bf2cc25e fix(GODT-2418): Do not issue connector renames for inferiors
Bump Gluon (https://github.com/ProtonMail/gluon/pull/328) and add test.
2023-03-20 15:01:53 +01:00
4f49c87bc6 chore: Replace go-rfc5322 with gluon's rfc5322 parser
Bumps Gluon version in order to get the fixes from
https://github.com/ProtonMail/gluon/pull/327.
2023-03-17 16:23:36 +01:00
02ca6428b5 fix(GODT-2407): Replace invalid email addresses with emtpy for new Drafts 2023-03-17 13:25:42 +01:00
b3d0dfc60b fix(GODT-2497): Do not report EOF and network errors 2023-03-16 14:19:58 +00:00
622b76b57b feat(GODT-2483): install cert without external tool on macOS. 2023-03-16 13:53:21 +00:00
e1dff67c10 fix(GODT-2481): Fix DBUS Secert Service
Fix the path we are checking for was not updated for V3.

Ensure that we only inspect items that start with the correct prefix.
Some implementation (e.g.: KeepassXC) return some values which are not
valid.

Finally, remove unnecessary attributes.
2023-03-16 11:48:41 +01:00
307 changed files with 15165 additions and 51867 deletions

4
.gitignore vendored
View File

@ -41,3 +41,7 @@ cmake-build-*/
# Doxygen doc files
_doc/
# gRPC auto-generated C++ source files
*.pb.cc
*.pb.h

View File

@ -16,7 +16,7 @@
# along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
---
image: harbor.protontech.ch/docker.io/library/golang:1.18
image: gitlab.protontech.ch:4567/go/bridge-internal:test-go1.20
variables:
GOPRIVATE: gitlab.protontech.ch
@ -30,13 +30,6 @@ stages:
- test
- build
.rules-branch-and-MR-always:
rules:
- if: $CI_COMMIT_BRANCH || $CI_PIPELINE_SOURCE == "merge_request_event"
when: always
allow_failure: false
- when: never
.rules-branch-and-MR-manual:
rules:
- if: $CI_COMMIT_BRANCH || $CI_PIPELINE_SOURCE == "merge_request_event"
@ -44,16 +37,6 @@ stages:
allow_failure: true
- when: never
.rules-branch-manual-MR-always:
rules:
- if: $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-MR-and-devel-always:
rules:
- if: $CI_COMMIT_BRANCH == "devel" || $CI_PIPELINE_SOURCE == "merge_request_event"
@ -64,42 +47,92 @@ stages:
allow_failure: true
- when: never
.after-script-code-coverage:
after_script:
- go get github.com/boumenot/gocover-cobertura
- go run github.com/boumenot/gocover-cobertura < /tmp/coverage.out > coverage.xml
- "go tool cover -func=/tmp/coverage.out | grep total:"
coverage: '/total:.*\(statements\).*\d+\.\d+%/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
# 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-and-MR-always
- .rules-branch-manual-MR-and-devel-always
script:
- make lint
tags:
- medium
.test-base:
.script-test:
stage: test
extends:
- .rules-branch-manual-MR-and-devel-always
script:
- make test
artifacts:
paths:
- coverage/**
test-linux:
extends:
- .test-base
- .rules-branch-manual-MR-and-devel-always
- .after-script-code-coverage
- .script-test
tags:
- medium
- large
test-linux-race:
extends:
@ -113,8 +146,6 @@ test-integration:
- test-linux
script:
- make test-integration
tags:
- large
test-integration-race:
extends:
@ -123,41 +154,48 @@ test-integration-race:
script:
- make test-integration-race
.windows-base:
before_script:
- export GOROOT=/c/Go1.18
- export PATH=$GOROOT/bin:$PATH
- export GOARCH=amd64
- export GOPATH=~/go18
- export GO111MODULE=on
- export PATH=$GOPATH/bin:$PATH
- export MSYSTEM=
tags:
- windows-bridge
test-windows:
extends:
- .rules-branch-manual-MR-always
- .windows-base
- .env-windows
- .script-test
- .rules-branch-and-MR-manual
test-darwin:
extends:
- .env-darwin
- .script-test
test-coverage:
stage: test
extends:
- .rules-branch-manual-MR-and-devel-always
script:
- make test
- ./utils/coverage.sh
coverage: '/total:.*\(statements\).*\d+\.\d+%/'
needs:
- test-linux
#- test-windows
- test-darwin
- test-integration
tags:
- small
artifacts:
paths:
- coverage*
- coverage/**
when: 'always'
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
# Stage: BUILD
.build-base:
.script-build:
stage: build
needs: ["lint"]
rules:
# GODT-1833: use `=~ /qa/` after mac and windows runners are fixed
- if: $CI_JOB_NAME =~ /build-linux-qa/ && $CI_PIPELINE_SOURCE == "merge_request_event"
when: always
allow_failure: false
- if: $CI_COMMIT_BRANCH || $CI_PIPELINE_SOURCE == "merge_request_event"
when: manual
allow_failure: true
- when: never
extends:
- .rules-branch-and-MR-manual
script:
- make build
- git diff && git diff-index --quiet HEAD
@ -170,58 +208,22 @@ test-windows:
- bridge_*.tgz
- vault-editor
.linux-build-setup:
image: gitlab.protontech.ch:4567/go/bridge-internal:qt6
variables:
VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.cache
cache:
key: linux-vcpkg
paths:
- .cache
when: 'always'
before_script:
- mkdir -p .cache/bin
- 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
build-linux:
extends:
- .build-base
- .linux-build-setup
- .script-build
- .env-linux-build
build-linux-qa:
extends:
- build-linux
- .rules-branch-manual-MR-and-devel-always
variables:
BUILD_TAGS: "build_qa"
.darwin-build-setup:
before_script:
- 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/go@1.13/bin:$PATH
- export PATH=/usr/local/opt/gnu-sed/libexec/gnubin:$PATH
- export GOPATH=~/go
- export PATH=$GOPATH/bin:$PATH
- export CGO_CPPFLAGS='-Wno-error -Wno-nullability-completeness -Wno-expansion-to-defined -Wno-builtin-requires-header'
- $(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}
cache: {}
tags:
- macOS
build-darwin:
extends:
- .build-base
- .darwin-build-setup
- .script-build
- .env-darwin
build-darwin-qa:
extends:
@ -229,28 +231,10 @@ build-darwin-qa:
variables:
BUILD_TAGS: "build_qa"
.windows-build-setup:
before_script:
- export GOROOT=/c/Go1.18/
- export PATH=$GOROOT/bin:$PATH
- export GOARCH=amd64
- export GOPATH=~/go18
- export GO111MODULE=on
- export PATH="${GOPATH}/bin:${PATH}"
- export MSYSTEM=
- export QT6DIR=/c/grrrQt/6.3.1/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}
cache: {}
tags:
- windows-bridge
build-windows:
extends:
- .build-base
- .windows-build-setup
- .script-build
- .env-windows
build-windows-qa:
extends:

View File

@ -36,6 +36,14 @@ issues:
- gosec
- goconst
- dogsled
- path: utils/smtp-send
linters:
- dupl
- gochecknoglobals
- gochecknoinits
- gosec
- goconst
- dogsled
linters-settings:
godox:
@ -48,18 +56,15 @@ linters:
disable-all: true
enable:
- deadcode # Finds unused code [fast: true, auto-fix: false]
- errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases [fast: true, auto-fix: false]
- gosimple # Linter for Go source code that specializes in simplifying a code [fast: true, auto-fix: false]
- govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string [fast: true, auto-fix: false]
- ineffassign # Detects when assignments to existing variables are not used [fast: true, auto-fix: false]
- staticcheck # Staticcheck is a go vet on steroids, applying a ton of static analysis checks [fast: true, auto-fix: false]
- structcheck # Finds unused struct fields [fast: true, auto-fix: false]
- typecheck # Like the front-end of a Go compiler, parses and type-checks Go code [fast: true, auto-fix: false]
- unused # Checks Go code for unused constants, variables, functions and types [fast: false, auto-fix: false]
- varcheck # Finds unused global variables and constants [fast: true, auto-fix: false]
- bodyclose # checks whether HTTP response body is closed successfully [fast: true, auto-fix: false]
- depguard # Go linter that checks if package imports are in a list of acceptable packages [fast: true, auto-fix: false]
#- depguard # Go linter that checks if package imports are in a list of acceptable packages [fast: true, auto-fix: false]
- dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) [fast: true, auto-fix: false]
- dupl # Tool for code clone detection [fast: true, auto-fix: false]
- gochecknoglobals # Checks that no globals are present in Go code [fast: true, auto-fix: false]
@ -119,3 +124,8 @@ linters:
# - testpackage # linter that makes you use a separate _test package [fast: true, auto-fix: false]
# - thelper # thelper detects golang test helpers without t.Helper() call and checks the consistency of test helpers [fast: false, auto-fix: false]
# - wrapcheck # Checks that errors returned from external packages are wrapped [fast: false, auto-fix: false]
# Deprecated:
# - structcheck # Finds unused struct fields [fast: true, auto-fix: false] deprecated (since v1.49.0) due to: The owner seems to have abandoned the linter. Replaced by unused.
# - deadcode # Finds unused code [fast: true, auto-fix: false] deprecated (since v1.49.0) due to: The owner seems to have abandoned the linter. Replaced by unused.
# - varcheck # Finds unused global variables and constants [fast: true, auto-fix: false] deprecated (since v1.49.0) due to: The owner seems to have abandoned the linter. Replaced by unused.

View File

@ -3,12 +3,15 @@
## Prerequisites
* 64-bit OS:
- the go-rfc5322 module cannot currently be compiled for 32-bit OSes
* Go 1.18
* Go 1.20
* Bash with basic build utils: make, gcc, sed, find, grep, ...
- For Windows, it is recommended to use MinGW 64bit shell from [MSYS2](https://www.msys2.org/)
* GCC (linux), msvc (windows) or Xcode (macOS)
* Windres (windows)
* libglvnd and libsecret development files (linux)
* GCC (Linux), msvc (Windows) or Xcode (macOS)
* Windres (Windows)
* libglvnd and libsecret development files (Linux)
* pkg-config (Linux)
* cmake, ninja-build and Qt 6 are required to build the graphical user interface. On Linux,
the Mesa OpenGL development files are also needed.
To enable the sending of crash reports using Sentry please set the
`DSN_SENTRY` environment variable with the client key of your sentry project before build.

View File

@ -43,7 +43,6 @@ Proton Mail Bridge includes the following 3rd party software:
* [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)
* [resty](https://github.com/go-resty/resty/v2) available under [license](https://github.com/go-resty/resty/v2/blob/master/LICENSE)
* [go-json](https://github.com/goccy/go-json) available under [license](https://github.com/goccy/go-json/blob/master/LICENSE)
* [dbus](https://github.com/godbus/dbus) available under [license](https://github.com/godbus/dbus/blob/master/LICENSE)
* [mock](https://github.com/golang/mock) available under [license](https://github.com/golang/mock/blob/master/LICENSE)
* [go-cmp](https://github.com/google/go-cmp) available under [license](https://github.com/google/go-cmp/blob/master/LICENSE)
@ -59,7 +58,7 @@ Proton Mail Bridge includes the following 3rd party software:
* [testify](https://github.com/stretchr/testify) available under [license](https://github.com/stretchr/testify/blob/master/LICENSE)
* [cli](https://github.com/urfave/cli/v2) available under [license](https://github.com/urfave/cli/v2/blob/master/LICENSE)
* [msgpack](https://github.com/vmihailenco/msgpack/v5) available under [license](https://github.com/vmihailenco/msgpack/v5/blob/master/LICENSE)
* [goleak](https://go.uber.org/goleak)
* [goleak](https://go.uber.org/goleak) available under [license](https://pkg.go.dev/go.uber.org/goleak?tab=licenses)
* [exp](https://golang.org/x/exp) available under [license](https://cs.opensource.google/go/x/exp/+/master:LICENSE)
* [net](https://golang.org/x/net) available under [license](https://cs.opensource.google/go/x/net/+/master:LICENSE)
* [sys](https://golang.org/x/sys) available under [license](https://cs.opensource.google/go/x/sys/+/master:LICENSE)
@ -67,16 +66,12 @@ Proton Mail Bridge includes the following 3rd party software:
* [grpc](https://google.golang.org/grpc) available under [license](https://github.com/grpc/grpc-go/blob/master/LICENSE)
* [protobuf](https://google.golang.org/protobuf) available under [license](https://github.com/protocolbuffers/protobuf/blob/main/LICENSE)
* [plist](https://howett.net/plist) available under [license](https://github.com/DHowett/go-plist/blob/main/LICENSE)
* [atlas](https://ariga.io/atlas)
* [ent](https://entgo.io/ent)
* [bcrypt](https://github.com/ProtonMail/bcrypt) available under [license](https://github.com/ProtonMail/bcrypt/blob/master/LICENSE)
* [go-crypto](https://github.com/ProtonMail/go-crypto) available under [license](https://github.com/ProtonMail/go-crypto/blob/master/LICENSE)
* [go-mime](https://github.com/ProtonMail/go-mime) available under [license](https://github.com/ProtonMail/go-mime/blob/master/LICENSE)
* [go-srp](https://github.com/ProtonMail/go-srp) available under [license](https://github.com/ProtonMail/go-srp/blob/master/LICENSE)
* [readline](https://github.com/abiosoft/readline) available under [license](https://github.com/abiosoft/readline/blob/master/LICENSE)
* [levenshtein](https://github.com/agext/levenshtein) available under [license](https://github.com/agext/levenshtein/blob/master/LICENSE)
* [cascadia](https://github.com/andybalholm/cascadia) available under [license](https://github.com/andybalholm/cascadia/blob/master/LICENSE)
* [go-textseg](https://github.com/apparentlymart/go-textseg/v13) available under [license](https://github.com/apparentlymart/go-textseg/v13/blob/master/LICENSE)
* [sonic](https://github.com/bytedance/sonic) available under [license](https://github.com/bytedance/sonic/blob/master/LICENSE)
* [base64x](https://github.com/chenzhuoyu/base64x) available under [license](https://github.com/chenzhuoyu/base64x/blob/master/LICENSE)
* [test](https://github.com/chzyer/test) available under [license](https://github.com/chzyer/test/blob/master/LICENSE)
@ -91,12 +86,13 @@ Proton Mail Bridge includes the following 3rd party software:
* [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)
* [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)
* [sse](https://github.com/gin-contrib/sse) available under [license](https://github.com/gin-contrib/sse/blob/master/LICENSE)
* [gin](https://github.com/gin-gonic/gin) available under [license](https://github.com/gin-gonic/gin/blob/master/LICENSE)
* [inflect](https://github.com/go-openapi/inflect) available under [license](https://github.com/go-openapi/inflect/blob/master/LICENSE)
* [locales](https://github.com/go-playground/locales) available under [license](https://github.com/go-playground/locales/blob/master/LICENSE)
* [universal-translator](https://github.com/go-playground/universal-translator) available under [license](https://github.com/go-playground/universal-translator/blob/master/LICENSE)
* [validator](https://github.com/go-playground/validator/v10) available under [license](https://github.com/go-playground/validator/v10/blob/master/LICENSE)
* [go-json](https://github.com/goccy/go-json) available under [license](https://github.com/goccy/go-json/blob/master/LICENSE)
* [uuid](https://github.com/gofrs/uuid) available under [license](https://github.com/gofrs/uuid/blob/master/LICENSE)
* [protobuf](https://github.com/golang/protobuf) available under [license](https://github.com/golang/protobuf/blob/master/LICENSE)
* [pprof](https://github.com/google/pprof) available under [license](https://github.com/google/pprof/blob/master/LICENSE)
@ -104,7 +100,6 @@ Proton Mail Bridge includes the following 3rd party software:
* [go-immutable-radix](https://github.com/hashicorp/go-immutable-radix) available under [license](https://github.com/hashicorp/go-immutable-radix/blob/master/LICENSE)
* [go-memdb](https://github.com/hashicorp/go-memdb) available under [license](https://github.com/hashicorp/go-memdb/blob/master/LICENSE)
* [golang-lru](https://github.com/hashicorp/golang-lru) available under [license](https://github.com/hashicorp/golang-lru/blob/master/LICENSE)
* [hcl](https://github.com/hashicorp/hcl/v2) available under [license](https://github.com/hashicorp/hcl/v2/blob/master/LICENSE)
* [multierror](https://github.com/joeshaw/multierror) available under [license](https://github.com/joeshaw/multierror/blob/master/LICENSE)
* [go](https://github.com/json-iterator/go) available under [license](https://github.com/json-iterator/go/blob/master/LICENSE)
* [cpuid](https://github.com/klauspost/cpuid/v2) available under [license](https://github.com/klauspost/cpuid/v2/blob/master/LICENSE)
@ -113,7 +108,6 @@ Proton Mail Bridge includes the following 3rd party software:
* [go-isatty](https://github.com/mattn/go-isatty) available under [license](https://github.com/mattn/go-isatty/blob/master/LICENSE)
* [go-runewidth](https://github.com/mattn/go-runewidth) available under [license](https://github.com/mattn/go-runewidth/blob/master/LICENSE)
* [go-sqlite3](https://github.com/mattn/go-sqlite3) available under [license](https://github.com/mattn/go-sqlite3/blob/master/LICENSE)
* [go-wordwrap](https://github.com/mitchellh/go-wordwrap) available under [license](https://github.com/mitchellh/go-wordwrap/blob/master/LICENSE)
* [concurrent](https://github.com/modern-go/concurrent) available under [license](https://github.com/modern-go/concurrent/blob/master/LICENSE)
* [reflect2](https://github.com/modern-go/reflect2) available under [license](https://github.com/modern-go/reflect2/blob/master/LICENSE)
* [tablewriter](https://github.com/olekukonko/tablewriter) available under [license](https://github.com/olekukonko/tablewriter/blob/master/LICENSE)
@ -129,14 +123,13 @@ 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)
* [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)
* [go-cty](https://github.com/zclconf/go-cty) available under [license](https://github.com/zclconf/go-cty/blob/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)
* [mod](https://golang.org/x/mod) available under [license](https://cs.opensource.google/go/x/mod/+/master:LICENSE)
* [sync](https://golang.org/x/sync) available under [license](https://cs.opensource.google/go/x/sync/+/master:LICENSE)
* [tools](https://golang.org/x/tools) available under [license](https://cs.opensource.google/go/x/tools/+/master:LICENSE)
* [genproto](https://google.golang.org/genproto)
gopkg.in/yaml.v3
* [genproto](https://google.golang.org/genproto) available under [license](https://pkg.go.dev/google.golang.org/genproto?tab=licenses)
* [yaml](https://gopkg.in/yaml.v3) available under [license](https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE)
* [docker-credential-helpers](https://github.com/ProtonMail/docker-credential-helpers) available under [license](https://github.com/ProtonMail/docker-credential-helpers/blob/master/LICENSE)
* [go-message](https://github.com/ProtonMail/go-message) available under [license](https://github.com/ProtonMail/go-message/blob/master/LICENSE)
* [go-keychain](https://github.com/cuthix/go-keychain) available under [license](https://github.com/cuthix/go-keychain/blob/master/LICENSE)

View File

@ -2,6 +2,206 @@
Changelog [format](http://keepachangelog.com/en/1.0.0/)
## Trift Bridge 3.4.2
### Fixed
* GODT-2902: Do not check for changed values. Related to GODT-2857.
## Trift Bridge 3.4.1
### Fixed
* GODT-2859: Trigger user resync while updating from 3.4.0 to 3.4.1.
* GODT-2833: Fix migration of message flags.
* GODT-2759: Use examine rather than select for fetching.
## Trift Bridge 3.4.0
### Added
### Changed
* Test: Add require.Eventually to TestBridge_UserAgentFromSMTPClient.
* Test: Add smtp-send utility.
* GODT-2759: Check for oprhan messages.
* GODT-2759: Add prompt to download missing messages for analysis.
* GODT-2759: CLI debug commands.
* Remove gRPC auto-generated C++ source files.
* Test: Force all unit test to use minimum sync spec.
* Test: Force sync limits to minimum with env variable.
* GODT-2691: Close logrus output file on exit.
* GODT-2522: New Gluon database layout.
* GODT-2678: When internet is off, do not display status dot icon for the user in the context menu.
* GODT-2686: Change the orientation of the expand/collapse arrow for Advanced settings.
* Test(GODT-2636): Add step for sending from EML.
* Log failed message ids during sync.
* GODT-2510: Remove Ent.
* Test(GODT-2600): Changing state (read/unread, starred/unstarred) of a message in integration tests.
* GODT-2703: Got rid of account details dialog with Apple Mail autoconf.
* GODT-2685: Update to bug report log attachment logic.
* GODT-2690: Update sentry reporting in GUI for new log file naming.
* GODT-2668: Implemented new log retention policy.
* Test(GODT-2683): Save Draft without "Date" & "From" in headers.
* GODT-2666: Feat(GODT-2667): introduce sessionID in bridge.
* GODT-2660: Calculate bridge coverage and refactor CI yaml file.
* Fix dependency_license script to handle dot formated version.
### Fixed
* GODT-2812: Fix rare sync deadlock.
* GODT-2822: Better handling 429 during sync and event loop.
* GODT-2763: Missing Answered flag on Sync and Message Create.
* GODT-2758: Fix panic in SetFlagsOnMessages.
* GODT-2578: Refresh literals appended to Sent folder.
* GODT-2753: Vault test now check that value auto-assigned is first available port.
* GODT-2522: Handle migration with unreferenced db values.
* GODT-2693: Allow missing whitespace after header field colon.
* GODT-2653: Only log when err is not nil.
* GODT-2680: Fix for C++ debugger not working on ARM64 because of OpenSSL 3.1.
* GODT-2675: Update GPA to applye togin-gonic/gin patch + update COPYING_NOTES.
## Stone Bridge 3.3.2
### Fixed
* GODT-2782: Filter all labels when doing perma delete check.
## Stone Bridge 3.3.1
### Changed
* GODT-2707: Set bridge-gui default log level to 'debug'.
* GODT-2674: Add more logs during update failed.
* GODT-2750: Disable raise on main window when a notification is clicked on Linux.
* GODT-2709: Remove the config status file when user is removed.
* GODT-2748: Log calls that cause main window to show, with reason.
* GODT-2705: Added log entries for focus service on client and server sides.
* GODT-2712: Feed config_status with user action while pending.
* GODT-2728: Remove the sentry report for gRPC event stream interruptions in bridge-gui.
* GODT-2715: Add Unitary test for configStatus event.
* GODT-2715: Add Functional test for configStatus telemetry event.
* Disable windows runner.
* GODT-2714: Apply PR comments.
* GODT-2714: Set Configuration Status to Failure and send Recovery event when issue is solved.
* GODT-2713: Send config_progress event once a day if the configuration is stucked in pending for more than a day.
* GODT-2711: Send config_abort event on User removal.
* GODT-2710: Send config success on IMAP/SMTP connection..
* GODT-2716: Make Configuration Statistics persistent.
* GODT-2709: Init Configuration status.
* Log errors on failed message Downloads.
### Fixed
* GODT-2774: Only check telemetry availability for the current user.
* GODT-2774: Add external context to telemetry tasks.
* GODT-2774: Add context to Authorize in `gluon.Connector`.
* GODT-2726: Fix Parsing of Details field in GPA error message.
* GODT-2708: Fix dimensions event format + handling of ReportClicked event.
* GODT-2756: Fix for 'Settings' context menu opening the 'Help' page.
## Stone Bridge 3.3.0
### Changed
* GODT-2653: Log API error details on Message import and send.
* GODT-2655: Display internal build time tag in log and GUI.
* Add error logs when messages fail to build during sync.
* GODT-2673: Use NoClient as UserAgent without any client connected and...
* GODT-2648: Make win build work on AWS machine.
* Disable building of bridgepp-test app in build script.
* GODT-2631: Bump go to 1.20.
* GODT-2639: Enhance sentry init log.
* GODT-2161: Auto-submit 2FA.
* Bump Gluon for GODT-2595, GODT-2634 and GODT-2619.
* Test: Fix TestBridge_Report.
* Extend the timeout for integration test form 20m to 30.
* Improve CPC code.
* GODT-2585: Only Start IMAP/SMTP once one user is loaded.
* GODT-2585: Server Manager.
* GODT-2585: Add CPC utility.
* GODT-2621: Display pop up warning when IMAP login fails because user is locked (connecting).
* Set default log level to Debug.
* GODT-2520: Update error message for free users.
* Test: Disable sync open files test.
* GODT-2346: Treat external address as disabled one.
* GODT-2610: Re-use previous password when removing and adding back account.
* GODT-2611: Bridge CLI exits on the first SIGINT / Ctrl+C.
* GODT-2540: Make icon loading failure behavior consistent.
* GODT-2540: Pop-up notification error icon is loaded on startup.
* GODT-2540: Notify user of wrong IMAP password.
### Fixed
* GODT-2683: Only validate messages that are not appended to Drafts.
* GODT-2683: Reduce message checks when appending into Drafts.
* Fix linter errors.
* GODT-2669: Display sentry ID in bridge init log.
* GODT-2672: Fix context cancelled when IMAP/SMTP parameters change is in progress.
* GODT-2650: Fix crash during header serialization.
* GODT-2437: Fix lint, test + bump Gluon with silenced report.
* GODT-2437: Silence harmless report to sentry.
* GODT-2649: Clean up cache files after failed connector create (Gluon).
* GODT-2638: Validate messages before import.
* GODT-2646: Bump GPA and Gluon dependecy after CIRCL upgrade.
* GODT-2454: Only Send status update if transaction succeeded.
* Test: fix flaky tests.
* GODT-2628: Attempt to fix closed channel panic on logout.
* GODT-2627: Properly handle recording of message with Bcc fields.
* GODT-2627: Fix crash on closed channel.
* GODT-2307: Removed deprecated macOS security framework function.
* GODT-2637: Fix address parser error due to trailing separator.
* GODT-2635: Ensure Bridge can be compiled with GCC 13.
* GODT-2626: Handle rare crash due to missing address update ch.
* GODT-2626: Server Events should not be merged.
* GODT-2606: Improve Vault concurrency scopes.
* GODT-2623: Log IMAP/SMTP login failure as error.
* GODT-2527: Cleanup 503 test since handled by GPA.
* GODT-2613: Install the TLS certificate in the user keychain.
* GODT-2618: Crash when address does not have unlocked keyring.
* GODT-2616: Silence out of Order UID report.
* Update Gluon for async.Group.Do() fix.
* Upgraded golangci-lint v1.52.2 and fixed all issues.
* GODT-2464: Filter attachment name from content-type parameter to not send it twice to the API.
## [Bridge 3.2.0] Rialto
### Added
* GODT-2552, GODT-2553, GODT-2555, GODT-2556: Add telemetry.
* GODT-2575: Add dev info to cookies.
### Changed
* GODT-2598: Map Message Size Error to Gluon Error.
* GODT-2569: Support multiple externalID matching if we send one of it when looking for parentID.
* GODT-2576: Connector can send any flags to Gluon.
* GODT-2496: Bump gopenPGP to 2.7.1-proton.
* GODT-2517: Replace status window with native tray icon context menu.
* GODT-2586: Two-columns layout for account details.
* GODT-2580: Updated link to support website in GUI.
* GODT-2239: Bridgepp worker/overseer unit tests.
* GODT-2538: Implement smart picking of default IMAP/SMTP ports.
* GODT-2502: Improve logs.
* GODT-2551: Store and Recover Last User Agent from Vault.
* GODT-2550: Verify IMAP ID is set properly.
* GODT-2554: Compute telemetry availability from API UserSettings.
* Add missing double quotes in test.
* GODT-2239: Unit tests for BridgeUtils.cpp in bridgepp.
* Replace go-rfc5322 with gluon's rfc5322 parser.
* GODT-2483: Install cert without external tool on macOS.
### Fixed
* GODT-2625: Update Bridge pubkey for updates.
* GODT-2620: Avoid stalls in case of panic in gluon.
* GODT-2615: Remove keyboard shortcut for tray icon context menu on Windows and Linux.
* GODT-2596: Fix bug when trying to generate Sentry report and there is not log.
* GODT-1374: Fix tray icon DPI change handling.
* GODT-2589: Update BUILDS.md.
* GODT-2581: Update outdated link to bridge homepage in CLI 'manual' command.
* GODT-2337: Filter reply-to on draft.
* GODT-2550: Announce IMAP ID Capability.
* GODT-2574: Fix label/unlabel of large amounts of messages.
* GODT-2573: Handle invalid header fields in message.
* GODT-2573: Crash on null update.
* GODT-2407: Replace invalid email addresses with emtpy for new Drafts.
## [Bridge 3.1.3] Quebec
### Changed
@ -14,7 +214,6 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
### Changed
* GODT-2582 Dedup recovered messages folder.
## [Bridge 3.1.1] Quebec
### Fixed

View File

@ -11,7 +11,7 @@ ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
.PHONY: build build-gui build-nogui build-launcher versioner hasher
# Keep version hardcoded so app build works also without Git repository.
BRIDGE_APP_VERSION?=3.1.3+git
BRIDGE_APP_VERSION?=3.4.2+git
APP_VERSION:=${BRIDGE_APP_VERSION}
APP_FULL_NAME:=Proton Mail Bridge
APP_VENDOR:=Proton AG
@ -19,7 +19,8 @@ SRC_ICO:=bridge.ico
SRC_ICNS:=Bridge.icns
SRC_SVG:=bridge.svg
EXE_NAME:=proton-bridge
REVISION:=$(shell git rev-parse --short=10 HEAD)
REVISION:=$(shell ./utils/get_revision.sh)
TAG:=$(shell ./utils/get_revision.sh tag)
BUILD_TIME:=$(shell date +%FT%T%z)
MACOS_MIN_VERSION_ARM64=11.0
MACOS_MIN_VERSION_AMD64=10.15
@ -27,7 +28,7 @@ BUILD_ENV?=dev
BUILD_FLAGS:=-tags='${BUILD_TAGS}'
BUILD_FLAGS_LAUNCHER:=${BUILD_FLAGS}
GO_LDFLAGS:=$(addprefix -X github.com/ProtonMail/proton-bridge/v3/internal/constants., Version=${APP_VERSION} Revision=${REVISION} BuildTime=${BUILD_TIME})
GO_LDFLAGS:=$(addprefix -X github.com/ProtonMail/proton-bridge/v3/internal/constants., Version=${APP_VERSION} Revision=${REVISION} Tag=${TAG} BuildTime=${BUILD_TIME})
GO_LDFLAGS+=-X "github.com/ProtonMail/proton-bridge/v3/internal/constants.FullAppName=${APP_FULL_NAME}"
ifneq "${DSN_SENTRY}" ""
@ -158,6 +159,7 @@ ${EXE_TARGET}: check-build-essentials ${EXE_NAME}
BRIDGE_VENDOR="${APP_VENDOR}" \
BRIDGE_APP_VERSION=${APP_VERSION} \
BRIDGE_REVISION=${REVISION} \
BRIDGE_TAG=${TAG} \
BRIDGE_DSN_SENTRY=${DSN_SENTRY} \
BRIDGE_BUILD_TIME=${BUILD_TIME} \
BRIDGE_GUI_BUILD_CONFIG=Release \
@ -183,7 +185,7 @@ ${RESOURCE_FILE}: ./dist/info.rc ./dist/${SRC_ICO} .FORCE
## Dev dependencies
.PHONY: install-devel-tools install-linter install-go-mod-outdated install-git-hooks
LINTVER:="v1.50.0"
LINTVER:="v1.52.2"
LINTSRC:="https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh"
install-dev-dependencies: install-devel-tools install-linter install-go-mod-outdated
@ -227,14 +229,28 @@ add-license:
change-copyright-year:
./utils/missing_license.sh change-year
GOCOVERAGE=-covermode=count -coverpkg=github.com/ProtonMail/proton-bridge/v3/internal/...,github.com/ProtonMail/proton-bridge/v3/pkg/...,
GOCOVERDIR=-args -test.gocoverdir=$$PWD/coverage
test: gofiles
go test -v -timeout=10m -p=1 -count=1 -coverprofile=/tmp/coverage.out -run=${TESTRUN} ./internal/... ./pkg/...
mkdir -p coverage/unit-${GOOS}
go test \
-v -timeout=20m -p=1 -count=1 \
${GOCOVERAGE} \
-run=${TESTRUN} ./internal/... ./pkg/... \
${GOCOVERDIR}/unit-${GOOS}
test-race: gofiles
go test -v -timeout=30m -p=1 -count=1 -race -failfast -run=${TESTRUN} ./internal/... ./pkg/...
go test -v -timeout=40m -p=1 -count=1 -race -failfast -run=${TESTRUN} ./internal/... ./pkg/...
test-integration: gofiles
go test -v -timeout=20m -p=1 -count=1 github.com/ProtonMail/proton-bridge/v3/tests
mkdir -p coverage/integration
go test \
-v -timeout=60m -p=1 -count=1 \
${GOCOVERAGE} \
github.com/ProtonMail/proton-bridge/v3/tests \
${GOCOVERDIR}/integration
test-integration-debug: gofiles
dlv test github.com/ProtonMail/proton-bridge/v3/tests -- -test.v -test.timeout=10m -test.parallel=1 -test.count=1
@ -256,6 +272,9 @@ mocks:
mockgen --package mocks github.com/ProtonMail/gluon/async PanicHandler > internal/bridge/mocks/async_mocks.go
mockgen --package mocks github.com/ProtonMail/gluon/reporter Reporter > internal/bridge/mocks/gluon_mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/updater Downloader,Installer > internal/updater/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/telemetry HeartbeatManager > internal/telemetry/mocks/mocks.go
cp internal/telemetry/mocks/mocks.go internal/bridge/mocks/telemetry_mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/user MessageDownloader > internal/user/mocks/mocks.go
lint: gofiles lint-golang lint-license lint-dependencies lint-changelog

View File

@ -18,7 +18,7 @@
package main
import (
"fmt"
"io"
"os"
"path/filepath"
"runtime"
@ -44,9 +44,10 @@ import (
)
const (
appName = "Proton Mail Launcher"
exeName = "bridge"
guiName = "bridge-gui"
appName = "Proton Mail Launcher"
exeName = "bridge"
guiName = "bridge-gui"
launcherName = "launcher"
FlagCLI = "cli"
FlagCLIShort = "c"
@ -54,6 +55,7 @@ const (
FlagNonInteractiveShort = "n"
FlagLauncher = "--launcher"
FlagWait = "--wait"
FlagSessionID = "--session-id"
)
func main() { //nolint:funlen
@ -76,12 +78,26 @@ func main() { //nolint:funlen
if err != nil {
l.WithError(err).Fatal("Failed to get logs path")
}
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath))
if err := logging.Init(logsPath, os.Getenv("VERBOSITY")); err != nil {
sessionID := logging.NewSessionID()
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath, sessionID, launcherName))
var closer io.Closer
if closer, err = logging.Init(
logsPath,
sessionID,
logging.LauncherShortAppName,
logging.DefaultMaxLogFileSize,
logging.NoPruning,
os.Getenv("VERBOSITY"),
); err != nil {
l.WithError(err).Fatal("Failed to setup logging")
}
defer func() {
_ = logging.Close(closer)
}()
updatesPath, err := locations.ProvideUpdatesPath()
if err != nil {
l.WithError(err).Fatal("Failed to get updates path")
@ -108,7 +124,7 @@ func main() { //nolint:funlen
args := os.Args[1:]
exe, err := getPathToUpdatedExecutable(filepath.Base(launcher), versioner, kr, reporter)
exe, err := getPathToUpdatedExecutable(filepath.Base(launcher), versioner, kr)
if err != nil {
exeToLaunch := guiName
if inCLIMode(args) {
@ -135,7 +151,7 @@ func main() { //nolint:funlen
}
}
cmd := execabs.Command(exe, appendLauncherPath(launcher, args)...) //nolint:gosec
cmd := execabs.Command(exe, appendLauncherPath(launcher, append(args, FlagSessionID, string(sessionID)))...) //nolint:gosec
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
@ -218,7 +234,6 @@ func getPathToUpdatedExecutable(
name string,
ver *versioner.Versioner,
kr *crypto.KeyRing,
reporter *sentry.Reporter,
) (string, error) {
versions, err := ver.ListVersions()
if err != nil {
@ -240,10 +255,6 @@ func getPathToUpdatedExecutable(
if err := version.VerifyFiles(kr); err != nil {
vlog.WithError(err).Error("Files failed verification and will be removed")
if err := reporter.ReportMessage(fmt.Sprintf("version %v failed verification: %v", version, err)); err != nil {
vlog.WithError(err).Error("Failed to report corrupt update files")
}
if err := version.Remove(); err != nil {
vlog.WithError(err).Error("Failed to remove files")
}

2
extern/vcpkg vendored

75
go.mod
View File

@ -1,18 +1,18 @@
module github.com/ProtonMail/proton-bridge/v3
go 1.18
go 1.20
require (
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
github.com/Masterminds/semver/v3 v3.2.0
github.com/ProtonMail/gluon v0.15.1-0.20230508082626-2467e0b45ed9
github.com/ProtonMail/gluon v0.16.1-0.20230901124123-075229a92cc4
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
github.com/ProtonMail/go-proton-api v0.4.1-0.20230406143739-c7596e170799
github.com/ProtonMail/gopenpgp/v2 v2.5.2
github.com/ProtonMail/go-proton-api v0.4.1-0.20230727082922-9115b4750ec7
github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton
github.com/PuerkitoBio/goquery v1.8.1
github.com/abiosoft/ishell v2.0.0+incompatible
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37
github.com/bradenaw/juniper v0.10.2
github.com/bradenaw/juniper v0.12.0
github.com/cucumber/godog v0.12.5
github.com/cucumber/messages-go/v16 v16.0.1
github.com/docker/docker-credential-helpers v0.6.3
@ -25,7 +25,6 @@ require (
github.com/fatih/color v1.13.0
github.com/getsentry/sentry-go v0.15.0
github.com/go-resty/resty/v2 v2.7.0
github.com/goccy/go-json v0.10.0
github.com/godbus/dbus v4.1.0+incompatible
github.com/golang/mock v1.6.0
github.com/google/go-cmp v0.5.9
@ -37,35 +36,31 @@ require (
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58
github.com/pkg/errors v0.9.1
github.com/pkg/profile v1.7.0
github.com/sirupsen/logrus v1.9.0
github.com/stretchr/testify v1.8.1
github.com/sirupsen/logrus v1.9.2
github.com/stretchr/testify v1.8.3
github.com/urfave/cli/v2 v2.24.4
github.com/vmihailenco/msgpack/v5 v5.3.5
go.uber.org/goleak v1.2.1
golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb
golang.org/x/net v0.7.0
golang.org/x/sys v0.5.0
golang.org/x/text v0.7.0
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
golang.org/x/net v0.10.0
golang.org/x/sys v0.8.0
golang.org/x/text v0.9.0
google.golang.org/grpc v1.53.0
google.golang.org/protobuf v1.28.1
google.golang.org/protobuf v1.30.0
howett.net/plist v1.0.0
)
require (
ariga.io/atlas v0.9.1-0.20230119145809-92243f7c55cb // indirect
entgo.io/ent v0.11.8 // indirect
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect
github.com/ProtonMail/go-mime v0.0.0-20221031134845-8fd9bc37cf08 // indirect
github.com/ProtonMail/go-srp v0.0.5 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 // indirect
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect
github.com/ProtonMail/go-srp v0.0.7 // indirect
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect
github.com/agext/levenshtein v1.2.3 // indirect
github.com/andybalholm/cascadia v1.3.1 // indirect
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
github.com/bytedance/sonic v1.8.1 // indirect
github.com/andybalholm/cascadia v1.3.2 // indirect
github.com/bytedance/sonic v1.9.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/chzyer/test v1.0.0 // indirect
github.com/cloudflare/circl v1.3.2 // indirect
github.com/cloudflare/circl v1.3.3 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/cronokirby/saferith v0.33.0 // indirect
github.com/cucumber/gherkin-go/v19 v19.0.3 // indirect
@ -73,15 +68,16 @@ require (
github.com/davecgh/go-spew v1.1.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-vcard v0.0.0-20220507122617-d4056df0ec4a // indirect
github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3 // indirect
github.com/felixge/fgprof v0.9.3 // indirect
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.9.0 // indirect
github.com/go-openapi/inflect v0.19.0 // indirect
github.com/gin-gonic/gin v1.9.1 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.11.2 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/gofrs/uuid v4.3.0+incompatible // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect
@ -89,20 +85,18 @@ require (
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/go-memdb v1.3.3 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/hashicorp/hcl/v2 v2.16.1 // indirect
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/mattn/go-sqlite3 v1.14.16 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/mattn/go-sqlite3 v1.14.17 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/pierrec/lz4/v4 v4.1.17 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
@ -111,21 +105,20 @@ require (
github.com/spf13/pflag v1.0.5 // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.10 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
github.com/zclconf/go-cty v1.12.1 // indirect
golang.org/x/arch v0.2.0 // indirect
golang.org/x/crypto v0.6.0 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.9.0 // indirect
golang.org/x/mod v0.8.0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/tools v0.3.1-0.20221202221704-aa9f4b2f3d57 // indirect
golang.org/x/sync v0.2.0 // indirect
golang.org/x/tools v0.6.0 // indirect
google.golang.org/genproto v0.0.0-20230221151758-ace64dc21148 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace (
github.com/docker/docker-credential-helpers => github.com/ProtonMail/docker-credential-helpers v1.1.0
github.com/emersion/go-message => github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753
github.com/keybase/go-keychain => github.com/cuthix/go-keychain v0.0.0-20220405075754-31e7cee908fe
github.com/emersion/go-message => github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7
github.com/keybase/go-keychain => github.com/cuthix/go-keychain v0.0.0-20230517073537-fc1740a83768
)

164
go.sum
View File

@ -1,5 +1,3 @@
ariga.io/atlas v0.9.1-0.20230119145809-92243f7c55cb h1:mbsFtavDqGdYwdDpP50LGOOZ2hgyGoJcZeOpbgKMyu4=
ariga.io/atlas v0.9.1-0.20230119145809-92243f7c55cb/go.mod h1:T230JFcENj4ZZzMkZrXFDSkv+2kXkUgpJ5FQQ5hMcKU=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
@ -13,13 +11,10 @@ cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqCl
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
entgo.io/ent v0.11.8 h1:M/M0QL1CYCUSdqGRXUrXhFYSDRJPsOOrr+RLEej/gyQ=
entgo.io/ent v0.11.8/go.mod h1:ericBi6Q8l3wBH1wEIDfKxw7rcQEuRPyBfbIzjtxJ18=
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557 h1:l6surSnJ3RP4qA1qmKJ+hQn3UjytosdoG27WGjrDlVs=
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557/go.mod h1:sTrmvD/TxuypdOERsDOS7SndZg0rzzcCi1b6wQMXUYM=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
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/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
@ -28,40 +23,37 @@ github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
github.com/ProtonMail/gluon v0.15.1-0.20230508082626-2467e0b45ed9 h1:UwxjrPVOgyLLSVFhnxkpXLItowNzGGacqwVCp01wn6E=
github.com/ProtonMail/gluon v0.15.1-0.20230508082626-2467e0b45ed9/go.mod h1:yA4hk6CJw0BMo+YL8Y3ckCYs5L20sysu9xseshwY3QI=
github.com/ProtonMail/gluon v0.16.1-0.20230901124123-075229a92cc4 h1:Uq2v2NYEtlTaK2WTh9BMph2Kv51JxMgvTkd7CjGPYc8=
github.com/ProtonMail/gluon v0.16.1-0.20230901124123-075229a92cc4/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo=
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
github.com/ProtonMail/go-crypto v0.0.0-20230124153114-0acdc8ae009b/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g=
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 h1:wPbRQzjjwFc0ih8puEVAOFGELsn1zoIIYdxvML7mDxA=
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g=
github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753 h1:I8IsYA297x0QLU80G5I6aLYUu3JYNSpo8j5fkXtFDW0=
github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
github.com/ProtonMail/go-mime v0.0.0-20221031134845-8fd9bc37cf08 h1:dS7r5z4iGS0qCjM7UwWdsEMzQesUQbGcXdSm2/tWboA=
github.com/ProtonMail/go-mime v0.0.0-20221031134845-8fd9bc37cf08/go.mod h1:qRZgbeASl2a9OwmsV85aWwRqic0NHPh+9ewGAzb4cgM=
github.com/ProtonMail/go-proton-api v0.4.1-0.20230406143739-c7596e170799 h1:slk4Drrkij1EVTnFOlIDyJsfjt69tnw8w2g1NMb253U=
github.com/ProtonMail/go-proton-api v0.4.1-0.20230406143739-c7596e170799/go.mod h1:kis4GD6FHp1ZWnenSBepldt8ai+vYalDPeey9yGwyXk=
github.com/ProtonMail/go-srp v0.0.5 h1:xhUioxZgDbCnpo9JehyFhwwsn9JLWkUGfB0oiKXgiGg=
github.com/ProtonMail/go-srp v0.0.5/go.mod h1:06iYHtLXW8vjLtccWj++x3MKy65sIT8yZd7nrJF49rs=
github.com/ProtonMail/gopenpgp/v2 v2.5.2 h1:97SjlWNAxXl9P22lgwgrZRshQdiEfAht0g3ZoiA1GCw=
github.com/ProtonMail/gopenpgp/v2 v2.5.2/go.mod h1:52qDaCnto6r+CoWbuU50T77XQt99lIs46HtHtvgFO3o=
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-20230518184743-7afd39499903 h1:ZK3C5DtzV2nVAQTx5S5jQvMeDqWtD1By5mOoyY/xJek=
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/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
github.com/ProtonMail/go-proton-api v0.4.1-0.20230727082922-9115b4750ec7 h1:Rmg3TPK6vFGNWR4hxmPoBhV75Sl716iB46wEi2U4Q+c=
github.com/ProtonMail/go-proton-api v0.4.1-0.20230727082922-9115b4750ec7/go.mod h1:+aTJoYu8bqzGECXL2DOdiZTZ64bGn3w0NC8VcFpJrFM=
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton h1:YS6M20yvjCJPR1r4ADW5TPn6rahs4iAyZaACei86bEc=
github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton/go.mod h1:S1lYsaGHykYpxxh2SnJL6ypcAlANKj5NRSY6HxKryKQ=
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw=
github.com/abiosoft/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzgHf/aS6+zVXRj14cVk9qg=
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8=
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db/go.mod h1:rB3B4rKii8V21ydCbIzH5hZiCQE7f5E9SzUb/ZZx530=
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37 h1:28uU3TtuvQ6KRndxg9TrC868jBWmSKgh0GTXkACCXmA=
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37/go.mod h1:6AXRstqK+32jeFmw89QGL2748+dj34Av4xc/I9oo9BY=
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw=
github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
@ -69,12 +61,12 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/bradenaw/juniper v0.10.2 h1:EY7r8SJJrigJ7lvWk6ews3K5RD4XTG9z+WSwHJKijP4=
github.com/bradenaw/juniper v0.10.2/go.mod h1:Z2B7aJlQ7xbfWsnMLROj5t/5FQ94/MkIdKC30J4WvzI=
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/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.8.1 h1:NqAHCaGaTzro0xMmnTCLUyRlbEP6r8MCA1cJUrH3Pu4=
github.com/bytedance/sonic v1.8.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
@ -88,8 +80,8 @@ github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
github.com/cloudflare/circl v1.3.2 h1:VWp8dY3yH69fdM7lM6A1+NhhVoDu9vqK0jOgmkQHFWk=
github.com/cloudflare/circl v1.3.2/go.mod h1:+CauBF6R70Jqcyl8N2hC8pAXYbWkGIezuSbuGLtRhnw=
github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
@ -107,8 +99,8 @@ github.com/cucumber/godog v0.12.5/go.mod h1:u6SD7IXC49dLpPN35kal0oYEjsXZWee4pW6T
github.com/cucumber/messages-go/v16 v16.0.0/go.mod h1:EJcyR5Mm5ZuDsKJnT2N9KRnBK30BGjtYotDKpwQ0v6g=
github.com/cucumber/messages-go/v16 v16.0.1 h1:fvkpwsLgnIm0qugftrw2YwNlio+ABe2Iu94Ap8GMYIY=
github.com/cucumber/messages-go/v16 v16.0.1/go.mod h1:EJcyR5Mm5ZuDsKJnT2N9KRnBK30BGjtYotDKpwQ0v6g=
github.com/cuthix/go-keychain v0.0.0-20220405075754-31e7cee908fe h1:KRj3wdvA9yE92prNmOjS7x5DOqoyjxqdE30qnrmTasc=
github.com/cuthix/go-keychain v0.0.0-20220405075754-31e7cee908fe/go.mod h1:ZoZU1fnBy3mOLWr3Pg+Y2+nTKtu6ypDte2kZg9HvSwY=
github.com/cuthix/go-keychain v0.0.0-20230517073537-fc1740a83768 h1:Jrcoxtrk4qpuzKIYPlEkjIK0M+bABs0oW2QzrOuwlzk=
github.com/cuthix/go-keychain v0.0.0-20230517073537-fc1740a83768/go.mod h1:ZoZU1fnBy3mOLWr3Pg+Y2+nTKtu6ypDte2kZg9HvSwY=
github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg=
github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0=
github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0=
@ -132,8 +124,8 @@ github.com/emersion/go-smtp v0.15.1-0.20221021114529-49b17434419d h1:hFRM6zCBSc+
github.com/emersion/go-smtp v0.15.1-0.20221021114529-49b17434419d/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/emersion/go-vcard v0.0.0-20220507122617-d4056df0ec4a h1:cltZpe6s0SJtqK5c/5y2VrIYi8BAtDM6qjmiGYqfTik=
github.com/emersion/go-vcard v0.0.0-20220507122617-d4056df0ec4a/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3 h1:hQ1wTMaKcGfobYRT88RM8NFNyX+IQHvagkm/tqViU98=
github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
@ -142,33 +134,32 @@ github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNu
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BMXYYRWTLOJKlh+lOBt6nUQgXAfB7oVIQt5cNreqSLI=
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:rZfgFAXFS/z/lEd6LJmf9HVZ1LkgYiHx5pHhV5DR16M=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/getsentry/sentry-go v0.15.0 h1:CP9bmA7pralrVUedYZsmIHWpq/pBtXTSew7xvVpfLaA=
github.com/getsentry/sentry-go v0.15.0/go.mod h1:RZPJKSw+adu8PBNygiri/A98FqVr2HtRckJk9XVxJ9I=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8=
github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4=
github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU=
github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s=
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4=
github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
@ -243,8 +234,6 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/hcl/v2 v2.16.1 h1:BwuxEMD/tsYgbhIW7UuI3crjovf3MzuFWiVgiv57iHg=
github.com/hashicorp/hcl/v2 v2.16.1/go.mod h1:JRmR89jycNkrrqnMmvPDMd56n1rQJ2Q6KocSLCMCXng=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
@ -277,9 +266,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
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/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
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/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
@ -289,13 +277,13 @@ github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
@ -304,8 +292,6 @@ github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceT
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
@ -324,8 +310,8 @@ github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FI
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/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc=
github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
@ -354,17 +340,16 @@ github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8=
github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y=
github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
@ -393,14 +378,16 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.10 h1:eimT6Lsr+2lzmSZxPhLFoOWFmQqwk0fllJJ5hEbTXtQ=
github.com/ugorji/go/codec v1.2.10/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/urfave/cli/v2 v2.24.4 h1:0gyJJEBYtCV87zI/x2nZCPyDxD51K6xM8SkwjHFCNEU=
github.com/urfave/cli/v2 v2.24.4/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
@ -412,8 +399,6 @@ github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRT
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
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/zclconf/go-cty v1.12.1 h1:PcupnljUm9EIvbgSHQnHhUr3fO6oFmkOrvs2BAFNXXY=
github.com/zclconf/go-cty v1.12.1/go.mod h1:s9IfD1LK5ccNMSWCVFCE2rJfHiZgi7JijgeWIMfhLvA=
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.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
@ -423,26 +408,25 @@ go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.2.0 h1:W1sUEHXiJTfjaFJ5SLo0N6lZn+0eO5gWD1MFeTGqQEY=
golang.org/x/arch v0.2.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/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-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb h1:PaBZQdo+iSDyHT053FjUCgZQ/9uqVwPOcl7KSWhKn6w=
golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -454,7 +438,6 @@ golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHl
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mobile v0.0.0-20221110043201-43a038452099/go.mod h1:aAjjkJNdrh3PMckS4B10TGS2nag27cbKR1y2BpUxsiY=
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.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@ -481,8 +464,12 @@ golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qx
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.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -493,9 +480,9 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190423024810-112230192c58/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-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -525,11 +512,16 @@ golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBc
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-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
@ -537,8 +529,10 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/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/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -562,8 +556,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.3.1-0.20221202221704-aa9f4b2f3d57 h1:/X0t/E4VxbZE7MLS7auvE7YICHeVvbIa9vkOVvYW/24=
golang.org/x/tools v0.3.1-0.20221202221704-aa9f4b2f3d57/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -595,8 +589,8 @@ google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc=
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
google.golang.org/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.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@ -19,13 +19,13 @@ package app
import (
"fmt"
"math/rand"
"io"
"net/http"
"net/http/cookiejar"
"net/url"
"os"
"path/filepath"
"runtime"
"time"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/gluon/async"
@ -35,6 +35,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/crash"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/focus"
"github.com/ProtonMail/proton-bridge/v3/internal/frontend/theme"
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
"github.com/ProtonMail/proton-bridge/v3/internal/sentry"
@ -76,10 +77,12 @@ const (
flagNoWindow = "no-window"
flagParentPID = "parent-pid"
flagSoftwareRenderer = "software-renderer"
flagSessionID = "session-id"
)
const (
appUsage = "Proton Mail IMAP and SMTP Bridge"
appUsage = "Proton Mail IMAP and SMTP Bridge"
appShortName = "bridge"
)
func New() *cli.App {
@ -150,6 +153,10 @@ func New() *cli.App {
Hidden: true,
Value: false,
},
&cli.StringFlag{
Name: flagSessionID,
Hidden: true,
},
}
app.Action = run
@ -158,9 +165,6 @@ func New() *cli.App {
}
func run(c *cli.Context) error {
// Seed the default RNG from the math/rand package.
rand.Seed(time.Now().UnixNano())
// Get the current bridge version.
version, err := semver.NewVersion(constants.Version)
if err != nil {
@ -186,6 +190,11 @@ func run(c *cli.Context) error {
exe = os.Args[0]
}
var logCloser io.Closer
defer func() {
_ = logging.Close(logCloser)
}()
// Restart the app if requested.
return withRestarter(exe, func(restarter *restarter.Restarter) error {
// Handle crashes with various actions.
@ -202,7 +211,9 @@ func run(c *cli.Context) error {
}
// Initialize logging.
return withLogging(c, crashHandler, locations, func() error {
return withLogging(c, crashHandler, locations, func(closer io.Closer) error {
logCloser = closer
// If there was an error during migration, log it now.
if migrationErr != nil {
logrus.WithError(migrationErr).Error("Failed to migrate old app data")
@ -217,16 +228,6 @@ func run(c *cli.Context) error {
return withSingleInstance(settings, locations.GetLockFile(), version, func() error {
// Unlock the encrypted vault.
return WithVault(locations, crashHandler, func(v *vault.Vault, insecure, corrupt bool) error {
// Report insecure vault.
if insecure {
_ = reporter.ReportMessageWithContext("Vault is insecure", map[string]interface{}{})
}
// Report corrupt vault.
if corrupt {
_ = reporter.ReportMessageWithContext("Vault is corrupt", map[string]interface{}{})
}
if !v.Migrated() {
// Migrate old settings into the vault.
if err := migrateOldSettings(v); err != nil {
@ -244,6 +245,15 @@ func run(c *cli.Context) error {
}
}
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.
@ -258,6 +268,9 @@ func run(c *cli.Context) error {
b.PushError(bridge.ErrVaultCorrupt)
}
// Start telemetry heartbeat process
b.StartHeartbeat(b)
// Run the frontend.
return runFrontend(c, crashHandler, restarter, locations, b, eventCh, quitCh, c.Int(flagParentPID))
})
@ -299,7 +312,7 @@ func withSingleInstance(settingPath, lockFile string, version *semver.Version, f
}
// Initialize our logging system.
func withLogging(c *cli.Context, crashHandler *crash.Handler, locations *locations.Locations, fn func() error) error {
func withLogging(c *cli.Context, crashHandler *crash.Handler, locations *locations.Locations, fn func(closer io.Closer) error) error {
logrus.Debug("Initializing logging")
defer logrus.Debug("Logging stopped")
@ -312,23 +325,34 @@ func withLogging(c *cli.Context, crashHandler *crash.Handler, locations *locatio
logrus.WithField("path", logsPath).Debug("Received logs path")
// Initialize logging.
if err := logging.Init(logsPath, c.String(flagLogLevel)); err != nil {
sessionID := logging.NewSessionIDFromString(c.String(flagSessionID))
var closer io.Closer
if closer, err = logging.Init(
logsPath,
sessionID,
logging.BridgeShortAppName,
logging.DefaultMaxLogFileSize,
logging.DefaultPruningSize,
c.String(flagLogLevel),
); err != nil {
return fmt.Errorf("could not initialize logging: %w", err)
}
// Ensure we dump a stack trace if we crash.
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath))
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath, sessionID, appShortName))
logrus.
WithField("appName", constants.FullAppName).
WithField("version", constants.Version).
WithField("revision", constants.Revision).
WithField("tag", constants.Tag).
WithField("build", constants.BuildTime).
WithField("runtime", runtime.GOOS).
WithField("args", os.Args).
WithField("SentryID", sentry.GetProtectedHostname()).
Info("Run app")
return fn()
return fn(closer)
}
// WithLocations provides access to locations where we store our files.
@ -417,6 +441,10 @@ func withCookieJar(vault *vault.Vault, fn func(http.CookieJar) error) error {
return fmt.Errorf("could not create cookie jar: %w", err)
}
if err := setDeviceCookies(persister); err != nil {
return fmt.Errorf("could not set device cookies: %w", err)
}
// Persist the cookies to the vault when we close.
defer func() {
logrus.Debug("Persisting cookies")
@ -428,3 +456,21 @@ func withCookieJar(vault *vault.Vault, fn func(http.CookieJar) error) error {
return fn(persister)
}
func setDeviceCookies(jar *cookies.Jar) error {
url, err := url.Parse(constants.APIHost)
if err != nil {
return err
}
for name, value := range map[string]string{
"hhn": sentry.GetProtectedHostname(),
"tz": sentry.GetTimeZone(),
"lng": sentry.GetSystemLang(),
"clr": string(theme.DefaultTheme()),
} {
jar.SetCookies(url, []*http.Cookie{{Name: name, Value: value, Secure: true}})
}
return nil
}

View File

@ -44,7 +44,7 @@ import (
// deleteOldGoIMAPFiles Set with `-ldflags -X app.deleteOldGoIMAPFiles=true` to enable cleanup of old imap cache data.
var deleteOldGoIMAPFiles bool //nolint:gochecknoglobals
// withBridge creates creates and tears down the bridge.
// withBridge creates and tears down the bridge.
func withBridge(
c *cli.Context,
exe string,

View File

@ -20,6 +20,7 @@
package bridge
import (
"crypto/tls"
"net/http"
"os"
@ -36,6 +37,14 @@ func newAPIOptions(
transport http.RoundTripper,
panicHandler async.PanicHandler,
) []proton.Option {
if allow := os.Getenv("BRIDGE_ALLOW_PROXY"); allow != "" {
transport = &http.Transport{
Proxy: http.ProxyFromEnvironment,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
}
opt := defaultAPIOptions(apiURL, version, cookieJar, transport, panicHandler)
if host := os.Getenv("BRIDGE_API_HOST"); host != "" {

View File

@ -29,7 +29,6 @@ import (
"time"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/gluon"
"github.com/ProtonMail/gluon/async"
imapEvents "github.com/ProtonMail/gluon/events"
"github.com/ProtonMail/gluon/imap"
@ -41,10 +40,10 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/focus"
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/sentry"
"github.com/ProtonMail/proton-bridge/v3/internal/telemetry"
"github.com/ProtonMail/proton-bridge/v3/internal/user"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/bradenaw/juniper/xslices"
"github.com/emersion/go-smtp"
"github.com/go-resty/resty/v2"
"github.com/sirupsen/logrus"
)
@ -66,18 +65,15 @@ type Bridge struct {
tlsConfig *tls.Config
// imapServer is the bridge's IMAP server.
imapServer *gluon.Server
imapListener net.Listener
imapEventCh chan imapEvents.Event
// smtpServer is the bridge's SMTP server.
smtpServer *smtp.Server
smtpListener net.Listener
imapEventCh chan imapEvents.Event
// updater is the bridge's updater.
updater Updater
installCh chan installJob
// heartbeat is the telemetry heartbeat for metrics.
heartbeat telemetry.Heartbeat
// curVersion is the current version of the bridge,
// newVersion is the version that was installed by the updater.
curVersion *semver.Version
@ -126,7 +122,12 @@ type Bridge struct {
// goUpdate triggers a check/install of updates.
goUpdate func()
// goHeartbeat triggers a check/sending if heartbeat is needed.
goHeartbeat func()
uidValidityGenerator imap.UIDValidityGenerator
serverManager *ServerManager
}
// New creates a new bridge.
@ -217,16 +218,6 @@ func newBridge(
return nil, fmt.Errorf("failed to load TLS config: %w", err)
}
gluonCacheDir, err := getGluonDir(vault)
if err != nil {
return nil, fmt.Errorf("failed to get Gluon directory: %w", err)
}
gluonDataDir, err := locator.ProvideGluonDataPath()
if err != nil {
return nil, fmt.Errorf("failed to get Gluon Database directory: %w", err)
}
firstStart := vault.GetFirstStart()
if err := vault.SetFirstStart(false); err != nil {
return nil, fmt.Errorf("failed to save first start indicator: %w", err)
@ -237,22 +228,7 @@ func newBridge(
return nil, fmt.Errorf("failed to save last version indicator: %w", err)
}
imapServer, err := newIMAPServer(
gluonCacheDir,
gluonDataDir,
curVersion,
tlsConfig,
reporter,
logIMAPClient,
logIMAPServer,
imapEventCh,
tasks,
uidValidityGenerator,
panicHandler,
)
if err != nil {
return nil, fmt.Errorf("failed to create IMAP server: %w", err)
}
identifier.SetClientString(vault.GetLastUserAgent())
focusService, err := focus.NewService(locator, curVersion, panicHandler)
if err != nil {
@ -270,7 +246,6 @@ func newBridge(
identifier: identifier,
tlsConfig: tlsConfig,
imapServer: imapServer,
imapEventCh: imapEventCh,
updater: updater,
@ -297,9 +272,13 @@ func newBridge(
tasks: tasks,
uidValidityGenerator: uidValidityGenerator,
serverManager: newServerManager(),
}
bridge.smtpServer = newSMTPServer(bridge, tlsConfig, logSMTP)
if err := bridge.serverManager.Init(bridge); err != nil {
return nil, err
}
return bridge, nil
}
@ -372,10 +351,6 @@ func (bridge *Bridge) init(tlsReporter TLSReporter) error {
})
})
// We need to load users before we can start the IMAP and SMTP servers.
// We must only start the servers once.
var once sync.Once
// Attempt to load users from the vault when triggered.
bridge.goLoad = bridge.tasks.Trigger(func(ctx context.Context) {
if err := bridge.loadUsers(ctx); err != nil {
@ -387,17 +362,6 @@ func (bridge *Bridge) init(tlsReporter TLSReporter) error {
}
bridge.publish(events.AllUsersLoaded{})
// Once all users have been loaded, start the bridge's IMAP and SMTP servers.
once.Do(func() {
if err := bridge.serveIMAP(); err != nil {
logrus.WithError(err).Error("Failed to start IMAP server")
}
if err := bridge.serveSMTP(); err != nil {
logrus.WithError(err).Error("Failed to start SMTP server")
}
})
})
defer bridge.goLoad()
@ -443,18 +407,13 @@ func (bridge *Bridge) GetErrors() []error {
func (bridge *Bridge) Close(ctx context.Context) {
logrus.Info("Closing bridge")
// Close the IMAP server.
if err := bridge.closeIMAP(ctx); err != nil {
logrus.WithError(err).Error("Failed to close IMAP server")
}
// Close the SMTP server.
if err := bridge.closeSMTP(); err != nil {
logrus.WithError(err).Error("Failed to close SMTP server")
// Close the servers
if err := bridge.serverManager.CloseServers(ctx); err != nil {
logrus.WithError(err).Error("Failed to close servers")
}
// Close all users.
safe.RLock(func() {
safe.Lock(func() {
for _, user := range bridge.users {
user.Close()
}

View File

@ -25,6 +25,7 @@ import (
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"time"
@ -49,7 +50,9 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/tests"
"github.com/bradenaw/juniper/xslices"
"github.com/emersion/go-imap/client"
imapid "github.com/emersion/go-imap-id"
"github.com/emersion/go-sasl"
"github.com/emersion/go-smtp"
"github.com/stretchr/testify/require"
)
@ -170,6 +173,210 @@ func TestBridge_UserAgent(t *testing.T) {
})
}
func TestBridge_UserAgent_Persistence(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
otherPassword := []byte("bar")
otherUser := "foo"
_, _, err := s.CreateUser(otherUser, otherPassword)
require.NoError(t, err)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
imapWaiter := waitForIMAPServerReady(b)
defer imapWaiter.Done()
smtpWaiter := waitForSMTPServerReady(b)
defer smtpWaiter.Done()
currentUserAgent := b.GetCurrentUserAgent()
require.Contains(t, currentUserAgent, useragent.DefaultUserAgent)
require.NoError(t, getErr(b.LoginFull(ctx, otherUser, otherPassword, nil, nil)))
imapWaiter.Wait()
smtpWaiter.Wait()
imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
require.NoError(t, err)
defer func() { _ = imapClient.Logout() }()
idClient := imapid.NewClient(imapClient)
// Set IMAP ID before Login to have the value capture in the Login API Call.
_, err = idClient.ID(imapid.ID{
imapid.FieldName: "MyFancyClient",
imapid.FieldVersion: "0.1.2",
})
require.NoError(t, err)
// Login the user.
_, err = b.LoginFull(context.Background(), username, password, nil, nil)
require.NoError(t, err)
// Assert that the user agent then contains the platform.
require.Contains(t, b.GetCurrentUserAgent(), "MyFancyClient/0.1.2")
})
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
currentUserAgent := bridge.GetCurrentUserAgent()
require.Contains(t, currentUserAgent, "MyFancyClient/0.1.2")
})
})
}
func TestBridge_UserAgentFromUnknownClient(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
otherPassword := []byte("bar")
otherUser := "foo"
_, _, err := s.CreateUser(otherUser, otherPassword)
require.NoError(t, err)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
imapWaiter := waitForIMAPServerReady(b)
defer imapWaiter.Done()
smtpWaiter := waitForSMTPServerReady(b)
defer smtpWaiter.Done()
currentUserAgent := b.GetCurrentUserAgent()
require.Contains(t, currentUserAgent, useragent.DefaultUserAgent)
userID, err := b.LoginFull(context.Background(), username, password, nil, nil)
require.NoError(t, err)
imapWaiter.Wait()
smtpWaiter.Wait()
imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
require.NoError(t, err)
defer func() { _ = imapClient.Logout() }()
info, err := b.GetUserInfo(userID)
require.NoError(t, err)
require.True(t, info.State == bridge.Connected)
require.NoError(t, imapClient.Login(info.Addresses[0], string(info.BridgePass)))
currentUserAgent = b.GetCurrentUserAgent()
require.Contains(t, currentUserAgent, "UnknownClient/0.0.1")
})
})
}
func TestBridge_UserAgentFromSMTPClient(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
otherPassword := []byte("bar")
otherUser := "foo"
_, _, err := s.CreateUser(otherUser, otherPassword)
require.NoError(t, err)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
imapWaiter := waitForIMAPServerReady(b)
defer imapWaiter.Done()
smtpWaiter := waitForSMTPServerReady(b)
defer smtpWaiter.Done()
currentUserAgent := b.GetCurrentUserAgent()
require.Contains(t, currentUserAgent, useragent.DefaultUserAgent)
userID, err := b.LoginFull(context.Background(), username, password, nil, nil)
require.NoError(t, err)
imapWaiter.Wait()
smtpWaiter.Wait()
client, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(b.GetSMTPPort())))
require.NoError(t, err)
defer client.Close() //nolint:errcheck
info, err := b.GetUserInfo(userID)
require.NoError(t, err)
require.True(t, info.State == bridge.Connected)
// Upgrade to TLS.
require.NoError(t, client.StartTLS(&tls.Config{InsecureSkipVerify: true}))
require.NoError(t, client.Auth(sasl.NewLoginClient(
info.Addresses[0],
string(info.BridgePass)),
))
require.Eventually(t, func() bool {
currentUserAgent = b.GetCurrentUserAgent()
return strings.Contains(currentUserAgent, "UnknownClient/0.0.1")
}, time.Minute, 5*time.Second)
})
})
}
func TestBridge_UserAgentFromIMAPID(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
var (
calls []server.Call
lock sync.Mutex
)
s.AddCallWatcher(func(call server.Call) {
lock.Lock()
defer lock.Unlock()
calls = append(calls, call)
})
otherPassword := []byte("bar")
otherUser := "foo"
_, _, err := s.CreateUser(otherUser, otherPassword)
require.NoError(t, err)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
imapWaiter := waitForIMAPServerReady(b)
defer imapWaiter.Done()
smtpWaiter := waitForSMTPServerReady(b)
defer smtpWaiter.Done()
require.NoError(t, getErr(b.LoginFull(ctx, otherUser, otherPassword, nil, nil)))
imapWaiter.Wait()
smtpWaiter.Wait()
imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
require.NoError(t, err)
defer func() { _ = imapClient.Logout() }()
idClient := imapid.NewClient(imapClient)
// Set IMAP ID before Login to have the value capture in the Login API Call.
_, err = idClient.ID(imapid.ID{
imapid.FieldName: "MyFancyClient",
imapid.FieldVersion: "0.1.2",
})
require.NoError(t, err)
// Login the user.
userID, err := b.LoginFull(context.Background(), username, password, nil, nil)
require.NoError(t, err)
info, err := b.GetUserInfo(userID)
require.NoError(t, err)
require.True(t, info.State == bridge.Connected)
require.NoError(t, imapClient.Login(info.Addresses[0], string(info.BridgePass)))
lock.Lock()
defer lock.Unlock()
userAgent := calls[len(calls)-1].RequestHeader.Get("User-Agent")
// Assert that the user agent was sent to the API.
require.Contains(t, userAgent, b.GetCurrentUserAgent())
require.Contains(t, userAgent, "MyFancyClient/0.1.2")
})
})
}
func TestBridge_Cookies(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
var (
@ -505,10 +712,22 @@ func TestBridge_InitGluonDirectory(t *testing.T) {
func TestBridge_LoginFailed(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
imapWaiter := waitForIMAPServerReady(bridge)
defer imapWaiter.Done()
smtpWaiter := waitForSMTPServerReady(bridge)
defer smtpWaiter.Done()
failCh, done := chToType[events.Event, events.IMAPLoginFailed](bridge.GetEvents(events.IMAPLoginFailed{}))
defer done()
imapClient, err := client.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
_, err := bridge.LoginFull(ctx, username, password, nil, nil)
require.NoError(t, err)
imapWaiter.Wait()
smtpWaiter.Wait()
imapClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
require.NoError(t, err)
require.Error(t, imapClient.Login("badUser", "badPass"))
@ -535,6 +754,12 @@ func TestBridge_ChangeCacheDirectory(t *testing.T) {
configDir, err := b.GetGluonDataDir()
require.NoError(t, err)
imapWaiter := waitForIMAPServerReady(b)
defer imapWaiter.Done()
smtpWaiter := waitForSMTPServerReady(b)
defer smtpWaiter.Done()
// Login the user.
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
defer done()
@ -568,7 +793,10 @@ func TestBridge_ChangeCacheDirectory(t *testing.T) {
require.NoError(t, err)
require.True(t, info.State == bridge.Connected)
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
imapWaiter.Wait()
smtpWaiter.Wait()
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }()
@ -608,7 +836,7 @@ func TestBridge_ChangeAddressOrder(t *testing.T) {
require.NoError(t, err)
require.True(t, info.State == bridge.Connected)
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }()
@ -629,7 +857,7 @@ func TestBridge_ChangeAddressOrder(t *testing.T) {
require.NoError(t, err)
require.True(t, info.State == bridge.Connected)
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }()
@ -691,6 +919,7 @@ func withBridgeNoMocks(
locator bridge.Locator,
vaultKey []byte,
tests func(*bridge.Bridge),
waitOnServers bool,
) {
// Bridge will disable the proxy by default at startup.
mocks.ProxyCtl.EXPECT().DisallowProxy()
@ -736,16 +965,22 @@ func withBridgeNoMocks(
require.NoError(t, err)
require.Empty(t, bridge.GetErrors())
// Start the Heartbeat process.
bridge.StartHeartbeat(mocks.Heartbeat)
// Wait for bridge to finish loading users.
waitForEvent(t, eventCh, events.AllUsersLoaded{})
// Wait for bridge to start the IMAP server.
waitForEvent(t, eventCh, events.IMAPServerReady{})
// Wait for bridge to start the SMTP server.
waitForEvent(t, eventCh, events.SMTPServerReady{})
// Set random IMAP and SMTP ports for the tests.
require.NoError(t, bridge.SetIMAPPort(0))
require.NoError(t, bridge.SetSMTPPort(0))
require.NoError(t, bridge.SetIMAPPort(ctx, 0))
require.NoError(t, bridge.SetSMTPPort(ctx, 0))
if waitOnServers {
// Wait for bridge to start the IMAP server.
waitForEvent(t, eventCh, events.IMAPServerReady{})
// Wait for bridge to start the SMTP server.
waitForEvent(t, eventCh, events.SMTPServerReady{})
}
// Close the bridge when done.
defer bridge.Close(ctx)
@ -767,7 +1002,24 @@ func withBridge(
withMocks(t, func(mocks *bridge.Mocks) {
withBridgeNoMocks(ctx, t, mocks, apiURL, netCtl, locator, vaultKey, func(bridge *bridge.Bridge) {
tests(bridge, mocks)
})
}, false)
})
}
// withBridgeWaitForServers is the same as withBridge, but it will wait until IMAP & SMTP servers are ready.
func withBridgeWaitForServers(
ctx context.Context,
t *testing.T,
apiURL string,
netCtl *proton.NetCtl,
locator bridge.Locator,
vaultKey []byte,
tests func(*bridge.Bridge, *bridge.Mocks),
) {
withMocks(t, func(mocks *bridge.Mocks) {
withBridgeNoMocks(ctx, t, mocks, apiURL, netCtl, locator, vaultKey, func(bridge *bridge.Bridge) {
tests(bridge, mocks)
}, true)
})
}
@ -820,3 +1072,48 @@ func chToType[In, Out any](inCh <-chan In, done func()) (<-chan Out, func()) {
return outCh, done
}
type eventWaiter struct {
evtCh <-chan events.Event
cancel func()
}
func (e *eventWaiter) Done() {
e.cancel()
}
func (e *eventWaiter) Wait() {
<-e.evtCh
}
func waitForSMTPServerReady(b *bridge.Bridge) *eventWaiter {
evtCh, cancel := b.GetEvents(events.SMTPServerReady{})
return &eventWaiter{
evtCh: evtCh,
cancel: cancel,
}
}
func waitForSMTPServerStopped(b *bridge.Bridge) *eventWaiter {
evtCh, cancel := b.GetEvents(events.SMTPServerStopped{})
return &eventWaiter{
evtCh: evtCh,
cancel: cancel,
}
}
func waitForIMAPServerReady(b *bridge.Bridge) *eventWaiter {
evtCh, cancel := b.GetEvents(events.IMAPServerReady{})
return &eventWaiter{
evtCh: evtCh,
cancel: cancel,
}
}
func waitForIMAPServerStopped(b *bridge.Bridge) *eventWaiter {
evtCh, cancel := b.GetEvents(events.IMAPServerStopped{})
return &eventWaiter{
evtCh: evtCh,
cancel: cancel,
}
}

View File

@ -18,23 +18,19 @@
package bridge
import (
"archive/zip"
"bytes"
"context"
"io"
"os"
"path/filepath"
"sort"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
)
const (
MaxTotalAttachmentSize = 7 * (1 << 20)
MaxCompressedFilesCount = 6
DefaultMaxBugReportZipSize = 7 * 1024 * 1024
DefaultMaxSessionCountForBugReport = 10
)
func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, description, username, email, client string, attachLogs bool) error {
@ -50,54 +46,25 @@ func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, descript
}
}
var atts []proton.ReportBugAttachment
var attachment []proton.ReportBugAttachment
if attachLogs {
logs, err := getMatchingLogs(bridge.locator, func(filename string) bool {
return logging.MatchLogName(filename) && !logging.MatchStackTraceName(filename)
})
logsPath, err := bridge.locator.ProvideLogsPath()
if err != nil {
return err
}
crashes, err := getMatchingLogs(bridge.locator, func(filename string) bool {
return logging.MatchLogName(filename) && logging.MatchStackTraceName(filename)
})
buffer, err := logging.ZipLogsForBugReport(logsPath, DefaultMaxSessionCountForBugReport, DefaultMaxBugReportZipSize)
if err != nil {
return err
}
guiLogs, err := getMatchingLogs(bridge.locator, func(filename string) bool {
return logging.MatchGUILogName(filename) && !logging.MatchStackTraceName(filename)
})
body, err := io.ReadAll(buffer)
if err != nil {
return err
}
var matchFiles []string
// Include bridge logs, up to a maximum amount.
matchFiles = append(matchFiles, logs[max(0, len(logs)-(MaxCompressedFilesCount/2)):]...)
// Include crash logs, up to a maximum amount.
matchFiles = append(matchFiles, crashes[max(0, len(crashes)-(MaxCompressedFilesCount/2)):]...)
// bridge-gui keeps just one small (~ 1kb) log file; we always include it.
if len(guiLogs) > 0 {
matchFiles = append(matchFiles, guiLogs[len(guiLogs)-1])
}
archive, err := zipFiles(matchFiles)
if err != nil {
return err
}
body, err := io.ReadAll(archive)
if err != nil {
return err
}
atts = append(atts, proton.ReportBugAttachment{
attachment = append(attachment, proton.ReportBugAttachment{
Name: "logs.zip",
Filename: "logs.zip",
MIMEType: "application/zip",
@ -105,6 +72,12 @@ func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, descript
})
}
safe.Lock(func() {
for _, user := range bridge.users {
user.ReportBugSent()
}
}, bridge.usersLock)
return bridge.api.ReportBug(ctx, proton.ReportBugReq{
OS: osType,
OSVersion: osVersion,
@ -118,116 +91,5 @@ func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, descript
Username: account,
Email: email,
}, atts...)
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
func getMatchingLogs(locator Locator, filenameMatchFunc func(string) bool) (filenames []string, err error) {
logsPath, err := locator.ProvideLogsPath()
if err != nil {
return nil, err
}
files, err := os.ReadDir(logsPath)
if err != nil {
return nil, err
}
var matchFiles []string
for _, file := range files {
if filenameMatchFunc(file.Name()) {
matchFiles = append(matchFiles, filepath.Join(logsPath, file.Name()))
}
}
sort.Strings(matchFiles) // Sorted by timestamp: oldest first.
return matchFiles, nil
}
type limitedBuffer struct {
capacity int
buf *bytes.Buffer
}
func newLimitedBuffer(capacity int) *limitedBuffer {
return &limitedBuffer{
capacity: capacity,
buf: bytes.NewBuffer(make([]byte, 0, capacity)),
}
}
func (b *limitedBuffer) Write(p []byte) (n int, err error) {
if len(p)+b.buf.Len() > b.capacity {
return 0, ErrSizeTooLarge
}
return b.buf.Write(p)
}
func (b *limitedBuffer) Read(p []byte) (n int, err error) {
return b.buf.Read(p)
}
func zipFiles(filenames []string) (io.Reader, error) {
if len(filenames) == 0 {
return nil, nil
}
buf := newLimitedBuffer(MaxTotalAttachmentSize)
w := zip.NewWriter(buf)
defer w.Close() //nolint:errcheck
for _, file := range filenames {
if err := addFileToZip(file, w); err != nil {
return nil, err
}
}
if err := w.Close(); err != nil {
return nil, err
}
return buf, nil
}
func addFileToZip(filename string, writer *zip.Writer) error {
fileReader, err := os.Open(filepath.Clean(filename))
if err != nil {
return err
}
defer fileReader.Close() //nolint:errcheck,gosec
fileInfo, err := fileReader.Stat()
if err != nil {
return err
}
header, err := zip.FileInfoHeader(fileInfo)
if err != nil {
return err
}
header.Method = zip.Deflate
header.Name = filepath.Base(filename)
fileWriter, err := writer.CreateHeader(header)
if err != nil {
return err
}
if _, err := io.Copy(fileWriter, fileReader); err != nil {
return err
}
return fileReader.Close()
}, attachment...)
}

View File

@ -0,0 +1,46 @@
// 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 (
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
)
func (bridge *Bridge) ReportBugClicked() {
safe.Lock(func() {
for _, user := range bridge.users {
user.ReportBugClicked()
}
}, bridge.usersLock)
}
func (bridge *Bridge) AutoconfigUsed(client string) {
safe.Lock(func() {
for _, user := range bridge.users {
user.AutoconfigUsed(client)
}
}, bridge.usersLock)
}
func (bridge *Bridge) KBArticleOpened(article string) {
safe.Lock(func() {
for _, user := range bridge.users {
user.KBArticleOpened(article)
}
}, bridge.usersLock)
}

View File

@ -18,6 +18,7 @@
package bridge
import (
"context"
"strings"
"github.com/ProtonMail/proton-bridge/v3/internal/clientconfig"
@ -31,7 +32,7 @@ import (
// ConfigureAppleMail configures apple mail for the given userID and address.
// If configuring apple mail for Catalina or newer, it ensures Bridge is using SSL.
func (bridge *Bridge) ConfigureAppleMail(userID, address string) error {
func (bridge *Bridge) ConfigureAppleMail(ctx context.Context, userID, address string) error {
logrus.WithFields(logrus.Fields{
"userID": userID,
"address": logging.Sensitive(address),
@ -56,7 +57,7 @@ func (bridge *Bridge) ConfigureAppleMail(userID, address string) error {
}
if useragent.IsCatalinaOrNewer() && !bridge.vault.GetSMTPSSL() {
if err := bridge.SetSMTPSSL(true); err != nil {
if err := bridge.SetSMTPSSL(ctx, true); err != nil {
return err
}
}

297
internal/bridge/debug.go Normal file
View File

@ -0,0 +1,297 @@
// 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 (
"context"
"fmt"
"io"
"os"
"path/filepath"
"github.com/ProtonMail/gluon/imap"
"github.com/ProtonMail/gluon/rfc822"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/proton-bridge/v3/internal/user"
"github.com/bradenaw/juniper/iterator"
"github.com/bradenaw/juniper/xslices"
goimap "github.com/emersion/go-imap"
goimapclient "github.com/emersion/go-imap/client"
"github.com/sirupsen/logrus"
"golang.org/x/exp/maps"
)
type CheckClientStateResult struct {
MissingMessages map[string]map[string]user.DiagMailboxMessage
}
func (c *CheckClientStateResult) AddMissingMessage(userID string, message user.DiagMailboxMessage) {
v, ok := c.MissingMessages[userID]
if !ok {
c.MissingMessages[userID] = map[string]user.DiagMailboxMessage{message.ID: message}
} else {
v[message.ID] = message
}
}
// CheckClientState checks the current IMAP client reported state against the proton server state and reports
// anything that is out of place.
func (bridge *Bridge) CheckClientState(ctx context.Context, checkFlags bool, progressCB func(string)) (CheckClientStateResult, error) {
bridge.usersLock.RLock()
defer bridge.usersLock.RUnlock()
users := maps.Values(bridge.users)
result := CheckClientStateResult{
MissingMessages: make(map[string]map[string]user.DiagMailboxMessage),
}
for _, usr := range users {
if progressCB != nil {
progressCB(fmt.Sprintf("Checking state for user %v", usr.Name()))
}
log := logrus.WithField("user", usr.Name()).WithField("diag", "state-check")
log.Debug("Retrieving all server metadata")
meta, err := usr.GetDiagnosticMetadata(ctx)
if err != nil {
return result, err
}
success := true
if len(meta.Metadata) != len(meta.MessageIDs) {
log.Errorf("Metadata (%v) and message(%v) list sizes do not match", len(meta.Metadata), len(meta.MessageIDs))
}
log.Debug("Building state")
state, err := meta.BuildMailboxToMessageMap(usr)
if err != nil {
log.WithError(err).Error("Failed to build state")
return result, err
}
info, err := bridge.GetUserInfo(usr.ID())
if err != nil {
log.WithError(err).Error("Failed to get user info")
return result, err
}
addr := fmt.Sprintf("127.0.0.1:%v", bridge.GetIMAPPort())
for account, mboxMap := range state {
if progressCB != nil {
progressCB(fmt.Sprintf("Checking state for user %v's account '%v'", usr.Name(), account))
}
if err := func(account string, mboxMap user.AccountMailboxMap) error {
client, err := goimapclient.Dial(addr)
if err != nil {
log.WithError(err).Error("Failed to connect to imap client")
return err
}
defer func() {
_ = client.Logout()
}()
if err := client.Login(account, string(info.BridgePass)); err != nil {
return fmt.Errorf("failed to login for user %v:%w", usr.Name(), err)
}
log := log.WithField("account", account)
for mboxName, messageList := range mboxMap {
log := log.WithField("mbox", mboxName)
status, err := client.Select(mboxName, true)
if err != nil {
log.WithError(err).Errorf("Failed to select mailbox %v", messageList)
return fmt.Errorf("failed to select '%v':%w", mboxName, err)
}
log.Debug("Checking message count")
if int(status.Messages) != len(messageList) {
success = false
log.Errorf("Message count doesn't match, got '%v' expected '%v'", status.Messages, len(messageList))
}
ids, err := clientGetMessageIDs(client, mboxName)
if err != nil {
return fmt.Errorf("failed to get message ids for mbox '%v': %w", mboxName, err)
}
for _, msg := range messageList {
imapFlags, ok := ids[msg.ID]
if !ok {
if meta.FailedMessageIDs.Contains(msg.ID) {
log.Warningf("Missing message '%v', but it is part of failed message set", msg.ID)
} else {
log.Errorf("Missing message '%v'", msg.ID)
}
result.AddMissingMessage(msg.UserID, msg)
continue
}
if checkFlags {
if !imapFlags.Equals(msg.Flags) {
log.Errorf("Message '%v' flags do mot match, got=%v, expected=%v",
msg.ID,
imapFlags.ToSlice(),
msg.Flags.ToSlice(),
)
}
}
}
}
if !success {
log.Errorf("State does not match")
} else {
log.Info("State matches")
}
return nil
}(account, mboxMap); err != nil {
return result, err
}
}
// Check for orphaned messages (only present in All Mail)
if progressCB != nil {
progressCB(fmt.Sprintf("Checking user %v for orphans", usr.Name()))
}
log.Debugf("Checking for orphans")
for _, m := range meta.Metadata {
filteredLabels := xslices.Filter(m.LabelIDs, func(t string) bool {
switch t {
case proton.AllMailLabel:
return false
case proton.AllSentLabel:
return false
case proton.AllDraftsLabel:
return false
case proton.OutboxLabel:
return false
default:
return true
}
})
if len(filteredLabels) == 0 {
log.Warnf("Message %v is only present in All Mail (Subject=%v)", m.ID, m.Subject)
}
}
}
return result, nil
}
func (bridge *Bridge) DebugDownloadFailedMessages(
ctx context.Context,
result CheckClientStateResult,
exportPath string,
progressCB func(string, int, int),
) error {
bridge.usersLock.RLock()
defer bridge.usersLock.RUnlock()
for userID, messages := range result.MissingMessages {
usr, ok := bridge.users[userID]
if !ok {
return fmt.Errorf("failed to find user with id %v", userID)
}
userDir := filepath.Join(exportPath, userID)
if err := os.MkdirAll(userDir, 0o700); err != nil {
return fmt.Errorf("failed to create directory '%v': %w", userDir, err)
}
if err := usr.DebugDownloadMessages(ctx, userDir, messages, progressCB); err != nil {
return err
}
}
return nil
}
func clientGetMessageIDs(client *goimapclient.Client, mailbox string) (map[string]imap.FlagSet, error) {
status, err := client.Select(mailbox, true)
if err != nil {
return nil, err
}
if status.Messages == 0 {
return nil, nil
}
resCh := make(chan *goimap.Message)
section, err := goimap.ParseBodySectionName("BODY[HEADER]")
if err != nil {
return nil, err
}
fetchItems := []goimap.FetchItem{"BODY[HEADER]", goimap.FetchFlags}
seq, err := goimap.ParseSeqSet("1:*")
if err != nil {
return nil, err
}
go func() {
if err := client.Fetch(
seq,
fetchItems,
resCh,
); err != nil {
panic(err)
}
}()
messages := iterator.Collect(iterator.Chan(resCh))
ids := make(map[string]imap.FlagSet, len(messages))
for i, m := range messages {
literal, err := io.ReadAll(m.GetBody(section))
if err != nil {
return nil, err
}
header, err := rfc822.NewHeader(literal)
if err != nil {
return nil, fmt.Errorf("failed to parse header for msg %v: %w", i, err)
}
internalID, ok := header.GetChecked("X-Pm-Internal-Id")
if !ok {
logrus.Errorf("Message %v does not have internal id", internalID)
continue
}
messageFlags := imap.NewFlagSet(m.Flags...)
// Recent and Deleted are not part of the proton flag set.
messageFlags.RemoveFromSelf("\\Recent")
messageFlags.RemoveFromSelf("\\Deleted")
ids[internalID] = messageFlags
}
return ids, nil
}

View File

@ -0,0 +1,175 @@
// 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_test
import (
"bytes"
"context"
"crypto/tls"
"fmt"
"io"
"net"
"strings"
"testing"
"time"
"github.com/ProtonMail/gluon/rfc822"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/go-proton-api/server"
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
go_imap "github.com/emersion/go-imap"
"github.com/emersion/go-sasl"
"github.com/emersion/go-smtp"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
)
func TestBridge_HandleDraftsSendFromOtherClient(t *testing.T) {
getGluonHeaderID := func(literal []byte) (string, string) {
h, err := rfc822.NewHeader(literal)
require.NoError(t, err)
gluonID, ok := h.GetChecked("X-Pm-Gluon-Id")
require.True(t, ok)
externalID, ok := h.GetChecked("Message-Id")
require.True(t, ok)
return gluonID, externalID
}
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
_, _, err := s.CreateUser("imap", password)
require.NoError(t, err)
_, _, err = s.CreateUser("bar", password)
require.NoError(t, err)
// The initial user should be fully synced.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
waiter := waitForIMAPServerReady(b)
defer waiter.Done()
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
defer done()
userID, err := b.LoginFull(ctx, "imap", password, nil, nil)
require.NoError(t, err)
require.Equal(t, userID, (<-syncCh).UserID)
waiter.Wait()
info, err := b.GetUserInfo(userID)
require.NoError(t, err)
require.True(t, info.State == bridge.Connected)
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }()
// Create first draft in client.
literal := fmt.Sprintf(`From: %v
To: %v
Date: Fri, 3 Feb 2023 01:04:32 +0100
Subject: Foo
Hello
`, info.Addresses[0], "bar@proton.local")
require.NoError(t, client.Append("Drafts", nil, time.Now(), strings.NewReader(literal)))
// Verify the draft is available in client.
require.Eventually(t, func() bool {
status, err := client.Status("Drafts", []go_imap.StatusItem{go_imap.StatusMessages})
require.NoError(t, err)
return status.Messages == 1
}, 2*time.Second, time.Second)
// Retrieve the new literal so we can have the Proton Message ID.
messages, err := clientFetch(client, "Drafts")
require.NoError(t, err)
require.Equal(t, 1, len(messages))
newLiteral, err := io.ReadAll(messages[0].GetBody(must(go_imap.ParseBodySectionName("BODY[]"))))
require.NoError(t, err)
logrus.Info(string(newLiteral))
newLiteralID, newLiteralExternID := getGluonHeaderID(newLiteral)
// Modify new literal.
newLiteralModified := append(newLiteral, []byte(" world from client2")...) //nolint:gocritic
func() {
smtpClient, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(b.GetSMTPPort())))
require.NoError(t, err)
defer func() { _ = smtpClient.Close() }()
// Upgrade to TLS.
require.NoError(t, smtpClient.StartTLS(&tls.Config{InsecureSkipVerify: true}))
// Authorize with SASL PLAIN.
require.NoError(t, smtpClient.Auth(sasl.NewPlainClient(
info.Addresses[0],
info.Addresses[0],
string(info.BridgePass)),
))
// Send the message.
require.NoError(t, smtpClient.SendMail(
info.Addresses[0],
[]string{"bar@proton.local"},
bytes.NewReader(newLiteralModified),
))
}()
// Append message to Sent as the imap client would.
require.NoError(t, client.Append("Sent", nil, time.Now(), strings.NewReader(literal)))
// Verify the sent message gets updated with the new literal.
require.Eventually(t, func() bool {
// Check if sent message matches the latest draft.
messagesClient1, err := clientFetch(client, "Sent", "BODY[TEXT]", "BODY[]")
require.NoError(t, err)
if len(messagesClient1) != 1 {
return false
}
sentLiteral, err := io.ReadAll(messagesClient1[0].GetBody(must(go_imap.ParseBodySectionName("BODY[]"))))
require.NoError(t, err)
sentLiteralID, sentLiteralExternID := getGluonHeaderID(sentLiteral)
sentLiteralText, err := io.ReadAll(messagesClient1[0].GetBody(must(go_imap.ParseBodySectionName("BODY[TEXT]"))))
require.NoError(t, err)
sentLiteralStr := string(sentLiteralText)
literalMatches := sentLiteralStr == "Hello\r\n world from client2\r\n"
idIsDifferent := sentLiteralID != newLiteralID
externIDMatches := sentLiteralExternID == newLiteralExternID
return literalMatches && idIsDifferent && externIDMatches
}, 2*time.Second, time.Second)
})
}, server.WithMessageDedup())
}

View File

@ -58,11 +58,7 @@ func moveFile(from, to string) error {
return err
}
if err := os.Rename(from, to); err != nil {
return err
}
return nil
return os.Rename(from, to)
}
func copyDir(from, to string) error {

View File

@ -0,0 +1,128 @@
// 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 (
"context"
"encoding/json"
"time"
"github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/telemetry"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
"github.com/sirupsen/logrus"
)
const HeartbeatCheckInterval = time.Hour
func (bridge *Bridge) IsTelemetryAvailable(ctx context.Context) bool {
var flag = true
if bridge.GetTelemetryDisabled() {
return false
}
safe.RLock(func() {
for _, user := range bridge.users {
flag = flag && user.IsTelemetryEnabled(ctx)
}
}, bridge.usersLock)
return flag
}
func (bridge *Bridge) SendHeartbeat(ctx context.Context, heartbeat *telemetry.HeartbeatData) bool {
data, err := json.Marshal(heartbeat)
if err != nil {
if err := bridge.reporter.ReportMessageWithContext("Cannot parse heartbeat data.", reporter.Context{
"error": err,
}); err != nil {
logrus.WithError(err).Error("Failed to parse heartbeat data.")
}
return false
}
var sent = false
safe.RLock(func() {
for _, user := range bridge.users {
if err := user.SendTelemetry(ctx, data); err == nil {
sent = true
break
}
}
}, bridge.usersLock)
return sent
}
func (bridge *Bridge) GetLastHeartbeatSent() time.Time {
return bridge.vault.GetLastHeartbeatSent()
}
func (bridge *Bridge) SetLastHeartbeatSent(timestamp time.Time) error {
return bridge.vault.SetLastHeartbeatSent(timestamp)
}
func (bridge *Bridge) StartHeartbeat(manager telemetry.HeartbeatManager) {
bridge.heartbeat = telemetry.NewHeartbeat(manager, 1143, 1025, bridge.GetGluonCacheDir(), keychain.DefaultHelper)
// Check for heartbeat when triggered.
bridge.goHeartbeat = bridge.tasks.PeriodicOrTrigger(HeartbeatCheckInterval, 0, func(ctx context.Context) {
logrus.Debug("Checking for heartbeat")
bridge.heartbeat.TrySending(ctx)
})
bridge.heartbeat.SetRollout(bridge.GetUpdateRollout())
bridge.heartbeat.SetAutoStart(bridge.GetAutostart())
bridge.heartbeat.SetAutoUpdate(bridge.GetAutoUpdate())
bridge.heartbeat.SetBeta(bridge.GetUpdateChannel())
bridge.heartbeat.SetDoh(bridge.GetProxyAllowed())
bridge.heartbeat.SetShowAllMail(bridge.GetShowAllMail())
bridge.heartbeat.SetIMAPConnectionMode(bridge.GetIMAPSSL())
bridge.heartbeat.SetSMTPConnectionMode(bridge.GetSMTPSSL())
bridge.heartbeat.SetIMAPPort(bridge.GetIMAPPort())
bridge.heartbeat.SetSMTPPort(bridge.GetSMTPPort())
bridge.heartbeat.SetCacheLocation(bridge.GetGluonCacheDir())
if val, err := bridge.GetKeychainApp(); err != nil {
bridge.heartbeat.SetKeyChainPref(val)
} else {
bridge.heartbeat.SetKeyChainPref(keychain.DefaultHelper)
}
bridge.heartbeat.SetPrevVersion(bridge.GetLastVersion().String())
safe.RLock(func() {
var splitMode = false
for _, user := range bridge.users {
if user.GetAddressMode() == vault.SplitMode {
splitMode = true
break
}
}
var nbAccount = len(bridge.users)
bridge.heartbeat.SetNbAccount(nbAccount)
bridge.heartbeat.SetSplitMode(splitMode)
// Do not try to send if there is no user yet.
if nbAccount > 0 {
defer bridge.goHeartbeat()
}
}, bridge.usersLock)
}

View File

@ -17,6 +17,8 @@
package bridge
import "github.com/sirupsen/logrus"
func (bridge *Bridge) GetCurrentUserAgent() string {
return bridge.identifier.GetUserAgent()
}
@ -24,3 +26,17 @@ func (bridge *Bridge) GetCurrentUserAgent() string {
func (bridge *Bridge) SetCurrentPlatform(platform string) {
bridge.identifier.SetPlatform(platform)
}
func (bridge *Bridge) setUserAgent(name, version string) {
currentUserAgent := bridge.identifier.GetClientString()
bridge.identifier.SetClient(name, version)
newUserAgent := bridge.identifier.GetClientString()
if currentUserAgent != newUserAgent {
if err := bridge.vault.SetLastUserAgent(newUserAgent); err != nil {
logrus.WithError(err).Error("Failed to write new user agent to vault")
}
}
}

View File

@ -20,10 +20,10 @@ package bridge
import (
"context"
"crypto/tls"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/gluon"
@ -37,205 +37,22 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
"github.com/ProtonMail/proton-bridge/v3/internal/user"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
"github.com/sirupsen/logrus"
)
const (
defaultClientName = "UnknownClient"
defaultClientVersion = "0.0.1"
)
func (bridge *Bridge) serveIMAP() error {
port, err := func() (int, error) {
if bridge.imapServer == nil {
return 0, fmt.Errorf("no IMAP server instance running")
}
logrus.Info("Starting IMAP server")
imapListener, err := newListener(bridge.vault.GetIMAPPort(), bridge.vault.GetIMAPSSL(), bridge.tlsConfig)
if err != nil {
return 0, fmt.Errorf("failed to create IMAP listener: %w", err)
}
bridge.imapListener = imapListener
if err := bridge.imapServer.Serve(context.Background(), bridge.imapListener); err != nil {
return 0, fmt.Errorf("failed to serve IMAP: %w", err)
}
if err := bridge.vault.SetIMAPPort(getPort(imapListener.Addr())); err != nil {
return 0, fmt.Errorf("failed to store IMAP port in vault: %w", err)
}
return getPort(imapListener.Addr()), nil
}()
if err != nil {
bridge.publish(events.IMAPServerError{
Error: err,
})
return err
}
bridge.publish(events.IMAPServerReady{
Port: port,
})
return nil
}
func (bridge *Bridge) restartIMAP() error {
logrus.Info("Restarting IMAP server")
if bridge.imapListener != nil {
if err := bridge.imapListener.Close(); err != nil {
return fmt.Errorf("failed to close IMAP listener: %w", err)
}
bridge.publish(events.IMAPServerStopped{})
}
return bridge.serveIMAP()
}
func (bridge *Bridge) closeIMAP(ctx context.Context) error {
logrus.Info("Closing IMAP server")
if bridge.imapServer != nil {
if err := bridge.imapServer.Close(ctx); err != nil {
return fmt.Errorf("failed to close IMAP server: %w", err)
}
bridge.imapServer = nil
}
if bridge.imapListener != nil {
if err := bridge.imapListener.Close(); err != nil {
return fmt.Errorf("failed to close IMAP listener: %w", err)
}
}
bridge.publish(events.IMAPServerStopped{})
return nil
func (bridge *Bridge) restartIMAP(ctx context.Context) error {
return bridge.serverManager.RestartIMAP(ctx)
}
// addIMAPUser connects the given user to gluon.
func (bridge *Bridge) addIMAPUser(ctx context.Context, user *user.User) error {
if bridge.imapServer == nil {
return fmt.Errorf("no imap server instance running")
}
imapConn, err := user.NewIMAPConnectors()
if err != nil {
return fmt.Errorf("failed to create IMAP connectors: %w", err)
}
for addrID, imapConn := range imapConn {
log := logrus.WithFields(logrus.Fields{
"userID": user.ID(),
"addrID": addrID,
})
if gluonID, ok := user.GetGluonID(addrID); ok {
log.WithField("gluonID", gluonID).Info("Loading existing IMAP user")
// Load the user, checking whether the DB was newly created.
isNew, err := bridge.imapServer.LoadUser(ctx, imapConn, gluonID, user.GluonKey())
if err != nil {
return fmt.Errorf("failed to load IMAP user: %w", err)
}
if isNew {
// If the DB was newly created, clear the sync status; gluon's DB was not found.
logrus.Warn("IMAP user DB was newly created, clearing sync status")
// Remove the user from IMAP so we can clear the sync status.
if err := bridge.imapServer.RemoveUser(ctx, gluonID, false); err != nil {
return fmt.Errorf("failed to remove IMAP user: %w", err)
}
// Clear the sync status -- we need to resync all messages.
if err := user.ClearSyncStatus(); err != nil {
return fmt.Errorf("failed to clear sync status: %w", err)
}
// Add the user back to the IMAP server.
if isNew, err := bridge.imapServer.LoadUser(ctx, imapConn, gluonID, user.GluonKey()); err != nil {
return fmt.Errorf("failed to add IMAP user: %w", err)
} else if isNew {
panic("IMAP user should already have a database")
}
} else if status := user.GetSyncStatus(); !status.HasLabels {
// Otherwise, the DB already exists -- if the labels are not yet synced, we need to re-create the DB.
if err := bridge.imapServer.RemoveUser(ctx, gluonID, true); err != nil {
return fmt.Errorf("failed to remove old IMAP user: %w", err)
}
if err := user.RemoveGluonID(addrID, gluonID); err != nil {
return fmt.Errorf("failed to remove old IMAP user ID: %w", err)
}
gluonID, err := bridge.imapServer.AddUser(ctx, imapConn, user.GluonKey())
if err != nil {
return fmt.Errorf("failed to add IMAP user: %w", err)
}
if err := user.SetGluonID(addrID, gluonID); err != nil {
return fmt.Errorf("failed to set IMAP user ID: %w", err)
}
log.WithField("gluonID", gluonID).Info("Re-created IMAP user")
}
} else {
log.Info("Creating new IMAP user")
gluonID, err := bridge.imapServer.AddUser(ctx, imapConn, user.GluonKey())
if err != nil {
return fmt.Errorf("failed to add IMAP user: %w", err)
}
if err := user.SetGluonID(addrID, gluonID); err != nil {
return fmt.Errorf("failed to set IMAP user ID: %w", err)
}
log.WithField("gluonID", gluonID).Info("Created new IMAP user")
}
}
// Trigger a sync for the user, if needed.
user.TriggerSync()
return nil
return bridge.serverManager.AddIMAPUser(ctx, user)
}
// removeIMAPUser disconnects the given user from gluon, optionally also removing its files.
func (bridge *Bridge) removeIMAPUser(ctx context.Context, user *user.User, withData bool) error {
if bridge.imapServer == nil {
return fmt.Errorf("no imap server instance running")
}
logrus.WithFields(logrus.Fields{
"userID": user.ID(),
"withData": withData,
}).Debug("Removing IMAP user")
for addrID, gluonID := range user.GetGluonIDs() {
if err := bridge.imapServer.RemoveUser(ctx, gluonID, withData); err != nil {
return fmt.Errorf("failed to remove IMAP user: %w", err)
}
if withData {
if err := user.RemoveGluonID(addrID, gluonID); err != nil {
return fmt.Errorf("failed to remove IMAP user ID: %w", err)
}
}
}
return nil
return bridge.serverManager.RemoveIMAPUser(ctx, user, withData)
}
func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) {
@ -249,11 +66,6 @@ func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) {
}).Info("Received mailbox message count")
}
case imapEvents.SessionAdded:
if !bridge.identifier.HasClient() {
bridge.identifier.SetClient(defaultClientName, defaultClientVersion)
}
case imapEvents.IMAPID:
logrus.WithFields(logrus.Fields{
"sessionID": event.SessionID,
@ -262,24 +74,22 @@ func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) {
}).Info("Received IMAP ID")
if event.IMAPID.Name != "" && event.IMAPID.Version != "" {
bridge.identifier.SetClient(event.IMAPID.Name, event.IMAPID.Version)
bridge.setUserAgent(event.IMAPID.Name, event.IMAPID.Version)
}
case imapEvents.LoginFailed:
logrus.WithFields(logrus.Fields{
"sessionID": event.SessionID,
"username": event.Username,
}).Info("Received IMAP login failure notification")
"pkg": "imap",
}).Error("Incorrect login credentials.")
bridge.publish(events.IMAPLoginFailed{Username: event.Username})
}
}
func getGluonDir(encVault *vault.Vault) (string, error) {
if err := os.MkdirAll(encVault.GetGluonCacheDir(), 0o700); err != nil {
return "", fmt.Errorf("failed to create gluon dir: %w", err)
case imapEvents.Login:
if strings.Contains(bridge.GetCurrentUserAgent(), useragent.DefaultUserAgent) {
bridge.setUserAgent(useragent.UnknownClient, useragent.DefaultVersion)
}
}
return encVault.GetGluonCacheDir(), nil
}
func ApplyGluonCachePathSuffix(basePath string) string {

View File

@ -0,0 +1,55 @@
// 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 (
"strings"
"github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/sirupsen/logrus"
)
func (bridge *Bridge) databaseResyncNeeded() bool {
if strings.HasPrefix(bridge.lastVersion.String(), "3.4.0") &&
strings.HasPrefix(bridge.curVersion.String(), "3.4.1") {
logrus.WithFields(logrus.Fields{
"lastVersion": bridge.lastVersion.String(),
"currVersion": bridge.curVersion.String(),
}).Warning("Database re-synchronisation needed")
return true
}
return false
}
func (bridge *Bridge) migrateUser(vault *vault.User) {
if bridge.databaseResyncNeeded() {
if err := bridge.reporter.ReportMessage("Database need to be re-sync for migration."); err != nil {
logrus.WithError(err).Error("Failed to report database re-sync for migration.")
}
if err := vault.ClearSyncStatus(); err != nil {
logrus.WithError(err).Error("Failed reset to SyncStatus.")
if err2 := bridge.reporter.ReportMessageWithContext("Failed to reset SyncStatus for Database migration.",
reporter.Context{
"error": err,
}); err2 != nil {
logrus.WithError(err2).Error("Failed to report reset SyncStatus error.")
}
}
}
}

View File

@ -24,6 +24,7 @@ type Mocks struct {
CrashHandler *mocks.MockPanicHandler
Reporter *mocks.MockReporter
Heartbeat *mocks.MockHeartbeatManager
}
func NewMocks(tb testing.TB, version, minAuto *semver.Version) *Mocks {
@ -39,14 +40,18 @@ func NewMocks(tb testing.TB, version, minAuto *semver.Version) *Mocks {
CrashHandler: mocks.NewMockPanicHandler(ctl),
Reporter: mocks.NewMockReporter(ctl),
Heartbeat: mocks.NewMockHeartbeatManager(ctl),
}
// When getting the TLS issue channel, we want to return the test channel.
mocks.TLSReporter.EXPECT().GetTLSIssueCh().Return(mocks.TLSIssueCh).AnyTimes()
// This is called at he end of any go-routine:
// This is called at the end of any go-routine:
mocks.CrashHandler.EXPECT().HandlePanic(gomock.Any()).AnyTimes()
// this is called at start of heartbeat process.
mocks.Heartbeat.EXPECT().IsTelemetryAvailable(gomock.Any()).AnyTimes()
return mocks
}
@ -139,13 +144,13 @@ func (testUpdater *TestUpdater) SetLatestVersion(version, minAuto *semver.Versio
}
}
func (testUpdater *TestUpdater) GetVersionInfo(ctx context.Context, downloader updater.Downloader, channel updater.Channel) (updater.VersionInfo, error) {
func (testUpdater *TestUpdater) GetVersionInfo(_ context.Context, _ updater.Downloader, _ updater.Channel) (updater.VersionInfo, error) {
testUpdater.lock.RLock()
defer testUpdater.lock.RUnlock()
return testUpdater.latest, nil
}
func (testUpdater *TestUpdater) InstallUpdate(ctx context.Context, downloader updater.Downloader, update updater.VersionInfo) error {
func (testUpdater *TestUpdater) InstallUpdate(_ context.Context, _ updater.Downloader, _ updater.VersionInfo) error {
return nil
}

View File

@ -0,0 +1,93 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/ProtonMail/proton-bridge/v3/internal/telemetry (interfaces: HeartbeatManager)
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
time "time"
telemetry "github.com/ProtonMail/proton-bridge/v3/internal/telemetry"
gomock "github.com/golang/mock/gomock"
)
// MockHeartbeatManager is a mock of HeartbeatManager interface.
type MockHeartbeatManager struct {
ctrl *gomock.Controller
recorder *MockHeartbeatManagerMockRecorder
}
// MockHeartbeatManagerMockRecorder is the mock recorder for MockHeartbeatManager.
type MockHeartbeatManagerMockRecorder struct {
mock *MockHeartbeatManager
}
// NewMockHeartbeatManager creates a new mock instance.
func NewMockHeartbeatManager(ctrl *gomock.Controller) *MockHeartbeatManager {
mock := &MockHeartbeatManager{ctrl: ctrl}
mock.recorder = &MockHeartbeatManagerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockHeartbeatManager) EXPECT() *MockHeartbeatManagerMockRecorder {
return m.recorder
}
// GetLastHeartbeatSent mocks base method.
func (m *MockHeartbeatManager) GetLastHeartbeatSent() time.Time {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetLastHeartbeatSent")
ret0, _ := ret[0].(time.Time)
return ret0
}
// GetLastHeartbeatSent indicates an expected call of GetLastHeartbeatSent.
func (mr *MockHeartbeatManagerMockRecorder) GetLastHeartbeatSent() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLastHeartbeatSent", reflect.TypeOf((*MockHeartbeatManager)(nil).GetLastHeartbeatSent))
}
// IsTelemetryAvailable mocks base method.
func (m *MockHeartbeatManager) IsTelemetryAvailable(arg0 context.Context) bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IsTelemetryAvailable", arg0)
ret0, _ := ret[0].(bool)
return ret0
}
// IsTelemetryAvailable indicates an expected call of IsTelemetryAvailable.
func (mr *MockHeartbeatManagerMockRecorder) IsTelemetryAvailable(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsTelemetryAvailable", reflect.TypeOf((*MockHeartbeatManager)(nil).IsTelemetryAvailable), arg0)
}
// SendHeartbeat mocks base method.
func (m *MockHeartbeatManager) SendHeartbeat(arg0 context.Context, arg1 *telemetry.HeartbeatData) bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SendHeartbeat", arg0, arg1)
ret0, _ := ret[0].(bool)
return ret0
}
// SendHeartbeat indicates an expected call of SendHeartbeat.
func (mr *MockHeartbeatManagerMockRecorder) SendHeartbeat(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendHeartbeat", reflect.TypeOf((*MockHeartbeatManager)(nil).SendHeartbeat), arg0, arg1)
}
// SetLastHeartbeatSent mocks base method.
func (m *MockHeartbeatManager) SetLastHeartbeatSent(arg0 time.Time) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetLastHeartbeatSent", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// SetLastHeartbeatSent indicates an expected call of SetLastHeartbeatSent.
func (mr *MockHeartbeatManagerMockRecorder) SetLastHeartbeatSent(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLastHeartbeatSent", reflect.TypeOf((*MockHeartbeatManager)(nil).SetLastHeartbeatSent), arg0)
}

View File

@ -28,7 +28,6 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/bradenaw/juniper/iterator"
"github.com/emersion/go-imap/client"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
)
@ -66,7 +65,7 @@ func TestBridge_Refresh(t *testing.T) {
require.NoError(t, err)
require.True(t, info.State == bridge.Connected)
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }()
@ -99,7 +98,7 @@ func TestBridge_Refresh(t *testing.T) {
require.NoError(t, err)
require.True(t, info.State == bridge.Connected)
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }()

View File

@ -34,7 +34,6 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/client"
"github.com/emersion/go-sasl"
"github.com/emersion/go-smtp"
"github.com/stretchr/testify/require"
@ -46,12 +45,17 @@ func TestBridge_Send(t *testing.T) {
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)
smtpWaiter.Wait()
senderInfo, err := bridge.GetUserInfo(senderUserID)
require.NoError(t, err)
@ -91,13 +95,13 @@ func TestBridge_Send(t *testing.T) {
}
// Connect the sender IMAP client.
senderIMAPClient, err := client.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
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 := client.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
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
@ -135,19 +139,19 @@ func TestBridge_SendDraftFlags(t *testing.T) {
})
// Start the bridge.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
withBridgeWaitForServers(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
// Get the sender user info.
userInfo, err := bridge.QueryUserInfo(username)
require.NoError(t, err)
// Connect the sender IMAP client.
imapClient, err := client.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
imapClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
require.NoError(t, err)
require.NoError(t, imapClient.Login(userInfo.Addresses[0], string(userInfo.BridgePass)))
defer imapClient.Logout() //nolint:errcheck
// The message to send.
const message = `Subject: Test\r\n\r\nHello world!`
message := fmt.Sprintf("From: %v\r\nDate: 01 Jan 1980 00:00:00 +0000\r\nSubject: Test\r\n\r\nHello world!", userInfo.Addresses[0])
// Save a draft.
require.NoError(t, imapClient.Append("Drafts", []string{imap.DraftFlag}, time.Now(), strings.NewReader(message)))
@ -245,13 +249,13 @@ func TestBridge_SendInvite(t *testing.T) {
})
// Start the bridge.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
withBridgeWaitForServers(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
// Get the sender user info.
userInfo, err := bridge.QueryUserInfo(username)
require.NoError(t, err)
// Connect the sender IMAP client.
imapClient, err := client.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
imapClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
require.NoError(t, err)
require.NoError(t, imapClient.Login(userInfo.Addresses[0], string(userInfo.BridgePass)))
defer imapClient.Logout() //nolint:errcheck
@ -401,6 +405,9 @@ SGVsbG8gd29ybGQK
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)
@ -420,6 +427,8 @@ SGVsbG8gd29ybGQK
messageMultipartWithoutTextWithTextAttachment,
}
smtpWaiter.Wait()
for _, m := range messages {
// Dial the server.
client, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
@ -444,13 +453,13 @@ SGVsbG8gd29ybGQK
}
// Connect the sender IMAP client.
senderIMAPClient, err := client.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
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 := client.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
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

View File

@ -36,6 +36,9 @@ import (
func TestBridge_Report(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
imapWaiter := waitForIMAPServerReady(b)
defer imapWaiter.Done()
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
defer done()
@ -51,6 +54,8 @@ func TestBridge_Report(t *testing.T) {
require.NoError(t, err)
require.True(t, info.State == bridge.Connected)
imapWaiter.Wait()
// Dial the IMAP port.
conn, err := net.Dial("tcp", fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
require.NoError(t, err)

View File

@ -0,0 +1,696 @@
// 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 (
"context"
"fmt"
"net"
"path/filepath"
"github.com/ProtonMail/gluon"
"github.com/ProtonMail/gluon/connector"
"github.com/ProtonMail/gluon/logging"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/user"
"github.com/ProtonMail/proton-bridge/v3/pkg/cpc"
"github.com/emersion/go-smtp"
"github.com/sirupsen/logrus"
)
// ServerManager manages the IMAP & SMTP servers and their listeners.
type ServerManager struct {
requests *cpc.CPC
imapServer *gluon.Server
imapListener net.Listener
smtpServer *smtp.Server
smtpListener net.Listener
loadedUserCount int
}
func newServerManager() *ServerManager {
return &ServerManager{
requests: cpc.NewCPC(),
}
}
func (sm *ServerManager) Init(bridge *Bridge) error {
imapServer, err := createIMAPServer(bridge)
if err != nil {
return err
}
smtpServer := createSMTPServer(bridge)
sm.imapServer = imapServer
sm.smtpServer = smtpServer
bridge.tasks.Once(func(ctx context.Context) {
logging.DoAnnotated(ctx, func(ctx context.Context) {
sm.run(ctx, bridge)
}, logging.Labels{
"service": "server-manager",
})
})
return nil
}
func (sm *ServerManager) CloseServers(ctx context.Context) error {
defer sm.requests.Close()
_, err := sm.requests.Send(ctx, &smRequestClose{})
return err
}
func (sm *ServerManager) RestartIMAP(ctx context.Context) error {
_, err := sm.requests.Send(ctx, &smRequestRestartIMAP{})
return err
}
func (sm *ServerManager) RestartSMTP(ctx context.Context) error {
_, err := sm.requests.Send(ctx, &smRequestRestartSMTP{})
return err
}
func (sm *ServerManager) AddIMAPUser(ctx context.Context, user *user.User) error {
_, err := sm.requests.Send(ctx, &smRequestAddIMAPUser{user: user})
return err
}
func (sm *ServerManager) RemoveIMAPUser(ctx context.Context, user *user.User, withData bool) error {
_, err := sm.requests.Send(ctx, &smRequestRemoveIMAPUser{
user: user,
withData: withData,
})
return err
}
func (sm *ServerManager) SetGluonDir(ctx context.Context, gluonDir string) error {
_, err := sm.requests.Send(ctx, &smRequestSetGluonDir{
dir: gluonDir,
})
return err
}
func (sm *ServerManager) AddGluonUser(ctx context.Context, conn connector.Connector, passphrase []byte) (string, error) {
reply, err := cpc.SendTyped[string](ctx, sm.requests, &smRequestAddGluonUser{
conn: conn,
passphrase: passphrase,
})
return reply, err
}
func (sm *ServerManager) RemoveGluonUser(ctx context.Context, gluonID string) error {
_, err := sm.requests.Send(ctx, &smRequestRemoveGluonUser{
userID: gluonID,
})
return err
}
func (sm *ServerManager) run(ctx context.Context, bridge *Bridge) {
eventCh, cancel := bridge.GetEvents()
defer cancel()
for {
select {
case <-ctx.Done():
sm.handleClose(ctx, bridge)
return
case evt := <-eventCh:
switch evt.(type) {
case events.ConnStatusDown:
logrus.Info("Server Manager, network down stopping listeners")
if err := sm.closeSMTPServer(bridge); err != nil {
logrus.WithError(err).Error("Failed to close SMTP server")
}
if err := sm.stopIMAPListener(bridge); err != nil {
logrus.WithError(err)
}
case events.ConnStatusUp:
logrus.Info("Server Manager, network up starting listeners")
sm.handleLoadedUserCountChange(ctx, bridge)
}
case request, ok := <-sm.requests.ReceiveCh():
if !ok {
return
}
switch r := request.Value().(type) {
case *smRequestClose:
sm.handleClose(ctx, bridge)
request.Reply(ctx, nil, nil)
return
case *smRequestRestartSMTP:
err := sm.restartSMTP(bridge)
request.Reply(ctx, nil, err)
case *smRequestRestartIMAP:
err := sm.restartIMAP(ctx, bridge)
request.Reply(ctx, nil, err)
case *smRequestAddIMAPUser:
err := sm.handleAddIMAPUser(ctx, r.user)
request.Reply(ctx, nil, err)
if err == nil {
sm.loadedUserCount++
sm.handleLoadedUserCountChange(ctx, bridge)
}
case *smRequestRemoveIMAPUser:
err := sm.handleRemoveIMAPUser(ctx, r.user, r.withData)
request.Reply(ctx, nil, err)
if err == nil {
sm.loadedUserCount--
sm.handleLoadedUserCountChange(ctx, bridge)
}
case *smRequestSetGluonDir:
err := sm.handleSetGluonDir(ctx, bridge, r.dir)
request.Reply(ctx, nil, err)
case *smRequestAddGluonUser:
id, err := sm.handleAddGluonUser(ctx, r.conn, r.passphrase)
request.Reply(ctx, id, err)
case *smRequestRemoveGluonUser:
err := sm.handleRemoveGluonUser(ctx, r.userID)
request.Reply(ctx, nil, err)
}
}
}
}
func (sm *ServerManager) handleLoadedUserCountChange(ctx context.Context, bridge *Bridge) {
logrus.Infof("Validating Listener State %v", sm.loadedUserCount)
if sm.shouldStartServers() {
if sm.imapListener == nil {
if err := sm.serveIMAP(ctx, bridge); err != nil {
logrus.WithError(err).Error("Failed to start IMAP server")
}
}
if sm.smtpListener == nil {
if err := sm.restartSMTP(bridge); err != nil {
logrus.WithError(err).Error("Failed to start SMTP server")
}
}
} else {
if sm.imapListener != nil {
if err := sm.stopIMAPListener(bridge); err != nil {
logrus.WithError(err).Error("Failed to stop IMAP server")
}
}
if sm.smtpListener != nil {
if err := sm.closeSMTPServer(bridge); err != nil {
logrus.WithError(err).Error("Failed to stop SMTP server")
}
}
}
}
func (sm *ServerManager) handleClose(ctx context.Context, bridge *Bridge) {
// Close the IMAP server.
if err := sm.closeIMAPServer(ctx, bridge); err != nil {
logrus.WithError(err).Error("Failed to close IMAP server")
}
// Close the SMTP server.
if err := sm.closeSMTPServer(bridge); err != nil {
logrus.WithError(err).Error("Failed to close SMTP server")
}
}
func (sm *ServerManager) handleAddIMAPUser(ctx context.Context, user *user.User) error {
if sm.imapServer == nil {
return fmt.Errorf("no imap server instance running")
}
imapConn, err := user.NewIMAPConnectors()
if err != nil {
return fmt.Errorf("failed to create IMAP connectors: %w", err)
}
for addrID, imapConn := range imapConn {
log := logrus.WithFields(logrus.Fields{
"userID": user.ID(),
"addrID": addrID,
})
if gluonID, ok := user.GetGluonID(addrID); ok {
log.WithField("gluonID", gluonID).Info("Loading existing IMAP user")
// Load the user, checking whether the DB was newly created.
isNew, err := sm.imapServer.LoadUser(ctx, imapConn, gluonID, user.GluonKey())
if err != nil {
return fmt.Errorf("failed to load IMAP user: %w", err)
}
if isNew {
// If the DB was newly created, clear the sync status; gluon's DB was not found.
logrus.Warn("IMAP user DB was newly created, clearing sync status")
// Remove the user from IMAP so we can clear the sync status.
if err := sm.imapServer.RemoveUser(ctx, gluonID, false); err != nil {
return fmt.Errorf("failed to remove IMAP user: %w", err)
}
// Clear the sync status -- we need to resync all messages.
if err := user.ClearSyncStatus(); err != nil {
return fmt.Errorf("failed to clear sync status: %w", err)
}
// Add the user back to the IMAP server.
if isNew, err := sm.imapServer.LoadUser(ctx, imapConn, gluonID, user.GluonKey()); err != nil {
return fmt.Errorf("failed to add IMAP user: %w", err)
} else if isNew {
panic("IMAP user should already have a database")
}
} else if status := user.GetSyncStatus(); !status.HasLabels {
// Otherwise, the DB already exists -- if the labels are not yet synced, we need to re-create the DB.
if err := sm.imapServer.RemoveUser(ctx, gluonID, true); err != nil {
return fmt.Errorf("failed to remove old IMAP user: %w", err)
}
if err := user.RemoveGluonID(addrID, gluonID); err != nil {
return fmt.Errorf("failed to remove old IMAP user ID: %w", err)
}
gluonID, err := sm.imapServer.AddUser(ctx, imapConn, user.GluonKey())
if err != nil {
return fmt.Errorf("failed to add IMAP user: %w", err)
}
if err := user.SetGluonID(addrID, gluonID); err != nil {
return fmt.Errorf("failed to set IMAP user ID: %w", err)
}
log.WithField("gluonID", gluonID).Info("Re-created IMAP user")
}
} else {
log.Info("Creating new IMAP user")
gluonID, err := sm.imapServer.AddUser(ctx, imapConn, user.GluonKey())
if err != nil {
return fmt.Errorf("failed to add IMAP user: %w", err)
}
if err := user.SetGluonID(addrID, gluonID); err != nil {
return fmt.Errorf("failed to set IMAP user ID: %w", err)
}
log.WithField("gluonID", gluonID).Info("Created new IMAP user")
}
}
// Trigger a sync for the user, if needed.
user.TriggerSync()
return nil
}
func (sm *ServerManager) handleRemoveIMAPUser(ctx context.Context, user *user.User, withData bool) error {
if sm.imapServer == nil {
return fmt.Errorf("no imap server instance running")
}
logrus.WithFields(logrus.Fields{
"userID": user.ID(),
"withData": withData,
}).Debug("Removing IMAP user")
for addrID, gluonID := range user.GetGluonIDs() {
if err := sm.imapServer.RemoveUser(ctx, gluonID, withData); err != nil {
return fmt.Errorf("failed to remove IMAP user: %w", err)
}
if withData {
if err := user.RemoveGluonID(addrID, gluonID); err != nil {
return fmt.Errorf("failed to remove IMAP user ID: %w", err)
}
}
}
return nil
}
func createIMAPServer(bridge *Bridge) (*gluon.Server, error) {
gluonDataDir, err := bridge.GetGluonDataDir()
if err != nil {
return nil, fmt.Errorf("failed to get Gluon Database directory: %w", err)
}
return newIMAPServer(
bridge.vault.GetGluonCacheDir(),
gluonDataDir,
bridge.curVersion,
bridge.tlsConfig,
bridge.reporter,
bridge.logIMAPClient,
bridge.logIMAPServer,
bridge.imapEventCh,
bridge.tasks,
bridge.uidValidityGenerator,
bridge.panicHandler,
)
}
func createSMTPServer(bridge *Bridge) *smtp.Server {
return newSMTPServer(bridge, bridge.tlsConfig, bridge.logSMTP)
}
func (sm *ServerManager) closeSMTPServer(bridge *Bridge) error {
// We close the listener ourselves even though it's also closed by smtpServer.Close().
// This is because smtpServer.Serve() is called in a separate goroutine and might be executed
// after we've already closed the server. However, go-smtp has a bug; it blocks on the listener
// even after the server has been closed. So we close the listener ourselves to unblock it.
if sm.smtpListener != nil {
logrus.Info("Closing SMTP Listener")
if err := sm.smtpListener.Close(); err != nil {
return fmt.Errorf("failed to close SMTP listener: %w", err)
}
sm.smtpListener = nil
}
if sm.smtpServer != nil {
logrus.Info("Closing SMTP server")
if err := sm.smtpServer.Close(); err != nil {
logrus.WithError(err).Debug("Failed to close SMTP server (expected -- we close the listener ourselves)")
}
sm.smtpServer = nil
bridge.publish(events.SMTPServerStopped{})
}
return nil
}
func (sm *ServerManager) closeIMAPServer(ctx context.Context, bridge *Bridge) error {
if sm.imapListener != nil {
logrus.Info("Closing IMAP Listener")
if err := sm.imapListener.Close(); err != nil {
return fmt.Errorf("failed to close IMAP listener: %w", err)
}
sm.imapListener = nil
bridge.publish(events.IMAPServerStopped{})
}
if sm.imapServer != nil {
logrus.Info("Closing IMAP server")
if err := sm.imapServer.Close(ctx); err != nil {
return fmt.Errorf("failed to close IMAP server: %w", err)
}
sm.imapServer = nil
}
return nil
}
func (sm *ServerManager) restartIMAP(ctx context.Context, bridge *Bridge) error {
logrus.Info("Restarting IMAP server")
if sm.imapListener != nil {
if err := sm.imapListener.Close(); err != nil {
return fmt.Errorf("failed to close IMAP listener: %w", err)
}
sm.imapListener = nil
bridge.publish(events.IMAPServerStopped{})
}
if sm.shouldStartServers() {
return sm.serveIMAP(ctx, bridge)
}
return nil
}
func (sm *ServerManager) restartSMTP(bridge *Bridge) error {
logrus.Info("Restarting SMTP server")
if err := sm.closeSMTPServer(bridge); err != nil {
return fmt.Errorf("failed to close SMTP: %w", err)
}
bridge.publish(events.SMTPServerStopped{})
sm.smtpServer = newSMTPServer(bridge, bridge.tlsConfig, bridge.logSMTP)
if sm.shouldStartServers() {
return sm.serveSMTP(bridge)
}
return nil
}
func (sm *ServerManager) serveSMTP(bridge *Bridge) error {
port, err := func() (int, error) {
logrus.WithFields(logrus.Fields{
"port": bridge.vault.GetSMTPPort(),
"ssl": bridge.vault.GetSMTPSSL(),
}).Info("Starting SMTP server")
smtpListener, err := newListener(bridge.vault.GetSMTPPort(), bridge.vault.GetSMTPSSL(), bridge.tlsConfig)
if err != nil {
return 0, fmt.Errorf("failed to create SMTP listener: %w", err)
}
sm.smtpListener = smtpListener
bridge.tasks.Once(func(context.Context) {
if err := sm.smtpServer.Serve(smtpListener); err != nil {
logrus.WithError(err).Info("SMTP server stopped")
}
})
if err := bridge.vault.SetSMTPPort(getPort(smtpListener.Addr())); err != nil {
return 0, fmt.Errorf("failed to store SMTP port in vault: %w", err)
}
return getPort(smtpListener.Addr()), nil
}()
if err != nil {
bridge.publish(events.SMTPServerError{
Error: err,
})
return err
}
bridge.publish(events.SMTPServerReady{
Port: port,
})
return nil
}
func (sm *ServerManager) serveIMAP(ctx context.Context, bridge *Bridge) error {
port, err := func() (int, error) {
if sm.imapServer == nil {
return 0, fmt.Errorf("no IMAP server instance running")
}
logrus.WithFields(logrus.Fields{
"port": bridge.vault.GetIMAPPort(),
"ssl": bridge.vault.GetIMAPSSL(),
}).Info("Starting IMAP server")
imapListener, err := newListener(bridge.vault.GetIMAPPort(), bridge.vault.GetIMAPSSL(), bridge.tlsConfig)
if err != nil {
return 0, fmt.Errorf("failed to create IMAP listener: %w", err)
}
sm.imapListener = imapListener
if err := sm.imapServer.Serve(ctx, sm.imapListener); err != nil {
return 0, fmt.Errorf("failed to serve IMAP: %w", err)
}
if err := bridge.vault.SetIMAPPort(getPort(imapListener.Addr())); err != nil {
return 0, fmt.Errorf("failed to store IMAP port in vault: %w", err)
}
return getPort(imapListener.Addr()), nil
}()
if err != nil {
bridge.publish(events.IMAPServerError{
Error: err,
})
return err
}
bridge.publish(events.IMAPServerReady{
Port: port,
})
return nil
}
func (sm *ServerManager) stopIMAPListener(bridge *Bridge) error {
logrus.Info("Stopping IMAP listener")
if sm.imapListener != nil {
if err := sm.imapListener.Close(); err != nil {
return err
}
sm.imapListener = nil
bridge.publish(events.IMAPServerStopped{})
}
return nil
}
func (sm *ServerManager) handleSetGluonDir(ctx context.Context, bridge *Bridge, newGluonDir string) error {
return safe.RLockRet(func() error {
currentGluonDir := bridge.GetGluonCacheDir()
newGluonDir = filepath.Join(newGluonDir, "gluon")
if newGluonDir == currentGluonDir {
return fmt.Errorf("new gluon dir is the same as the old one")
}
if err := sm.closeIMAPServer(context.Background(), bridge); err != nil {
return fmt.Errorf("failed to close IMAP: %w", err)
}
sm.loadedUserCount = 0
if err := bridge.moveGluonCacheDir(currentGluonDir, newGluonDir); err != nil {
logrus.WithError(err).Error("failed to move GluonCacheDir")
if err := bridge.vault.SetGluonDir(currentGluonDir); err != nil {
return fmt.Errorf("failed to revert GluonCacheDir: %w", err)
}
}
bridge.heartbeat.SetCacheLocation(newGluonDir)
gluonDataDir, err := bridge.GetGluonDataDir()
if err != nil {
return fmt.Errorf("failed to get Gluon Database directory: %w", err)
}
imapServer, err := newIMAPServer(
bridge.vault.GetGluonCacheDir(),
gluonDataDir,
bridge.curVersion,
bridge.tlsConfig,
bridge.reporter,
bridge.logIMAPClient,
bridge.logIMAPServer,
bridge.imapEventCh,
bridge.tasks,
bridge.uidValidityGenerator,
bridge.panicHandler,
)
if err != nil {
return fmt.Errorf("failed to create new IMAP server: %w", err)
}
sm.imapServer = imapServer
for _, bridgeUser := range bridge.users {
if err := sm.handleAddIMAPUser(ctx, bridgeUser); err != nil {
return fmt.Errorf("failed to add users to new IMAP server: %w", err)
}
sm.loadedUserCount++
}
if sm.shouldStartServers() {
if err := sm.serveIMAP(ctx, bridge); err != nil {
return fmt.Errorf("failed to serve IMAP: %w", err)
}
}
return nil
}, bridge.usersLock)
}
func (sm *ServerManager) handleAddGluonUser(ctx context.Context, conn connector.Connector, passphrase []byte) (string, error) {
if sm.imapServer == nil {
return "", fmt.Errorf("no imap server instance running")
}
return sm.imapServer.AddUser(ctx, conn, passphrase)
}
func (sm *ServerManager) handleRemoveGluonUser(ctx context.Context, userID string) error {
if sm.imapServer == nil {
return fmt.Errorf("no imap server instance running")
}
return sm.imapServer.RemoveUser(ctx, userID, true)
}
func (sm *ServerManager) shouldStartServers() bool {
return sm.loadedUserCount >= 1
}
type smRequestClose struct{}
type smRequestRestartIMAP struct{}
type smRequestRestartSMTP struct{}
type smRequestAddIMAPUser struct {
user *user.User
}
type smRequestRemoveIMAPUser struct {
user *user.User
withData bool
}
type smRequestSetGluonDir struct {
dir string
}
type smRequestAddGluonUser struct {
conn connector.Connector
passphrase []byte
}
type smRequestRemoveGluonUser struct {
userID string
}

View File

@ -0,0 +1,179 @@
// 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_test
import (
"context"
"fmt"
"testing"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/go-proton-api/server"
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/stretchr/testify/require"
)
func TestServerManager_NoLoadedUsersNoServers(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
_, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
require.Error(t, err)
})
})
}
func TestServerManager_ServersStartAfterFirstConnectedUser(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
imapWaiter := waitForIMAPServerReady(bridge)
defer imapWaiter.Done()
smtpWaiter := waitForSMTPServerReady(bridge)
defer smtpWaiter.Done()
_, err := bridge.LoginFull(ctx, username, password, nil, nil)
require.NoError(t, err)
imapWaiter.Wait()
smtpWaiter.Wait()
})
})
}
func TestServerManager_ServersStopsAfterUserLogsOut(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
imapWaiter := waitForIMAPServerReady(bridge)
defer imapWaiter.Done()
smtpWaiter := waitForSMTPServerReady(bridge)
defer smtpWaiter.Done()
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
require.NoError(t, err)
imapWaiter.Wait()
smtpWaiter.Wait()
imapWaiterStopped := waitForIMAPServerStopped(bridge)
defer imapWaiterStopped.Done()
require.NoError(t, bridge.LogoutUser(ctx, userID))
imapWaiterStopped.Wait()
})
})
}
func TestServerManager_ServersDoNotStopWhenThereIsStillOneActiveUser(t *testing.T) {
otherPassword := []byte("bar")
otherUser := "foo"
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
_, _, err := s.CreateUser(otherUser, otherPassword)
require.NoError(t, err)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
imapWaiter := waitForIMAPServerReady(bridge)
defer imapWaiter.Done()
smtpWaiter := waitForSMTPServerReady(bridge)
defer smtpWaiter.Done()
_, err := bridge.LoginFull(ctx, username, password, nil, nil)
require.NoError(t, err)
userIDOther, err := bridge.LoginFull(ctx, otherUser, otherPassword, nil, nil)
require.NoError(t, err)
imapWaiter.Wait()
smtpWaiter.Wait()
evtCh, cancel := bridge.GetEvents(events.UserDeauth{})
defer cancel()
require.NoError(t, s.RevokeUser(userIDOther))
waitForEvent(t, evtCh, events.UserDeauth{})
imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, imapClient.Logout())
})
})
}
func TestServerManager_ServersStartIfAtLeastOneUserIsLoggedIn(t *testing.T) {
otherPassword := []byte("bar")
otherUser := "foo"
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
userIDOther, _, err := s.CreateUser(otherUser, otherPassword)
require.NoError(t, err)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
_, err := bridge.LoginFull(ctx, username, password, nil, nil)
require.NoError(t, err)
_, err = bridge.LoginFull(ctx, otherUser, otherPassword, nil, nil)
require.NoError(t, err)
})
require.NoError(t, s.RevokeUser(userIDOther))
withBridgeWaitForServers(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, imapClient.Logout())
})
})
}
func TestServerManager_NetworkLossStopsServers(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
imapWaiter := waitForIMAPServerReady(bridge)
defer imapWaiter.Done()
smtpWaiter := waitForSMTPServerReady(bridge)
defer smtpWaiter.Done()
imapWaiterStop := waitForIMAPServerStopped(bridge)
defer imapWaiterStop.Done()
smtpWaiterStop := waitForSMTPServerStopped(bridge)
defer smtpWaiterStop.Done()
_, err := bridge.LoginFull(ctx, username, password, nil, nil)
require.NoError(t, err)
imapWaiter.Wait()
smtpWaiter.Wait()
netCtl.Disable()
imapWaiterStop.Wait()
smtpWaiterStop.Wait()
netCtl.Enable()
imapWaiter.Wait()
smtpWaiter.Wait()
})
})
}

View File

@ -22,7 +22,6 @@ import (
"fmt"
"net"
"os"
"path/filepath"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
@ -46,6 +45,8 @@ func (bridge *Bridge) SetKeychainApp(helper string) error {
return err
}
bridge.heartbeat.SetKeyChainPref(helper)
return vault.SetHelper(vaultDir, helper)
}
@ -53,7 +54,7 @@ func (bridge *Bridge) GetIMAPPort() int {
return bridge.vault.GetIMAPPort()
}
func (bridge *Bridge) SetIMAPPort(newPort int) error {
func (bridge *Bridge) SetIMAPPort(ctx context.Context, newPort int) error {
if newPort == bridge.vault.GetIMAPPort() {
return nil
}
@ -62,14 +63,16 @@ func (bridge *Bridge) SetIMAPPort(newPort int) error {
return err
}
return bridge.restartIMAP()
bridge.heartbeat.SetIMAPPort(newPort)
return bridge.restartIMAP(ctx)
}
func (bridge *Bridge) GetIMAPSSL() bool {
return bridge.vault.GetIMAPSSL()
}
func (bridge *Bridge) SetIMAPSSL(newSSL bool) error {
func (bridge *Bridge) SetIMAPSSL(ctx context.Context, newSSL bool) error {
if newSSL == bridge.vault.GetIMAPSSL() {
return nil
}
@ -78,14 +81,16 @@ func (bridge *Bridge) SetIMAPSSL(newSSL bool) error {
return err
}
return bridge.restartIMAP()
bridge.heartbeat.SetIMAPConnectionMode(newSSL)
return bridge.restartIMAP(ctx)
}
func (bridge *Bridge) GetSMTPPort() int {
return bridge.vault.GetSMTPPort()
}
func (bridge *Bridge) SetSMTPPort(newPort int) error {
func (bridge *Bridge) SetSMTPPort(ctx context.Context, newPort int) error {
if newPort == bridge.vault.GetSMTPPort() {
return nil
}
@ -94,14 +99,16 @@ func (bridge *Bridge) SetSMTPPort(newPort int) error {
return err
}
return bridge.restartSMTP()
bridge.heartbeat.SetSMTPPort(newPort)
return bridge.restartSMTP(ctx)
}
func (bridge *Bridge) GetSMTPSSL() bool {
return bridge.vault.GetSMTPSSL()
}
func (bridge *Bridge) SetSMTPSSL(newSSL bool) error {
func (bridge *Bridge) SetSMTPSSL(ctx context.Context, newSSL bool) error {
if newSSL == bridge.vault.GetSMTPSSL() {
return nil
}
@ -110,7 +117,9 @@ func (bridge *Bridge) SetSMTPSSL(newSSL bool) error {
return err
}
return bridge.restartSMTP()
bridge.heartbeat.SetSMTPConnectionMode(newSSL)
return bridge.restartSMTP(ctx)
}
func (bridge *Bridge) GetGluonCacheDir() string {
@ -122,61 +131,7 @@ func (bridge *Bridge) GetGluonDataDir() (string, error) {
}
func (bridge *Bridge) SetGluonDir(ctx context.Context, newGluonDir string) error {
return safe.RLockRet(func() error {
currentGluonDir := bridge.GetGluonCacheDir()
newGluonDir = filepath.Join(newGluonDir, "gluon")
if newGluonDir == currentGluonDir {
return fmt.Errorf("new gluon dir is the same as the old one")
}
if err := bridge.closeIMAP(context.Background()); err != nil {
return fmt.Errorf("failed to close IMAP: %w", err)
}
if err := bridge.moveGluonCacheDir(currentGluonDir, newGluonDir); err != nil {
logrus.WithError(err).Error("failed to move GluonCacheDir")
if err := bridge.vault.SetGluonDir(currentGluonDir); err != nil {
return fmt.Errorf("failed to revert GluonCacheDir: %w", err)
}
}
gluonDataDir, err := bridge.GetGluonDataDir()
if err != nil {
return fmt.Errorf("failed to get Gluon Database directory: %w", err)
}
imapServer, err := newIMAPServer(
bridge.vault.GetGluonCacheDir(),
gluonDataDir,
bridge.curVersion,
bridge.tlsConfig,
bridge.reporter,
bridge.logIMAPClient,
bridge.logIMAPServer,
bridge.imapEventCh,
bridge.tasks,
bridge.uidValidityGenerator,
bridge.panicHandler,
)
if err != nil {
return fmt.Errorf("failed to create new IMAP server: %w", err)
}
bridge.imapServer = imapServer
for _, user := range bridge.users {
if err := bridge.addIMAPUser(ctx, user); err != nil {
return fmt.Errorf("failed to add users to new IMAP server: %w", err)
}
}
if err := bridge.serveIMAP(); err != nil {
return fmt.Errorf("failed to serve IMAP: %w", err)
}
return nil
}, bridge.usersLock)
return bridge.serverManager.SetGluonDir(ctx, newGluonDir)
}
func (bridge *Bridge) moveGluonCacheDir(oldGluonDir, newGluonDir string) error {
@ -207,6 +162,8 @@ func (bridge *Bridge) SetProxyAllowed(allowed bool) error {
bridge.proxyCtl.DisallowProxy()
}
bridge.heartbeat.SetDoh(allowed)
return bridge.vault.SetProxyAllowed(allowed)
}
@ -220,6 +177,8 @@ func (bridge *Bridge) SetShowAllMail(show bool) error {
user.SetShowAllMail(show)
}
bridge.heartbeat.SetShowAllMail(show)
return bridge.vault.SetShowAllMail(show)
}, bridge.usersLock)
}
@ -233,6 +192,8 @@ func (bridge *Bridge) SetAutostart(autostart bool) error {
if err := bridge.vault.SetAutostart(autostart); err != nil {
return err
}
bridge.heartbeat.SetAutoStart(autostart)
}
var err error
@ -253,6 +214,10 @@ func (bridge *Bridge) SetAutostart(autostart bool) error {
return err
}
func (bridge *Bridge) GetUpdateRollout() float64 {
return bridge.vault.GetUpdateRollout()
}
func (bridge *Bridge) GetAutoUpdate() bool {
return bridge.vault.GetAutoUpdate()
}
@ -266,11 +231,28 @@ func (bridge *Bridge) SetAutoUpdate(autoUpdate bool) error {
return err
}
bridge.heartbeat.SetAutoUpdate(autoUpdate)
bridge.goUpdate()
return nil
}
func (bridge *Bridge) GetTelemetryDisabled() bool {
return bridge.vault.GetTelemetryDisabled()
}
func (bridge *Bridge) SetTelemetryDisabled(isDisabled bool) error {
if err := bridge.vault.SetTelemetryDisabled(isDisabled); err != nil {
return err
}
// If telemetry is re-enabled locally, try to send the heartbeat.
if !isDisabled {
defer bridge.goHeartbeat()
}
return nil
}
func (bridge *Bridge) GetUpdateChannel() updater.Channel {
return bridge.vault.GetUpdateChannel()
}
@ -284,6 +266,8 @@ func (bridge *Bridge) SetUpdateChannel(channel updater.Channel) error {
return err
}
bridge.heartbeat.SetBeta(channel)
bridge.goUpdate()
return nil
@ -313,10 +297,11 @@ func (bridge *Bridge) SetColorScheme(colorScheme string) error {
// Note: it does not clear the keychain. The only entry in the keychain is the vault password,
// which we need at next startup to decrypt the vault.
func (bridge *Bridge) FactoryReset(ctx context.Context) {
useTelemetry := !bridge.GetTelemetryDisabled()
// Delete all the users.
safe.Lock(func() {
for _, user := range bridge.users {
bridge.logoutUser(ctx, user, true, true)
bridge.logoutUser(ctx, user, true, true, useTelemetry)
}
}, bridge.usersLock)

View File

@ -57,7 +57,7 @@ func TestBridge_Settings_IMAPPort(t *testing.T) {
curPort := bridge.GetIMAPPort()
// Set the port to 1144.
require.NoError(t, bridge.SetIMAPPort(1144))
require.NoError(t, bridge.SetIMAPPort(ctx, 1144))
// Get the new setting.
require.Equal(t, 1144, bridge.GetIMAPPort())
@ -75,7 +75,7 @@ func TestBridge_Settings_IMAPSSL(t *testing.T) {
require.False(t, bridge.GetIMAPSSL())
// Enable IMAP SSL.
require.NoError(t, bridge.SetIMAPSSL(true))
require.NoError(t, bridge.SetIMAPSSL(ctx, true))
// Get the new setting.
require.True(t, bridge.GetIMAPSSL())
@ -89,7 +89,7 @@ func TestBridge_Settings_SMTPPort(t *testing.T) {
curPort := bridge.GetSMTPPort()
// Set the port to 1024.
require.NoError(t, bridge.SetSMTPPort(1024))
require.NoError(t, bridge.SetSMTPPort(ctx, 1024))
// Get the new setting.
require.Equal(t, 1024, bridge.GetSMTPPort())
@ -107,7 +107,7 @@ func TestBridge_Settings_SMTPSSL(t *testing.T) {
require.False(t, bridge.GetSMTPSSL())
// Enable SMTP SSL.
require.NoError(t, bridge.SetSMTPSSL(true))
require.NoError(t, bridge.SetSMTPSSL(ctx, true))
// Get the new setting.
require.True(t, bridge.GetSMTPSSL())

View File

@ -20,90 +20,16 @@ package bridge
import (
"context"
"crypto/tls"
"fmt"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
"github.com/emersion/go-sasl"
"github.com/emersion/go-smtp"
"github.com/sirupsen/logrus"
)
func (bridge *Bridge) serveSMTP() error {
port, err := func() (int, error) {
logrus.Info("Starting SMTP server")
smtpListener, err := newListener(bridge.vault.GetSMTPPort(), bridge.vault.GetSMTPSSL(), bridge.tlsConfig)
if err != nil {
return 0, fmt.Errorf("failed to create SMTP listener: %w", err)
}
bridge.smtpListener = smtpListener
bridge.tasks.Once(func(context.Context) {
if err := bridge.smtpServer.Serve(smtpListener); err != nil {
logrus.WithError(err).Info("SMTP server stopped")
}
})
if err := bridge.vault.SetSMTPPort(getPort(smtpListener.Addr())); err != nil {
return 0, fmt.Errorf("failed to store SMTP port in vault: %w", err)
}
return getPort(smtpListener.Addr()), nil
}()
if err != nil {
bridge.publish(events.SMTPServerError{
Error: err,
})
return err
}
bridge.publish(events.SMTPServerReady{
Port: port,
})
return nil
}
func (bridge *Bridge) restartSMTP() error {
logrus.Info("Restarting SMTP server")
if err := bridge.closeSMTP(); err != nil {
return fmt.Errorf("failed to close SMTP: %w", err)
}
bridge.publish(events.SMTPServerStopped{})
bridge.smtpServer = newSMTPServer(bridge, bridge.tlsConfig, bridge.logSMTP)
return bridge.serveSMTP()
}
// We close the listener ourselves even though it's also closed by smtpServer.Close().
// This is because smtpServer.Serve() is called in a separate goroutine and might be executed
// after we've already closed the server. However, go-smtp has a bug; it blocks on the listener
// even after the server has been closed. So we close the listener ourselves to unblock it.
func (bridge *Bridge) closeSMTP() error {
logrus.Info("Closing SMTP server")
if bridge.smtpListener != nil {
if err := bridge.smtpListener.Close(); err != nil {
return fmt.Errorf("failed to close SMTP listener: %w", err)
}
}
if err := bridge.smtpServer.Close(); err != nil {
logrus.WithError(err).Debug("Failed to close SMTP server (expected -- we close the listener ourselves)")
}
bridge.publish(events.SMTPServerStopped{})
return nil
func (bridge *Bridge) restartSMTP(ctx context.Context) error {
return bridge.serverManager.RestartSMTP(ctx)
}
func newSMTPServer(bridge *Bridge, tlsConfig *tls.Config, logSMTP bool) *smtp.Server {

View File

@ -18,11 +18,15 @@
package bridge
import (
"context"
"fmt"
"io"
"strings"
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
"github.com/emersion/go-smtp"
"github.com/sirupsen/logrus"
)
type smtpBackend struct {
@ -54,10 +58,29 @@ func (s *smtpSession) AuthPlain(username, password string) error {
s.userID = user.ID()
s.authID = addrID
if strings.Contains(s.Bridge.GetCurrentUserAgent(), useragent.DefaultUserAgent) {
s.Bridge.setUserAgent(useragent.UnknownClient, useragent.DefaultVersion)
}
user.SendConfigStatusSuccess(context.Background())
return nil
}
return fmt.Errorf("invalid username or password")
logrus.WithFields(logrus.Fields{
"username": username,
"pkg": "smtp",
}).Error("Incorrect login credentials.")
err := fmt.Errorf("invalid username or password")
for _, user := range s.users {
for _, mail := range user.Emails() {
if mail == username {
user.ReportConfigStatusFailure(err.Error())
return err
}
}
}
return err
}, s.usersLock)
}
@ -71,7 +94,7 @@ func (s *smtpSession) Logout() error {
return nil
}
func (s *smtpSession) Mail(from string, opts *smtp.MailOptions) error {
func (s *smtpSession) Mail(from string, _ *smtp.MailOptions) error {
s.from = from
return nil
}
@ -85,7 +108,7 @@ func (s *smtpSession) Rcpt(to string) error {
}
func (s *smtpSession) Data(r io.Reader) error {
return safe.RLockRet(func() error {
err := safe.RLockRet(func() error {
user, ok := s.users[s.userID]
if !ok {
return ErrNoSuchUser
@ -93,4 +116,10 @@ func (s *smtpSession) Data(r io.Reader) error {
return user.SendMail(s.authID, s.from, s.to, r)
}, s.usersLock)
if err != nil {
logrus.WithField("pkg", "smtp").WithError(err).Error("Send mail failed.")
}
return err
}

View File

@ -80,7 +80,7 @@ func TestBridge_Sync(t *testing.T) {
require.NoError(t, err)
require.True(t, info.State == bridge.Connected)
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }()
@ -112,15 +112,6 @@ func TestBridge_Sync(t *testing.T) {
info, err := b.GetUserInfo(userID)
require.NoError(t, err)
require.True(t, info.State == bridge.Connected)
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }()
status, err := client.Select(`Folders/folder`, false)
require.NoError(t, err)
require.Less(t, status.Messages, uint32(numMsg))
}
// Remove the network limit, allowing the sync to finish.
@ -136,7 +127,7 @@ func TestBridge_Sync(t *testing.T) {
require.NoError(t, err)
require.True(t, info.State == bridge.Connected)
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }()
@ -187,7 +178,7 @@ func _TestBridge_Sync_BadMessage(t *testing.T) { //nolint:unused,deadcode
require.NoError(t, err)
require.True(t, info.State == bridge.Connected)
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }()
@ -273,15 +264,6 @@ func TestBridge_SyncWithOngoingEvents(t *testing.T) {
info, err := b.GetUserInfo(userID)
require.NoError(t, err)
require.True(t, info.State == bridge.Connected)
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }()
status, err := client.Select(`Folders/folder`, false)
require.NoError(t, err)
require.Less(t, status.Messages, uint32(numMsg))
}
// Create a new mailbox and move that last 1/3 of the messages into it to simulate user
@ -311,7 +293,7 @@ func TestBridge_SyncWithOngoingEvents(t *testing.T) {
require.NoError(t, err)
require.True(t, info.State == bridge.Connected)
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }()
@ -417,6 +399,10 @@ func createNumMessages(ctx context.Context, t *testing.T, c *proton.Client, addr
}
func createMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID, labelID string, messages ...[]byte) []string {
return createMessagesWithFlags(ctx, t, c, addrID, labelID, 0, messages...)
}
func createMessagesWithFlags(ctx context.Context, t *testing.T, c *proton.Client, addrID, labelID string, flags proton.MessageFlag, messages ...[]byte) []string {
user, err := c.GetUser(ctx)
require.NoError(t, err)
@ -435,6 +421,13 @@ func createMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID,
_, ok := addrKRs[addrID]
require.True(t, ok)
var msgFlags proton.MessageFlag
if flags == 0 {
msgFlags = proton.MessageFlagReceived
} else {
msgFlags = flags
}
str, err := c.ImportMessages(
ctx,
addrKRs[addrID],
@ -445,7 +438,7 @@ func createMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID,
Metadata: proton.ImportMetadata{
AddressID: addrID,
LabelIDs: []string{labelID},
Flags: proton.MessageFlagReceived,
Flags: msgFlags,
},
Message: message,
}

View File

@ -0,0 +1,82 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
//go:build !windows
package bridge_test
import (
"context"
"syscall"
"testing"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/go-proton-api/server"
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/stretchr/testify/require"
)
// Disabled due to flakyness.
func _TestBridge_SyncExistsWithErrorWhenTooManyFilesAreOpen(t *testing.T) { //nolint:unused
var rlimitCurrent syscall.Rlimit
require.NoError(t, syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rlimitCurrent))
// Restore RLimit for Process at the end of this test
defer func() {
require.NoError(t, syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rlimitCurrent))
}()
rlimit := syscall.Rlimit{
Max: 100,
Cur: 100,
}
require.NoError(t, syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rlimit))
numMsg := 1 << 8
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []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, numMsg)
})
// The initial user should be fully synced.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
syncCh, done := bridge.GetEvents(events.SyncFailed{})
defer done()
userID, err := bridge.LoginFull(ctx, "imap", password, nil, nil)
require.NoError(t, err)
evt := <-syncCh
switch e := evt.(type) {
case events.SyncFailed:
require.Equal(t, userID, e.UserID)
default:
require.Fail(t, "Expected events.SyncFailed{}")
}
})
}, server.WithTLS(false))
}

View File

@ -28,6 +28,7 @@ type Locator interface {
ProvideLogsPath() (string, error)
ProvideGluonCachePath() (string, error)
ProvideGluonDataPath() (string, error)
ProvideStatsPath() (string, error)
GetLicenseFilePath() string
GetDependencyLicensesLink() string
Clear(...string) error
@ -38,6 +39,8 @@ type Identifier interface {
HasClient() bool
SetClient(name, version string)
SetPlatform(platform string)
SetClientString(client string)
GetClientString() string
}
type ProxyController interface {

View File

@ -229,7 +229,7 @@ func (bridge *Bridge) LogoutUser(ctx context.Context, userID string) error {
return ErrNoSuchUser
}
bridge.logoutUser(ctx, user, true, false)
bridge.logoutUser(ctx, user, true, false, false)
bridge.publish(events.UserLoggedOut{
UserID: userID,
@ -249,7 +249,7 @@ func (bridge *Bridge) DeleteUser(ctx context.Context, userID string) error {
}
if user, ok := bridge.users[userID]; ok {
bridge.logoutUser(ctx, user, true, true)
bridge.logoutUser(ctx, user, true, true, !bridge.GetTelemetryDisabled())
}
if err := bridge.vault.DeleteUser(userID); err != nil {
@ -295,6 +295,15 @@ func (bridge *Bridge) SetAddressMode(ctx context.Context, userID string, mode va
AddressMode: mode,
})
var splitMode = false
for _, user := range bridge.users {
if user.GetAddressMode() == vault.SplitMode {
splitMode = true
break
}
}
bridge.heartbeat.SetSplitMode(splitMode)
return nil
}, bridge.usersLock)
}
@ -342,7 +351,7 @@ func (bridge *Bridge) SendBadEventUserFeedback(_ context.Context, userID string,
logrus.WithError(rerr).Error("Failed to report feedback failure")
}
bridge.logoutUser(ctx, user, true, false)
bridge.logoutUser(ctx, user, true, false, false)
bridge.publish(events.UserLoggedOut{
UserID: userID,
@ -399,7 +408,7 @@ func (bridge *Bridge) loadUsers(ctx context.Context) error {
return nil
}
log.Info("Loading connected user")
log.WithField("mode", user.AddressMode()).Info("Loading connected user")
bridge.publish(events.UserLoading{
UserID: user.UserID(),
@ -510,6 +519,14 @@ func (bridge *Bridge) addUserWithVault(
apiUser proton.User,
vault *vault.User,
) error {
statsPath, err := bridge.locator.ProvideStatsPath()
if err != nil {
return fmt.Errorf("failed to get Statistics directory: %w", err)
}
// re-set SyncStatus if database need to be re-synced for migration.
bridge.migrateUser(vault)
user, err := user.New(
ctx,
vault,
@ -519,6 +536,8 @@ func (bridge *Bridge) addUserWithVault(
bridge.panicHandler,
bridge.vault.GetShowAllMail(),
bridge.vault.GetMaxSyncMemory(),
statsPath,
bridge,
)
if err != nil {
return fmt.Errorf("failed to create user: %w", err)
@ -550,7 +569,7 @@ func (bridge *Bridge) addUserWithVault(
// As such, if we find this ID in the context, we should use it to update our user agent.
client.AddPreRequestHook(func(_ *resty.Client, r *resty.Request) error {
if imapID, ok := imap.GetIMAPIDFromContext(r.Context()); ok {
bridge.identifier.SetClient(imapID.Name, imapID.Version)
bridge.setUserAgent(imapID.Name, imapID.Version)
}
return nil
@ -559,8 +578,12 @@ func (bridge *Bridge) addUserWithVault(
// Finally, save the user in the bridge.
safe.Lock(func() {
bridge.users[apiUser.ID] = user
bridge.heartbeat.SetNbAccount(len(bridge.users))
}, bridge.usersLock)
// As we need at least one user to send heartbeat, try to send it.
defer bridge.goHeartbeat()
return nil
}
@ -571,35 +594,18 @@ func (bridge *Bridge) newVaultUser(
authUID, authRef string,
saltedKeyPass []byte,
) (*vault.User, bool, error) {
if !bridge.vault.HasUser(apiUser.ID) {
user, err := bridge.vault.AddUser(apiUser.ID, apiUser.Name, apiUser.Email, authUID, authRef, saltedKeyPass)
if err != nil {
return nil, false, fmt.Errorf("failed to add user to vault: %w", err)
}
return user, true, nil
}
user, err := bridge.vault.NewUser(apiUser.ID)
if err != nil {
return nil, false, err
}
if err := user.SetAuth(authUID, authRef); err != nil {
return nil, false, err
}
if err := user.SetKeyPass(saltedKeyPass); err != nil {
return nil, false, err
}
return user, false, nil
return bridge.vault.GetOrAddUser(apiUser.ID, apiUser.Name, apiUser.Email, authUID, authRef, saltedKeyPass)
}
// logout logs out the given user, optionally logging them out from the API too.
func (bridge *Bridge) logoutUser(ctx context.Context, user *user.User, withAPI, withData bool) {
func (bridge *Bridge) logoutUser(ctx context.Context, user *user.User, withAPI, withData, withTelemetry bool) {
defer delete(bridge.users, user.ID())
// if this is actually a remove account
if withData && withAPI {
user.SendConfigStatusAbort(ctx, withTelemetry)
}
logrus.WithFields(logrus.Fields{
"userID": user.ID(),
"withAPI": withAPI,
@ -614,6 +620,8 @@ func (bridge *Bridge) logoutUser(ctx context.Context, user *user.User, withAPI,
logrus.WithError(err).Error("Failed to logout user")
}
bridge.heartbeat.SetNbAccount(len(bridge.users))
user.Close()
}

View File

@ -76,8 +76,6 @@ func TestBridge_User_RefreshEvent(t *testing.T) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
syncCh, closeCh := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).MinTimes(1)
require.Equal(t, userID, (<-syncCh).UserID)
closeCh()
@ -141,6 +139,9 @@ func test_badMessage_badEvent(userFeedback func(t *testing.T, ctx context.Contex
})
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
smtpWaiter := waitForSMTPServerReady(bridge)
defer smtpWaiter.Done()
userLoginAndSync(ctx, t, bridge, "user", password)
var messageIDs []string
@ -176,6 +177,8 @@ func test_badMessage_badEvent(userFeedback func(t *testing.T, ctx context.Contex
userFeedback(t, ctx, bridge, badUserID)
smtpWaiter.Wait()
userContinueEventProcess(ctx, t, s, bridge)
})
})
@ -194,6 +197,9 @@ func TestBridge_User_BadMessage_NoBadEvent(t *testing.T) {
})
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
smtpWaiter := waitForSMTPServerReady(bridge)
defer smtpWaiter.Done()
userLoginAndSync(ctx, t, bridge, "user", password)
var messageIDs []string
@ -217,6 +223,7 @@ func TestBridge_User_BadMessage_NoBadEvent(t *testing.T) {
require.NoError(t, c.DeleteMessage(ctx, messageIDs...))
})
smtpWaiter.Wait()
userContinueEventProcess(ctx, t, s, bridge)
})
})
@ -412,6 +419,17 @@ func TestBridge_User_DropConn_NoBadEvent(t *testing.T) {
})
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
var count int32
// The first 10 times bridge attempts to sync any of the messages, drop the connection.
s.AddStatusHook(func(req *http.Request) (int, bool) {
if strings.Contains(req.URL.Path, "/mail/v4/messages") {
if atomic.AddInt32(&count, 1) < 10 {
dropListener.DropAll()
}
}
return 0, false
})
userLoginAndSync(ctx, t, bridge, "user", password)
mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).AnyTimes()
@ -421,36 +439,82 @@ func TestBridge_User_DropConn_NoBadEvent(t *testing.T) {
createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 10)
})
var count int
// The first 10 times bridge attempts to sync any of the messages, drop the connection.
s.AddStatusHook(func(req *http.Request) (int, bool) {
if strings.Contains(req.URL.Path, "/mail/v4/messages") {
if count++; count < 10 {
dropListener.DropAll()
}
}
return 0, false
})
info, err := bridge.QueryUserInfo("user")
require.NoError(t, err)
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
cli, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }()
require.NoError(t, cli.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = cli.Logout() }()
// The IMAP client will eventually see 20 messages.
require.Eventually(t, func() bool {
status, err := client.Status("INBOX", []imap.StatusItem{imap.StatusMessages})
status, err := cli.Status("INBOX", []imap.StatusItem{imap.StatusMessages})
return err == nil && status.Messages == 20
}, 10*time.Second, 100*time.Millisecond)
})
}, server.WithListener(dropListener))
}
func TestBridge_User_UpdateDraft(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
// Create a bridge user.
_, _, err := s.CreateUser("user", password)
require.NoError(t, err)
// Initially sync the user.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
userLoginAndSync(ctx, t, bridge, "user", password)
})
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
user, err := c.GetUser(ctx)
require.NoError(t, err)
addrs, err := c.GetAddresses(ctx)
require.NoError(t, err)
salts, err := c.GetSalts(ctx)
require.NoError(t, err)
keyPass, err := salts.SaltForKey(password, user.Keys.Primary().ID)
require.NoError(t, err)
_, addrKRs, err := proton.Unlock(user, addrs, keyPass, async.NoopPanicHandler{})
require.NoError(t, err)
// Create a draft (generating a "create draft message" event).
draft, err := c.CreateDraft(ctx, addrKRs[addrs[0].ID], proton.CreateDraftReq{
Message: proton.DraftTemplate{
Subject: "subject",
Sender: &mail.Address{Name: "sender", Address: addrs[0].Email},
Body: "body",
MIMEType: rfc822.TextPlain,
},
})
require.NoError(t, err)
require.Empty(t, draft.ReplyTos)
// Process those events
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
userContinueEventProcess(ctx, t, s, bridge)
})
// Update the draft (generating an "update draft message" event).
draft2, err := c.UpdateDraft(ctx, draft.ID, addrKRs[addrs[0].ID], proton.UpdateDraftReq{
Message: proton.DraftTemplate{
Subject: "subject 2",
Sender: &mail.Address{Name: "sender", Address: addrs[0].Email},
Body: "body 2",
MIMEType: rfc822.TextPlain,
},
})
require.NoError(t, err)
require.Empty(t, draft2.ReplyTos)
})
})
}
func TestBridge_User_UpdateDraftAndCreateOtherMessage(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
// Create a bridge user.
@ -579,12 +643,12 @@ func TestBridge_User_SendDraftRemoveDraftFlag(t *testing.T) {
info, err := bridge.QueryUserInfo("user")
require.NoError(t, err)
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
cli, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }()
require.NoError(t, cli.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = cli.Logout() }()
messages, err := clientFetch(client, "Drafts")
messages, err := clientFetch(cli, "Drafts")
require.NoError(t, err)
require.Len(t, messages, 1)
require.Contains(t, messages[0].Flags, imap.DraftFlag)
@ -618,12 +682,12 @@ func TestBridge_User_SendDraftRemoveDraftFlag(t *testing.T) {
info, err := bridge.QueryUserInfo("user")
require.NoError(t, err)
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
cli, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }()
require.NoError(t, cli.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = cli.Logout() }()
messages, err := clientFetch(client, "Sent")
messages, err := clientFetch(cli, "Sent")
require.NoError(t, err)
require.Len(t, messages, 1)
require.NotContains(t, messages[0].Flags, imap.DraftFlag)
@ -712,15 +776,24 @@ func TestBridge_User_CreateDisabledAddress(t *testing.T) {
func TestBridge_User_HandleParentLabelRename(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
imapWaiter := waitForIMAPServerReady(bridge)
defer imapWaiter.Done()
smtpWaiter := waitForSMTPServerReady(bridge)
defer smtpWaiter.Done()
require.NoError(t, getErr(bridge.LoginFull(ctx, username, password, nil, nil)))
info, err := bridge.QueryUserInfo(username)
require.NoError(t, err)
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
imapWaiter.Wait()
smtpWaiter.Wait()
cli, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }()
require.NoError(t, cli.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = cli.Logout() }()
withClient(ctx, t, s, username, password, func(ctx context.Context, c *proton.Client) {
parentName := uuid.NewString()
@ -736,7 +809,7 @@ func TestBridge_User_HandleParentLabelRename(t *testing.T) {
// Wait for the parent folder to be created.
require.Eventually(t, func() bool {
return xslices.IndexFunc(clientList(client), func(mailbox *imap.MailboxInfo) bool {
return xslices.IndexFunc(clientList(cli), func(mailbox *imap.MailboxInfo) bool {
return mailbox.Name == fmt.Sprintf("Folders/%v", parentName)
}) >= 0
}, 100*user.EventPeriod, user.EventPeriod)
@ -753,7 +826,7 @@ func TestBridge_User_HandleParentLabelRename(t *testing.T) {
// Wait for the parent folder to be created.
require.Eventually(t, func() bool {
return xslices.IndexFunc(clientList(client), func(mailbox *imap.MailboxInfo) bool {
return xslices.IndexFunc(clientList(cli), func(mailbox *imap.MailboxInfo) bool {
return mailbox.Name == fmt.Sprintf("Folders/%v/%v", parentName, childName)
}) >= 0
}, 100*user.EventPeriod, user.EventPeriod)
@ -768,14 +841,14 @@ func TestBridge_User_HandleParentLabelRename(t *testing.T) {
// Wait for the parent folder to be renamed.
require.Eventually(t, func() bool {
return xslices.IndexFunc(clientList(client), func(mailbox *imap.MailboxInfo) bool {
return xslices.IndexFunc(clientList(cli), func(mailbox *imap.MailboxInfo) bool {
return mailbox.Name == fmt.Sprintf("Folders/%v", newParentName)
}) >= 0
}, 100*user.EventPeriod, user.EventPeriod)
// Wait for the child folder to be renamed.
require.Eventually(t, func() bool {
return xslices.IndexFunc(clientList(client), func(mailbox *imap.MailboxInfo) bool {
return xslices.IndexFunc(clientList(cli), func(mailbox *imap.MailboxInfo) bool {
return mailbox.Name == fmt.Sprintf("Folders/%v/%v", newParentName, childName)
}) >= 0
}, 100*user.EventPeriod, user.EventPeriod)
@ -827,10 +900,10 @@ func userContinueEventProcess(
info, err := bridge.QueryUserInfo("user")
require.NoError(t, err)
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
cli, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }()
require.NoError(t, cli.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = cli.Logout() }()
randomLabel := uuid.NewString()
@ -845,8 +918,21 @@ func userContinueEventProcess(
// Wait for the label to be created.
require.Eventually(t, func() bool {
return xslices.IndexFunc(clientList(client), func(mailbox *imap.MailboxInfo) bool {
return xslices.IndexFunc(clientList(cli), func(mailbox *imap.MailboxInfo) bool {
return mailbox.Name == "Labels/"+randomLabel
}) >= 0
}, 100*user.EventPeriod, user.EventPeriod)
}
func eventuallyDial(addr string) (cli *client.Client, err error) {
var sleep = 1 * time.Second
for i := 0; i < 5; i++ {
cli, err := client.Dial(addr)
if err == nil {
return cli, nil
}
time.Sleep(sleep)
sleep *= 2
}
return nil, fmt.Errorf("after 5 attempts, last error: %s", err)
}

View File

@ -75,11 +75,7 @@ func (bridge *Bridge) handleUserAddressCreated(ctx context.Context, user *user.U
return nil
}
if bridge.imapServer == nil {
return fmt.Errorf("no imap server instance running")
}
gluonID, err := bridge.imapServer.AddUser(ctx, user.NewIMAPConnector(event.AddressID), user.GluonKey())
gluonID, err := bridge.serverManager.AddGluonUser(ctx, user.NewIMAPConnector(event.AddressID), user.GluonKey())
if err != nil {
return fmt.Errorf("failed to add user to IMAP server: %w", err)
}
@ -96,7 +92,7 @@ func (bridge *Bridge) handleUserAddressEnabled(ctx context.Context, user *user.U
return nil
}
gluonID, err := bridge.imapServer.AddUser(ctx, user.NewIMAPConnector(event.AddressID), user.GluonKey())
gluonID, err := bridge.serverManager.AddGluonUser(ctx, user.NewIMAPConnector(event.AddressID), user.GluonKey())
if err != nil {
return fmt.Errorf("failed to add user to IMAP server: %w", err)
}
@ -118,7 +114,7 @@ func (bridge *Bridge) handleUserAddressDisabled(ctx context.Context, user *user.
return fmt.Errorf("gluon ID not found for address %s", event.AddressID)
}
if err := bridge.imapServer.RemoveUser(ctx, gluonID, true); err != nil {
if err := bridge.serverManager.RemoveGluonUser(ctx, gluonID); err != nil {
return fmt.Errorf("failed to remove user from IMAP server: %w", err)
}
@ -134,16 +130,12 @@ func (bridge *Bridge) handleUserAddressDeleted(ctx context.Context, user *user.U
return nil
}
if bridge.imapServer == nil {
return fmt.Errorf("no imap server instance running")
}
gluonID, ok := user.GetGluonID(event.AddressID)
if !ok {
return fmt.Errorf("gluon ID not found for address %s", event.AddressID)
}
if err := bridge.imapServer.RemoveUser(ctx, gluonID, true); err != nil {
if err := bridge.serverManager.handleRemoveGluonUser(ctx, gluonID); err != nil {
return fmt.Errorf("failed to remove user from IMAP server: %w", err)
}
@ -174,7 +166,8 @@ func (bridge *Bridge) handleUserRefreshed(ctx context.Context, user *user.User,
func (bridge *Bridge) handleUserDeauth(ctx context.Context, user *user.User) {
safe.Lock(func() {
bridge.logoutUser(ctx, user, false, false)
bridge.logoutUser(ctx, user, false, false, false)
user.ReportConfigStatusFailure("User deauth.")
}, bridge.usersLock)
}

View File

@ -28,10 +28,8 @@ import (
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/go-proton-api/server"
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
mocksPkg "github.com/ProtonMail/proton-bridge/v3/internal/bridge/mocks"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
)
@ -678,11 +676,6 @@ func TestBridge_UserInfo_Alias(t *testing.T) {
func TestBridge_User_Refresh(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
mocks.Reporter.EXPECT().ReportMessageWithContext(
gomock.Eq("Warning: refresh occurred"),
mocksPkg.NewRefreshContextMatcher(proton.RefreshAll),
).Return(nil)
// Get a channel of sync started events.
syncStartCh, done := chToType[events.Event, events.SyncStarted](bridge.GetEvents(events.SyncStarted{}))
defer done()
@ -708,7 +701,26 @@ func TestBridge_User_Refresh(t *testing.T) {
})
}
func TestBridge_User_GetAddresses(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
// Create a user.
userID, _, err := s.CreateUser("user", password)
require.NoError(t, err)
addrID2, err := s.CreateAddress(userID, "user@external.com", []byte("password"))
require.NoError(t, err)
require.NoError(t, s.ChangeAddressType(userID, addrID2, proton.AddressTypeExternal))
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
userLoginAndSync(ctx, t, bridge, "user", password)
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")
})
})
}
// getErr returns the error that was passed to it.
func getErr[T any](val T, err error) error {
func getErr[T any](_ T, err error) error {
return err
}

View File

@ -17,69 +17,140 @@
package certs
import (
"os"
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation -framework Security
#import <Foundation/Foundation.h>
#import <Security/Security.h>
"golang.org/x/sys/execabs"
int installTrustedCert(char const *bytes, unsigned long long length) {
if (length == 0) {
return errSecInvalidData;
}
NSData *der = [NSData dataWithBytes:bytes length:length];
// Step 1. Import the certificate in the keychain.
SecCertificateRef cert = SecCertificateCreateWithData(NULL, (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) {
return errSecInvalidData;
}
NSData *der = [NSData dataWithBytes: bytes length: length];
SecCertificateRef cert = SecCertificateCreateWithData(NULL, (CFDataRef) der);
// 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,
(id)kSecMatchItemList: @[(__bridge id)cert],
(id)kSecMatchLimit: (id)kSecMatchLimitOne,
};
status = SecItemDelete((__bridge CFDictionaryRef) query);
CFRelease(cert);
return status;
}
*/
import "C"
import (
"encoding/pem"
"errors"
"fmt"
"unsafe"
)
// some of the error codes returned by Apple's Security framework.
const (
errSecSuccess = 0
errAuthorizationCanceled = -60006
)
// certPEMToDER converts a certificate in PEM format to DER format, which is the format required by Apple's Security framework.
func certPEMToDER(certPEM []byte) ([]byte, error) {
block, left := pem.Decode(certPEM)
if block == nil {
return []byte{}, errors.New("invalid PEM certificate")
}
if len(left) > 0 {
return []byte{}, errors.New("trailing data found at the end of a PEM certificate")
}
return block.Bytes, nil
}
func installCert(certPEM []byte) error {
name, err := writeToTempFile(certPEM)
certDER, err := certPEMToDER(certPEM)
if err != nil {
return err
}
return addTrustedCert(name)
p := C.CBytes(certDER)
defer C.free(unsafe.Pointer(p)) //nolint:unconvert
errCode := C.installTrustedCert((*C.char)(p), (C.ulonglong)(len(certDER)))
switch errCode {
case errSecSuccess:
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)
}
}
func uninstallCert(certPEM []byte) error {
name, err := writeToTempFile(certPEM)
certDER, err := certPEMToDER(certPEM)
if err != nil {
return err
}
return removeTrustedCert(name)
}
p := C.CBytes(certDER)
defer C.free(unsafe.Pointer(p)) //nolint:unconvert
func addTrustedCert(certPath string) error {
return execabs.Command( //nolint:gosec
"/usr/bin/security",
"execute-with-privileges",
"/usr/bin/security",
"add-trusted-cert",
"-d",
"-r", "trustRoot",
"-p", "ssl",
"-k", "/Library/Keychains/System.keychain",
certPath,
).Run()
}
func removeTrustedCert(certPath string) error {
return execabs.Command( //nolint:gosec
"/usr/bin/security",
"execute-with-privileges",
"/usr/bin/security",
"remove-trusted-cert",
"-d",
certPath,
).Run()
}
// writeToTempFile writes the given data to a temporary file and returns the path.
func writeToTempFile(data []byte) (string, error) {
f, err := os.CreateTemp("", "tls")
if err != nil {
return "", err
if errCode := C.removeTrustedCert((*C.char)(p), (C.ulonglong)(len(certDER))); errCode != 0 {
return fmt.Errorf("could not install certificate from keychain (error %v)", errCode)
}
if _, err := f.Write(data); err != nil {
return "", err
}
if err := f.Close(); err != nil {
return "", err
}
return f.Name(), nil
return nil
}

View File

@ -0,0 +1,44 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
//go:build darwin
package certs
import (
"testing"
"github.com/stretchr/testify/require"
)
// This test implies human interactions to enter password and is disabled by default.
func _TestTrustedCertsDarwin(t *testing.T) { //nolint:unused
template, err := NewTLSTemplate()
require.NoError(t, err)
certPEM, _, err := GenerateCert(template)
require.NoError(t, err)
require.Error(t, installCert([]byte{0})) // Cannot install an invalid cert.
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

@ -70,9 +70,11 @@ func prepareMobileConfig(
password []byte,
) *mobileconfig.Config {
return &mobileconfig.Config{
DisplayName: username,
EmailAddress: addresses,
Identifier: "protonmail " + username + strconv.FormatInt(time.Now().Unix(), 10),
DisplayName: username,
EmailAddress: addresses,
AccountName: username,
AccountDescription: username,
Identifier: "protonmail " + username + strconv.FormatInt(time.Now().Unix(), 10),
IMAP: &mobileconfig.IMAP{
Hostname: hostname,
Port: imapPort,

View File

@ -0,0 +1,221 @@
// 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 configstatus
import (
"encoding/json"
"fmt"
"os"
"strconv"
"time"
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/sirupsen/logrus"
)
const version = "1.0.0"
func LoadConfigurationStatus(filepath string) (*ConfigurationStatus, error) {
status := ConfigurationStatus{
FilePath: filepath,
DataLock: safe.NewRWMutex(),
Data: &ConfigurationStatusData{},
}
if _, err := os.Stat(filepath); err == nil {
if err := status.Load(); err == nil {
return &status, nil
}
logrus.WithError(err).Warn("Cannot load configuration status file. Reset it.")
}
status.Data.init()
if err := status.Save(); err != nil {
return &status, err
}
return &status, nil
}
func (status *ConfigurationStatus) Load() error {
bytes, err := os.ReadFile(status.FilePath)
if err != nil {
return err
}
var metadata MetadataOnly
if err := json.Unmarshal(bytes, &metadata); err != nil {
return err
}
if metadata.Metadata.Version != version {
return fmt.Errorf("unsupported configstatus file version %s", metadata.Metadata.Version)
}
return json.Unmarshal(bytes, status.Data)
}
func (status *ConfigurationStatus) Save() error {
temp := status.FilePath + "_temp"
f, err := os.Create(temp) //nolint:gosec
if err != nil {
return err
}
enc := json.NewEncoder(f)
enc.SetIndent("", " ")
err = enc.Encode(status.Data)
if err := f.Close(); err != nil {
logrus.WithError(err).Error("Error while closing configstatus file.")
}
if err != nil {
return err
}
return os.Rename(temp, status.FilePath)
}
func (status *ConfigurationStatus) IsPending() bool {
status.DataLock.RLock()
defer status.DataLock.RUnlock()
return !status.Data.DataV1.PendingSince.IsZero()
}
func (status *ConfigurationStatus) IsFromFailure() bool {
status.DataLock.RLock()
defer status.DataLock.RUnlock()
return status.Data.DataV1.FailureDetails != ""
}
func (status *ConfigurationStatus) ApplySuccess() error {
status.DataLock.Lock()
defer status.DataLock.Unlock()
status.Data.init()
status.Data.DataV1.PendingSince = time.Time{}
return status.Save()
}
func (status *ConfigurationStatus) ApplyFailure(err string) error {
status.DataLock.Lock()
defer status.DataLock.Unlock()
status.Data.init()
status.Data.DataV1.FailureDetails = err
return status.Save()
}
func (status *ConfigurationStatus) ApplyProgress() error {
status.DataLock.Lock()
defer status.DataLock.Unlock()
status.Data.DataV1.LastProgress = time.Now()
return status.Save()
}
func (status *ConfigurationStatus) RecordLinkClicked(link uint) error {
status.DataLock.Lock()
defer status.DataLock.Unlock()
if !status.Data.hasLinkClicked(link) {
status.Data.setClickedLink(link)
return status.Save()
}
return nil
}
func (status *ConfigurationStatus) ReportClicked() error {
status.DataLock.Lock()
defer status.DataLock.Unlock()
if !status.Data.DataV1.ReportClick {
status.Data.DataV1.ReportClick = true
return status.Save()
}
return nil
}
func (status *ConfigurationStatus) ReportSent() error {
status.DataLock.Lock()
defer status.DataLock.Unlock()
if !status.Data.DataV1.ReportSent {
status.Data.DataV1.ReportSent = true
return status.Save()
}
return nil
}
func (status *ConfigurationStatus) AutoconfigUsed(client string) error {
status.DataLock.Lock()
defer status.DataLock.Unlock()
if client != status.Data.DataV1.Autoconf {
status.Data.DataV1.Autoconf = client
return status.Save()
}
return nil
}
func (status *ConfigurationStatus) Remove() error {
status.DataLock.Lock()
defer status.DataLock.Unlock()
return os.Remove(status.FilePath)
}
func (data *ConfigurationStatusData) init() {
data.Metadata = Metadata{
Version: version,
}
data.DataV1.PendingSince = time.Now()
data.DataV1.LastProgress = time.Time{}
data.DataV1.Autoconf = ""
data.DataV1.ClickedLink = 0
data.DataV1.ReportSent = false
data.DataV1.ReportClick = false
data.DataV1.FailureDetails = ""
}
func (data *ConfigurationStatusData) setClickedLink(pos uint) {
data.DataV1.ClickedLink |= 1 << pos
}
func (data *ConfigurationStatusData) hasLinkClicked(pos uint) bool {
val := data.DataV1.ClickedLink & (1 << pos)
return val > 0
}
func (data *ConfigurationStatusData) clickedLinkToString() string {
var str = ""
var first = true
for i := 0; i < 64; i++ {
if data.hasLinkClicked(uint(i)) {
if !first {
str += ","
} else {
first = false
str += "["
}
str += strconv.Itoa(i)
}
}
if str != "" {
str += "]"
}
return str
}

View File

@ -0,0 +1,252 @@
// 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 configstatus_test
import (
"encoding/json"
"os"
"path/filepath"
"testing"
"time"
"github.com/ProtonMail/proton-bridge/v3/internal/configstatus"
"github.com/stretchr/testify/require"
)
func TestConfigStatus_init_virgin(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, "1.0.0", config.Data.Metadata.Version)
require.Equal(t, false, config.Data.DataV1.PendingSince.IsZero())
require.Equal(t, true, config.Data.DataV1.LastProgress.IsZero())
require.Equal(t, "", config.Data.DataV1.Autoconf)
require.Equal(t, uint64(0), config.Data.DataV1.ClickedLink)
require.Equal(t, false, config.Data.DataV1.ReportSent)
require.Equal(t, false, config.Data.DataV1.ReportClick)
require.Equal(t, "", config.Data.DataV1.FailureDetails)
}
func TestConfigStatus_init_existing(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
var data = configstatus.ConfigurationStatusData{
Metadata: configstatus.Metadata{Version: "1.0.0"},
DataV1: configstatus.DataV1{Autoconf: "Mr TBird"},
}
require.NoError(t, dumpConfigStatusInFile(&data, file))
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, "1.0.0", config.Data.Metadata.Version)
require.Equal(t, "Mr TBird", config.Data.DataV1.Autoconf)
}
func TestConfigStatus_init_bad_version(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
var data = configstatus.ConfigurationStatusData{
Metadata: configstatus.Metadata{Version: "2.0.0"},
DataV1: configstatus.DataV1{Autoconf: "Mr TBird"},
}
require.NoError(t, dumpConfigStatusInFile(&data, file))
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, "1.0.0", config.Data.Metadata.Version)
require.Equal(t, "", config.Data.DataV1.Autoconf)
}
func TestConfigStatus_IsPending(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, true, config.IsPending())
config.Data.DataV1.PendingSince = time.Time{}
require.Equal(t, false, config.IsPending())
}
func TestConfigStatus_IsFromFailure(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, false, config.IsFromFailure())
config.Data.DataV1.FailureDetails = "test"
require.Equal(t, true, config.IsFromFailure())
}
func TestConfigStatus_ApplySuccess(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, true, config.IsPending())
require.NoError(t, config.ApplySuccess())
require.Equal(t, false, config.IsPending())
config2, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, "1.0.0", config2.Data.Metadata.Version)
require.Equal(t, true, config2.Data.DataV1.PendingSince.IsZero())
require.Equal(t, true, config2.Data.DataV1.LastProgress.IsZero())
require.Equal(t, "", config2.Data.DataV1.Autoconf)
require.Equal(t, uint64(0), config2.Data.DataV1.ClickedLink)
require.Equal(t, false, config2.Data.DataV1.ReportSent)
require.Equal(t, false, config2.Data.DataV1.ReportClick)
require.Equal(t, "", config2.Data.DataV1.FailureDetails)
}
func TestConfigStatus_ApplyFailure(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.NoError(t, config.ApplySuccess())
require.NoError(t, config.ApplyFailure("Big Failure"))
require.Equal(t, true, config.IsFromFailure())
require.Equal(t, true, config.IsPending())
config2, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, "1.0.0", config2.Data.Metadata.Version)
require.Equal(t, false, config2.Data.DataV1.PendingSince.IsZero())
require.Equal(t, true, config2.Data.DataV1.LastProgress.IsZero())
require.Equal(t, "", config2.Data.DataV1.Autoconf)
require.Equal(t, uint64(0), config2.Data.DataV1.ClickedLink)
require.Equal(t, false, config2.Data.DataV1.ReportSent)
require.Equal(t, false, config2.Data.DataV1.ReportClick)
require.Equal(t, "Big Failure", config2.Data.DataV1.FailureDetails)
}
func TestConfigStatus_ApplyProgress(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, true, config.IsPending())
require.Equal(t, true, config.Data.DataV1.LastProgress.IsZero())
require.NoError(t, config.ApplyProgress())
config2, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, "1.0.0", config2.Data.Metadata.Version)
require.Equal(t, false, config2.Data.DataV1.PendingSince.IsZero())
require.Equal(t, false, config2.Data.DataV1.LastProgress.IsZero())
require.Equal(t, "", config2.Data.DataV1.Autoconf)
require.Equal(t, uint64(0), config2.Data.DataV1.ClickedLink)
require.Equal(t, false, config2.Data.DataV1.ReportSent)
require.Equal(t, false, config2.Data.DataV1.ReportClick)
require.Equal(t, "", config2.Data.DataV1.FailureDetails)
}
func TestConfigStatus_RecordLinkClicked(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, uint64(0), config.Data.DataV1.ClickedLink)
require.NoError(t, config.RecordLinkClicked(0))
require.Equal(t, uint64(1), config.Data.DataV1.ClickedLink)
require.NoError(t, config.RecordLinkClicked(1))
require.Equal(t, uint64(3), config.Data.DataV1.ClickedLink)
config2, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, "1.0.0", config2.Data.Metadata.Version)
require.Equal(t, false, config2.Data.DataV1.PendingSince.IsZero())
require.Equal(t, true, config2.Data.DataV1.LastProgress.IsZero())
require.Equal(t, "", config2.Data.DataV1.Autoconf)
require.Equal(t, uint64(3), config2.Data.DataV1.ClickedLink)
require.Equal(t, false, config2.Data.DataV1.ReportSent)
require.Equal(t, false, config2.Data.DataV1.ReportClick)
require.Equal(t, "", config2.Data.DataV1.FailureDetails)
}
func TestConfigStatus_ReportClicked(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, false, config.Data.DataV1.ReportClick)
require.NoError(t, config.ReportClicked())
require.Equal(t, true, config.Data.DataV1.ReportClick)
config2, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, "1.0.0", config2.Data.Metadata.Version)
require.Equal(t, false, config2.Data.DataV1.PendingSince.IsZero())
require.Equal(t, true, config2.Data.DataV1.LastProgress.IsZero())
require.Equal(t, "", config2.Data.DataV1.Autoconf)
require.Equal(t, uint64(0), config2.Data.DataV1.ClickedLink)
require.Equal(t, false, config2.Data.DataV1.ReportSent)
require.Equal(t, true, config2.Data.DataV1.ReportClick)
require.Equal(t, "", config2.Data.DataV1.FailureDetails)
}
func TestConfigStatus_ReportSent(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, false, config.Data.DataV1.ReportSent)
require.NoError(t, config.ReportSent())
require.Equal(t, true, config.Data.DataV1.ReportSent)
config2, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, "1.0.0", config2.Data.Metadata.Version)
require.Equal(t, false, config2.Data.DataV1.PendingSince.IsZero())
require.Equal(t, true, config2.Data.DataV1.LastProgress.IsZero())
require.Equal(t, "", config2.Data.DataV1.Autoconf)
require.Equal(t, uint64(0), config2.Data.DataV1.ClickedLink)
require.Equal(t, true, config2.Data.DataV1.ReportSent)
require.Equal(t, false, config2.Data.DataV1.ReportClick)
require.Equal(t, "", config2.Data.DataV1.FailureDetails)
}
func dumpConfigStatusInFile(data *configstatus.ConfigurationStatusData, file string) error {
f, err := os.Create(file)
if err != nil {
return err
}
defer func() { _ = f.Close() }()
return json.NewEncoder(f).Encode(data)
}

View File

@ -0,0 +1,57 @@
// 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 configstatus
import (
"strconv"
"time"
)
type ConfigAbortValues struct {
Duration int `json:"duration"`
}
type ConfigAbortDimensions struct {
ReportClick string `json:"report_click"`
ReportSent string `json:"report_sent"`
ClickedLink string `json:"clicked_link"`
}
type ConfigAbortData struct {
MeasurementGroup string
Event string
Values ConfigSuccessValues
Dimensions ConfigSuccessDimensions
}
type ConfigAbortBuilder struct{}
func (*ConfigAbortBuilder) New(data *ConfigurationStatusData) ConfigAbortData {
return ConfigAbortData{
MeasurementGroup: "bridge.any.configuration",
Event: "bridge_config_abort",
Values: ConfigSuccessValues{
Duration: int(time.Since(data.DataV1.PendingSince).Minutes()),
},
Dimensions: ConfigSuccessDimensions{
ReportClick: strconv.FormatBool(data.DataV1.ReportClick),
ReportSent: strconv.FormatBool(data.DataV1.ReportSent),
ClickedLink: data.clickedLinkToString(),
},
}
}

View File

@ -0,0 +1,75 @@
// 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 configstatus_test
import (
"path/filepath"
"testing"
"time"
"github.com/ProtonMail/proton-bridge/v3/internal/configstatus"
"github.com/stretchr/testify/require"
)
func TestConfigurationAbort_default(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
var builder = configstatus.ConfigAbortBuilder{}
req := builder.New(config.Data)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_abort", req.Event)
require.Equal(t, 0, req.Values.Duration)
require.Equal(t, "false", req.Dimensions.ReportClick)
require.Equal(t, "false", req.Dimensions.ReportSent)
require.Equal(t, "", req.Dimensions.ClickedLink)
}
func TestConfigurationAbort_fed(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
var data = configstatus.ConfigurationStatusData{
Metadata: configstatus.Metadata{Version: "1.0.0"},
DataV1: configstatus.DataV1{
PendingSince: time.Now().Add(-10 * time.Minute),
LastProgress: time.Time{},
Autoconf: "Mr TBird",
ClickedLink: 42,
ReportSent: false,
ReportClick: true,
FailureDetails: "Not an error",
},
}
require.NoError(t, dumpConfigStatusInFile(&data, file))
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
var builder = configstatus.ConfigAbortBuilder{}
req := builder.New(config.Data)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_abort", req.Event)
require.Equal(t, 10, req.Values.Duration)
require.Equal(t, "true", req.Dimensions.ReportClick)
require.Equal(t, "false", req.Dimensions.ReportSent)
require.Equal(t, "[1,3,5]", req.Dimensions.ClickedLink)
}

View File

@ -0,0 +1,60 @@
// 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 configstatus
import "time"
type ConfigProgressValues struct {
NbDay int `json:"nb_day"`
NbDaySinceLast int `json:"nb_day_since_last"`
}
type ConfigProgressData struct {
MeasurementGroup string
Event string
Values ConfigProgressValues
Dimensions struct{}
}
type ConfigProgressBuilder struct{}
func (*ConfigProgressBuilder) New(data *ConfigurationStatusData) ConfigProgressData {
return ConfigProgressData{
MeasurementGroup: "bridge.any.configuration",
Event: "bridge_config_progress",
Values: ConfigProgressValues{
NbDay: numberOfDay(time.Now(), data.DataV1.PendingSince),
NbDaySinceLast: numberOfDay(time.Now(), data.DataV1.LastProgress),
},
}
}
func numberOfDay(now, prev time.Time) int {
if now.IsZero() || prev.IsZero() {
return 1
}
if now.Year() > prev.Year() {
if now.YearDay() > prev.YearDay() {
return 365 + (now.YearDay() - prev.YearDay())
}
return (prev.YearDay() + now.YearDay()) - 365
} else if now.YearDay() > prev.YearDay() {
return now.YearDay() - prev.YearDay()
}
return 0
}

View File

@ -0,0 +1,71 @@
// 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 configstatus_test
import (
"path/filepath"
"testing"
"time"
"github.com/ProtonMail/proton-bridge/v3/internal/configstatus"
"github.com/stretchr/testify/require"
)
func TestConfigurationProgress_default(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
var builder = configstatus.ConfigProgressBuilder{}
req := builder.New(config.Data)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_progress", req.Event)
require.Equal(t, 0, req.Values.NbDay)
require.Equal(t, 1, req.Values.NbDaySinceLast)
}
func TestConfigurationProgress_fed(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
var data = configstatus.ConfigurationStatusData{
Metadata: configstatus.Metadata{Version: "1.0.0"},
DataV1: configstatus.DataV1{
PendingSince: time.Now().AddDate(0, 0, -5),
LastProgress: time.Now().AddDate(0, 0, -2),
Autoconf: "Mr TBird",
ClickedLink: 42,
ReportSent: false,
ReportClick: true,
FailureDetails: "Not an error",
},
}
require.NoError(t, dumpConfigStatusInFile(&data, file))
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
var builder = configstatus.ConfigProgressBuilder{}
req := builder.New(config.Data)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_progress", req.Event)
require.Equal(t, 5, req.Values.NbDay)
require.Equal(t, 2, req.Values.NbDaySinceLast)
}

View File

@ -0,0 +1,61 @@
// 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 configstatus
import (
"strconv"
"time"
)
type ConfigRecoveryValues struct {
Duration int `json:"duration"`
}
type ConfigRecoveryDimensions struct {
Autoconf string `json:"autoconf"`
ReportClick string `json:"report_click"`
ReportSent string `json:"report_sent"`
ClickedLink string `json:"clicked_link"`
FailureDetails string `json:"failure_details"`
}
type ConfigRecoveryData struct {
MeasurementGroup string
Event string
Values ConfigRecoveryValues
Dimensions ConfigRecoveryDimensions
}
type ConfigRecoveryBuilder struct{}
func (*ConfigRecoveryBuilder) New(data *ConfigurationStatusData) ConfigRecoveryData {
return ConfigRecoveryData{
MeasurementGroup: "bridge.any.configuration",
Event: "bridge_config_recovery",
Values: ConfigRecoveryValues{
Duration: int(time.Since(data.DataV1.PendingSince).Minutes()),
},
Dimensions: ConfigRecoveryDimensions{
Autoconf: data.DataV1.Autoconf,
ReportClick: strconv.FormatBool(data.DataV1.ReportClick),
ReportSent: strconv.FormatBool(data.DataV1.ReportSent),
ClickedLink: data.clickedLinkToString(),
FailureDetails: data.DataV1.FailureDetails,
},
}
}

View File

@ -0,0 +1,79 @@
// 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 configstatus_test
import (
"path/filepath"
"testing"
"time"
"github.com/ProtonMail/proton-bridge/v3/internal/configstatus"
"github.com/stretchr/testify/require"
)
func TestConfigurationRecovery_default(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
var builder = configstatus.ConfigRecoveryBuilder{}
req := builder.New(config.Data)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_recovery", req.Event)
require.Equal(t, 0, req.Values.Duration)
require.Equal(t, "", req.Dimensions.Autoconf)
require.Equal(t, "false", req.Dimensions.ReportClick)
require.Equal(t, "false", req.Dimensions.ReportSent)
require.Equal(t, "", req.Dimensions.ClickedLink)
require.Equal(t, "", req.Dimensions.FailureDetails)
}
func TestConfigurationRecovery_fed(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
var data = configstatus.ConfigurationStatusData{
Metadata: configstatus.Metadata{Version: "1.0.0"},
DataV1: configstatus.DataV1{
PendingSince: time.Now().Add(-10 * time.Minute),
LastProgress: time.Time{},
Autoconf: "Mr TBird",
ClickedLink: 42,
ReportSent: false,
ReportClick: true,
FailureDetails: "Not an error",
},
}
require.NoError(t, dumpConfigStatusInFile(&data, file))
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
var builder = configstatus.ConfigRecoveryBuilder{}
req := builder.New(config.Data)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_recovery", req.Event)
require.Equal(t, 10, req.Values.Duration)
require.Equal(t, "Mr TBird", req.Dimensions.Autoconf)
require.Equal(t, "true", req.Dimensions.ReportClick)
require.Equal(t, "false", req.Dimensions.ReportSent)
require.Equal(t, "[1,3,5]", req.Dimensions.ClickedLink)
require.Equal(t, "Not an error", req.Dimensions.FailureDetails)
}

View File

@ -0,0 +1,59 @@
// 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 configstatus
import (
"strconv"
"time"
)
type ConfigSuccessValues struct {
Duration int `json:"duration"`
}
type ConfigSuccessDimensions struct {
Autoconf string `json:"autoconf"`
ReportClick string `json:"report_click"`
ReportSent string `json:"report_sent"`
ClickedLink string `json:"clicked_link"`
}
type ConfigSuccessData struct {
MeasurementGroup string
Event string
Values ConfigSuccessValues
Dimensions ConfigSuccessDimensions
}
type ConfigSuccessBuilder struct{}
func (*ConfigSuccessBuilder) New(data *ConfigurationStatusData) ConfigSuccessData {
return ConfigSuccessData{
MeasurementGroup: "bridge.any.configuration",
Event: "bridge_config_success",
Values: ConfigSuccessValues{
Duration: int(time.Since(data.DataV1.PendingSince).Minutes()),
},
Dimensions: ConfigSuccessDimensions{
Autoconf: data.DataV1.Autoconf,
ReportClick: strconv.FormatBool(data.DataV1.ReportClick),
ReportSent: strconv.FormatBool(data.DataV1.ReportSent),
ClickedLink: data.clickedLinkToString(),
},
}
}

View File

@ -0,0 +1,77 @@
// 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 configstatus_test
import (
"path/filepath"
"testing"
"time"
"github.com/ProtonMail/proton-bridge/v3/internal/configstatus"
"github.com/stretchr/testify/require"
)
func TestConfigurationSuccess_default(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
var builder = configstatus.ConfigSuccessBuilder{}
req := builder.New(config.Data)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_success", req.Event)
require.Equal(t, 0, req.Values.Duration)
require.Equal(t, "", req.Dimensions.Autoconf)
require.Equal(t, "false", req.Dimensions.ReportClick)
require.Equal(t, "false", req.Dimensions.ReportSent)
require.Equal(t, "", req.Dimensions.ClickedLink)
}
func TestConfigurationSuccess_fed(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
var data = configstatus.ConfigurationStatusData{
Metadata: configstatus.Metadata{Version: "1.0.0"},
DataV1: configstatus.DataV1{
PendingSince: time.Now().Add(-10 * time.Minute),
LastProgress: time.Time{},
Autoconf: "Mr TBird",
ClickedLink: 42,
ReportSent: false,
ReportClick: true,
FailureDetails: "Not an error",
},
}
require.NoError(t, dumpConfigStatusInFile(&data, file))
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
var builder = configstatus.ConfigSuccessBuilder{}
req := builder.New(config.Data)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_success", req.Event)
require.Equal(t, 10, req.Values.Duration)
require.Equal(t, "Mr TBird", req.Dimensions.Autoconf)
require.Equal(t, "true", req.Dimensions.ReportClick)
require.Equal(t, "false", req.Dimensions.ReportSent)
require.Equal(t, "[1,3,5]", req.Dimensions.ClickedLink)
}

View File

@ -0,0 +1,56 @@
// 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 configstatus
import (
"time"
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
)
const ProgressCheckInterval = time.Hour
type Metadata struct {
Version string `json:"version"`
}
type MetadataOnly struct {
Metadata Metadata `json:"metadata"`
}
type DataV1 struct {
PendingSince time.Time `json:"pending_since"`
LastProgress time.Time `json:"last_progress"`
Autoconf string `json:"auto_conf"`
ClickedLink uint64 `json:"clicked_link"`
ReportSent bool `json:"report_sent"`
ReportClick bool `json:"report_click"`
FailureDetails string `json:"failure_details"`
}
type ConfigurationStatusData struct {
Metadata Metadata `json:"metadata"`
DataV1 DataV1 `json:"dataV1"`
}
type ConfigurationStatus struct {
FilePath string
DataLock safe.RWMutex
Data *ConfigurationStatusData
}

View File

@ -33,9 +33,12 @@ var (
// Version of the build.
Version = "0.0.0"
// Revision is current hash of the build.
// Revision is build time commit hash.
Revision = ""
// Tag is build time git describe.
Tag = ""
// BuildTime stamp of the build.
BuildTime = ""

View File

@ -93,6 +93,8 @@ func newProxyProvider(dialer TLSDialer, hostURL string, providers []string, pani
}
// findReachableServer returns a working API server (either proxy or standard API).
//
//nolint:nakedret
func (p *proxyProvider) findReachableServer() (proxy string, err error) {
logrus.Debug("Trying to find a reachable server")
@ -204,6 +206,8 @@ func (p *proxyProvider) canReach(url string) bool {
// It looks up DNS TXT records for the given query URL using the given DoH provider.
// It returns a list of all found TXT records.
// If the whole process takes more than proxyDoHTimeout then an error is returned.
//
//nolint:nakedret
func (p *proxyProvider) defaultDoHLookup(ctx context.Context, query, dohProvider string) (data []string, err error) {
ctx, cancel := context.WithTimeout(ctx, p.dohTimeout)
defer cancel()

View File

@ -30,6 +30,7 @@ import (
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/protobuf/types/known/emptypb"
"google.golang.org/protobuf/types/known/wrapperspb"
)
// TryRaise tries to raise the application by dialing the focus service.
@ -38,7 +39,7 @@ func TryRaise(settingsPath string) bool {
var raised bool
if err := withClientConn(context.Background(), settingsPath, func(ctx context.Context, client proto.FocusClient) error {
if _, err := client.Raise(ctx, &emptypb.Empty{}); err != nil {
if _, err := client.Raise(ctx, &wrapperspb.StringValue{Value: "TryRaise"}); err != nil {
return fmt.Errorf("failed to call client.Raise: %w", err)
}

View File

@ -18,7 +18,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.0
// protoc v3.21.3
// protoc v3.21.12
// source: focus.proto
package proto
@ -27,6 +27,7 @@ import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
emptypb "google.golang.org/protobuf/types/known/emptypb"
wrapperspb "google.golang.org/protobuf/types/known/wrapperspb"
reflect "reflect"
sync "sync"
)
@ -91,22 +92,24 @@ var file_focus_proto_rawDesc = []byte{
0x0a, 0x0b, 0x66, 0x6f, 0x63, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x66,
0x6f, 0x63, 0x75, 0x73, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
0x75, 0x66, 0x2f, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x22, 0x2b, 0x0a, 0x0f, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18,
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x32, 0x7b,
0x0a, 0x05, 0x46, 0x6f, 0x63, 0x75, 0x73, 0x12, 0x37, 0x0a, 0x05, 0x52, 0x61, 0x69, 0x73, 0x65,
0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79,
0x12, 0x39, 0x0a, 0x07, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f,
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d,
0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x66, 0x6f, 0x63, 0x75, 0x73, 0x2e, 0x56, 0x65, 0x72, 0x73,
0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x3d, 0x5a, 0x3b, 0x67,
0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x6e,
0x4d, 0x61, 0x69, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x6e, 0x2d, 0x62, 0x72, 0x69, 0x64,
0x67, 0x65, 0x2f, 0x76, 0x33, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x66,
0x6f, 0x63, 0x75, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x33,
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x32, 0x81,
0x01, 0x0a, 0x05, 0x46, 0x6f, 0x63, 0x75, 0x73, 0x12, 0x3d, 0x0a, 0x05, 0x52, 0x61, 0x69, 0x73,
0x65, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a,
0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x39, 0x0a, 0x07, 0x56, 0x65, 0x72, 0x73, 0x69,
0x6f, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x66, 0x6f, 0x63,
0x75, 0x73, 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x42, 0x3d, 0x5a, 0x3b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
0x2f, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x6e, 0x4d, 0x61, 0x69, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x6e, 0x2d, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x2f, 0x76, 0x33, 0x2f, 0x69, 0x6e, 0x74,
0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x66, 0x6f, 0x63, 0x75, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@ -123,13 +126,14 @@ func file_focus_proto_rawDescGZIP() []byte {
var file_focus_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_focus_proto_goTypes = []interface{}{
(*VersionResponse)(nil), // 0: focus.VersionResponse
(*emptypb.Empty)(nil), // 1: google.protobuf.Empty
(*VersionResponse)(nil), // 0: focus.VersionResponse
(*wrapperspb.StringValue)(nil), // 1: google.protobuf.StringValue
(*emptypb.Empty)(nil), // 2: google.protobuf.Empty
}
var file_focus_proto_depIdxs = []int32{
1, // 0: focus.Focus.Raise:input_type -> google.protobuf.Empty
1, // 1: focus.Focus.Version:input_type -> google.protobuf.Empty
1, // 2: focus.Focus.Raise:output_type -> google.protobuf.Empty
1, // 0: focus.Focus.Raise:input_type -> google.protobuf.StringValue
2, // 1: focus.Focus.Version:input_type -> google.protobuf.Empty
2, // 2: focus.Focus.Raise:output_type -> google.protobuf.Empty
0, // 3: focus.Focus.Version:output_type -> focus.VersionResponse
2, // [2:4] is the sub-list for method output_type
0, // [0:2] is the sub-list for method input_type

View File

@ -18,6 +18,7 @@
syntax = "proto3";
import "google/protobuf/empty.proto";
import "google/protobuf/wrappers.proto";
option go_package = "github.com/ProtonMail/proton-bridge/v3/internal/focus/proto";
@ -27,7 +28,7 @@ package focus; // ignored by Go, used as namespace name in C++.
// Service Declaration
//**********************************************************************************************************************≠––
service Focus {
rpc Raise(google.protobuf.Empty) returns (google.protobuf.Empty);
rpc Raise(google.protobuf.StringValue) returns (google.protobuf.Empty);
rpc Version(google.protobuf.Empty) returns (VersionResponse);
}

View File

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.2.0
// - protoc v3.21.3
// - protoc v3.21.12
// source: focus.proto
package proto
@ -12,6 +12,7 @@ import (
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
emptypb "google.golang.org/protobuf/types/known/emptypb"
wrapperspb "google.golang.org/protobuf/types/known/wrapperspb"
)
// This is a compile-time assertion to ensure that this generated file
@ -23,7 +24,7 @@ const _ = grpc.SupportPackageIsVersion7
//
// 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.
type FocusClient interface {
Raise(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
Raise(ctx context.Context, in *wrapperspb.StringValue, opts ...grpc.CallOption) (*emptypb.Empty, error)
Version(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*VersionResponse, error)
}
@ -35,7 +36,7 @@ func NewFocusClient(cc grpc.ClientConnInterface) FocusClient {
return &focusClient{cc}
}
func (c *focusClient) Raise(ctx context.Context, in *emptypb.Empty, 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)
err := c.cc.Invoke(ctx, "/focus.Focus/Raise", in, out, opts...)
if err != nil {
@ -57,7 +58,7 @@ func (c *focusClient) Version(ctx context.Context, in *emptypb.Empty, opts ...gr
// All implementations must embed UnimplementedFocusServer
// for forward compatibility
type FocusServer interface {
Raise(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
Raise(context.Context, *wrapperspb.StringValue) (*emptypb.Empty, error)
Version(context.Context, *emptypb.Empty) (*VersionResponse, error)
mustEmbedUnimplementedFocusServer()
}
@ -66,7 +67,7 @@ type FocusServer interface {
type UnimplementedFocusServer struct {
}
func (UnimplementedFocusServer) Raise(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
func (UnimplementedFocusServer) Raise(context.Context, *wrapperspb.StringValue) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method Raise not implemented")
}
func (UnimplementedFocusServer) Version(context.Context, *emptypb.Empty) (*VersionResponse, error) {
@ -86,7 +87,7 @@ func RegisterFocusServer(s grpc.ServiceRegistrar, srv FocusServer) {
}
func _Focus_Raise_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(emptypb.Empty)
in := new(wrapperspb.StringValue)
if err := dec(in); err != nil {
return nil, err
}
@ -98,7 +99,7 @@ func _Focus_Raise_Handler(srv interface{}, ctx context.Context, dec func(interfa
FullMethod: "/focus.Focus/Raise",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(FocusServer).Raise(ctx, req.(*emptypb.Empty))
return srv.(FocusServer).Raise(ctx, req.(*wrapperspb.StringValue))
}
return interceptor(ctx, in, info, handler)
}

View File

@ -30,6 +30,7 @@ import (
"github.com/sirupsen/logrus"
"google.golang.org/grpc"
"google.golang.org/protobuf/types/known/emptypb"
"google.golang.org/protobuf/types/known/wrapperspb"
)
const (
@ -45,6 +46,7 @@ type Service struct {
raiseCh chan struct{}
version *semver.Version
log *logrus.Entry
panicHandler async.PanicHandler
}
@ -55,13 +57,14 @@ func NewService(locator service.Locator, version *semver.Version, panicHandler a
server: grpc.NewServer(),
raiseCh: make(chan struct{}, 1),
version: version,
log: logrus.WithField("pkg", "focus/service"),
panicHandler: panicHandler,
}
proto.RegisterFocusServer(serv.server, serv)
if listener, err := net.Listen("tcp", net.JoinHostPort(Host, fmt.Sprint(0))); err != nil {
logrus.WithError(err).Warn("Failed to start focus serv")
serv.log.WithError(err).Warn("Failed to start focus service")
} else {
config := service.Config{}
// retrieve the port assigned by the system, so that we can put it in the config file.
@ -71,9 +74,9 @@ func NewService(locator service.Locator, version *semver.Version, panicHandler a
}
config.Port = address.Port
if path, err := service.SaveGRPCServerConfigFile(locator, &config, serverConfigFileName); err != nil {
logrus.WithError(err).WithField("path", path).Warn("Could not write focus gRPC service config file")
serv.log.WithError(err).WithField("path", path).Warn("Could not write focus gRPC service config file")
} else {
logrus.WithField("path", path).Info("Successfully saved gRPC Focus service config file")
serv.log.WithField("path", path).Info("Successfully saved gRPC Focus service config file")
}
go func() {
@ -89,13 +92,15 @@ func NewService(locator service.Locator, version *semver.Version, panicHandler a
}
// Raise implements the gRPC FocusService interface; it raises the application.
func (service *Service) Raise(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
func (service *Service) Raise(_ context.Context, reason *wrapperspb.StringValue) (*emptypb.Empty, error) {
service.log.WithField("Reason", reason.Value).Debug("Raise")
service.raiseCh <- struct{}{}
return &emptypb.Empty{}, nil
}
// Version implements the gRPC FocusService interface; it returns the version of the service.
func (service *Service) Version(context.Context, *emptypb.Empty) (*proto.VersionResponse, error) {
service.log.Debug("Version")
return &proto.VersionResponse{
Version: service.version.Original(),
}, nil

View File

@ -0,0 +1,4 @@
# The following fix an issue happening using LLDB with OpenSSL 3.1 on ARM64 architecture. (GODT-2680)
# WARNING: this file is ignored if you do not enable reading lldb config from cwd in ~/.lldbinit (`settings set target.load-cwd-lldbinit true`)
settings set platform.plugin.darwin.ignored-exceptions EXC_BAD_INSTRUCTION
process handle SIGILL -n false -p true -s false

View File

@ -39,6 +39,7 @@ void GRPCQtProxy::connectSignals() {
connect(this, &GRPCQtProxy::setIsAutostartOnReceived, &settingsTab, &SettingsTab::setIsAutostartOn);
connect(this, &GRPCQtProxy::setIsBetaEnabledReceived, &settingsTab, &SettingsTab::setIsBetaEnabled);
connect(this, &GRPCQtProxy::setIsAllMailVisibleReceived, &settingsTab, &SettingsTab::setIsAllMailVisible);
connect(this, &GRPCQtProxy::setIsTelemetryDisabledReceived, &settingsTab, &SettingsTab::setIsTelemetryDisabled);
connect(this, &GRPCQtProxy::setColorSchemeNameReceived, &settingsTab, &SettingsTab::setColorSchemeName);
connect(this, &GRPCQtProxy::reportBugReceived, &settingsTab, &SettingsTab::setBugReport);
connect(this, &GRPCQtProxy::exportTLSCertificatesReceived, &settingsTab, &SettingsTab::exportTLSCertificates);
@ -89,6 +90,13 @@ void GRPCQtProxy::setIsAllMailVisible(bool visible) {
}
//****************************************************************************************************************************************************
/// \param[in] isDisabled Is telemetry disabled?
//****************************************************************************************************************************************************
void GRPCQtProxy::setIsTelemetryDisabled(bool isDisabled) {
emit setIsTelemetryDisabledReceived(isDisabled);
}
//****************************************************************************************************************************************************
/// \param[in] name The color scheme.
//****************************************************************************************************************************************************

View File

@ -41,6 +41,7 @@ public: // member functions.
void setIsAutostartOn(bool on); ///< Forwards a SetIsAutostartOn call via a Qt signal.
void setIsBetaEnabled(bool enabled); ///< Forwards a SetIsBetaEnabled call via a Qt signal.
void setIsAllMailVisible(bool visible); ///< Forwards a SetIsAllMailVisible call via a Qt signal.
void setIsTelemetryDisabled(bool isDisabled); ///< Forwards a SetIsTelemetryDisabled 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,
QString const &description, bool includeLogs); ///< Forwards a ReportBug call via a Qt signal.
@ -62,6 +63,7 @@ signals:
void setIsAutostartOnReceived(bool on); ///< Forwards a SetIsAutostartOn call via a Qt signal.
void setIsBetaEnabledReceived(bool enabled); ///< Forwards a SetIsBetaEnabled call via a Qt signal.
void setIsAllMailVisibleReceived(bool enabled); ///< Forwards a SetIsBetaEnabled call via a Qt signal.
void setIsTelemetryDisabledReceived(bool isDisabled); ///< Forwards a SetIsTelemetryDisabled 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,
QString const &description, bool includeLogs); ///< Signal for the ReportBug gRPC call

View File

@ -192,6 +192,28 @@ Status GRPCService::IsAllMailVisible(ServerContext *, Empty const *request, Bool
}
//****************************************************************************************************************************************************
/// \param[in] request The request.
/// \return The status for the call.
//****************************************************************************************************************************************************
grpc::Status GRPCService::SetIsTelemetryDisabled(::grpc::ServerContext *, ::google::protobuf::BoolValue const *request, ::google::protobuf::Empty *) {
app().log().debug(__FUNCTION__);
qtProxy_.setIsTelemetryDisabledReceived(request->value());
return Status::OK;
}
//****************************************************************************************************************************************************
/// \param[out] response The response.
/// \return The status for the call.
//****************************************************************************************************************************************************
grpc::Status GRPCService::IsTelemetryDisabled(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::BoolValue *response) {
app().log().debug(__FUNCTION__);
response->set_value(app().mainWindow().settingsTab().isTelemetryDisabled());
return Status::OK;
}
//****************************************************************************************************************************************************
/// \return The status for the call.
//****************************************************************************************************************************************************
@ -820,3 +842,4 @@ void GRPCService::finishLogin() {
qtProxy_.sendDelayedEvent(newLoginFinishedEvent(user->id(), alreadyExist));
}

View File

@ -51,6 +51,8 @@ public: // member functions.
grpc::Status IsBetaEnabled(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::BoolValue *response) override;
grpc::Status SetIsAllMailVisible(::grpc::ServerContext *context, ::google::protobuf::BoolValue const *request, ::google::protobuf::Empty *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 IsTelemetryDisabled(::grpc::ServerContext *, ::google::protobuf::Empty const *request, ::google::protobuf::BoolValue *response) 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 LogsPath(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::StringValue *response) override;

View File

@ -202,6 +202,22 @@ void SettingsTab::setIsAllMailVisible(bool visible) {
}
//****************************************************************************************************************************************************
/// \return the value for the 'Disabled Telemetry' check.
//****************************************************************************************************************************************************
bool SettingsTab::isTelemetryDisabled() const {
return ui_.checkIsTelemetryDisabled->isChecked();
}
//****************************************************************************************************************************************************
/// \param[in] isDisabled The new value for the 'Disable Telemetry' check box.
//****************************************************************************************************************************************************
void SettingsTab::setIsTelemetryDisabled(bool isDisabled) {
ui_.checkIsTelemetryDisabled->setChecked(isDisabled);
}
//****************************************************************************************************************************************************
/// \return The delay to apply before sending automatically generated events.
//****************************************************************************************************************************************************

View File

@ -45,6 +45,7 @@ public: // member functions.
bool isAutostartOn() const; ///< Get the value for the 'Autostart' check.
bool isBetaEnabled() const; ///< Get the value for the 'Beta Enabled' check.
bool isAllMailVisible() const; ///< Get the value for the 'All Mail Visible' check.
bool isTelemetryDisabled() const; ///< Get the value for the 'Disable Telemetry' check box.
QString colorSchemeName() const; ///< Get the value of the 'Use Dark Theme' checkbox.
qint32 eventDelayMs() const; ///< Get the delay for sending automatically generated events.
QString logsPath() const; ///< Get the content of the 'Logs Path' edit.
@ -74,6 +75,7 @@ public slots:
void setIsAutostartOn(bool on); ///< Set the value for the 'Autostart' check box.
void setIsBetaEnabled(bool enabled); ///< Set the value for the 'Beta Enabled' check box.
void setIsAllMailVisible(bool visible); ///< Set the value for the 'All Mail Visible' check box.
void setIsTelemetryDisabled(bool isDisabled); ///< Set the value for the 'Disable Telemetry' 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,
bool includeLogs); ///< Set the content of the bug report box.

View File

@ -170,6 +170,13 @@
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="checkIsTelemetryDisabled">
<property name="text">
<string>Disable Telemetry</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>

View File

@ -0,0 +1,4 @@
# The following fix an issue happening using LLDB with OpenSSL 3.1 on ARM64 architecture. (GODT-2680)
# WARNING: this file is ignored if you do not enable reading lldb config from cwd in ~/.lldbinit (`settings set target.load-cwd-lldbinit true`)
settings set platform.plugin.darwin.ignored-exceptions EXC_BAD_INSTRUCTION
process handle SIGILL -n false -p true -s false

View File

@ -117,7 +117,27 @@ void AppController::restart(bool isCrashing) {
}
//****************************************************************************************************************************************************
/// \param[in] launcher The launcher.
/// \param[in] args The launcher arguments.
//****************************************************************************************************************************************************
void AppController::setLauncherArgs(const QString &launcher, const QStringList &args) {
launcher_ = launcher;
launcherArgs_ = args;
}
//****************************************************************************************************************************************************
/// \param[in] sessionID The sessionID.
//****************************************************************************************************************************************************
void AppController::setSessionID(const QString &sessionID) {
sessionID_ = sessionID;
}
//****************************************************************************************************************************************************
/// \return The sessionID.
//****************************************************************************************************************************************************
QString AppController::sessionID() {
return sessionID_;
}

View File

@ -37,7 +37,7 @@ class Exception;
/// \brief App controller class.
//****************************************************************************************************************************************************
class AppController : public QObject {
Q_OBJECT
Q_OBJECT
friend AppController &app();
public: // member functions.
@ -52,10 +52,12 @@ public: // member functions.
std::unique_ptr<bridgepp::Overseer> &bridgeOverseer() { return bridgeOverseer_; }; ///< Returns a reference the bridge overseer
bridgepp::ProcessMonitor *bridgeMonitor() const; ///< Return the bridge worker.
Settings &settings();; ///< Return the application settings.
void setLauncherArgs(const QString &launcher, const QStringList &args);
void setLauncherArgs(const QString &launcher, const QStringList &args); ///< Set the launcher arguments.
void setSessionID(QString const &sessionID); ///< Set the sessionID.
QString sessionID(); ///< Get the sessionID.
public slots:
void onFatalError(bridgepp::Exception const& e); ///< Handle fatal errors.
void onFatalError(bridgepp::Exception const &e); ///< Handle fatal errors.
private: // member functions
AppController(); ///< Default constructor.
@ -67,8 +69,9 @@ private: // data members
std::unique_ptr<bridgepp::Log> log_; ///< The log.
std::unique_ptr<bridgepp::Overseer> bridgeOverseer_; ///< The overseer for the bridge monitor worker.
std::unique_ptr<Settings> settings_; ///< The application settings.
QString launcher_;
QStringList launcherArgs_;
QString launcher_; ///< The launcher.
QStringList launcherArgs_; ///< The launcher arguments.
QString sessionID_; ///< The sessionID.
};

View File

@ -23,6 +23,7 @@
#define PROJECT_VENDOR "@BRIDGE_VENDOR@"
#define PROJECT_VER "@BRIDGE_APP_VERSION@"
#define PROJECT_REVISION "@BRIDGE_REVISION@"
#define PROJECT_TAG "@BRIDGE_TAG@"
#define PROJECT_BUILD_TIME "@BRIDGE_BUILD_TIME@"
#define PROJECT_DSN_SENTRY "@BRIDGE_DSN_SENTRY@"
#define PROJECT_BUILD_ENV "@BRIDGE_BUILD_ENV@"

View File

@ -75,7 +75,7 @@ if(NOT UNIX)
set(CMAKE_INSTALL_BINDIR ".")
endif(NOT UNIX)
find_package(Qt6 COMPONENTS Core Quick Qml QuickControls2 Widgets REQUIRED)
find_package(Qt6 COMPONENTS Core Quick Qml QuickControls2 Widgets Svg REQUIRED)
qt_standard_project_setup()
set(CMAKE_AUTORCC ON)
message(STATUS "Using Qt ${Qt6_VERSION}")
@ -114,6 +114,7 @@ add_executable(bridge-gui
EventStreamWorker.cpp EventStreamWorker.h
LogUtils.cpp LogUtils.h
main.cpp
TrayIcon.cpp TrayIcon.h
Pch.h
QMLBackend.cpp QMLBackend.h
UserList.cpp UserList.h
@ -146,6 +147,7 @@ target_link_libraries(bridge-gui
Qt6::Quick
Qt6::Qml
Qt6::QuickControls2
Qt6::Svg
sentry::sentry
bridgepp
)

View File

@ -19,6 +19,7 @@
#include "Pch.h"
#include "CommandLine.h"
#include "Settings.h"
#include <bridgepp/SessionID/SessionID.h>
using namespace bridgepp;
@ -142,5 +143,14 @@ CommandLineOptions parseCommandLine(int argc, char *argv[]) {
options.logLevel = parseLogLevel(argc, argv);
QString sessionID = parseGoCLIStringArgument(argc, argv, { "session-id" });
if (sessionID.isEmpty()) {
// The session ID was not passed to us on the command-line -> create one and add to the command-line for bridge
sessionID = newSessionID();
options.bridgeArgs.append("--session-id");
options.bridgeArgs.append(sessionID);
}
app().setSessionID(sessionID);
return options;
}

View File

@ -43,12 +43,9 @@ EventStreamReader::EventStreamReader(QObject *parent)
void EventStreamReader::run() {
try {
emit started();
grpc::Status const status = app().grpc().runEventStreamReader();
if (!status.ok()) {
throw Exception(QString::fromStdString(status.error_message()));
}
// Status code for the call below is ignored. The event stream may have interrupted by system shutdown or OS user sign-out, and we do not
// want this to generate a sentry report.
app().grpc().runEventStreamReader();
emit finished();
}
catch (Exception const &e) {

View File

@ -19,7 +19,6 @@
#include "LogUtils.h"
#include "BuildConfig.h"
#include <bridgepp/Log/LogUtils.h>
#include <bridgepp/BridgeUtils.h>
using namespace bridgepp;
@ -33,15 +32,10 @@ Log &initLog() {
log.registerAsQtMessageHandler();
log.setEchoInConsole(true);
// remove old gui log files
QDir const logsDir(userLogsDir());
for (QFileInfo const fileInfo: logsDir.entryInfoList({ "gui_v*.log" }, QDir::Filter::Files)) { // entryInfolist apparently only support wildcards, not regex.
QFile(fileInfo.absoluteFilePath()).remove();
}
// create new GUI log file
QString error;
if (!log.startWritingToFile(logsDir.absoluteFilePath(QString("gui_v%1_%2.log").arg(PROJECT_VER).arg(QDateTime::currentSecsSinceEpoch())), &error)) {
if (!log.startWritingToFile(QDir(userLogsDir()).absoluteFilePath(QString("%1_gui_000_v%2_%3.log").arg(app().sessionID(),
PROJECT_VER, PROJECT_TAG)), &error)) {
log.error(error);
}

View File

@ -33,7 +33,7 @@ using namespace bridgepp;
/// \brief handle notification of attempt to re-open the application.
//****************************************************************************************************************************************************
void applicationShouldHandleReopen(id, SEL) {
app().backend().showMainWindow();
app().backend().showMainWindow("macOS applicationShouldHandleReopen notification");
}

View File

@ -25,6 +25,7 @@
#include <QtQml>
#include <QtWidgets>
#include <QtQuickControls2>
#include <QtSvg>
#include <AppController.h>

View File

@ -24,7 +24,6 @@
#include <bridgepp/Log/LogUtils.h>
#include <bridgepp/GRPC/GRPCClient.h>
#include <bridgepp/Worker/Overseer.h>
#include <bridgepp/BridgeUtils.h>
#define HANDLE_EXCEPTION(x) try { x } \
@ -50,6 +49,9 @@ QMLBackend::QMLBackend()
/// \param[in] serviceConfig
//****************************************************************************************************************************************************
void QMLBackend::init(GRPCConfig const &serviceConfig) {
trayIcon_.reset(new TrayIcon());
this->setNormalTrayIcon();
connect(this, &QMLBackend::fatalError, &app(), &AppController::onFatalError);
users_ = new UserList(this);
@ -59,7 +61,7 @@ void QMLBackend::init(GRPCConfig const &serviceConfig) {
app().grpc().setLog(&log);
this->connectGrpcEvents();
app().grpc().connectToServer(bridgepp::userConfigDir(), serviceConfig, app().bridgeMonitor());
app().grpc().connectToServer(app().sessionID(), bridgepp::userConfigDir(), serviceConfig, app().bridgeMonitor());
app().log().info("Connected to backend via gRPC service.");
QString bridgeVer;
@ -100,6 +102,60 @@ bool QMLBackend::waitForEventStreamReaderToFinish(qint32 timeoutMs) {
}
//****************************************************************************************************************************************************
/// \return The list of users
//****************************************************************************************************************************************************
UserList const &QMLBackend::users() const {
return *users_;
}
//****************************************************************************************************************************************************
/// \return the if bridge considers internet is on.
//****************************************************************************************************************************************************
bool QMLBackend::isInternetOn() const {
return isInternetOn_;
}
//****************************************************************************************************************************************************
/// \param[in] reason The reason for the request.
//****************************************************************************************************************************************************
void QMLBackend::showMainWindow(QString const&reason) {
app().log().debug(QString("main window show requested: %1").arg(reason));
emit showMainWindow();
}
//****************************************************************************************************************************************************
/// \param[in] reason The reason for the request.
//****************************************************************************************************************************************************
void QMLBackend::showHelp(QString const&reason) {
app().log().debug(QString("main window show requested (help page): %1").arg(reason));
emit showHelp();
}
//****************************************************************************************************************************************************
/// \param[in] reason The reason for the request.
//****************************************************************************************************************************************************
void QMLBackend::showSettings(QString const&reason) {
app().log().debug(QString("main window show requested (settings page): %1").arg(reason));
emit showSettings();
}
//****************************************************************************************************************************************************
/// \param[in] userID The userID.
/// \param[in] forceShowWindow Should the window be force to display.
/// \param[in] reason The reason for the request.
//****************************************************************************************************************************************************
void QMLBackend::selectUser(QString const &userID, bool forceShowWindow, QString const &reason) {
if (forceShowWindow) {
app().log().debug(QString("main window show requested (user page): %1").arg(reason));
}
emit selectUser(userID, forceShowWindow);
}
//****************************************************************************************************************************************************
/// \return The build year as a string (e.g. 2023)
//****************************************************************************************************************************************************
@ -276,7 +332,7 @@ QString QMLBackend::vendor() const {
//****************************************************************************************************************************************************
/// \return The value for the 'vendor' property.
/// \return The value for the 'version' property.
//****************************************************************************************************************************************************
QString QMLBackend::version() const {
HANDLE_EXCEPTION_RETURN_QSTRING(
@ -286,6 +342,15 @@ QString QMLBackend::version() const {
)
}
//****************************************************************************************************************************************************
/// \return The value for the 'tag' property.
//****************************************************************************************************************************************************
QString QMLBackend::tag() const {
HANDLE_EXCEPTION_RETURN_QSTRING(
return QString(PROJECT_TAG);
)
}
//****************************************************************************************************************************************************
/// \return The value for the 'hostname' property.
@ -335,6 +400,18 @@ bool QMLBackend::isAllMailVisible() const {
}
//****************************************************************************************************************************************************
/// \return The value for the 'isAllMailVisible' property.
//****************************************************************************************************************************************************
bool QMLBackend::isTelemetryDisabled() const {
HANDLE_EXCEPTION_RETURN_BOOL(
bool v;
app().grpc().isTelemetryDisabled(v);
return v;
)
}
//****************************************************************************************************************************************************
/// \return The value for the 'colorSchemeName' property.
//****************************************************************************************************************************************************
@ -569,6 +646,17 @@ void QMLBackend::changeIsAllMailVisible(bool isVisible) {
}
//****************************************************************************************************************************************************
/// \param[in] isDisabled The new state of the 'Is telemetry disabled property'.
//****************************************************************************************************************************************************
void QMLBackend::toggleIsTelemetryDisabled(bool isDisabled) {
HANDLE_EXCEPTION(
app().grpc().setIsTelemetryDisabled(isDisabled);
emit isTelemetryDisabledChanged(isDisabled);
)
}
//****************************************************************************************************************************************************
/// \param[in] scheme the scheme name
//****************************************************************************************************************************************************
@ -598,7 +686,7 @@ void QMLBackend::login(QString const &username, QString const &password) const {
HANDLE_EXCEPTION(
if (username.compare("coco@bandicoot", Qt::CaseInsensitive) == 0) {
throw Exception("User requested bridge-gui to crash by trying to log as coco@bandicoot",
"This error exists for test purposes and should be ignored.", __func__, tailOfLatestBridgeLog());
"This error exists for test purposes and should be ignored.", __func__, tailOfLatestBridgeLog(app().sessionID()));
}
app().grpc().login(username, password);
)
@ -832,7 +920,94 @@ void QMLBackend::sendBadEventUserFeedback(QString const &userID, bool doResync)
if (!badEventDisplayQueue_.isEmpty()) {
// we introduce a small delay here, so that the user notices the dialog disappear and pops up again.
QTimer::singleShot(500, [&]() { this->displayBadEventDialog(badEventDisplayQueue_.front()); });
}
)
}
//****************************************************************************************************************************************************
///
//****************************************************************************************************************************************************
void QMLBackend::notifyReportBugClicked() const {
HANDLE_EXCEPTION(
app().grpc().reportBugClicked();
)
}
//****************************************************************************************************************************************************
/// \param[in] client The selected Mail client for autoconfig.
//****************************************************************************************************************************************************
void QMLBackend::notifyAutoconfigClicked(QString const &client) const {
HANDLE_EXCEPTION(
app().grpc().autoconfigClicked(client);
)
}
//****************************************************************************************************************************************************
/// \param[in] article The url of the KB article.
//****************************************************************************************************************************************************
void QMLBackend::notifyKBArticleClicked(QString const &article) const {
HANDLE_EXCEPTION(
app().grpc().KBArticleClicked(article);
)
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void QMLBackend::setNormalTrayIcon() {
if (trayIcon_) {
trayIcon_->setState(TrayIcon::State::Normal, tr("Connected"), ":/qml/icons/ic-connected.svg");
}
}
//****************************************************************************************************************************************************
/// \param[in] stateString A string describing the state.
/// \param[in] statusIcon The path of the status icon.
//****************************************************************************************************************************************************
void QMLBackend::setErrorTrayIcon(QString const &stateString, QString const &statusIcon) {
if (trayIcon_) {
trayIcon_->setState(TrayIcon::State::Error, stateString, statusIcon);
}
}
//****************************************************************************************************************************************************
/// \param[in] stateString A string describing the state.
/// \param[in] statusIcon The path of the status icon.
//****************************************************************************************************************************************************
void QMLBackend::setWarnTrayIcon(QString const &stateString, QString const &statusIcon) {
if (trayIcon_) {
trayIcon_->setState(TrayIcon::State::Warn, stateString, statusIcon);
}
}
//****************************************************************************************************************************************************
/// \param[in] stateString A string describing the state.
/// \param[in] statusIcon The path of the status icon.
//****************************************************************************************************************************************************
void QMLBackend::setUpdateTrayIcon(QString const &stateString, QString const &statusIcon) {
if (trayIcon_) {
trayIcon_->setState(TrayIcon::State::Update, stateString, statusIcon);
}
}
//****************************************************************************************************************************************************
/// \param[in] isOn Does bridge consider internet as on.
//****************************************************************************************************************************************************
void QMLBackend::internetStatusChanged(bool isOn) {
HANDLE_EXCEPTION(
if (isInternetOn_ == isOn) {
return;
}
isInternetOn_ = isOn;
if (isOn) {
emit internetOn();
} else {
emit internetOff();
}
)
}
@ -892,7 +1067,7 @@ void QMLBackend::onLoginAlreadyLoggedIn(QString const &userID) {
//****************************************************************************************************************************************************
/// \param[in] userID The userID.
//****************************************************************************************************************************************************
void QMLBackend::onUserBadEvent(QString const &userID, QString const& ) {
void QMLBackend::onUserBadEvent(QString const &userID, QString const &) {
HANDLE_EXCEPTION(
if (badEventDisplayQueue_.contains(userID)) {
app().log().error("Received 'bad event' for a user that is already in the queue.");
@ -918,14 +1093,44 @@ void QMLBackend::onUserBadEvent(QString const &userID, QString const& ) {
void QMLBackend::onIMAPLoginFailed(QString const &username) {
HANDLE_EXCEPTION(
SPUser const user = users_->getUserWithUsernameOrEmail(username);
if ((!user) || (user->state() != UserState::SignedOut)) { // We want to pop-up only if a signed-out user has been detected
if (!user) {
return;
}
if (user->isInIMAPLoginFailureCooldown())
return;
user->startImapLoginFailureCooldown(60 * 60 * 1000); // 1 hour cooldown during which we will not display this notification to this user again.
emit selectUser(user->id());
emit imapLoginWhileSignedOut(username);
qint64 const cooldownDurationMs = 10 * 60 * 1000; // 10 minutes cooldown period for notifications
switch (user->state()) {
case UserState::SignedOut:
if (user->isNotificationInCooldown(User::ENotification::IMAPLoginWhileSignedOut)) {
return;
}
user->startNotificationCooldownPeriod(User::ENotification::IMAPLoginWhileSignedOut, cooldownDurationMs);
emit selectUser(user->id(), true);
emit imapLoginWhileSignedOut(username);
break;
case UserState::Connected:
if (user->isNotificationInCooldown(User::ENotification::IMAPPasswordFailure)) {
return;
}
user->startNotificationCooldownPeriod(User::ENotification::IMAPPasswordFailure, cooldownDurationMs);
emit selectUser(user->id(), false);
trayIcon_->showErrorPopupNotification(tr("Incorrect password"),
tr("Your email client can't connect to Proton Bridge. Make sure you are using the local Bridge password shown in Bridge."));
break;
case UserState::Locked:
if (user->isNotificationInCooldown(User::ENotification::IMAPLoginWhileLocked)) {
return;
}
user->startNotificationCooldownPeriod(User::ENotification::IMAPLoginWhileLocked, cooldownDurationMs);
emit selectUser(user->id(), false);
trayIcon_->showErrorPopupNotification(tr("Connection in progress"),
tr("Your Proton account in Bridge is being connected. Please wait or restart Bridge."));
break;
default:
break;
}
)
}
@ -971,13 +1176,13 @@ void QMLBackend::connectGrpcEvents() {
GRPCClient *client = &app().grpc();
// app events
connect(client, &GRPCClient::internetStatus, this, [&](bool isOn) { if (isOn) { emit internetOn(); } else { emit internetOff(); }});
connect(client, &GRPCClient::internetStatus, this, &QMLBackend::internetStatusChanged);
connect(client, &GRPCClient::toggleAutostartFinished, this, &QMLBackend::toggleAutostartFinished);
connect(client, &GRPCClient::resetFinished, this, &QMLBackend::onResetFinished);
connect(client, &GRPCClient::reportBugFinished, this, &QMLBackend::reportBugFinished);
connect(client, &GRPCClient::reportBugSuccess, this, &QMLBackend::bugReportSendSuccess);
connect(client, &GRPCClient::reportBugError, this, &QMLBackend::bugReportSendError);
connect(client, &GRPCClient::showMainWindow, this, &QMLBackend::showMainWindow);
connect(client, &GRPCClient::showMainWindow, [&]() { this->showMainWindow("gRPC showMainWindow event"); });
// cache events
connect(client, &GRPCClient::diskCacheUnavailable, this, &QMLBackend::diskCacheUnavailable);
@ -1057,7 +1262,7 @@ void QMLBackend::displayBadEventDialog(QString const &userID) {
emit userBadEvent(userID,
tr("Bridge ran into an internal error and it is not able to proceed with the account %1. Synchronize your local database now or logout"
" to do it later. Synchronization time depends on the size of your mailbox.").arg(elideLongString(user->primaryEmailOrUsername(), 30)));
emit selectUser(userID);
emit selectUser(userID, true);
emit showMainWindow();
)
}

View File

@ -22,6 +22,7 @@
#include "MacOS/DockIcon.h"
#include "BuildConfig.h"
#include "TrayIcon.h"
#include "UserList.h"
#include <bridgepp/GRPC/GRPCClient.h>
#include <bridgepp/GRPC/GRPCUtils.h>
@ -43,6 +44,12 @@ public: // member functions.
QMLBackend &operator=(QMLBackend &&) = delete; ///< Disabled move assignment operator.
void init(GRPCConfig const &serviceConfig); ///< Initialize the backend.
bool waitForEventStreamReaderToFinish(qint32 timeoutMs); ///< Wait for the event stream reader to finish.
UserList const& users() const; ///< Return the list of users
bool isInternetOn() const; ///< Check if bridge considers internet as on.
void showMainWindow(QString const &reason); ///< Show the main window.
void showHelp(QString const &reason); ///< Show the help page.
void showSettings(QString const &reason); ///< Show the settings page.
void selectUser(QString const &userID, bool forceShowWindow, QString const &reason); ///< Select the user and display its account details (or login screen).
// invokable methods can be called from QML. They generally return a value, which slots cannot do.
Q_INVOKABLE static QString buildYear(); ///< Return the application build year.
@ -63,10 +70,12 @@ public: // Qt/QML properties. Note that the NOTIFY-er signal is required even fo
Q_PROPERTY(QString appname READ appname NOTIFY appnameChanged)
Q_PROPERTY(QString vendor READ vendor NOTIFY vendorChanged)
Q_PROPERTY(QString version READ version NOTIFY versionChanged)
Q_PROPERTY(QString tag READ tag NOTIFY tagChanged)
Q_PROPERTY(QString hostname READ hostname NOTIFY hostnameChanged)
Q_PROPERTY(bool isAutostartOn READ isAutostartOn NOTIFY isAutostartOnChanged)
Q_PROPERTY(bool isBetaEnabled READ isBetaEnabled NOTIFY isBetaEnabledChanged)
Q_PROPERTY(bool isAllMailVisible READ isAllMailVisible NOTIFY isAllMailVisibleChanged)
Q_PROPERTY(bool isTelemetryDisabled READ isTelemetryDisabled NOTIFY isTelemetryDisabledChanged)
Q_PROPERTY(QString colorSchemeName READ colorSchemeName NOTIFY colorSchemeNameChanged)
Q_PROPERTY(QUrl diskCachePath READ diskCachePath NOTIFY diskCachePathChanged)
Q_PROPERTY(bool useSSLForIMAP READ useSSLForIMAP WRITE setUseSSLForIMAP NOTIFY useSSLForIMAPChanged)
@ -81,7 +90,6 @@ public: // Qt/QML properties. Note that the NOTIFY-er signal is required even fo
Q_PROPERTY(UserList *users MEMBER users_ NOTIFY usersChanged)
Q_PROPERTY(bool dockIconVisible READ dockIconVisible WRITE setDockIconVisible NOTIFY dockIconVisibleChanged)
// Qt Property system setters & getters.
bool showOnStartup() const; ///< Getter for the 'showOnStartup' property.
void setShowSplashScreen(bool show); ///< Setter for the 'showSplashScreen' property.
@ -95,10 +103,12 @@ public: // Qt/QML properties. Note that the NOTIFY-er signal is required even fo
QString appname() const; ///< Getter for the 'appname' property.
QString vendor() const; ///< Getter for the 'vendor' property.
QString version() const; ///< Getter for the 'version' property.
QString tag() const; ///< Getter for the 'tag' property.
QString hostname() const; ///< Getter for the 'hostname' property.
bool isAutostartOn() const; ///< Getter for the 'isAutostartOn' property.
bool isBetaEnabled() const; ///< Getter for the 'isBetaEnabled' property.
bool isAllMailVisible() const; ///< Getter for the 'isAllMailVisible' property.
bool isTelemetryDisabled() const; ///< Getter for the 'isTelemetryDisabled' property.
QString colorSchemeName() const; ///< Getter for the 'colorSchemeName' property.
QUrl diskCachePath() const; ///< Getter for the 'diskCachePath' property.
void setUseSSLForIMAP(bool value); ///< Setter for the 'useSSLForIMAP' property.
@ -129,6 +139,7 @@ signals: // Signal used by the Qt property system. Many of them are unused but r
void isAutomaticUpdateOnChanged(bool value); ///<Signal for the change of the 'isAutomaticUpdateOn' property.
void isBetaEnabledChanged(bool value); ///<Signal for the change of the 'isBetaEnabled' property.
void isAllMailVisibleChanged(bool value); ///<Signal for the change of the 'isAllMailVisible' property.
void isTelemetryDisabledChanged(bool isDisabled); ///<Signal for the change of the 'isTelemetryDisabled' property.
void colorSchemeNameChanged(QString const &scheme); ///<Signal for the change of the 'colorSchemeName' property.
void isDoHEnabledChanged(bool value); ///<Signal for the change of the 'isDoHEnabled' property.
void logsPathChanged(QUrl const &path); ///<Signal for the change of the 'logsPath' property.
@ -139,6 +150,7 @@ signals: // Signal used by the Qt property system. Many of them are unused but r
void appnameChanged(QString const &appname); ///<Signal for the change of the 'appname' property.
void vendorChanged(QString const &vendor); ///<Signal for the change of the 'vendor' property.
void versionChanged(QString const &version); ///<Signal for the change of the 'version' property.
void tagChanged(QString const &tag); ///<Signal for the change of the 'tag' property.
void currentEmailClientChanged(QString const &email); ///<Signal for the change of the 'currentEmailClient' property.
void currentKeychainChanged(QString const &keychain); ///<Signal for the change of the 'currentKeychain' property.
void availableKeychainChanged(QStringList const &keychains); ///<Signal for the change of the 'availableKeychain' property.
@ -151,6 +163,7 @@ public slots: // slot for signals received from QML -> To be forwarded to Bridge
void toggleAutostart(bool active); ///< Slot for the autostart toggle.
void toggleBeta(bool active); ///< Slot for the beta toggle.
void changeIsAllMailVisible(bool isVisible); ///< Slot for the changing of 'All Mail' visibility.
void toggleIsTelemetryDisabled(bool isDisabled); ///< Slot for toggling telemetry on/off.
void changeColorScheme(QString const &scheme); ///< Slot for the change of the theme.
void setDiskCachePath(QUrl const &path) const; ///< Slot for the change of the disk cache path.
void login(QString const &username, QString const &password) const; ///< Slot for the login button (initial login).
@ -174,8 +187,18 @@ public slots: // slot for signals received from QML -> To be forwarded to Bridge
void onVersionChanged(); ///< Slot for the version change signal.
void setMailServerSettings(int imapPort, int smtpPort, bool useSSLForIMAP, bool useSSLForSMTP) const; ///< Forwards a connection mode change request from QML to gRPC
void sendBadEventUserFeedback(QString const &userID, bool doResync); ///< Slot the providing user feedback for a bad event.
void notifyReportBugClicked() const; ///< Slot for the ReportBugClicked gRPC event.
void notifyAutoconfigClicked(QString const &client) const; ///< Slot for gAutoconfigClicked gRPC event.
void notifyKBArticleClicked(QString const &article) const; ///< Slot for KBArticleClicked gRPC event.
public slots: // slots for functions that need to be processed locally.
void setNormalTrayIcon(); ///< Set the tray icon to normal.
void setErrorTrayIcon(QString const& stateString, QString const &statusIcon); ///< Set the tray icon to 'error' state.
void setWarnTrayIcon(QString const& stateString, QString const &statusIcon); ///< Set the tray icon to 'warn' state.
void setUpdateTrayIcon(QString const& stateString, QString const &statusIcon); ///< Set the tray icon to 'update' state.
public slots: // slot for signals received from gRPC that need transformation instead of simple forwarding
void internetStatusChanged(bool isOn); ///< Check if bridge considers internet as on.
void onMailServerSettingsChanged(int imapPort, int smtpPort, bool useSSLForIMAP, bool useSSLForSMTP); ///< Slot for the ConnectionModeChanged gRPC event.
void onGenericError(bridgepp::ErrorInfo const &info); ///< Slot for generic errors received from the gRPC service.
void onLoginFinished(QString const &userID, bool wasSignedOut); ///< Slot for LoginFinished gRPC event.
@ -233,8 +256,10 @@ signals: // Signals received from the Go backend, to be forwarded to QML
void bugReportSendError(); ///< Signal for the 'bugReportSendError' gRPC stream event.
void showMainWindow(); ///< Signal for the 'showMainWindow' gRPC stream event.
void hideMainWindow(); ///< Signal for the 'hideMainWindow' gRPC stream event.
void showHelp(); ///< Signal for the 'showHelp' event (from the context menu).
void showSettings(); ///< Signal for the 'showHelp' 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 genericError(QString const &title, QString const &description); ///< Signal for the 'genericError' gRPC stream event.
void selectUser(QString const); ///< Signal that request the given user account to be displayed.
void imapLoginWhileSignedOut(QString const& username); ///< Signal for the notification of IMAP login attempt on a signed out account.
// This signal is emitted when an exception is intercepted is calls triggered by QML. QML engine would intercept the exception otherwise.
@ -256,8 +281,9 @@ private: // data members
int smtpPort_ { 0 }; ///< The cached value for the SMTP port.
bool useSSLForIMAP_ { false }; ///< The cached value for useSSLForIMAP.
bool useSSLForSMTP_ { false }; ///< The cached value for useSSLForSMTP.
bool isInternetOn_ { true }; ///< Does bridge consider internet as on?
QList<QString> badEventDisplayQueue_; ///< THe queue for displaying 'bad event feedback request dialog'.
std::unique_ptr<TrayIcon> trayIcon_; ///< The tray icon for the application.
friend class AppController;
};

View File

@ -5,11 +5,7 @@
<file>qml/AccountView.qml</file>
<file>qml/Banner.qml</file>
<file>qml/Bridge.qml</file>
<file>qml/Bridge_test.qml</file>
<file>qml/bridgeqml.qmlproject</file>
<file>qml/BridgeTest/UserControl.qml</file>
<file>qml/BridgeTest/UserList.qml</file>
<file>qml/BridgeTest/UserModel.qml</file>
<file>qml/BugReportView.qml</file>
<file>qml/Configuration.qml</file>
<file>qml/ConfigurationItem.qml</file>
@ -23,11 +19,13 @@
<file>qml/icons/ic-card-identity.svg</file>
<file>qml/icons/ic-check.svg</file>
<file>qml/icons/ic-chevron-down.svg</file>
<file>qml/icons/ic-chevron-right.svg</file>
<file>qml/icons/ic-chevron-up.svg</file>
<file>qml/icons/ic-cog-wheel.svg</file>
<file>qml/icons/ic-connected.svg</file>
<file>qml/icons/ic-copy.svg</file>
<file>qml/icons/ic-cross-close.svg</file>
<file>qml/icons/ic-dot.svg</file>
<file>qml/icons/ic-drive.svg</file>
<file>qml/icons/ic-exclamation-circle-filled.svg</file>
<file>qml/icons/ic-external-link.svg</file>
@ -104,17 +102,6 @@
<file>qml/ConnectionModeSettings.qml</file>
<file>qml/SplashScreen.qml</file>
<file>qml/Status.qml</file>
<file>qml/StatusWindow.qml</file>
<file>qml/tests/Buttons.qml</file>
<file>qml/tests/ButtonsColumn.qml</file>
<file>qml/tests/CheckBoxes.qml</file>
<file>qml/tests/ComboBoxes.qml</file>
<file>qml/tests/RadioButtons.qml</file>
<file>qml/tests/Switches.qml</file>
<file>qml/tests/Test.qml</file>
<file>qml/tests/TestComponents.qml</file>
<file>qml/tests/TextAreas.qml</file>
<file>qml/tests/TextFields.qml</file>
<file>qml/WelcomeGuide.qml</file>
</qresource>
</RCC>

View File

@ -49,7 +49,7 @@ QString sentryAttachmentFilePath() {
//****************************************************************************************************************************************************
QByteArray getProtectedHostname() {
QByteArray hostname = QCryptographicHash::hash(QSysInfo::machineHostName().toUtf8(), QCryptographicHash::Sha256);
return hostname.toHex();
return hostname.toBase64();
}
//****************************************************************************************************************************************************

View File

@ -22,6 +22,7 @@
#include <sentry.h>
void initSentry();
QByteArray getProtectedHostname();
void setSentryReportScope();
sentry_options_t* newSentryOptions(const char * sentryDNS, const char * cacheDir);
sentry_uuid_t reportSentryEvent(sentry_level_t level, const char *message);

View File

@ -0,0 +1,388 @@
// 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/>.
#include "TrayIcon.h"
#include "QMLBackend.h"
#include <bridgepp/Exception/Exception.h>
#include <bridgepp/BridgeUtils.h>
using namespace bridgepp;
namespace {
QColor const normalColor(30, 168, 133); /// The normal state color.
QColor const errorColor(220, 50, 81); ///< The error state color.
QColor const warnColor(255, 153, 0); ///< The warn state color.
QColor const updateColor(35, 158, 206); ///< The warn state color.
QColor const greyColor(112, 109, 107); ///< The grey color.
qint64 const iconRefreshTimerIntervalMs = 1000; ///< The interval for the refresh timer when switching DPI / screen config, in milliseconds.
qint64 const iconRefreshDurationSecs = 10; ///< The total number of seconds during which we periodically refresh the icon after a DPI change.
//****************************************************************************************************************************************************
/// \brief Create a single resolution icon from an image. Throw an exception in case of failure.
//****************************************************************************************************************************************************
QIcon loadIconFromImage(QString const &path) {
QPixmap const pixmap(path);
if (pixmap.isNull()) {
throw Exception(QString("Could not create an icon from an image '%1'.").arg(path));
}
return QIcon(pixmap);
}
//****************************************************************************************************************************************************
/// \brief Generate an icon from a SVG renderer (a.k.a. path).
///
/// \param[in] renderer The SVG renderer.
/// \param[in] color The color to use in case the SVG path is to be used as a mask.
/// \return The icon.
//****************************************************************************************************************************************************
QIcon loadIconFromSVGRenderer(QSvgRenderer &renderer, QColor const &color = QColor()) {
if (!renderer.isValid()) {
return QIcon();
}
QIcon icon;
qint32 size = 256;
while (size >= 16) {
QPixmap pixmap(size, size);
pixmap.fill(QColor(0, 0, 0, 0));
QPainter painter(&pixmap);
renderer.render(&painter);
if (color.isValid()) {
painter.setCompositionMode(QPainter::CompositionMode_SourceIn);
painter.fillRect(pixmap.rect(), color);
}
painter.end();
icon.addPixmap(pixmap);
size /= 2;
}
return icon;
}
//****************************************************************************************************************************************************
/// \brief Load a multi-resolution icon from a SVG file. The image is assumed to be square. SVG is rasterized in 256, 128, 64, 32 and 16px.
///
/// Note: QPixmap can load SVG files directly, but our SVG file are defined in small shape size and QPixmap will rasterize them a very low resolution
/// by default (eg. 16x16), which is insufficient for some uses. As a consequence, we manually generate a multi-resolution icon that render smoothly
/// at any acceptable resolution for an icon.
///
/// \param[in] path The path of the SVG file.
/// \return The icon.
//****************************************************************************************************************************************************
QIcon loadIconFromSVG(QString const &path, QColor const &color = QColor()) {
QSvgRenderer renderer(path);
QIcon const icon = loadIconFromSVGRenderer(renderer, color);
if (icon.isNull()) {
Exception(QString("Could not create an icon from a vector image '%1'.").arg(path));
}
return icon;
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
QIcon loadIcon(QString const &path) {
if (path.endsWith(".svg", Qt::CaseInsensitive)) {
return loadIconFromSVG(path);
}
return loadIconFromImage(path);
}
//****************************************************************************************************************************************************
/// \brief Retrieve the color associated with a tray icon state.
///
/// \param[in] state The state.
/// \return The color associated with a given tray icon state.
//****************************************************************************************************************************************************
QColor stateColor(TrayIcon::State state) {
switch (state) {
case TrayIcon::State::Normal:
return normalColor;
case TrayIcon::State::Error:
return errorColor;
case TrayIcon::State::Warn:
return warnColor;
case TrayIcon::State::Update:
return updateColor;
default:
app().log().error(QString("Unknown tray icon state %1.").arg(static_cast<qint32>(state)));
return normalColor;
}
}
//****************************************************************************************************************************************************
/// \brief Return the text identifying a state in resource file names.
///
/// \param[in] state The state.
/// \param[in] The text identifying the state in resource file names.
//****************************************************************************************************************************************************
QString stateText(TrayIcon::State state) {
switch (state) {
case TrayIcon::State::Normal:
return "norm";
case TrayIcon::State::Error:
return "error";
case TrayIcon::State::Warn:
return "warn";
case TrayIcon::State::Update:
return "update";
default:
app().log().error(QString("Unknown tray icon state %1.").arg(static_cast<qint32>(state)));
return "norm";
}
}
//****************************************************************************************************************************************************
/// \brief converts a QML resource path to Qt resource path.
/// QML resource paths are a bit different from qt resource paths
/// \param[in] path The resource path.
/// \return
//****************************************************************************************************************************************************
QString qmlResourcePathToQt(QString const &path) {
QString result = path;
result.replace(QRegularExpression(R"(^\.\/)"), ":/qml/");
return result;
}
} // anonymous namespace
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
TrayIcon::TrayIcon()
: QSystemTrayIcon()
, menu_(new QMenu)
, notificationErrorIcon_(loadIconFromSVG(":/qml/icons/ic-alert.svg")) {
this->generateDotIcons();
this->setContextMenu(menu_.get());
connect(menu_.get(), &QMenu::aboutToShow, this, &TrayIcon::onMenuAboutToShow);
connect(this, &TrayIcon::selectUser, &app().backend(), [](QString const& userID, bool forceShowWindow) {
app().backend().selectUser(userID, forceShowWindow);
});
connect(this, &TrayIcon::activated, this, &TrayIcon::onActivated);
// some OSes/Desktop managers will automatically show main window when clicked, but not all, so we do it manually.
if (!onLinux()) { // we disable this on linux because of a Qt bug that causes the signal to be emitted for other apps (GODT-2750)
connect(this, &TrayIcon::messageClicked, []() { app().backend().showMainWindow("tray icon popup notification clicked"); });
}
this->show();
// TrayIcon does not expose its screen, so we connect relevant screen events to our DPI change handler.
for (QScreen *screen: QGuiApplication::screens()) {
connect(screen, &QScreen::logicalDotsPerInchChanged, this, &TrayIcon::handleDPIChange);
}
connect(qApp, &QApplication::screenAdded, [&](QScreen *screen) { connect(screen, &QScreen::logicalDotsPerInchChanged, this, &TrayIcon::handleDPIChange); });
connect(qApp, &QApplication::primaryScreenChanged, [&](QScreen *screen) { connect(screen, &QScreen::logicalDotsPerInchChanged, this, &TrayIcon::handleDPIChange); });
iconRefreshTimer_.setSingleShot(false);
iconRefreshTimer_.setInterval(iconRefreshTimerIntervalMs);
connect(&iconRefreshTimer_, &QTimer::timeout, this, &TrayIcon::onIconRefreshTimer);
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void TrayIcon::onMenuAboutToShow() {
this->refreshContextMenu();
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void TrayIcon::onUserClicked() {
try {
auto action = dynamic_cast<QAction *>(this->sender());
if (!action) {
throw Exception("Could not retrieve context menu action.");
}
QString const &userID = action->data().toString();
if (userID.isNull()) {
throw Exception("Could not retrieve context menu's selected user.");
}
app().backend().selectUser(userID, true, "tray menu user clicked");
} catch (Exception const &e) {
app().log().error(e.qwhat());
}
}
//****************************************************************************************************************************************************
/// \param[in] reason The icon activation reason.
//****************************************************************************************************************************************************
void TrayIcon::onActivated(QSystemTrayIcon::ActivationReason reason) {
if ((QSystemTrayIcon::Trigger == reason) && !onMacOS()) {
app().backend().showMainWindow("tray icon activated");
}
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void TrayIcon::handleDPIChange() {
this->setIcon();
// Windows forces us to apply a hack. Tray icon does not redraw by itself, so we use the Qt signal that detects screen and DPI changes.
// But the moment we get the signal the DPI change is not yet in effect. so redrawing now will have no effect, and we don't really
// know when we can safely redraw. So we will redraw the icon every second for some time.
iconRefreshDeadline_ = QDateTime::currentDateTime().addSecs(iconRefreshDurationSecs);
if (!iconRefreshTimer_.isActive()) {
iconRefreshTimer_.start();
}
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void TrayIcon::setIcon() {
QString const style = onMacOS() ? "mono" : "color";
QString const text = stateText(state_);
QIcon icon = loadIconFromImage(QString(":/qml/icons/systray-%1-%2.png").arg(style, text));
icon.setIsMask(true);
QSystemTrayIcon::setIcon(icon);
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void TrayIcon::onIconRefreshTimer() {
this->setIcon();
if (QDateTime::currentDateTime() > iconRefreshDeadline_) {
iconRefreshTimer_.stop();
}
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void TrayIcon::generateDotIcons() {
QSvgRenderer dotSVG(QString(":/qml/icons/ic-dot.svg"));
struct IconColor {
QIcon &icon;
QColor color;
};
for (auto pair: QList<IconColor> {{ greenDot_, normalColor }, { greyDot_, greyColor }, { orangeDot_, warnColor }}) {
pair.icon = loadIconFromSVGRenderer(dotSVG, pair.color);
if (pair.icon.isNull()) {
throw Exception("Could not generate dot icon from vector file.");
}
}
}
//****************************************************************************************************************************************************
/// \param[in] state The state.
/// \param[in] stateString A string describing the state.
/// \param[in] statusIconPath The status icon path.
/// \param[in] statusIconColor The color for the status icon in hex.
//****************************************************************************************************************************************************
void TrayIcon::setState(TrayIcon::State state, QString const &stateString, QString const &statusIconPath) {
stateString_ = stateString;
state_ = state;
this->setIcon();
this->generateStatusIcon(statusIconPath, stateColor(state));
}
//****************************************************************************************************************************************************
/// \param[in] title The title.
/// \param[in] message The message.
//****************************************************************************************************************************************************
void TrayIcon::showErrorPopupNotification(QString const &title, QString const &message) {
this->showMessage(title, message, notificationErrorIcon_);
}
//****************************************************************************************************************************************************
/// \param[in] svgPath The path of the SVG file for the icon.
/// \param[in] color The color to apply to the icon.
//****************************************************************************************************************************************************
void TrayIcon::generateStatusIcon(QString const &svgPath, QColor const &color) {
// We use the SVG path as pixmap mask and fill it with the appropriate color
statusIcon_ = loadIconFromSVG(qmlResourcePathToQt(svgPath), color);
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void TrayIcon::refreshContextMenu() {
if (!menu_) {
app().log().error("Native tray icon context menu is null.");
return;
}
bool const internetOn = app().backend().isInternetOn();
menu_->clear();
menu_->addAction(statusIcon_, stateString_, []() {app().backend().showMainWindow("tray menu status clicked");});
menu_->addSeparator();
QKeySequence noShortcut;
UserList const &users = app().backend().users();
qint32 const userCount = users.count();
bool const onMac = onMacOS();
for (qint32 i = 0; i < userCount; i++) {
User const &user = *users.get(i);
UserState const state = user.state();
auto action = new QAction(user.primaryEmailOrUsername());
if (internetOn) {
action->setIcon((UserState::Connected == state) ? greenDot_ : (UserState::Locked == state ? orangeDot_ : greyDot_));
}
action->setData(user.id());
connect(action, &QAction::triggered, this, &TrayIcon::onUserClicked);
if ((i < 10) && onMac) {
action->setShortcut(QKeySequence(QString("Ctrl+%1").arg((i + 1) % 10)));
}
menu_->addAction(action);
}
if (userCount) {
menu_->addSeparator();
}
menu_->addAction(tr("&Open Bridge"), onMac ? QKeySequence("Ctrl+O") : noShortcut, []() {
app().backend().showMainWindow("tray menu 'open bridge' clicked");
});
menu_->addAction(tr("&Help"), onMac ? QKeySequence("Ctrl+F1") : noShortcut, []() {
app().backend().showHelp("tray menu 'Help' clicked");
});
menu_->addAction(tr("&Settings"), onMac ? QKeySequence("Ctrl+,") : noShortcut, []() {
app().backend().showSettings("tray menu 'Settings' clicked");
});
menu_->addSeparator();
menu_->addAction(tr("&Quit Bridge"), onMac ? QKeySequence("Ctrl+Q") : noShortcut, &app().backend(), &QMLBackend::quit);
}

View File

@ -0,0 +1,78 @@
// 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/>.
#ifndef BRIDGE_GUI_NATIVE_TRAY_ICON_H
#define BRIDGE_GUI_NATIVE_TRAY_ICON_H
//**********************************************************************************************************************
/// \brief A native tray icon.
//**********************************************************************************************************************
class TrayIcon: public QSystemTrayIcon {
Q_OBJECT
public: // typedef enum
enum class State {
Normal,
Error,
Warn,
Update,
}; ///< Enumeration for the state.
public: // data members
TrayIcon(); ///< Default constructor.
~TrayIcon() override = default; ///< Destructor.
TrayIcon(TrayIcon const&) = delete; ///< Disabled copy-constructor.
TrayIcon(TrayIcon&&) = delete; ///< Disabled assignment copy-constructor.
TrayIcon& operator=(TrayIcon const&) = delete; ///< Disabled assignment operator.
TrayIcon& operator=(TrayIcon&&) = delete; ///< Disabled move assignment operator.
void setState(State state, QString const& stateString, QString const &statusIconPath); ///< Set the state of the icon
void showErrorPopupNotification(QString const& title, QString const &message); ///< Display a pop up notification.
signals:
void selectUser(QString const& userID, bool forceShowWindow); ///< Signal for selecting a user with a given userID
private slots:
void onMenuAboutToShow(); ///< Slot called before the context menu is shown.
void onUserClicked(); ///< Slot triggered when clicking on a user in the context menu.
static void onActivated(QSystemTrayIcon::ActivationReason reason); ///< Slot for the activation of the system tray icon.
void handleDPIChange(); ///< Handles DPI change.
void setIcon(); ///< set the tray icon.
void onIconRefreshTimer(); ///< Timer for icon refresh.
private: // member functions.
void generateDotIcons(); ///< generate the colored dot icons used for user status.
void generateStatusIcon(QString const &svgPath, QColor const& color); ///< Generate the status icon.
void refreshContextMenu(); ///< Refresh the context menu.
private: // data members
State state_ { State::Normal }; ///< The state of the tray icon.
QString stateString_; ///< The current state string.
std::unique_ptr<QMenu> menu_; ///< The context menu for the tray icon. Not owned by the tray icon.
QIcon statusIcon_; ///< The path of the status icon displayed in the context menu.
QIcon greenDot_; ///< The green dot icon.
QIcon greyDot_; ///< The grey dot icon.
QIcon orangeDot_; ///< The orange dot icon.
QIcon const notificationErrorIcon_; ///< The error icon used for notifications.
QTimer iconRefreshTimer_; ///< The timer used to periodically refresh the icon when DPI changes.
QDateTime iconRefreshDeadline_; ///< The deadline for refreshing the icon
};
#endif //BRIDGE_GUI_NATIVE_TRAY_ICON_H

View File

@ -76,6 +76,7 @@ function check_exit() {
Write-host "Running build for version $bridgeVersion - $buildConfig in $buildDir"
$REVISION_HASH = git rev-parse --short=10 HEAD
$bridgeTag = ($env:BRIDGE_TAG)
$bridgeDsnSentry = ($env:BRIDGE_DSN_SENTRY)
$bridgeBuidTime = ($env:BRIDGE_BUILD_TIME)
@ -93,6 +94,7 @@ git submodule update --init --recursive $vcpkgRoot
-DBRIDGE_APP_FULL_NAME="$bridgeFullName" `
-DBRIDGE_VENDOR="$bridgeVendor" `
-DBRIDGE_REVISION="$REVISION_HASH" `
-DBRIDGE_TAG="$bridgeTag" `
-DBRIDGE_APP_VERSION="$bridgeVersion" `
-DBRIDGE_BUILD_TIME="$bridgeBuidTime" `
-DBRIDGE_DSN_SENTRY="$bridgeDsnSentry" `

View File

@ -56,6 +56,7 @@ BUILD_CONFIG=${BRIDGE_GUI_BUILD_CONFIG:-Debug}
BUILD_DIR=$(echo "./cmake-build-${BUILD_CONFIG}" | tr '[:upper:]' '[:lower:]')
VCPKG_ROOT="${BRIDGE_REPO_ROOT}/extern/vcpkg"
BRIDGE_REVISION=$(git rev-parse --short=10 HEAD)
BRIDGE_TAG=${BRIDGE_TAG:-"NOTAG"}
BRIDGE_DSN_SENTRY=${BRIDGE_DSN_SENTRY}
BRIDGE_BUILD_TIME=${BRIDGE_BUILD_TIME}
BRIDGE_BUILD_ENV= ${BRIDGE_BUILD_ENV:-"dev"}
@ -97,6 +98,7 @@ cmake \
-DBRIDGE_APP_FULL_NAME="${BRIDGE_APP_FULL_NAME}" \
-DBRIDGE_VENDOR="${BRIDGE_VENDOR}" \
-DBRIDGE_REVISION="${BRIDGE_REVISION}" \
-DBRIDGE_TAG="${BRIDGE_TAG}" \
-DBRIDGE_DSN_SENTRY="${BRIDGE_DSN_SENTRY}" \
-DBRIDGE_BRIDGE_TIME="${BRIDGE_BRIDGE_TIME}" \
-DBRIDGE_BUILD_ENV="${BRIDGE_BUILD_ENV}" \
@ -106,10 +108,10 @@ cmake \
-B "${BUILD_DIR}"
check_exit "CMake failed"
cmake --build "${BUILD_DIR}"
cmake --build "${BUILD_DIR}" -v
check_exit "build failed"
if [ "$1" == "install" ]; then
cmake --install "${BUILD_DIR}"
cmake --install "${BUILD_DIR}" -v
check_exit "install failed"
fi

View File

@ -196,7 +196,7 @@ bool isBridgeRunning() {
//****************************************************************************************************************************************************
void focusOtherInstance() {
try {
FocusGRPCClient client;
FocusGRPCClient client(app().log());
GRPCConfig sc;
QString const path = FocusGRPCClient::grpcFocusServerConfigPath(bridgepp::userConfigDir());
QFile file(path);
@ -209,12 +209,11 @@ void focusOtherInstance() {
throw Exception("Server did not provide gRPC Focus service configuration.");
}
QString error;
if (!client.connectToServer(5000, sc.port, &error)) {
throw Exception("Could not connect to bridge focus service for a raise call.", error);
}
if (!client.raise().ok()) {
if (!client.raise("focusOtherInstance").ok()) {
throw Exception(QString("The raise call to the bridge focus service failed."));
}
}
@ -286,7 +285,9 @@ int main(int argc, char *argv[]) {
initQtApplication();
CommandLineOptions const cliOptions = parseCommandLine(argc, argv);
Log &log = initLog();
log.setLevel(cliOptions.logLevel);
QLockFile lock(bridgepp::userCacheDir() + "/" + bridgeGUILock);
if (!checkSingleInstance(lock)) {
@ -294,8 +295,6 @@ int main(int argc, char *argv[]) {
return EXIT_FAILURE;
}
CommandLineOptions const cliOptions = parseCommandLine(argc, argv);
#ifdef Q_OS_MACOS
registerSecondInstanceHandler();
setDockIconVisibleState(!cliOptions.noWindow);
@ -304,23 +303,25 @@ int main(int argc, char *argv[]) {
// In attached mode, we do not intercept stderr and stdout of bridge, as we did not launch it ourselves, so we output the log to the console.
// When not in attached mode, log entries are forwarded to bridge, which output it on stdout/stderr. bridge-gui's process monitor intercept
// these outputs and output them on the command-line.
log.setLevel(cliOptions.logLevel);
QString bridgeexec;
log.info(QString("New Sentry reporter - id: %1.").arg(getProtectedHostname()));
QString const &sessionID = app().sessionID();
QString bridgeExe;
if (!cliOptions.attach) {
if (isBridgeRunning()) {
throw Exception("An orphan instance of bridge is already running. Please terminate it and relaunch the application.",
QString(), __FUNCTION__, tailOfLatestBridgeLog());
QString(), __FUNCTION__, tailOfLatestBridgeLog(sessionID));
}
// before launching bridge, we remove any trailing service config file, because we need to make sure we get a newly generated one.
FocusGRPCClient::removeServiceConfigFile(configDir);
GRPCClient::removeServiceConfigFile(configDir);
bridgeexec = launchBridge(cliOptions.bridgeArgs);
bridgeExe = launchBridge(cliOptions.bridgeArgs);
}
log.info(QString("Retrieving gRPC service configuration from '%1'").arg(QDir::toNativeSeparators(grpcServerConfigPath(configDir))));
app().backend().init(GRPCClient::waitAndRetrieveServiceConfig(configDir, cliOptions.attach ? 0 : grpcServiceConfigWaitDelayMs,
app().bridgeMonitor()));
app().backend().init(GRPCClient::waitAndRetrieveServiceConfig(sessionID, configDir,
cliOptions.attach ? 0 : grpcServiceConfigWaitDelayMs, app().bridgeMonitor()));
if (!cliOptions.attach) {
GRPCClient::removeServiceConfigFile(configDir);
}
@ -378,9 +379,9 @@ int main(int argc, char *argv[]) {
QStringList args = cliOptions.bridgeGuiArgs;
args.append(waitFlag);
args.append(mainexec);
if (!bridgeexec.isEmpty()) {
if (!bridgeExe.isEmpty()) {
args.append(waitFlag);
args.append(bridgeexec);
args.append(bridgeExe);
}
app().setLauncherArgs(cliOptions.launcher, args);
result = QGuiApplication::exec();

View File

@ -27,67 +27,55 @@ Item {
property var notifications
property var user
signal showSignIn()
signal showSignIn
signal showSetupGuide(var user, string address)
property int _leftMargin: 64
property int _rightMargin: 64
property int _contentWidth: 640
property int _topMargin: 32
property int _detailsTopMargin: 25
property int _bottomMargin: 12
property int _detailsMargin: 25
property int _spacing: 20
property int _lineWidth: 1
ScrollView {
id: scrollView
clip: true
property int _lineThickness: 1
property bool _connected: root.user ? root.user.state === EUserState.Connected : false
Rectangle {
anchors.fill: parent
Component.onCompleted: contentItem.boundsBehavior = Flickable.StopAtBounds // Disable the springy effect when scroll reaches top/bottom.
color: root.colorScheme.background_weak
Item {
// can't use parent here because parent is not ScrollView (Flickable inside contentItem inside ScrollView)
width: scrollView.availableWidth
height: scrollView.availableHeight
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
// do not set implicitWidth because implicit width of ColumnLayout will be equal to maximum implicit width of
// internal items. And if one of internal items would be a Text or Label - implicit width of those is always
// equal to non-wrapped text (i.e. one line only). That will lead to enabling horizontal scroll when not needed
implicitWidth: width
ScrollView {
id: scrollView
anchors.fill: parent
Component.onCompleted: contentItem.boundsBehavior = Flickable.StopAtBounds
ColumnLayout {
id: topLevelColumnLayout
anchors.fill: parent
spacing: 0
anchors.fill: parent
Rectangle {
id: topRectangle
id: topArea
color: root.colorScheme.background_norm
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
clip: true
Layout.fillWidth: true
implicitHeight: childrenRect.height
ColumnLayout {
spacing: root._spacing
id: topLayout
width: _contentWidth
anchors.horizontalCenter: parent.horizontalCenter
spacing: _spacing
anchors.fill: parent
anchors.leftMargin: root._leftMargin
anchors.rightMargin: root._rightMargin
anchors.topMargin: root._topMargin
anchors.bottomMargin: root._bottomMargin
RowLayout { // account delegate with action buttons
RowLayout {
// account delegate with action buttons
Layout.fillWidth: true
Layout.topMargin: _topMargin
AccountDelegate {
Layout.fillWidth: true
colorScheme: root.colorScheme
user: root.user
type: AccountDelegate.LargeView
enabled: root.user ? (root.user.state === EUserState.Connected) : false
enabled: _connected
}
Button {
@ -95,10 +83,11 @@ Item {
colorScheme: root.colorScheme
text: qsTr("Sign out")
secondary: true
visible: root.user ? (root.user.state === EUserState.Connected) : false
visible: _connected
onClicked: {
if (!root.user) return
root.user.logout()
if (!root.user)
return;
root.user.logout();
}
}
@ -109,8 +98,9 @@ Item {
secondary: true
visible: root.user ? (root.user.state === EUserState.SignedOut) : false
onClicked: {
if (!root.user) return
root.showSignIn()
if (!root.user)
return;
root.showSignIn();
}
}
@ -120,8 +110,9 @@ Item {
icon.source: "/qml/icons/ic-trash.svg"
secondary: true
onClicked: {
if (!root.user) return
root.notifications.askDeleteAccount(root.user)
if (!root.user)
return;
root.notifications.askDeleteAccount(root.user);
}
visible: root.user ? root.user.state !== EUserState.Locked : false
}
@ -129,7 +120,7 @@ Item {
Rectangle {
Layout.fillWidth: true
height: root._lineWidth
height: root._lineThickness
color: root.colorScheme.border_weak
}
@ -139,12 +130,12 @@ Item {
actionText: qsTr("Configure")
description: qsTr("Using the mailbox details below (re)configure your client.")
type: SettingsItem.Button
enabled: root.user ? root.user.state === EUserState.Connected : false
visible: root.user ? !root.user.splitMode || root.user.addresses.length==1 : false
visible: _connected && (!root.user.splitMode) || (root.user.addresses.length === 1)
showSeparator: splitMode.visible
onClicked: {
if (!root.user) return
root.showSetupGuide(root.user, user.addresses[0])
if (!root.user)
return;
root.showSetupGuide(root.user, user.addresses[0]);
}
Layout.fillWidth: true
@ -157,15 +148,14 @@ Item {
description: qsTr("Setup multiple email addresses individually.")
type: SettingsItem.Toggle
checked: root.user ? root.user.splitMode : false
visible: root.user ? root.user.addresses.length > 1 : false
enabled: root.user ? (root.user.state === EUserState.Connected) : false
visible: _connected && root.user.addresses.length > 1
showSeparator: addressSelector.visible
onClicked: {
if (!splitMode.checked){
root.notifications.askEnableSplitMode(user)
if (!splitMode.checked) {
root.notifications.askEnableSplitMode(user);
} else {
addressSelector.currentIndex = 0
root.user.toggleSplitMode(!splitMode.checked)
addressSelector.currentIndex = 0;
root.user.toggleSplitMode(!splitMode.checked);
}
}
@ -174,8 +164,8 @@ Item {
RowLayout {
Layout.fillWidth: true
enabled: root.user ? (root.user.state === EUserState.Connected) : false
visible: root.user ? root.user.splitMode : false
Layout.bottomMargin: _spacing
visible: _connected && root.user.splitMode
ComboBox {
id: addressSelector
@ -189,60 +179,68 @@ Item {
text: qsTr("Configure")
secondary: true
onClicked: {
if (!root.user) return
root.showSetupGuide(root.user, addressSelector.displayText)
if (!root.user)
return;
root.showSetupGuide(root.user, addressSelector.displayText);
}
}
}
Rectangle {
height: 0
} // just for some extra space before separator
}
}
Rectangle {
id: bottomArea
Layout.fillWidth: true
implicitHeight: bottomLayout.implicitHeight
color: root.colorScheme.background_weak
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
Layout.fillWidth: true
ColumnLayout {
id: configuration
anchors.fill: parent
anchors.leftMargin: root._leftMargin
anchors.rightMargin: root._rightMargin
anchors.topMargin: root._detailsTopMargin
anchors.bottomMargin: root._spacing
spacing: root._spacing
visible: root.user ? (root.user.state === EUserState.Connected) : false
property string currentAddress: addressSelector.displayText
id: bottomLayout
width: _contentWidth
anchors.horizontalCenter: parent.horizontalCenter
spacing: _spacing
visible: _connected
Label {
Layout.topMargin: _detailsMargin
colorScheme: root.colorScheme
text: qsTr("Mailbox details")
type: Label.Body_semibold
}
Configuration {
colorScheme: root.colorScheme
title: qsTr("IMAP")
hostname: Backend.hostname
port: Backend.imapPort.toString()
username: configuration.currentAddress
password: root.user ? root.user.password : ""
security : Backend.useSSLForIMAP ? "SSL" : "STARTTLS"
}
RowLayout {
id: configuration
spacing: _spacing
Layout.fillWidth: true
Layout.fillHeight: true
Configuration {
colorScheme: root.colorScheme
title: qsTr("SMTP")
hostname : Backend.hostname
port : Backend.smtpPort.toString()
username : configuration.currentAddress
password : root.user ? root.user.password : ""
security : Backend.useSSLForSMTP ? "SSL" : "STARTTLS"
property string currentAddress: addressSelector.displayText
Configuration {
Layout.fillWidth: true
colorScheme: root.colorScheme
title: qsTr("IMAP")
hostname: Backend.hostname
port: Backend.imapPort.toString()
username: configuration.currentAddress
password: root.user ? root.user.password : ""
security: Backend.useSSLForIMAP ? "SSL" : "STARTTLS"
}
Configuration {
Layout.fillWidth: true
colorScheme: root.colorScheme
title: qsTr("SMTP")
hostname: Backend.hostname
port: Backend.smtpPort.toString()
username: configuration.currentAddress
password: root.user ? root.user.password : ""
security: Backend.useSSLForSMTP ? "SSL" : "STARTTLS"
}
}
}
}

View File

@ -26,11 +26,8 @@ import Notifications
QtObject {
id: root
function isInInterval(num, lower_limit, upper_limit) {
return lower_limit <= num && num <= upper_limit
}
function bound(num, lower_limit, upper_limit) {
return Math.max(lower_limit, Math.min(upper_limit, num))
function bound(num, lowerLimit, upperLimit) {
return Math.max(lowerLimit, Math.min(upperLimit, num))
}
property var title: Backend.appname
@ -38,10 +35,30 @@ QtObject {
property Notifications _notifications: Notifications {
id: notifications
frontendMain: mainWindow
frontendStatus: statusWindow
frontendTray: trayIcon
}
property NotificationFilter _trayNotificationFilter: NotificationFilter {
id: trayNotificationFilter
source: root._notifications ? root._notifications.all : undefined
onTopmostChanged: {
if (topmost) {
switch (topmost.type) {
case Notification.NotificationType.Danger:
Backend.setErrorTrayIcon(topmost.brief, topmost.icon)
return
case Notification.NotificationType.Warning:
Backend.setWarnTrayIcon(topmost.brief, topmost.icon)
return
case Notification.NotificationType.Info:
Backend.setUpdateTrayIcon(topmost.brief, topmost.icon)
return
}
}
Backend.setNormalTrayIcon()
}
}
property MainWindow _mainWindow: MainWindow {
id: mainWindow
visible: false
@ -66,190 +83,6 @@ QtObject {
}
}
property StatusWindow _statusWindow: StatusWindow {
id: statusWindow
visible: false
title: root.title
notifications: root._notifications
onShowMainWindow: {
mainWindow.showAndRise()
}
onShowHelp: {
mainWindow.showHelp()
mainWindow.showAndRise()
}
onShowSettings: {
mainWindow.showSettings()
mainWindow.showAndRise()
}
onSelectUser: function(userID) {
mainWindow.selectUser(userID)
mainWindow.showAndRise()
}
onQuit: {
mainWindow.hide()
trayIcon.visible = false
Backend.quit()
}
property rect screenRect
property rect iconRect
// use binding from function with width and height as arguments so it will be recalculated every time width and height are changed
property point position: getPosition(width, height)
x: position.x
y: position.y
function getPosition(_width, _height) {
if (screenRect.width === 0 || screenRect.height === 0) {
return Qt.point(0, 0)
}
var _x = 0
var _y = 0
// fit above
_y = iconRect.top - height
if (isInInterval(_y, screenRect.top, screenRect.bottom - height)) {
// position preferably in the horizontal center but bound to the screen rect
_x = bound(iconRect.left + (iconRect.width - width)/2, screenRect.left, screenRect.right - width)
return Qt.point(_x, _y)
}
// fit below
_y = iconRect.bottom
if (isInInterval(_y, screenRect.top, screenRect.bottom - height)) {
// position preferably in the horizontal center but bound to the screen rect
_x = bound(iconRect.left + (iconRect.width - width)/2, screenRect.left, screenRect.right - width)
return Qt.point(_x, _y)
}
// fit to the left
_x = iconRect.left - width
if (isInInterval(_x, screenRect.left, screenRect.right - width)) {
// position preferably in the vertical center but bound to the screen rect
_y = bound(iconRect.top + (iconRect.height - height)/2, screenRect.top, screenRect.bottom - height)
return Qt.point(_x, _y)
}
// fit to the right
_x = iconRect.right
if (isInInterval(_x, screenRect.left, screenRect.right - width)) {
// position preferably in the vertical center but bound to the screen rect
_y = bound(iconRect.top + (iconRect.height - height)/2, screenRect.top, screenRect.bottom - height)
return Qt.point(_x, _y)
}
// Fallback: position status window right above icon and let window manager decide.
console.warn("Can't position status window: screenRect =", screenRect, "iconRect =", iconRect)
_x = bound(iconRect.left + (iconRect.width - width)/2, screenRect.left, screenRect.right - width)
_y = bound(iconRect.top + (iconRect.height - height)/2, screenRect.top, screenRect.bottom - height)
return Qt.point(_x, _y)
}
}
property SystemTrayIcon _trayIcon: SystemTrayIcon {
id: trayIcon
visible: true
icon.source: getTrayIconPath()
icon.mask: true // make sure that systems like macOS will use proper color
tooltip: `${root.title} v${Backend.version}`
onActivated: function(reason) {
function calcStatusWindowPosition() {
// On some platforms (X11 / Plasma) Qt does not provide icon position and geometry info.
// In this case we rely on cursor position
var iconRect = Qt.rect(geometry.x, geometry.y, geometry.width, geometry.height)
if (geometry.width == 0 && geometry.height == 0) {
var mousePos = Backend.getCursorPos()
iconRect.x = mousePos.x
iconRect.y = mousePos.y
iconRect.width = 0
iconRect.height = 0
}
// Find screen
var screen
for (var i in Qt.application.screens) {
var _screen = Qt.application.screens[i]
if (
isInInterval(iconRect.x, _screen.virtualX, _screen.virtualX + _screen.width) &&
isInInterval(iconRect.y, _screen.virtualY, _screen.virtualY + _screen.height)
) {
screen = _screen
break
}
}
if (!screen) {
// Fallback to primary screen
screen = Qt.application.screens[0]
}
// In case we used mouse to detect icon position - we want to make a fake icon rectangle from a point
if (iconRect.width == 0 && iconRect.height == 0) {
iconRect.x = bound(iconRect.x - 16, screen.virtualX, screen.virtualX + screen.width - 32)
iconRect.y = bound(iconRect.y - 16, screen.virtualY, screen.virtualY + screen.height - 32)
iconRect.width = 32
iconRect.height = 32
}
statusWindow.screenRect = Qt.rect(screen.virtualX, screen.virtualY, screen.width, screen.height)
statusWindow.iconRect = iconRect
}
function toggleWindow(win) {
if (win.visible) {
win.close()
} else {
win.showAndRise()
}
}
switch (reason) {
case SystemTrayIcon.Unknown:
break;
case SystemTrayIcon.Context:
case SystemTrayIcon.Trigger:
case SystemTrayIcon.DoubleClick:
case SystemTrayIcon.MiddleClick:
calcStatusWindowPosition()
toggleWindow(statusWindow)
break;
default:
break;
}
}
property NotificationFilter _systrayfilter: NotificationFilter {
source: root._notifications ? root._notifications.all : undefined
}
function getTrayIconPath() {
var color = Backend.goos == "darwin" ? "mono" : "color"
var level = "norm"
if (_systrayfilter.topmost) {
switch (_systrayfilter.topmost.type) {
case Notification.NotificationType.Danger:
level = "error"
break;
case Notification.NotificationType.Warning:
level = "warn"
break;
case Notification.NotificationType.Info:
level = "update"
break;
}
}
return `qrc:/qml/icons/systray-${color}-${level}.png`
}
}
Component.onCompleted: {
if (!Backend) {
@ -266,7 +99,7 @@ QtObject {
var c = Backend.users.count
var u = Backend.users.get(0)
// DEBUG
if (c != 0) {
if (c !== 0) {
console.log("users non zero", c)
console.log("first user", u )
}
@ -290,7 +123,7 @@ QtObject {
}
function setColorScheme() {
if (Backend.colorSchemeName == "light") ProtonStyle.currentStyle = ProtonStyle.lightStyle
if (Backend.colorSchemeName == "dark") ProtonStyle.currentStyle = ProtonStyle.darkStyle
if (Backend.colorSchemeName === "light") ProtonStyle.currentStyle = ProtonStyle.lightStyle
if (Backend.colorSchemeName === "dark") ProtonStyle.currentStyle = ProtonStyle.darkStyle
}
}

View File

@ -1,315 +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 QtQml
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Proton
ColumnLayout {
id: root
property var user
property var userIndex
spacing : 5
Layout.fillHeight: true
//Layout.fillWidth: true
property ColorScheme colorScheme
TextField {
colorScheme: root.colorScheme
Layout.fillWidth: true
text: user !== undefined ? user.username : ""
onEditingFinished: {
user.username = text
}
}
ColumnLayout {
Layout.fillWidth: true
Switch {
id: userLoginSwitch
colorScheme: root.colorScheme
text: "LoggedIn"
enabled: user !== undefined && user.username.length > 0
checked: user ? root.user.state == EUserState.Connected : false
onCheckedChanged: {
if (!user) {
return
}
if (checked) {
if (user === Backend.loginUser) {
var newUserObject = Backend.userComponent.createObject(Backend, {username: user.username, loggedIn: true, setupGuideSeen: user.setupGuideSeen})
Backend.users.append( { object: newUserObject } )
user.username = ""
user.resetLoginRequests()
return
}
user.state = EUserState.Connected
user.resetLoginRequests()
return
} else {
user.state = EUserState.SignedOut
user.resetLoginRequests()
}
}
}
Switch {
colorScheme: root.colorScheme
text: "Setup guide seen"
enabled: user !== undefined && user.username.length > 0
checked: user ? user.setupGuideSeen : false
onCheckedChanged: {
if (!user) {
return
}
user.setupGuideSeen = checked
}
}
}
RowLayout {
Layout.fillWidth: true
Label {
colorScheme: root.colorScheme
id: loginLabel
text: "Login:"
Layout.preferredWidth: Math.max(loginLabel.implicitWidth, faLabel.implicitWidth, passLabel.implicitWidth)
}
Button {
colorScheme: root.colorScheme
text: "name/pass error"
enabled: user !== undefined //&& user.isLoginRequested && !user.isLogin2FARequested && !user.isLogin2PasswordProvided
onClicked: {
Backend.loginUsernamePasswordError("")
user.resetLoginRequests()
}
}
Button {
colorScheme: root.colorScheme
text: "free user error"
enabled: user !== undefined //&& user.isLoginRequested
onClicked: {
Backend.loginFreeUserError()
user.resetLoginRequests()
}
}
Button {
colorScheme: root.colorScheme
text: "connection error"
enabled: user !== undefined //&& user.isLoginRequested
onClicked: {
Backend.loginConnectionError("")
user.resetLoginRequests()
}
}
}
RowLayout {
Layout.fillWidth: true
Label {
colorScheme: root.colorScheme
id: faLabel
text: "2FA:"
Layout.preferredWidth: Math.max(loginLabel.implicitWidth, faLabel.implicitWidth, passLabel.implicitWidth)
}
Button {
colorScheme: root.colorScheme
text: "request"
enabled: user !== undefined //&& user.isLoginRequested && !user.isLogin2FARequested && !user.isLogin2PasswordRequested
onClicked: {
Backend.login2FARequested(user.username)
user.isLogin2FARequested = true
}
}
Button {
colorScheme: root.colorScheme
text: "error"
enabled: user !== undefined //&& user.isLogin2FAProvided && !(user.isLogin2PasswordRequested && !user.isLogin2PasswordProvided)
onClicked: {
Backend.login2FAError("")
user.isLogin2FAProvided = false
}
}
Button {
colorScheme: root.colorScheme
text: "Abort"
enabled: user !== undefined //&& user.isLogin2FAProvided && !(user.isLogin2PasswordRequested && !user.isLogin2PasswordProvided)
onClicked: {
Backend.login2FAErrorAbort("")
user.resetLoginRequests()
}
}
}
RowLayout {
Layout.fillWidth: true
Label {
colorScheme: root.colorScheme
id: passLabel
text: "2 Password:"
Layout.preferredWidth: Math.max(loginLabel.implicitWidth, faLabel.implicitWidth, passLabel.implicitWidth)
}
Button {
colorScheme: root.colorScheme
text: "request"
enabled: user !== undefined //&& user.isLoginRequested && !user.isLogin2PasswordRequested && !(user.isLogin2FARequested && !user.isLogin2FAProvided)
onClicked: {
Backend.login2PasswordRequested("")
user.isLogin2PasswordRequested = true
}
}
Button {
colorScheme: root.colorScheme
text: "error"
enabled: user !== undefined //&& user.isLogin2PasswordProvided && !(user.isLogin2FARequested && !user.isLogin2FAProvided)
onClicked: {
Backend.login2PasswordError("")
user.isLogin2PasswordProvided = false
}
}
Button {
colorScheme: root.colorScheme
text: "Abort"
enabled: user !== undefined //&& user.isLogin2PasswordProvided && !(user.isLogin2FARequested && !user.isLogin2FAProvided)
onClicked: {
Backend.login2PasswordErrorAbort("")
user.resetLoginRequests()
}
}
}
RowLayout {
Button {
colorScheme: root.colorScheme
text: "Login Finished"
onClicked: {
Backend.loginFinished(0+loginFinishedIndex.text)
user.resetLoginRequests()
}
}
TextField {
id: loginFinishedIndex
colorScheme: root.colorScheme
label: "Index:"
text: root.userIndex
}
}
RowLayout {
Button {
colorScheme: root.colorScheme
text: "Already logged in"
onClicked: {
Backend.loginAlreadyLoggedIn(0+loginAlreadyLoggedInIndex.text)
user.resetLoginRequests()
}
}
TextField {
id: loginAlreadyLoggedInIndex
colorScheme: root.colorScheme
label: "Index:"
text: root.userIndex
}
}
RowLayout {
TextField {
colorScheme: root.colorScheme
label: "used:"
text: user && user.usedBytes ? user.usedBytes : 0
onEditingFinished: {
user.usedBytes = parseFloat(text)
}
implicitWidth: 200
}
TextField {
colorScheme: root.colorScheme
label: "total:"
text: user && user.totalBytes ? user.totalBytes : 0
onEditingFinished: {
user.totalBytes = parseFloat(text)
}
implicitWidth: 200
}
}
RowLayout {
Label {colorScheme: root.colorScheme; text: "Split mode"}
Toggle { colorScheme: root.colorScheme; checked: user ? user.splitMode : false; onClicked: {user.splitMode = !user.splitMode}}
Button { colorScheme: root.colorScheme; text: "Toggle Finished"; onClicked: {user.toggleSplitModeFinished()}}
}
TextArea { // TODO: this is causing binding loop on implicitWidth
colorScheme: root.colorScheme
text: user && user.addresses ? user.addresses.join("\n") : "user@protonmail.com"
Layout.fillWidth: true
onEditingFinished: {
user.addresses = text.split("\n")
}
}
Item {
Layout.fillHeight: true
}
}

View File

@ -1,101 +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 Proton
ColumnLayout {
id: root
property ColorScheme colorScheme
property alias currentIndex: usersListView.currentIndex
ListView {
id: usersListView
Layout.fillHeight: true
Layout.preferredWidth: 200
model: Backend.usersTest
highlightFollowsCurrentItem: true
delegate: Item {
implicitHeight: children[0].implicitHeight + anchors.topMargin + anchors.bottomMargin
implicitWidth: children[0].implicitWidth + anchors.leftMargin + anchors.rightMargin
width: usersListView.width
anchors.margins: 10
Label {
colorScheme: root.colorScheme
text: modelData.username
anchors.margins: 10
anchors.fill: parent
MouseArea {
anchors.fill: parent
onClicked: {
usersListView.currentIndex = index
}
}
}
}
highlight: Rectangle {
color: root.colorScheme.interaction_default_active
}
}
RowLayout {
Layout.fillWidth: true
Button {
colorScheme: root.colorScheme
text: "+"
onClicked: {
var newUserObject = Backend.userComponent.createObject(Backend)
newUserObject.username = Backend.loginUser.username.length > 0 ? Backend.loginUser.username : "test@protonmail.com"
newUserObject.state = EUserState.Connected
newUserObject.setupGuideSeen = true // Backend.loginUser.setupGuideSeen
Backend.loginUser.username = ""
Backend.loginUser.state = EUserState.SignedOut
Backend.loginUser.setupGuideSeen = false
Backend.users.append( { object: newUserObject } )
}
}
Button {
colorScheme: root.colorScheme
text: "-"
enabled: usersListView.currentIndex != 0
onClicked: {
// var userObject = Backend.users.get(usersListView.currentIndex - 1)
Backend.users.remove(usersListView.currentIndex - 1)
// userObject.deleteLater()
}
}
}
}

View File

@ -1,982 +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 QtQml
import QtQuick
import QtQuick.Window
import QtQuick.Layouts
import QtQuick.Controls
import QtQml.Models
import Qt.labs.platform
import Proton
import "./BridgeTest"
import BridgePreview
import Notifications
Window {
id: root
x: 10
y: 10
width: 800
height: 800
property ColorScheme colorScheme: ProtonStyle.darkStyle
flags : Qt.Window | Qt.Dialog
visible : true
title : "Bridge Test GUI"
// This is needed because on MacOS if first window shown is not transparent -
// all other windows of application will not have transparent background (black
// instead of transparency). In our case that mean that if BridgeTest will be
// shown before StatusWindow - StatusWindow will not have transparent corners.
color: "transparent"
function getCursorPos() {
return BridgePreview.getCursorPos()
}
function restart() {
root.quit()
console.log("Restarting....")
root.openBridge()
}
function openBridge() {
bridge = bridgeComponent.createObject()
var showSetupGuide = false
if (showSetupGuide) {
var newUserObject = root.userComponent.createObject(root)
newUserObject.username = "LerooooyJenkins@protonmail.com"
newUserObject.state = EUserState.Connected
newUserObject.setupGuideSeen = false
root.users.append( { object: newUserObject } )
}
}
function quit() {
if (bridge !== undefined && bridge !== null) {
bridge.destroy()
}
}
function guiReady() {
console.log("Gui Ready")
}
function _log(msg, color) {
logTextArea.text += "<p style='color: " + color + ";'>" + msg + "</p>"
logTextArea.text += "\n"
}
function log(msg) {
console.log(msg)
_log(msg, root.colorScheme.signal_info)
}
function error(msg) {
console.error(msg)
_log(msg, root.colorScheme.signal_danger)
}
// No user object should be put in this list until a successful login
property var users: UserModel {
id: _users
onRowsInserted: {
for (var i = first; i <= last; i++) {
_usersTest.insert(i + 1, { object: get(i) } )
}
}
onRowsRemoved: {
_usersTest.remove(first + 1, first - last + 1)
}
onRowsMoved: {
_usersTest.move(start + 1, row + 1, end - start + 1)
}
onDataChanged: {
for (var i = topLeft.row; i <= bottomRight.row; i++) {
_usersTest.set(i + 1, { object: get(i) } )
}
}
}
// this list is used on test gui: it contains same users list as users above + fake user to represent login request of new user on pos 0
property var usersTest: UserModel {
id: _usersTest
}
property var userComponent: Component {
id: _userComponent
QtObject {
property string username: ""
property bool loggedIn: false
property bool splitMode: false
property bool setupGuideSeen: true
property var usedBytes: 5350*1024*1024
property var totalBytes: 20*1024*1024*1024
property string avatarText: "jd"
property string password: "SMj975NnEYYsqu55GGmlpv"
property var addresses: [
"jaanedoe@protonmail.com",
"jane@pm.me",
"jdoe@pm.me"
]
signal loginUsernamePasswordError()
signal loginFreeUserError()
signal loginConnectionError()
signal login2FARequested()
signal login2FAError()
signal login2FAErrorAbort()
signal login2PasswordRequested()
signal login2PasswordError()
signal login2PasswordErrorAbort()
// Test purpose only:
property bool isFakeUser: this === root.loginUser
function userSignal(msg) {
if (isFakeUser) {
return
}
root.log("<- User (" + username + "): " + msg)
}
function toggleSplitMode(makeActive) {
userSignal("toggle split mode "+makeActive)
}
signal toggleSplitModeFinished()
function configureAppleMail(address){
userSignal("configure apple mail "+address)
}
function logout(){
userSignal("logout")
loggedIn = false
}
function remove(){
console.log("remove this", users.count)
for (var i=0; i<users.count; i++) {
if (users.get(i) === this) {
users.remove(i,1)
return
}
}
}
onLoginUsernamePasswordError: {
userSignal("loginUsernamePasswordError")
}
onLoginFreeUserError: {
userSignal("loginFreeUserError")
}
onLoginConnectionError: {
userSignal("loginConnectionError")
}
onLogin2FARequested: {
userSignal("login2FARequested")
}
onLogin2FAError: {
userSignal("login2FAError")
}
onLogin2FAErrorAbort: {
userSignal("login2FAErrorAbort")
}
onLogin2PasswordRequested: {
userSignal("login2PasswordRequested")
}
onLogin2PasswordError: {
userSignal("login2PasswordError")
}
onLogin2PasswordErrorAbort: {
userSignal("login2PasswordErrorAbort")
}
function resetLoginRequests() {
isLoginRequested = false
isLogin2FARequested = false
isLogin2FAProvided = false
isLogin2PasswordRequested = false
isLogin2PasswordProvided = false
}
property bool isLoginRequested: false
property bool isLogin2FARequested: false
property bool isLogin2FAProvided: false
property bool isLogin2PasswordRequested: false
property bool isLogin2PasswordProvided: false
}
}
// this it fake user used only for representing first login request
property var loginUser
Component.onCompleted: {
var newLoginUser = _userComponent.createObject()
root.loginUser = newLoginUser
root.loginUser.setupGuideSeen = false
_usersTest.append({object: newLoginUser})
newLoginUser.loginUsernamePasswordError.connect(root.loginUsernamePasswordError)
newLoginUser.loginFreeUserError.connect(root.loginFreeUserError)
newLoginUser.loginConnectionError.connect(root.loginConnectionError)
newLoginUser.login2FARequested.connect(root.login2FARequested)
newLoginUser.login2FAError.connect(root.login2FAError)
newLoginUser.login2FAErrorAbort.connect(root.login2FAErrorAbort)
newLoginUser.login2PasswordRequested.connect(root.login2PasswordRequested)
newLoginUser.login2PasswordError.connect(root.login2PasswordError)
newLoginUser.login2PasswordErrorAbort.connect(root.login2PasswordErrorAbort)
// add one user on start
var hasUserOnStart = false
if (hasUserOnStart) {
var newUserObject = root.userComponent.createObject(root)
newUserObject.username = "LerooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooyJenkins@protonmail.com"
newUserObject.loggedIn = EUserState.Connected
newUserObject.setupGuideSeen = true
root.users.append( { object: newUserObject } )
}
}
TabBar {
id: tabBar
anchors.left: parent.left
anchors.right: parent.right
TabButton {
text: "Global settings"
}
TabButton {
text: "User control"
}
TabButton {
text: "Notifications"
}
TabButton {
text: "Log"
}
TabButton {
text: "Settings signals"
}
}
Rectangle {
color: root.colorScheme.background_norm
anchors.top: tabBar.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
implicitHeight: children[0].contentHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
implicitWidth: children[0].contentWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
StackLayout {
anchors.fill: parent
currentIndex: tabBar.currentIndex
anchors.margins: 10
RowLayout {
id: globalTab
spacing : 5
ColumnLayout {
spacing : 5
Label {
colorScheme: root.colorScheme
text: "Global settings"
}
ButtonGroup {
id: styleRadioGroup
}
RadioButton {
colorScheme: root.colorScheme
Layout.fillWidth: true
text: "Light UI"
checked: ProtonStyle.currentStyle === ProtonStyle.lightStyle
ButtonGroup.group: styleRadioGroup
onCheckedChanged: {
if (checked && ProtonStyle.currentStyle !== ProtonStyle.lightStyle) {
root.colorSchemeName = "light"
}
}
}
RadioButton {
colorScheme: root.colorScheme
Layout.fillWidth: true
text: "Dark UI"
checked: ProtonStyle.currentStyle === ProtonStyle.darkStyle
ButtonGroup.group: styleRadioGroup
onCheckedChanged: {
if (checked && ProtonStyle.currentStyle !== ProtonStyle.darkStyle) {
root.colorSchemeName = "dark"
}
}
}
CheckBox {
id: showOnStartupCheckbox
colorScheme: root.colorScheme
text: "Show on startup"
checked: root.showOnStartup
onCheckedChanged: {
root.showOnStartup = checked
}
}
CheckBox {
id: showSplashScreen
colorScheme: root.colorScheme
text: "Show splash screen"
checked: root.showSplashScreen
onCheckedChanged: {
root.showSplashScreen = checked
}
}
Button {
colorScheme: root.colorScheme
//Layout.fillWidth: true
text: "Open Bridge"
enabled: bridge === undefined || bridge === null
onClicked: root.openBridge()
}
Button {
colorScheme: root.colorScheme
//Layout.fillWidth: true
text: "Close Bridge"
enabled: bridge !== undefined && bridge !== null
onClicked: {
bridge.destroy()
}
}
Item {
Layout.fillHeight: true
}
}
ColumnLayout {
spacing : 5
Label {
colorScheme: root.colorScheme
text: "Notifications"
}
Button {
colorScheme: root.colorScheme
text: "Notify: danger"
enabled: bridge !== undefined && bridge !== null
onClicked: {
bridge.mainWindow.notifyOnlyPaidUsers()
}
}
Button {
colorScheme: root.colorScheme
text: "Notify: warning"
enabled: bridge !== undefined && bridge !== null
onClicked: {
bridge.mainWindow.notifyUpdateManually()
}
}
Button {
colorScheme: root.colorScheme
text: "Notify: success"
enabled: bridge !== undefined && bridge !== null
onClicked: {
bridge.mainWindow.notifyUserAdded()
}
}
Item {
Layout.fillHeight: true
}
}
}
RowLayout {
id: usersTab
UserList {
id: usersListView
Layout.fillHeight: true
colorScheme: root.colorScheme
backend: root
}
UserControl {
colorScheme: root.colorScheme
backend: root
user: ((root.usersTest.count > usersListView.currentIndex) && usersListView.currentIndex != -1) ? root.usersTest.get(usersListView.currentIndex) : undefined
userIndex: usersListView.currentIndex - 1 // -1 because 0 index is fake user
}
}
RowLayout {
id: notificationsTab
spacing: 5
ColumnLayout {
spacing: 5
Switch {
text: "Internet connection"
colorScheme: root.colorScheme
checked: true
onCheckedChanged: {
checked ? root.internetOn() : root.internetOff()
}
}
Button {
text: "Update manual ready"
colorScheme: root.colorScheme
onClicked: {
root.updateManualReady("3.14.1592")
}
}
Button {
text: "Update manual done"
colorScheme: root.colorScheme
onClicked: {
root.updateManualRestartNeeded()
}
}
Button {
text: "Update manual error"
colorScheme: root.colorScheme
onClicked: {
root.updateManualError()
}
}
Button {
text: "Update force"
colorScheme: root.colorScheme
onClicked: {
root.updateForce("3.14.1592")
}
}
Button {
text: "Update force error"
colorScheme: root.colorScheme
onClicked: {
root.updateForceError()
}
}
Button {
text: "Update silent done"
colorScheme: root.colorScheme
onClicked: {
root.updateSilentRestartNeeded()
}
}
Button {
text: "Update silent error"
colorScheme: root.colorScheme
onClicked: {
root.updateSilentError()
}
}
Button {
text: "Update is latest version"
colorScheme: root.colorScheme
onClicked: {
root.updateIsLatestVersion()
}
}
Button {
text: "Bug report send OK"
colorScheme: root.colorScheme
onClicked: {
root.reportBugFinished()
root.bugReportSendSuccess()
}
}
}
ColumnLayout {
spacing: 5
Button {
text: "Bug report send error"
colorScheme: root.colorScheme
onClicked: {
root.reportBugFinished()
root.bugReportSendError()
}
}
Button {
text: "Cache anavailable"
colorScheme: root.colorScheme
onClicked: {
root.cacheUnavailable()
}
}
Button {
text: "Cache can't move"
colorScheme: root.colorScheme
onClicked: {
root.cacheCantMove()
}
}
Button {
text: "Cache location change success"
onClicked: {
root.cacheLocationChangeSuccess()
}
colorScheme: root.colorScheme
}
Button {
text: "Disk full"
colorScheme: root.colorScheme
onClicked: {
root.diskFull()
}
}
Button {
text: "No keychain"
colorScheme: root.colorScheme
onClicked: {
root.notifyHasNoKeychain()
}
}
Button {
text: "Rebuild keychain"
colorScheme: root.colorScheme
onClicked: {
root.notifyRebuildKeychain()
}
}
Button {
text: "Address changed"
colorScheme: root.colorScheme
onClicked: {
root.addressChanged("p@v.el")
}
}
Button {
text: "Address changed + Logout"
colorScheme: root.colorScheme
onClicked: {
root.addressChangedLogout("p@v.el")
}
}
}
}
TextArea {
id: logTextArea
colorScheme: root.colorScheme
Layout.fillHeight: true
Layout.fillWidth: true
Layout.preferredWidth: 400
Layout.preferredHeight: 200
textFormat: TextEdit.RichText
//readOnly: true
}
ScrollView {
id: settingsTab
ColumnLayout {
RowLayout {
Label {colorScheme : root.colorScheme ; text : "GOOS : "}
Button {colorScheme : root.colorScheme ; text : "Linux" ; onClicked : root.goos = "linux" ; enabled: root.goos != "linux"}
Button {colorScheme : root.colorScheme ; text : "Windows" ; onClicked : root.goos = "windows" ; enabled: root.goos != "windows"}
Button {colorScheme : root.colorScheme ; text : "macOS" ; onClicked : root.goos = "darwin" ; enabled: root.goos != "darwin"}
}
RowLayout {
Label {colorScheme: root.colorScheme; text: "Automatic updates:"}
Toggle {colorScheme: root.colorScheme; checked: root.isAutomaticUpdateOn; onClicked: root.isAutomaticUpdateOn = !root.isAutomaticUpdateOn}
}
RowLayout {
Label {colorScheme: root.colorScheme; text: "Autostart:"}
Toggle {colorScheme: root.colorScheme; checked: root.isAutostartOn; onClicked: root.isAutostartOn = !root.isAutostartOn}
Button {colorScheme: root.colorScheme; text: "Toggle finished"; onClicked: root.toggleAutostartFinished()}
}
RowLayout {
Label {colorScheme: root.colorScheme; text: "Beta:"}
Toggle {colorScheme: root.colorScheme; checked: root.isBetaEnabled; onClicked: root.isBetaEnabled = !root.isBetaEnabled}
}
RowLayout {
Label {colorScheme: root.colorScheme; text: "DoH:"}
Toggle {colorScheme: root.colorScheme; checked: root.isDoHEnabled; onClicked: root.isDoHEnabled = !root.isDoHEnabled}
}
RowLayout {
Label {colorScheme: root.colorScheme; text: "All Mail disabled:"}
Toggle {colorScheme: root.colorScheme; checked: root.isAllMailVisible; onClicked: root.isAllMailVisible = !root.isAllMailVisible}
}
RowLayout {
Label {colorScheme: root.colorScheme; text: "Ports:"}
TextField {
colorScheme:root.colorScheme
label: "IMAP"
text: root.portIMAP
onEditingFinished: root.portIMAP = this.text*1
validator: IntValidator {bottom: 1; top: 65536}
}
TextField {
colorScheme:root.colorScheme
label: "SMTP"
text: root.portSMTP
onEditingFinished: root.portSMTP = this.text*1
validator: IntValidator {bottom: 1; top: 65536}
}
Button {colorScheme: root.colorScheme; text: "Change finished"; onClicked: root.changePortFinished()}
}
RowLayout {
Label {colorScheme: root.colorScheme; text: "SMTP using SSL:"}
Toggle {colorScheme: root.colorScheme; checked: root.useSSLForSMTP; onClicked: root.useSSLForSMTP = !root.useSSLForSMTP}
}
RowLayout {
Label {colorScheme: root.colorScheme; text: "Local cache:"}
Toggle {colorScheme: root.colorScheme; checked: root.isDiskCacheEnabled; onClicked: root.isDiskCacheEnabled = !root.isDiskCacheEnabled}
TextField {
colorScheme:root.colorScheme
label: "Path"
text: root.diskCachePath.toString().replace("file://", "")
implicitWidth: 160
onEditingFinished: {
root.diskCachePath = Qt.resolvedUrl("file://"+text)
}
}
Button {colorScheme: root.colorScheme; text: "Change finished"; onClicked: root.changeLocalCacheFinished()}
}
RowLayout {
Label {colorScheme: root.colorScheme; text: "Reset:"}
Button {colorScheme: root.colorScheme; text: "Finished"; onClicked: root.resetFinished()}
}
RowLayout {
Label {colorScheme: root.colorScheme; text: "Check update:"}
Button {colorScheme: root.colorScheme; text: "Finished"; onClicked: root.checkUpdatesFinished()}
}
}
}
}
}
property Bridge bridge
property string goos: "darwin"
property bool showOnStartup: true // this actually needs to be false, but since we use Bridge_test for testing purpose - lets default this to true just for convenience
property bool dockIconVisible: false
// this signals are used only when trying to login with new user (i.e. not in users model)
signal loginUsernamePasswordError(string errorMsg)
signal loginFreeUserError()
signal loginConnectionError(string errorMsg)
signal login2FARequested(string username)
signal login2FAError(string errorMsg)
signal login2FAErrorAbort(string errorMsg)
signal login2PasswordRequested()
signal login2PasswordError(string errorMsg)
signal login2PasswordErrorAbort(string errorMsg)
signal loginFinished(int index)
signal loginAlreadyLoggedIn(int index)
signal internetOff()
signal internetOn()
signal updateManualReady(var version)
signal updateManualRestartNeeded()
signal updateManualError()
signal updateForce(var version)
signal updateForceError()
signal updateSilentRestartNeeded()
signal updateSilentError()
signal updateIsLatestVersion()
function checkUpdates(){
console.log("check updates")
}
signal checkUpdatesFinished()
function installUpdate() {
console.log("manuall install update triggered")
}
property bool isDiskCacheEnabled: true
// Qt.resolvedUrl("file:///C:/Users/user/AppData/Roaming/protonmail/bridge-v3/cache/c11/messages")
property url diskCachePath: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
signal cacheUnavailable()
signal cacheCantMove()
signal cacheLocationChangeSuccess()
signal diskFull()
function changeLocalCache(enableDiskCache, diskCachePath) {
console.debug("-> disk cache", enableDiskCache, diskCachePath)
}
signal changeLocalCacheFinished()
// Settings
property bool isAutomaticUpdateOn : true
function toggleAutomaticUpdate(makeItActive) {
console.debug("-> silent updates", makeItActive, root.isAutomaticUpdateOn)
var callback = function () {
root.isAutomaticUpdateOn = makeItActive;
console.debug("-> CHANGED silent updates", makeItActive, root.isAutomaticUpdateOn)
}
atimer.onTriggered.connect(callback)
atimer.restart()
}
Timer {
id: atimer
interval: 2000
running: false
repeat: false
}
property bool isAutostartOn : true // Example of settings with loading state
function toggleAutostart(makeItActive) {
console.debug("-> autostart", makeItActive, root.isAutostartOn)
}
signal toggleAutostartFinished()
property bool isBetaEnabled : false
function toggleBeta(makeItActive){
console.debug("-> beta", makeItActive, root.isBetaEnabled)
root.isBetaEnabled = makeItActive
}
property bool isDoHEnabled : true
function toggleDoH(makeItActive){
console.debug("-> DoH", makeItActive, root.isDoHEnabled)
root.isDoHEnabled = makeItActive
}
property bool isAllMailVisible : true
function changeIsAllMailVisible(isVisible){
console.debug("-> All Mail Visible", isVisible, root.isAllMailVisible)
root.isAllMailVisible = isVisible
}
property bool useSSLForSMTP: false
function toggleUseSSLForSMTP(makeItActive){
console.debug("-> SMTP SSL", makeItActive, root.useSSLForSMTP)
}
signal toggleUseSSLFinished()
property string hostname: "127.0.0.1"
property int portIMAP: 1143
property int portSMTP: 1025
function changePorts(imapPort, smtpPort){
console.debug("-> ports", imapPort, smtpPort)
}
function isPortFree(port){
if (port == portIMAP) return false
if (port == portSMTP) return false
if (port == 12345) return false
return true
}
signal changePortFinished()
signal imapPortStartupError()
signal smtpPortStartupError()
function triggerReset() {
console.debug("-> trigger reset")
}
signal resetFinished()
property string version: "2.0.X-BridePreview"
property url logsPath: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
property url licensePath: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
property url releaseNotesLink: Qt.resolvedUrl("https://proton.me/download/bridge/early_releases.html")
property url dependencyLicensesLink: Qt.resolvedUrl("https://github.com/ProtonMail/proton-bridge/v3/blob/master/COPYING_NOTES.md#dependencies")
property url landingPageLink: Qt.resolvedUrl("https://proton.me/mail/bridge#download")
property string colorSchemeName: "light"
function changeColorScheme(newScheme){
root.colorSchemeName = newScheme
}
property string currentEmailClient: "" // "Apple Mail 14.0"
function updateCurrentMailClient(){
currentEmailClient = "Apple Mail 14.0"
}
function reportBug(description,address,emailClient,includeLogs){
console.log("report bug")
console.log(" description",description)
console.log(" address",address)
console.log(" emailClient",emailClient)
console.log(" includeLogs",includeLogs)
}
signal reportBugFinished()
signal bugReportSendSuccess()
signal bugReportSendError()
property var availableKeychain: ["gnome-keyring", "pass", "macos-keychain", "windows-credentials"]
property string currentKeychain: availableKeychain[0]
function changeKeychain(wantedKeychain){
console.log("Changing keychain from", root.currentKeychain, "to", wantedKeychain)
root.currentKeychain = wantedKeychain
root.changeKeychainFinished()
}
signal changeKeychainFinished()
signal notifyHasNoKeychain()
signal notifyRebuildKeychain()
signal noActiveKeyForRecipient(string email)
signal showMainWindow()
signal addressChanged(string address)
signal addressChangedLogout(string address)
signal userDisconnected(string username)
signal apiCertIssue()
property bool showSplashScreen: false
function login(username, password) {
root.log("-> login(" + username + ", " + password + ")")
loginUser.username = username
loginUser.isLoginRequested = true
}
function login2FA(username, code) {
root.log("-> login2FA(" + username + ", " + code + ")")
loginUser.isLogin2FAProvided = true
}
function login2Password(username, password) {
root.log("-> login2FA(" + username + ", " + password + ")")
loginUser.isLogin2PasswordProvided = true
}
function loginAbort(username) {
root.log("-> loginAbort(" + username + ")")
loginUser.resetLoginRequests()
}
onLoginUsernamePasswordError: {
console.debug("<- loginUsernamePasswordError")
}
onLoginFreeUserError: {
console.debug("<- loginFreeUserError")
}
onLoginConnectionError: {
console.debug("<- loginConnectionError")
}
onLogin2FARequested: {
console.debug("<- login2FARequested", username)
}
onLogin2FAError: {
console.debug("<- login2FAError")
}
onLogin2FAErrorAbort: {
console.debug("<- login2FAErrorAbort")
}
onLogin2PasswordRequested: {
console.debug("<- login2PasswordRequested")
}
onLogin2PasswordError: {
console.debug("<- login2PasswordError")
}
onLogin2PasswordErrorAbort: {
console.debug("<- login2PasswordErrorAbort")
}
onLoginFinished: {
console.debug("<- loginFinished", index)
}
onLoginAlreadyLoggedIn: {
console.debug("<- loginAlreadyLoggedIn", index)
}
onInternetOff: {
console.debug("<- internetOff")
}
onInternetOn: {
console.debug("<- internetOn")
}
Component {
id: bridgeComponent
Bridge {
backend: root
}
}
onClosing: {
Qt.quit()
}
}

View File

@ -61,8 +61,6 @@ Rectangle {
type: Label.Body_semibold
}
Item{}
ConfigurationItem{ colorScheme: root.colorScheme; label: qsTr("Hostname") ; value: root.hostname }
ConfigurationItem{ colorScheme: root.colorScheme; label: qsTr("Port") ; value: root.port }
ConfigurationItem{ colorScheme: root.colorScheme; label: qsTr("Username") ; value: root.username }

View File

@ -90,9 +90,9 @@ SettingsView {
RowLayout {
ColorImage {
Layout.alignment: Qt.AlignTop
Layout.alignment: Qt.AlignCenter
source: root._isAdvancedShown ? "/qml/icons/ic-chevron-up.svg" : "/qml/icons/ic-chevron-down.svg"
source: root._isAdvancedShown ? "/qml/icons/ic-chevron-down.svg" : "/qml/icons/ic-chevron-right.svg"
color: root.colorScheme.interaction_norm
height: root.colorScheme.body_font_size
sourceSize.height: root.colorScheme.body_font_size
@ -169,6 +169,19 @@ SettingsView {
Layout.fillWidth: true
}
SettingsItem {
id: telemetry
Layout.fillWidth: true
checked: !Backend.isTelemetryDisabled
colorScheme: root.colorScheme
description: qsTr("Help us improve Proton services by sending anonymous usage statistics.")
text: qsTr("Collect usage diagnostics")
type: SettingsItem.Toggle
visible: root._isAdvancedShown
onClicked: Backend.toggleIsTelemetryDisabled(telemetry.checked)
}
SettingsItem {
id: ports
visible: root._isAdvancedShown

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