Compare commits

..

305 Commits

Author SHA1 Message Date
2f7f898cee fix(GODT-2582): fix hash for changed boundary. 2023-04-26 14:00:45 +02:00
8d53ee855b chore: Bridge Quebec 3.1.2 changelog 2023-04-25 13:06:58 +02:00
ec0db47f32 fix(GODT-2582): Dedup recovered messages folder
same code as:
    * https://github.com/ProtonMail/gluon/pull/338
    * https://github.com/ProtonMail/gluon/pull/339
2023-04-25 11:36:21 +02:00
6fcea2ad83 chore: Bridge Quebec 3.1.1 changelog 2023-04-11 14:31:39 +02:00
098f294cac fix(GODT-2573): Crash on null update
Ensure that if we don't produce an update we don't construct an update
array with nil values.
2023-04-11 10:36:26 +02:00
a6f5cc870c fix(GODT-2500): pass handler pointer down the road. 2023-04-06 16:39:29 +02:00
7330406752 fix(GODT-2500): Recover in deferred function. 2023-04-05 09:25:24 +02:00
fd100eecc2 chore: update changelog for Quebec 3.1.0. 2023-04-03 14:16:30 +02:00
a11559fe58 chore: merge branch release/perth_narrows into release/Quebec 2023-04-03 14:08:05 +02:00
7d8e71c9ea fix(GODT-2505): show notification only for cases when user needs to do actions. 2023-04-03 11:21:46 +02:00
8b80938e49 fix(GODT-2516): log error when the vault key cannot be created/loaded from the keychain.
Backported from release/perth-narrows.
2023-04-03 08:44:26 +00:00
9bbeabcf50 feat(GODT-2523): use software QML rendering backend by default on Windows.
(cherry picked from commit 934749b278e95a9d69818ddf6b45ee7bb896af03)
2023-04-03 09:55:19 +02:00
de5fd07a22 feat(GODT-2500): Reorganise async methods. 2023-04-03 07:07:22 +02:00
ec92c918cd feat(GODT-2500): Add panic handlers everywhere. 2023-04-03 06:38:31 +02:00
9f59e61b14 feat(GODT-2511): add bridge-gui switches to permanently select the QML rendering backend.
(cherry picked from commit 03c3e95b34)
2023-03-30 20:16:38 +02:00
9756a7b51f fix(GODT-2526): Fix high memory usage with fetch/search
https://github.com/ProtonMail/gluon/pull/333
2023-03-29 14:49:36 +02:00
579e996d3a fix(GODT-2514): Apply Retry-After to 503
https://github.com/ProtonMail/go-proton-api/pull/67
2023-03-27 16:07:54 +02:00
7ba523393c 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-27 13:00:56 +00:00
0a0bcd7b90 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-27 13:49:27 +02:00
ee87e60239 fix(GODT-2504): Fix missing attachments in imported message
https://github.com/ProtonMail/go-proton-api/pull/64
2023-03-23 13:14:13 +01:00
babb4412ae chore: Bridge Perth Narrows 3.0.21 2023-03-23 10:10:07 +01:00
2166224e91 fix(GODT-2513): Scanner Crash in Gluon
Gluon MR: https://github.com/ProtonMail/gluon/pull/330
2023-03-22 13:19:15 +01:00
60df01eece fix(GODT-2513): Crash in scanner
Gluon MR: https://github.com/ProtonMail/gluon/pull/330
2023-03-22 13:12:34 +01:00
f1989193c0 feat(GODT-2509): Migrate TLS cert from v1/v2 location during upgrade to v3 2023-03-22 11:00:45 +01:00
4e7acd9091 feat(GODT-2509): Migrate TLS cert from v1/v2 location during upgrade to v3 2023-03-22 10:26:22 +01:00
3ca5d0af71 fix(GODT-2516): log error when the vault key cannot be created/loaded from the keychain. 2023-03-21 17:25:35 +01:00
9425e091d8 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-21 15:43:41 +01:00
bb770f3871 fix(GODT-2512): Catch unhandled API errors
Bump GPA https://github.com/ProtonMail/go-proton-api/pull/63
2023-03-21 13:46:55 +00:00
b1ad0ab6dc 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 14:37:20 +01:00
b63b56960e fix(GODT-2512): Catch unhandled API errors
Bump GPA https://github.com/ProtonMail/go-proton-api/pull/63
2023-03-21 11:56:32 +01:00
2d6e0c66a5 fix(GODT-2507): Memory consumption bug
Fix was made in Gluon: https://github.com/ProtonMail/gluon/pull/329
2023-03-21 08:45:01 +01:00
ed05823c78 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 13:19:05 +01:00
ec351330f1 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-20 10:25:41 +01:00
dd7c81ca4b fix(GODT-2497): Do not report EOF and network errors 2023-03-16 14:40:51 +01:00
f71a89c265 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 13:30:25 +01:00
31de358bfd feat(GODT-2487): add windows test job and worker. 2023-03-15 06:19:39 +00:00
212eac0925 chore: Bridge Quebec 3.1.0 - update changelog 2023-03-15 07:18:46 +01:00
e36c2b3d6e chore: Update GPA to include detailed error messages
https://github.com/ProtonMail/go-proton-api/pull/62
2023-03-14 15:47:48 +01:00
8b33d56b59 fix(GODT-2479): Ensure messages always have a text body part
Proton Backend requires that all messages have at least one text/plain
or text/html body part, even if it is empty.
2023-03-14 14:29:42 +01:00
48274ee178 feat(GODT-2482): more attachment to relevant exceptions. 2023-03-14 12:43:35 +00:00
4273405393 chore: Bridge Quebec 3.1.0 - update changelog 2023-03-13 16:51:33 +01:00
3a85de2f3c chore: merge branch release/perth_narrows into devel 2023-03-13 15:36:37 +01:00
a3aafabde3 feat(GODT-2455): upper limit for number of merged events. 2023-03-13 12:45:36 +00:00
30c1c14505 fix(GODT-2480): Do not override X-Original-Date with invalid Date 2023-03-13 12:37:27 +01:00
8164c41728 chore: merge release/perth_narrows into devel 2023-03-13 11:44:57 +01:00
1820af5021 chore: merge release/perth_narrows into devel 2023-03-13 11:40:54 +01:00
b57ca1506d fix(GODT-2473): Fix handling of complex mime types
When rebuilding attachments, ensure that more complicated mime types are
properly re-constructed.

If we fail to parse the mime type, set the value as is.
2023-03-13 10:10:36 +01:00
7eaf1655b2 chore: Bump Gluon for fixes
- GODT-2442: Handle DB deletes on windows
- GODT-2419: Improve Error Messages
- GODT-2430: Collect more info to diagnose issue
2023-03-10 16:35:33 +01:00
f627151d04 fix(GODT-2469): Fix sentry revision hash for cmake on windows. 2023-03-09 14:52:34 +01:00
7c232b1331 fix(GODT-2469): Fix sentry revision hash for cmake on windows. 2023-03-09 14:50:34 +01:00
7be46a4740 fix(GODT-2467): elide long email adresses in 'bad event' QML notification dialog. 2023-03-09 12:16:26 +01:00
b57c7abe92 fix(GODT-2442): Ensure DB gets moved during RemoveUser 2023-03-09 11:54:25 +01:00
c86c428718 chore: Bridge Perth Narrows 3.0.20 2023-03-09 07:26:47 +01:00
ed6e17a0ab fix(GODT-2442): bump GPA: refresh All cleans event history. 2023-03-08 17:53:59 +01:00
c3454360fc fix(GODT-2442): Remove unnecessary call to go-sync
This is not required in this call since address mode changes are always
proceeded by the removal of the IMAP user and then the creation of a new
IMAP user which will trigger the sync again.
2023-03-08 17:39:08 +01:00
182dab18a6 fix(GODT-2442): Handle event poll not starting after resync
It is possible, on slower machines, that the new event poll task is not
yet registered and attempts to cancel have nothing to cancel.

In this case, we need the refresh event to cancel the task, at that
point it is guaranteed that the task exists.
2023-03-08 17:39:04 +01:00
13c8a98389 fix(GODT-2442): move gluon DB before removal. 2023-03-08 17:21:03 +01:00
05a2c9d254 fix(GODT-2442): cli error 2023-03-08 15:32:25 +01:00
d926dd3806 chore: refactor: error cause type. 2023-03-08 11:57:19 +01:00
7cc2f3361d feat(GODT-2444): added queue system for UserBadEvent from different accounts. 2023-03-08 09:50:38 +01:00
c496d6c71c fix(GODT-2442): GUI changes for new bad event dialog. 2023-03-07 20:39:15 +01:00
7fc907a874 fix(GODT-2442): must publish loggedOut event. 2023-03-07 18:56:06 +01:00
b953468af2 fix(GODT-2419): avoid context canceled reports 2023-03-07 18:32:33 +01:00
9f4caa4948 feat(GODT-2442): add notification and feedback to CLI. 2023-03-07 17:59:04 +01:00
86630ce137 chore(GODT-2442): improve naming, remove unrelated changes 2023-03-07 17:59:04 +01:00
2c9477d65c fix(GODT-2442): WIP: bad events just aborts polls, feedback processed in separete channel. 2023-03-07 17:59:04 +01:00
34c002ff68 test(GODT-2442): test bad event feedback and clean-up. 2023-03-07 17:59:04 +01:00
f03688ba72 feat(GODT-2442): add gRPC interface to send feedback. 2023-03-07 17:59:04 +01:00
8c0bb22de3 feat(GODT-2442): handle bad event resync resolution. 2023-03-07 17:59:04 +01:00
53c2cbcaee test(GODT-2442): test refresh event 2023-03-07 17:59:04 +01:00
07339aff21 fix(GODT-2458): Wait for both bridge and bridge-gui to be ended before restarting on crash. 2023-03-07 16:47:07 +00:00
3ca56cfab3 fix(GODT-2458): Wait for both bridge and bridge-gui to be ended before restarting on crash. 2023-03-07 15:44:58 +00:00
59cf5e890b fix(GODT-2419): Reduce error spam an improve error messages
https://github.com/ProtonMail/gluon/pull/319
https://github.com/ProtonMail/gluon/pull/320
2023-03-07 13:32:04 +01:00
70950e0048 fix(GODT-2457): Include address if GetPublickKeys() error message 2023-03-07 12:58:12 +01:00
2781f06549 chore: Bridge Quebec v3.1.0. 2023-03-07 11:53:02 +01:00
febc994cec feat(GODT-2446): Attach logs to sentry reports for relevant bridge-gui exceptions.
Cherry picked from release/perth_narrows (2aa4e7c)

# Conflicts:
#	internal/frontend/bridge-gui/bridge-gui/AppController.cpp
#	internal/frontend/bridge-gui/bridge-gui/AppController.h
#	internal/frontend/bridge-gui/bridge-gui/LogUtils.cpp
#	internal/frontend/bridge-gui/bridge-gui/LogUtils.h
#	internal/frontend/bridge-gui/bridge-gui/QMLBackend.cpp
#	internal/frontend/bridge-gui/bridge-gui/SentryUtils.cpp
#	internal/frontend/bridge-gui/bridge-gui/SentryUtils.h
#	internal/frontend/bridge-gui/bridge-gui/main.cpp
2023-03-07 10:04:21 +01:00
21ffde316d fix(GODT-2449): fix bug in Bridge-GUI's Exception::what().
Cherry picked from release/perth_narrows (1d426e6)
2023-03-07 08:52:57 +01:00
2aa4e7c9da feat(GODT-2446): Attach logs to sentry reports for relevant bridge-gui exceptions. 2023-03-06 18:36:40 +01:00
9058544f5c feat(GODT-2448): Supported Answered flag
Bump gluon and implement changes required for the flag changes.
2023-03-06 16:04:49 +01:00
227bbf1c03 fix(GODT-2425): Out of sync messages and read status
Apply fix required for gluon update.

Update Gluon to include revision where the fix was made. Includes:
 * https://github.com/ProtonMail/gluon/pull/313
 * https://github.com/ProtonMail/gluon/pull/316
 * https://github.com/ProtonMail/gluon/pull/317
2023-03-06 12:18:03 +00:00
667998c207 feat(GODT-2435): Group report exception by message if exception message looks corrupted. 2023-03-06 13:04:29 +01:00
1d426e621c fix(GODT-2449): fix bug in Bridge-GUI's Exception::what(). 2023-03-06 12:01:56 +00:00
6e4dcdb93b feat(GODT-2435): Group report exception by message if exception message looks corrupted. 2023-03-06 09:33:20 +01:00
20f35edc83 chore: fix missing import after cherry-pick. 2023-03-06 08:25:24 +00:00
26cf684fb8 chore: fill sentry user.id with hostname. 2023-03-06 08:25:24 +00:00
2a4cb6a916 chore: fix sentry tag for dev and release on GUI side. 2023-03-06 08:25:24 +00:00
caa4a5cbdb feat(GODT-2356): unify sentry release description and add more context to it. 2023-03-06 08:25:24 +00:00
91900a7942 feat(GODT-2357): Hide DSN_SENTRY and use single setting point for DSN_SENTRY. 2023-03-06 08:25:24 +00:00
04a7a81e27 chore(GODT-2444): Bad event info 2023-03-03 13:02:09 +00:00
53d5619c51 fix(GODT-2447): Don't assume timestamp exists in log filename 2023-03-03 10:27:21 +01:00
f793b71569 fix(GODT-2447): Don't assume timestamp exists in log filename 2023-03-03 10:10:06 +01:00
8aec11a634 chore: Bump Gluon for GODT-2427, GODT-2419, GODT-2429
https://github.com/ProtonMail/gluon/pull/313
https://github.com/ProtonMail/gluon/pull/312
2023-03-02 12:50:22 +00:00
30651a531b fix(GODT-2427): Fix header parser
Fix is included in gluon
2023-03-02 13:48:14 +01:00
91ab77dce9 fix(GODT-2424): Sync Builder Message Split
Incorrect math was causing some messages to not be built and be missing
from Gluon.
2023-03-01 13:13:20 +01:00
69aa784d32 fix(GODT-2426): Fix crash on user delete
Ensure we are always acquiring a write lock when modifying the user's
`updateCh` contents.
2023-03-01 12:02:12 +01:00
825031e052 fix(GODT-2426): Fix crash on user delete
Ensure we are always acquiring a write lock when modifying the user's
`updateCh` contents.
2023-03-01 11:22:05 +01:00
ee4a8939d5 fix(GODT-2419): Use connector.ErrOperationNotAllowed
Return this error when we detect operations that we know are not allowed
so that gluon does not report them to sentry.

Includes Gluon update for the connector error
(https://github.com/ProtonMail/gluon/pull/309)
2023-03-01 10:44:18 +01:00
89117bbd59 fix(GODT-2413): use qEnvironmentVariable() instead of qgetenv().
(cherry picked from commit 10cf153678)
2023-03-01 07:51:03 +01:00
9e4310712c fix(GODT-2419): Use connector.ErrOperationNotAllowed
Return this error when we detect operations that we know are not allowed
so that gluon does not report them to sentry.

Includes Gluon update for the connector error
(https://github.com/ProtonMail/gluon/pull/309)
2023-02-28 17:02:35 +01:00
0b35b275d3 fix(GODT-2333): Do not allow modifications to All Mail label
Rather than waiting for API to reply, prevent these operations from
taking place in the first place.
2023-02-28 16:51:23 +01:00
d6acb0fb19 fix(GODT-2418): Ensure child folders are updated when parent is 2023-02-28 14:25:11 +01:00
2db5a04e7a fix(GODT-2417): Gluon update to address ticket issues
* GODT-2417: Fixes connector requests for recovered messages.
* GODT-2416: Allow message updates to work if the literal is missing.
2023-02-28 13:41:15 +01:00
d7bfee2414 feat(GODT-2382): Added bridge-gui settings file with 'UseSoftwareRenderer' value. 2023-02-28 13:04:49 +01:00
4a6460f3ed feat(GODT-2411): allow qmake executable to be named qmake6.
And a free a typo fix.

(cherry picked from commit 4153a6925321e89e3ace7d747109484c1d28162e)
2023-02-28 09:05:50 +01:00
c6f1f159f3 chore: Bridge Perth Narrows 3.0.18 2023-02-28 06:53:16 +01:00
82af4e01bc feat(GODT-2364): wait and retry once if the gRPC service config file exists but cannot be opened. 2023-02-28 06:21:36 +01:00
9ad5f74409 feat(GODT-2364): added optional details to C++ exceptions. 2023-02-28 06:21:25 +01:00
10cf153678 fix(GODT-2413): use qEnvironmentVariable() instead of qgetenv(). 2023-02-27 15:41:26 +01:00
2b09796d1c fix(GODT-2399): Fix immediate message deletion during updates
Fix was applied in Gluon.
2023-02-27 15:09:11 +01:00
5ba07db7e3 chore: Bump Gluon for GODT-2399, GODT-2400 and GODT-2414
fix(GODT-2399): Defer updated message deletion
fix(GODT-2400): Allow state updates to be applied if command fails
fix(GODT-2414): Multiple deletion bug in WriteControlledStore
2023-02-27 14:53:37 +01:00
ad0d4ebd36 fix(GODT-2412): Don't treat context cancellation as BadEvent 2023-02-27 14:34:35 +01:00
9f3c14ab1e fix(GODT-2404): Handle unexpected EOF
When fetching too many attachment bodies at once, the read can fail with
io.ErrUnexpectedEOF. In that case, we returun an error so the fetch is retried.
2023-02-27 14:33:44 +01:00
74cf5d422b fix(GODT-2390): Missing changes from pervious commit
Always reports error type to sentry.

Add error checks for get event as well.
2023-02-27 14:33:38 +01:00
dcf694588c fix(GODT-2390): Add reports for uncaught json and net.opErr
Report to sentry if we see some uncaught network err, but don't force
the user logout.

If we catch an uncaught json parser error we report the error to sentry
and let the user be logged out later.

Finally this patch also prints the error type in UserBadEvent sentry
report to further help diagnose issues.
2023-02-27 14:33:21 +01:00
a2b9fc3dee feat(GODT-2273): menu with "Close window" and "Quit Bridge" button in main window. 2023-02-27 14:19:00 +01:00
761c16d8cd fix(GODT-2412): Don't treat context cancellation as BadEvent 2023-02-27 12:48:22 +00:00
810705ba01 fix(GODT-1945): Handle disabled addresses correctly
When listing all a user's email addresses (e.g. for apple mail autoconf)
we need to exclude disabled addresses. Similarly, we need to remove
them from gluon if using split mode.
2023-02-27 12:52:39 +01:00
c15917aba4 fix(GODT-2404): Handle unexpected EOF
When fetching too many attachment bodies at once, the read can fail with
io.ErrUnexpectedEOF. In that case, we returun an error so the fetch is retried.
2023-02-24 16:02:21 +00:00
51cbb91513 Revert GODT-2373 (bridgelib). 2023-02-24 15:22:12 +01:00
f8bfbaf361 fix(GODT-2390): Missing changes from pervious commit
Always reports error type to sentry.

Add error checks for get event as well.
2023-02-24 12:45:13 +01:00
3e878058e7 fix(GODT-2390): Add reports for uncaught json and net.opErr
Report to sentry if we see some uncaught network err, but don't force
the user logout.

If we catch an uncaught json parser error we report the error to sentry
and let the user be logged out later.

Finally this patch also prints the error type in UserBadEvent sentry
report to further help diagnose issues.
2023-02-24 11:01:44 +01:00
065dcd4d47 fix(GODT-2393): improved handling of unrecoverable error.
Fix a bug introduced in 265af2d. In the top level exception handler, we may not have add the time to establish connection, so we should not try to use the gRPC service to quit bridge.
2023-02-23 18:08:23 +01:00
00256fafe8 fix(GODT-2394): Bump Gluon for golang.org/x/text DoS risk
This also fixes a harmless error related to multiple deletions in the store.
2023-02-23 13:09:06 +01:00
671db7c516 chore: Fix typo loggin back in 2023-02-23 11:53:41 +01:00
e9f20aee7a fix(GODT-2387): Ensure vault can be unlocked after factory reset
When performing a factory reset, we don't want to wipe all keychain
entries. The only keychain entry should be the vault's passphrase,
and we need this to be able to decrypt the vault at next startup
(to avoid it being reported as corrupt).
2023-02-23 08:11:20 +00:00
8534da98ea chore: fix missing windows header for bridgelib.
bridgelib was moved from bridge-gui to bridgepp project, and windows.h is not auto-included anymore (only Qt core is in the pre-compiled headers).
2023-02-23 06:54:05 +00:00
265af2d299 fix(GODT-2389): close bridge on exception and add max termination wait time. 2023-02-23 07:22:30 +01:00
82c388a0dd chore: Bridge Perth Narrows 3.0.18 2023-02-23 06:58:54 +01:00
89112baf96 feat(GODT-2261): sync progress in GUI. 2023-02-22 12:11:42 +00:00
5007d451c2 feat(GODT-2385): Gluon cache fallback
Update Gluon to have access to the cache fallback reader.

Provide fallback reader to handle old cache file format.

Remove the old logic to erase all cache files on start as the fallback
option renders this irrelevant.
2023-02-22 12:00:23 +01:00
4775fb4b22 fix(GODT-2201): Add missing rfc5322.CharsetReader initialization 2023-02-22 09:07:21 +01:00
94ed09b437 feat(GODT-2366): Handle failed message updates as creates
This handles the following case:
- event says message was created
- we try to fetch the message but API says the doesn’t exist yet — we skip applying the “message created” update
- event then says message was updated at some point in the future
- we try to handle it but fail because we don’t have the message — we should treat it as a creation
2023-02-21 16:07:27 +01:00
57962e5757 chore: Bump gluon to create missing messages during MessageUpdated 2023-02-21 16:07:27 +01:00
8a5c8eaf6e chore: Use gluon temp/hotfix-perth-narrows branch 2023-02-21 16:07:27 +01:00
a75a72a2b9 chore: Bump Gluon version for Go rfc5322 date parser 2023-02-21 15:46:24 +01:00
038eb6d243 feat(GODT-2366): Handle failed message updates as creates
This handles the following case:
- event says message was created
- we try to fetch the message but API says the doesn’t exist yet — we skip applying the “message created” update
- event then says message was updated at some point in the future
- we try to handle it but fail because we don’t have the message — we should treat it as a creation
2023-02-21 15:20:05 +01:00
2bd8f6938a chore: Bump gluon to create missing messages during MessageUpdated 2023-02-21 12:53:05 +01:00
889f3286b5 chore: Bump gluon to use new address parser 2023-02-21 12:15:37 +01:00
9dfdd07f7a fix(GODT-2381): Unset draft flag on sent messages 2023-02-20 15:37:36 +01:00
0b796f4401 feat(GODT-2373): bridgelib tests. 2023-02-20 14:47:14 +01:00
a741ffb595 feat(GODT-2373): introducing bridgelib Go dynamic library in bridge-gui. 2023-02-20 12:21:02 +01:00
cf8284a489 fix(GODT-2380): Only set external ID in header if non-empty 2023-02-20 11:16:50 +01:00
6a9f6a173a feat(GODT-2201): Bump Gluon to use pure Go IMAP parser 2023-02-17 14:25:16 +00:00
54c013012e feat(GODT-2374): Import TLS certs via shell 2023-02-17 13:49:04 +00:00
cac0cf35f6 feat(GODT-2361): Bump GPA to use simple encrypter 2023-02-17 14:06:15 +01:00
30029f489e doc: changelog typo 2023-02-17 13:34:03 +01:00
2faeebe9e7 chore: Bridge Perth Narrows 3.0.16/17 2023-02-16 17:46:31 +01:00
f6727a56d2 fix(GODT-2371): Continue, not return, when handling draft 2023-02-16 17:46:24 +01:00
4c24c004db fix(GODT-2371): Continue, not return, when handling draft 2023-02-16 17:23:22 +01:00
571133f2ff chore: added bridge-gui CMake variable for crashpad_handler path.
Ignored in release mode.
2023-02-15 17:49:15 +00:00
0207fa04f1 feat(GODT-2364): wait and retry once if the gRPC service config file exists but cannot be opened. 2023-02-15 17:56:56 +01:00
98fdf45fa3 feat(GODT-2364): added optional details to C++ exceptions. 2023-02-15 16:25:21 +00:00
21b3a4bca3 chore: fill sentry user.id with hostname. 2023-02-15 16:11:04 +00:00
7225fc31da chore: fix sentry tag for dev and release on GUI side. 2023-02-15 16:10:58 +01:00
eca4810f19 test: step definitions changed 2023-02-15 14:56:46 +01:00
249658c05b chore: merge branch release/perth_narrows to devel 2023-02-15 14:39:28 +01:00
da82d7a107 fix(GODT-2365): Use predictable remote ID for placeholder mailboxes 2023-02-15 10:42:47 +01:00
08dab2d115 feat(GODT-1264): constraint on Scheduled mailbox in connector + Integration tests. 2023-02-15 07:37:09 +00:00
13db1b0db8 feat(GODT-2356): unify sentry release description and add more context to it. 2023-02-14 16:27:55 +00:00
c1921a811b ci: always use the 'eventually' variant step and remove others. 2023-02-14 17:08:10 +01:00
473be3d485 feat(GODT-2357): Hide DSN_SENTRY and use single setting point for DSN_SENTRY. 2023-02-13 20:31:32 +01:00
d7fd39503f chore: Bridge Perth Narrows 3.0.15/16 2023-02-13 15:06:36 +01:00
b4b66f94ec feat(GODT-2355): improve wording and actions on bad event 2023-02-13 14:27:34 +01:00
0823d393ed feat(GODT-1264): creation and visibility of the 'Scheduled' system label.
feat(GODT-1264): typo in error message
feat(GODT-1264): fix split mode broken by previous commit.
2023-02-10 15:24:31 +01:00
cbd36184bd feat(GODT-2354): report failed to load users. 2023-02-10 11:53:05 +00:00
465f754803 feat(GODT-2353): show popup only after 3.0.16 2023-02-09 17:00:58 +01:00
8b9265ad96 feat(GODT-2283): Limit max import size to 30MB (bump GPA to v0.4.0) 2023-02-09 16:35:08 +01:00
5f930c262c feat(GODT-2352): only copy resource file when needed. 2023-02-09 13:01:38 +01:00
afa95d4799 fix(GODT-2351): Bump GPA to automatically retry on net.OpError 2023-02-09 12:36:44 +01:00
2fa7c97f39 fix(GODT-2351): Bump GPA to better handle net.OpError 2023-02-09 12:04:39 +01:00
2b75fcf773 feat(GODT-2352): use go-build-finalize macro to build vault-editor for both mac arch 2023-02-09 10:44:18 +00:00
cdff2ef792 fix(GODT-2351): Bump GPA to properly handle net.OpError and add tests 2023-02-08 15:16:28 +00:00
a740a8f962 feat(GODT-2278): properly override server_name for go. 2023-02-08 15:28:58 +01:00
d1f1c390f6 test: Bump test timeout from 5m to 10m 2023-02-08 13:17:31 +00:00
1c88ce3cc0 feat(GODT-2255): Randomize the focus service port. 2023-02-08 10:06:53 +00:00
c4ef1a24c0 fix(GODT-2347): Prevent updates from being dropped if goroutine doesn't start fast
If the install handler goroutine is busy, the update is dropped.
This was intended to prevent two installs from happening at once.
However, it also means that updates can be dropped at startup if the
goroutine isn't spawned soon enough.

A fix is to allow all jobs through and just reject ones that are
for an old version.
2023-02-07 18:26:13 +01:00
a79fce907e test: Bump GPA to fix flaky test TestBridge_User_BadMessage_NoBadEvent
It was necessary to return an actual proton.APIError json object
in order for our detection to work properly in one of the tests.
2023-02-07 18:26:13 +01:00
c9d496956c test: Refactor account management, fix map-random-order race condition
Some SMTP tests made use of disabled addresses. We stored addresses
in a map, meaning the order was randomized. This lead to tests sometimes
attempting to authenticate over SMTP using a disabled address, failing.
2023-02-07 18:26:13 +01:00
31dce41276 test: Fix race condition with initialization of messageIDs
Need to make sure the messageIDs slice is created properly before
it is used async in a different goroutine.
2023-02-07 18:03:36 +01:00
c6576dfc4b fix(GODT-2327): Remove unnecessary sync when changing address mode 2023-02-07 17:53:53 +01:00
9048b14fdb chore: Bridge Perth Narrows v3.0.14 2023-02-07 16:36:38 +01:00
43100d11bf fix(GODT-2323): Fix Expunge not issued for move
When moving between system labels the expunge commands were not being
issued.
2023-02-07 15:09:23 +01:00
4876314cf5 fix(GODT-2341): Handle URL error 2023-02-07 14:31:06 +01:00
2f75131710 fix(GODT-2340): improve logging 2023-02-07 14:31:06 +01:00
1e09fd6662 feat(GODT-2278): improve sentry logs. 2023-02-07 14:31:06 +01:00
48f2c56caa fix(GODT-2327): Better sleep (with context) 2023-02-07 14:31:06 +01:00
20d83dd476 fix(GODT-2327): Loop to retry until sync has complete 2023-02-07 14:31:06 +01:00
9c6be78b4c fix(GODT-2327): Don't retry with abortable context because it's canceled 2023-02-07 14:31:06 +01:00
0a8e71771e fix(GODT-2327): Fix lint issue 2023-02-07 14:31:06 +01:00
29d1c7bccd fix(GODT-2327): Remove unnecessary sync abort call 2023-02-07 14:31:06 +01:00
ca1996a670 fix(GODT-2327): Properly cancel event stream when handling refresh 2023-02-07 14:31:06 +01:00
ab1c1c474a fix(GODT-2327): Clear update channels whenever clearing sync status 2023-02-07 14:31:06 +01:00
d7cac8a8f0 fix(GODT-2327): avoid windows delete all deadlock 2023-02-07 14:31:06 +01:00
63bc87cc86 fix(GODT-2327): Only start processing events once sync is finished 2023-02-07 14:31:06 +01:00
232875d5cc fix(GODT-2327): Delay event processing until gluon user exists
We don't want to start processing events until those events have
somewhere to be sent to.

Also, to be safe, ensure remove and re-add the gluon user while
clearing its sync status. This shouldn't be necessary.
2023-02-07 14:31:02 +01:00
f3c5e300cd fix(GODT-2327): Delay event processing until gluon user exists
We don't want to start processing events until those events have
somewhere to be sent to.

Also, to be safe, ensure remove and re-add the gluon user while
clearing its sync status. This shouldn't be necessary.

fix(GODT-2327): Only start processing events once sync is finished

fix(GODT-2327): avoid windows delete all deadlock

fix(GODT-2327): Clear update channels whenever clearing sync status

fix(GODT-2327): Properly cancel event stream when handling refresh

fix(GODT-2327): Remove unnecessary sync abort call

fix(GODT-2327): Fix lint issue

fix(GODT-2327): Don't retry with abortable context because it's canceled

fix(GODT-2327): Loop to retry until sync has complete

fix(GODT-2327): Better sleep (with context)
2023-02-07 13:41:16 +01:00
29072f0285 fix(GODT-2343): Only poll after send if sync is complete 2023-02-06 16:39:32 +00:00
5ea53ea5c0 fix(GODT-2318): Remove gluon DB if label sync was incomplete 2023-02-06 16:36:15 +00:00
40aca0fe73 fix(GODT-2336): Recover from changed address order while bridge is down 2023-02-06 16:03:33 +00:00
f4a2fb9687 test: Add failing test for changing address order while bridge is down 2023-02-06 16:03:33 +00:00
367c505444 fix(GODT-1804): Use cherry-picked mail settings in GPA 2023-02-06 15:57:24 +00:00
3bd39b3ea5 fix(GODT-1804): Only promote content headers if non-empty
When attaching public key, we take the root mime part, create a new root,
and put the old root alongside an additional public key mime part.
But when moving the root, we would copy all content headers, even empty ones.
So we’d be left with Content-Disposition: "" which would fail to parse.
2023-02-06 15:57:24 +00:00
e89dcb2cca fix(GODT-2343): Only poll after send if sync is complete 2023-02-06 16:33:53 +01:00
ad65bdde9d fix(GODT-2326): Fix potential Win32 API deadlock
Update gluon so that the store implementation uses `os.Remove` instead
of `os.RemoveAll`. The latter has an issue where it can deadlock on
windows. See https://github.com/golang/go/issues/36375 for more details.
2023-02-06 16:30:03 +01:00
34cd611a8b chore: Disable funlen linter 2023-02-06 14:29:13 +00:00
d82b71de89 fix(GODT-1804): Only promote content headers if non-empty
When attaching public key, we take the root mime part, create a new root,
and put the old root alongside an additional public key mime part.
But when moving the root, we would copy all content headers, even empty ones.
So we’d be left with Content-Disposition: "" which would fail to parse.
2023-02-06 13:18:08 +00:00
8894a982f2 ci(GODT-2287): add test coverage on devel 2023-02-06 10:50:47 +01:00
2cb2ca15c7 fix(GODT-2336): Recover from changed address order while bridge is down 2023-02-03 16:05:30 +01:00
db41645159 test: Add failing test for changing address order while bridge is down 2023-02-03 15:51:18 +01:00
a74d1ce9ca feat(GODT-2144): Handle IMAP/SMTP server errors via event stream 2023-02-03 14:11:53 +00:00
2e832520e6 chore: Remove panics from SetGluonDir 2023-02-03 14:11:53 +00:00
62285a141e feat(GODT-2144): Delay IMAP/SMTP server start until all users are loaded 2023-02-03 14:11:53 +00:00
c3d5a0b8f8 feat(GODT-2295): notifications for IMAP login when signed out. 2023-02-03 11:18:59 +01:00
a36dbbf422 fix(GODT-2333): Do not allow modifications to All Mail label
Rather than waiting for API to reply, prevent these operations from
taking place in the first place.
2023-02-03 05:26:42 +00:00
4cf23bb2e6 fix(GODT-2328): Ignore labels that aren't part of user label set 2023-02-02 16:59:07 +01:00
e2c1f38ed3 fix(GODT-2328): Ignore labels that aren't part of user label set 2023-02-02 15:39:33 +00:00
5ec1da34b4 feat(GODT-2278): improve sentry logs. 2023-02-02 15:36:37 +00:00
ea11c1046a chore: Bridge Perth Narrows v3.0.13 2023-02-02 16:30:45 +01:00
df40f27069 test(GODT-2326): Remove user tests
These tests no longer work due to sync only being started after an
account has been added. Functionality of these tests is covered in the
bridge unit tests.
2023-02-02 16:26:38 +01:00
76d732f247 fix(GODT-2326): Only run sync after addIMAPUser()
There is concurrency bug due to competing sync calls that can occur when
we clear the sync status in the Vault. Running sync at the end of
addIMAPUser() avoids the problem.

This patch also remove the execution of a sync task for
`user.ClearSyncStatus()`
2023-02-02 16:26:12 +01:00
219400de8d test(GODT-2298): add integration test for In-Reply-To header use cases. 2023-02-02 15:25:27 +00:00
8901d83c94 test(GODT-2326): Remove user tests
These tests no longer work due to sync only being started after an
account has been added. Functionality of these tests is covered in the
bridge unit tests.
2023-02-02 13:04:05 +01:00
0c8d4e8dd8 fix(GODT-2326): Only run sync after addIMAPUser()
There is concurrency bug due to competing sync calls that can occur when
we clear the sync status in the Vault. Running sync at the end of
addIMAPUser() avoids the problem.

This patch also remove the execution of a sync task for
`user.ClearSyncStatus()`
2023-02-02 11:32:54 +01:00
a955dcbaa9 fix(GODT-2323): Fix Expunge not issued for move
When moving between system labels the expunge commands were not being
issued.
2023-02-02 07:21:01 +01:00
fbac5134ca fix(GODT-2224): Properly handle context cancellation during sync
There was an issue where new attachment download requests would hang
forever due to not checking whether the context was cancelled. At this
point there were no more workers to consume to channel messages.
2023-02-01 15:08:41 +01:00
45ec6b6e74 feat(GODT-2289): UIDValidity as Timestamp
Update UIDValidity to be timestamp with the number of seconds since
the 1st of February 2023. This avoids the problem where we lose the
last UIDValidity value due to the vault being missing/corrupted/deleted.
2023-02-01 14:04:45 +01:00
b17bdad864 chore: Bridge Perth Narrows v3.0.13 2023-02-01 10:42:33 +01:00
52daa165a2 fix(GODT-2319): seed the math/rand RNG on app startup. 2023-02-01 10:42:09 +01:00
4c5ba04822 fix(GODT-1804): Preserve MIME parameters when uploading attachments 2023-02-01 10:41:55 +01:00
79c2523585 chore: Add recipient to SMTP error message 2023-01-31 17:35:51 +01:00
f14ad8b3fa chore: README update. 2023-01-31 15:48:07 +00:00
590fdacba3 fix(GODT-2319): seed the math/rand RNG on app startup. 2023-01-31 15:28:03 +00:00
342a2a5568 fix(GODT-2272): use shorter filename for gRPC file socket. 2023-01-31 15:51:06 +01:00
e382687168 fix(GODT-2318): Remove gluon DB if label sync was incomplete 2023-01-31 12:40:42 +00:00
4577a40b1e fix(GODT-2224): Restore parallel attachment download
Feature was not restored in previous MR. Attachment are now download in
parallel. There is a pool of maxParallelDownloads attachment downloaders
shared with all message downloads.
2023-01-31 12:25:20 +01:00
c0aacb7d62 GODT-2224: Allow the user to specify max sync memory usage in Vault 2023-01-31 09:40:22 +01:00
c8065c8092 GODT-2312: used space is properly updated. 2023-01-31 07:22:03 +00:00
ce03bfbf0f GODT-1804: Preserve MIME parameters when uploading attachments 2023-01-30 20:12:41 +01:00
0182e2c0bc GODT-2287: Add code coverage to artifacts and pipeline. 2023-01-30 16:07:11 +00:00
e464e11ab9 GODT-2224: Refactor bridge sync to use less memory
Updates go-proton-api and Gluon to includes memory reduction changes and
modify the sync process to take into account how much memory is used
during the sync stage.

The sync process now has an extra stage which first download the message
metada to ensure that we only download up to `syncMaxDownloadRequesMem`
messages or 250 messages total. This allows for scaling the download
request automatically to accommodate many small or few very large
messages.

The IDs are then sent to a download go-routine which downloads the
message and its attachments. The result is then forwarded to another
go-routine which builds the actual message. This stage tries to ensure
that we don't use more than `syncMaxMessageBuildingMem` to build these
messages.

Finally the result is sent to a last go-routine which applies the
changes to Gluon and waits for them to be completed.

The new process is currently limited to 2GB. Dynamic scaling will be
implemented in a follow up. For systems with less than 2GB of memory we
limit the values to a set of values that is known to work.
2023-01-30 15:05:43 +01:00
d7ff54d679 Other: Bridge Perth Narrows v3.0.1 hotfix 2 2023-01-30 08:35:22 +01:00
4aa1091f62 GODT-2210: fix splash screen always showing on CentOS and Ubuntu. 2023-01-27 14:10:14 +01:00
6d024d2055 Other: Other: Bridge Perth Narrows v3.0.1 hotfix 2023-01-27 10:15:53 +01:00
c86cdf737f GODT-2311: Fix missing headers in re-downloaded Gluon messages 2023-01-27 09:55:02 +01:00
7e36b215fe GODT-1453: clicking 'Sign in' from status window now selects the right account. 2023-01-26 17:27:55 +01:00
badebbef9f GODT-2297: More significantly improve GPA's paging algorithm 2023-01-26 12:43:30 +01:00
5e072c3282 Other: Slightly improve GPA's paging algorithm 2023-01-26 09:48:19 +01:00
048c3a900c GODT-2145: Fix button spacing w/ Qt 6.4. 2023-01-25 17:35:21 +01:00
88a9fe410c Other: Bridge Perth Narrows v3.0.12 2023-01-25 15:14:30 +01:00
cf32b84257 GODT-2305: Detect missing gluon DB 2023-01-25 14:55:13 +01:00
e7dea0a77f GODT-2223(test): Fix array access off-by-one in test code 2023-01-25 13:05:05 +01:00
160489a771 GODT-2223: Testing event processing scenarios. 2023-01-25 13:05:04 +01:00
996c6826b9 GODT-2223: Handle gracefully multiple create events with same id. 2023-01-25 13:04:04 +01:00
43b871a124 GODT-2210: Typo fix 2023-01-25 09:51:18 +00:00
d1bf186040 Other: changed splash screen icon. 2023-01-25 09:42:00 +00:00
24c68f100e GODT-2210: v3.0 splash screen.
Other: new splash screen content.
2023-01-25 09:42:00 +00:00
1bfabf9a83 GODT-2296: Log error rather than fail if cannot get parent ID 2023-01-25 09:09:14 +00:00
e8a778feca GODT-2266: Pause event stream while sending 2023-01-25 09:47:27 +01:00
5d4c10c56e Other(test): Fix some more integration test placeholders 2023-01-24 16:58:25 +00:00
60b1c4d8f7 GODT-2177: Use correct attachment disposition when content ID is set 2023-01-24 16:31:14 +01:00
f1404cd3ee GODT-1556: If no references, use the in-reply-to header as ParentID. 2023-01-24 15:36:23 +01:00
ee4da8a89c GODT-2291: Change gluon store default location from Cache to Data. 2023-01-24 13:35:45 +00:00
5a70a16149 GODT-1770: handle UserBadEvent in CLI and gRPC. 2023-01-24 13:07:27 +01:00
f019ba3713 Other: Disable dialer test until badssl cert is bumbed. 2023-01-24 09:59:32 +00:00
4b966c4845 GODT-2292: Updated BUILDS.md doc. 2023-01-24 09:28:43 +01:00
94703bcf37 GODT-2223: Treat label/message deletion errors as non-critical 2023-01-23 21:25:15 +01:00
40cc6b54c9 Other: make GUI Tester more resilient to Bridge abrupt termination. 2023-01-23 08:26:52 +01:00
584ea7e9f8 GODT-2275: fixed location of bridge-gui log files. 2023-01-20 15:38:38 +01:00
cbdbc124db GODT-2258: suggest email as login when signing in via status window. 2023-01-20 15:02:44 +01:00
b9c3fa9401 GODT-2266: Add test for sent message flags 2023-01-20 13:04:01 +00:00
0e4ec8a8b8 Other: Ensure SMTP debug dump works on windows 2023-01-20 13:33:14 +01:00
c3e4bb80a8 Other: Fix MaxLogs off-by-one limit and bump limit to 10 2023-01-20 12:52:22 +01:00
6459840507 GODT-2253: Restart Launcher from the gui when GUI crashes. 2023-01-20 07:43:02 +00:00
87abbe9396 Other: Report corrupt and/or insecure vaults to sentry 2023-01-20 08:00:52 +01:00
d26a8319b6 Other: Better user load logs 2023-01-19 17:47:45 +01:00
c9c80fd861 Other(test): Make All Mail copy test more robust 2023-01-19 15:09:56 +00:00
fea4cc7b3b Other: fix path of temp folder in README. 2023-01-19 14:40:13 +00:00
cba5da22ae Other(CI): Make race checks manual 2023-01-19 14:22:43 +00:00
59a29da054 Other(debug): Dump raw SMTP input to user's home dir 2023-01-19 13:30:28 +01:00
c70674471e Other: Remove old cert/key file location handling. 2023-01-19 09:28:44 +00:00
7882324439 GODT-2271: Update README with new system files path. 2023-01-19 09:11:14 +00:00
1f8866a48a Other: Bridge Perth Narrows v3.0.11 2023-01-18 16:18:39 +01:00
faf28a6d4e GODT-2223: Fix mutex double lock 2023-01-18 14:10:23 +00:00
59745e6fb6 GODT-2223: Ensure apiLabels map is properly up to date 2023-01-18 14:10:23 +00:00
c8925cd270 GODT-2223(test): Assert that bad request errors lead to user logout 2023-01-18 14:10:23 +00:00
e35f3b6056 GODT-2223: Fix handling of create/update label events 2023-01-18 14:10:23 +00:00
d68014ec7b GODT-2223: Handle attempting to fetch a message that was just deleted 2023-01-18 14:10:23 +00:00
b63029054d GODT-2223: Fix handling deletion of nonexistent labels 2023-01-18 14:10:23 +00:00
849c8bee78 GODT-2223: Handle bad events by logging user out 2023-01-18 14:10:23 +00:00
70f0384cc3 GODT-2165: Reduce UTF8 parsing errors from TLS header input
Updates Gluon to include fix which does not report UTF-8 errors when we
parse a TLS header on a non TLS connection.
2023-01-18 13:06:14 +01:00
5459720523 Others: chores fix a QML warning when no account is present, and a few typos in QML. 2023-01-18 11:04:13 +01:00
a00e2acb5c Other: Don't clean settings path on teardown 2023-01-18 08:24:31 +00:00
1d405076e6 Other(test): Fix integration test steps 2023-01-18 07:16:24 +00:00
7119c566ef Other: Bump GPA to v0.3.0 2023-01-17 14:10:07 +00:00
0e9428aaae GODT-2252: Recover from deleted cached messages
Update to latest Gluon version and implement the new
`Connector.GetMessageLiteral` function.
2023-01-17 14:02:39 +01:00
fe009ca235 GODT-2258: change login label and suggest email instead of username. 2023-01-17 13:13:43 +01:00
a377384553 Other: added user's primary email address to the vault. 2023-01-17 11:27:54 +01:00
03c8c323bc GODT-2251: Store gluon cache in user cache rather than user data 2023-01-16 16:27:41 +01:00
fdbc380421 GODT-2251: Store gluon DB in user config rather than cache directory
Gluon data was stored in the user's "data dir". This is
~/.local/share on linux, but was the user's "cache dir" on windows/mac.
As a result, it would sometimes be deleted to reclaim disk space.

This change ensures the "data dir" is persistent on windows/mac.
2023-01-16 15:14:00 +00:00
7056134b24 GODT-2093: use the primary email address in the account view and status view. 2023-01-16 14:45:15 +01:00
93c7552a41 GODT-2202: Report update errors from Gluon
For every update sent to gluon wait and check the error code to see if
an error occurred.

Note: Updates can't be inspect on the call site as it can lead to
deadlocks.
2023-01-13 15:54:31 +01:00
931ed119bb GODT-2226: Fix moving drafts to trash
Only handle draft updates if the event was a message update. Also
includes Gluon update.
2023-01-12 14:25:27 +01:00
0580842ad2 GODT-2229: Own the full path for gluon and do not change Database path. 2023-01-12 13:23:09 +00:00
8d9db83a87 GODT-2246: do not report API error 422 when using an invalid email address. 2023-01-12 12:36:46 +01:00
c3eb6b2dbf GODT-1797: copyright notice shows a date range with the build year. 2023-01-11 16:59:05 +01:00
250 changed files with 16838 additions and 8490 deletions

View File

@ -32,7 +32,7 @@ stages:
.rules-branch-and-MR-always:
rules:
- if: $CI_COMMIT_BRANCH || $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH || $CI_PIPELINE_SOURCE == "merge_request_event"
when: always
allow_failure: false
- when: never
@ -54,16 +54,27 @@ stages:
allow_failure: true
- when: never
.rules-branch-manual-MR-always-allow-failure:
.rules-branch-manual-MR-and-devel-always:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == "devel" || $CI_PIPELINE_SOURCE == "merge_request_event"
when: always
allow_failure: true
allow_failure: false
- if: $CI_COMMIT_BRANCH
when: manual
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
# Stage: TEST
@ -76,46 +87,62 @@ lint:
tags:
- medium
test-linux:
.test-base:
stage: test
extends:
- .rules-branch-manual-MR-always
script:
- make test
test-linux:
extends:
- .test-base
- .rules-branch-manual-MR-and-devel-always
- .after-script-code-coverage
tags:
- medium
test-linux-race:
stage: test
extends:
- .rules-branch-manual-MR-always-allow-failure
- test-linux
- .rules-branch-and-MR-manual
script:
- make test-race
tags:
- medium
test-integration:
stage: test
extends:
- .rules-branch-manual-MR-always
- test-linux
script:
- make test-integration
tags:
- large
test-integration-race:
stage: test
extends:
- .rules-branch-manual-MR-always-allow-failure
- test-integration
- .rules-branch-and-MR-manual
script:
- make test-integration-race
tags:
- large
dependency-updates:
.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
stage: test
script:
- make updates
- make test
# Stage: BUILD
@ -131,31 +158,20 @@ dependency-updates:
when: manual
allow_failure: true
- when: never
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}
script:
- make build
- git diff && git diff-index --quiet HEAD
- make vault-editor
artifacts:
# Note: The latest artifacts for refs are locked against deletion, and kept
# regardless of the expiry time. Introduced in GitLab 13.0 behind a
# disabled feature flag, and made the default behavior in GitLab 13.4.
expire_in: 1 day
when: always
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
paths:
- bridge_*.tgz
- vault-editor
tags:
- large
build-linux:
extends: .build-base
.linux-build-setup:
image: gitlab.protontech.ch:4567/go/bridge-internal:qt6
variables:
VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.cache
@ -164,19 +180,29 @@ build-linux:
paths:
- .cache
when: 'always'
artifacts:
name: "bridge-linux-$CI_COMMIT_SHORT_SHA"
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
build-linux-qa:
extends: build-linux
extends:
- build-linux
variables:
BUILD_TAGS: "build_qa"
artifacts:
name: "bridge-linux-qa-$CI_COMMIT_SHORT_SHA"
.build-darwin-base:
extends: .build-base
.darwin-build-setup:
before_script:
- export PATH=/usr/local/bin:$PATH
- export PATH=/usr/local/opt/git/bin:$PATH
@ -188,30 +214,22 @@ build-linux-qa:
- 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}
script:
- go version
- make build-nogui
- git diff && git diff-index --quiet HEAD
- make vault-editor
cache: {}
tags:
- macOS
build-darwin:
extends: .build-darwin-base
artifacts:
name: "bridge-darwin-$CI_COMMIT_SHORT_SHA"
extends:
- .build-base
- .darwin-build-setup
build-darwin-qa:
extends: .build-darwin-base
extends:
- build-darwin
variables:
BUILD_TAGS: "build_qa"
artifacts:
name: "bridge-darwin-qa-$CI_COMMIT_SHORT_SHA"
.build-windows-base:
extends: .build-base
.windows-build-setup:
before_script:
- export GOROOT=/c/Go1.18/
- export PATH=$GOROOT/bin:$PATH
@ -225,23 +243,19 @@ build-darwin-qa:
- 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}
script:
- make build-nogui
- git diff && git diff-index --quiet HEAD
- make vault-editor
cache: {}
tags:
- windows-bridge
build-windows:
extends: .build-windows-base
artifacts:
name: "bridge-windows-$CI_COMMIT_SHORT_SHA"
extends:
- .build-base
- .windows-build-setup
build-windows-qa:
extends: .build-windows-base
extends:
- build-windows
variables:
BUILD_TAGS: "build_qa"
artifacts:
name: "bridge-windows-qa-$CI_COMMIT_SHORT_SHA"
# TODO: PUT BACK ALL THE JOBS! JUST DID THIS FOR NOW TO GET CI WORKING AGAIN...

View File

@ -23,7 +23,6 @@ issues:
- path: _test\.go
linters:
- dupl
- funlen
- gochecknoglobals
- gochecknoinits
- gosec
@ -32,7 +31,6 @@ issues:
- path: test
linters:
- dupl
- funlen
- gochecknoglobals
- gochecknoinits
- gosec
@ -64,7 +62,6 @@ linters:
- 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]
- funlen # Tool for detection of long functions [fast: true, auto-fix: false]
- gochecknoglobals # Checks that no globals are present in Go code [fast: true, auto-fix: false]
- gochecknoinits # Checks that no init functions are present in Go code [fast: true, auto-fix: false]
- goconst # Finds repeated strings that could be replaced by a constant [fast: true, auto-fix: false]

View File

@ -5,13 +5,13 @@
- the go-rfc5322 module cannot currently be compiled for 32-bit OSes
* Go 1.18
* Bash with basic build utils: make, gcc, sed, find, grep, ...
- For Windows it is recommended to use MinGW 64bit shell from [MSYS2](https://www.msys2.org/)
- For Windows, it is recommended to use MinGW 64bit shell from [MSYS2](https://www.msys2.org/)
* GCC (linux), msvc (windows) or Xcode (macOS)
* Windres (windows)
* libglvnd and libsecret development files (linux)
To enable the sending of crash reports using Sentry please set the
`main.DSNSentry` value with the client key of your sentry project before build.
`DSN_SENTRY` environment variable with the client key of your sentry project before build.
Otherwise, the sending of crash reports will be disabled.
## Build
@ -44,9 +44,10 @@ make build
make build-nogui
```
* Bridge without GUI will start by default without any interface (i.e., there is no way to add or remove client, get bridge password, etc)
* Bridge always has the option (whether built with Qt or without) to use a CLI interface by starting it with the argument `-c`
* NOTE: You still need to setup supported keychain on your system
* To launch Bridge without GUI, you can invoke the `bridge` executable with one the following command-line switches:
* `--noninteractive` or `-n` to start Bridge without any interface (i.e., there is no way to add or remove client, get bridge password, etc.)
* `--cli` or `-c` to start Bridge with an interactive terminal interface.
* NOTE: You still need to set up a supported keychain on your system.
## Launchers
Launchers are only included in official distributions and provide the public

View File

@ -26,7 +26,6 @@ Proton Mail Bridge includes the following 3rd party software:
* [gluon](https://github.com/ProtonMail/gluon) available under [license](https://github.com/ProtonMail/gluon/blob/master/LICENSE)
* [go-autostart](https://github.com/ProtonMail/go-autostart) available under [license](https://github.com/ProtonMail/go-autostart/blob/master/LICENSE)
* [go-proton-api](https://github.com/ProtonMail/go-proton-api) available under [license](https://github.com/ProtonMail/go-proton-api/blob/master/LICENSE)
* [go-rfc5322](https://github.com/ProtonMail/go-rfc5322) available under [license](https://github.com/ProtonMail/go-rfc5322/blob/master/LICENSE)
* [gopenpgp](https://github.com/ProtonMail/gopenpgp/v2) available under [license](https://github.com/ProtonMail/gopenpgp/v2/blob/master/LICENSE)
* [goquery](https://github.com/PuerkitoBio/goquery) available under [license](https://github.com/PuerkitoBio/goquery/blob/master/LICENSE)
* [ishell](https://github.com/abiosoft/ishell) available under [license](https://github.com/abiosoft/ishell/blob/master/LICENSE)
@ -53,6 +52,7 @@ Proton Mail Bridge includes the following 3rd party software:
* [html2text](https://github.com/jaytaylor/html2text) available under [license](https://github.com/jaytaylor/html2text/blob/master/LICENSE)
* [go-keychain](https://github.com/keybase/go-keychain) available under [license](https://github.com/keybase/go-keychain/blob/master/LICENSE)
* [dns](https://github.com/miekg/dns) available under [license](https://github.com/miekg/dns/blob/master/LICENSE)
* [memory](https://github.com/pbnjay/memory) available under [license](https://github.com/pbnjay/memory/blob/master/LICENSE)
* [errors](https://github.com/pkg/errors) available under [license](https://github.com/pkg/errors/blob/master/LICENSE)
* [profile](https://github.com/pkg/profile) available under [license](https://github.com/pkg/profile/blob/master/LICENSE)
* [logrus](https://github.com/sirupsen/logrus) available under [license](https://github.com/sirupsen/logrus/blob/master/LICENSE)
@ -76,8 +76,9 @@ Proton Mail Bridge includes the following 3rd party software:
* [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)
* [antlr](https://github.com/antlr/antlr4/runtime/Go/antlr) available under [license](https://github.com/antlr/antlr4/runtime/Go/antlr/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)
* [circl](https://github.com/cloudflare/circl) available under [license](https://github.com/cloudflare/circl/blob/master/LICENSE)
* [go-md2man](https://github.com/cpuguy83/go-md2man/v2) available under [license](https://github.com/cpuguy83/go-md2man/v2/blob/master/LICENSE)
@ -88,6 +89,7 @@ Proton Mail Bridge includes the following 3rd party software:
* [go-windows](https://github.com/elastic/go-windows) available under [license](https://github.com/elastic/go-windows/blob/master/LICENSE)
* [go-textwrapper](https://github.com/emersion/go-textwrapper) available under [license](https://github.com/emersion/go-textwrapper/blob/master/LICENSE)
* [go-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)
* [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)
@ -97,6 +99,7 @@ Proton Mail Bridge includes the following 3rd party software:
* [validator](https://github.com/go-playground/validator/v10) available under [license](https://github.com/go-playground/validator/v10/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)
* [errwrap](https://github.com/hashicorp/errwrap) available under [license](https://github.com/hashicorp/errwrap/blob/master/LICENSE)
* [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)
@ -104,6 +107,7 @@ Proton Mail Bridge includes the following 3rd party software:
* [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)
* [go-urn](https://github.com/leodido/go-urn) available under [license](https://github.com/leodido/go-urn/blob/master/LICENSE)
* [go-colorable](https://github.com/mattn/go-colorable) available under [license](https://github.com/mattn/go-colorable/blob/master/LICENSE)
* [go-isatty](https://github.com/mattn/go-isatty) available under [license](https://github.com/mattn/go-isatty/blob/master/LICENSE)
@ -114,22 +118,24 @@ Proton Mail Bridge includes the following 3rd party software:
* [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)
* [go-toml](https://github.com/pelletier/go-toml/v2) available under [license](https://github.com/pelletier/go-toml/v2/blob/master/LICENSE)
* [lz4](https://github.com/pierrec/lz4/v4) available under [license](https://github.com/pierrec/lz4/v4/blob/master/LICENSE)
* [go-difflib](https://github.com/pmezard/go-difflib) available under [license](https://github.com/pmezard/go-difflib/blob/master/LICENSE)
* [procfs](https://github.com/prometheus/procfs) available under [license](https://github.com/prometheus/procfs/blob/master/LICENSE)
* [uniseg](https://github.com/rivo/uniseg) available under [license](https://github.com/rivo/uniseg/blob/master/LICENSE)
* [blackfriday](https://github.com/russross/blackfriday/v2) available under [license](https://github.com/russross/blackfriday/v2/blob/master/LICENSE)
* [pflag](https://github.com/spf13/pflag) available under [license](https://github.com/spf13/pflag/blob/master/LICENSE)
* [bom](https://github.com/ssor/bom) available under [license](https://github.com/ssor/bom/blob/master/LICENSE)
* [golang-asm](https://github.com/twitchyliquid64/golang-asm) available under [license](https://github.com/twitchyliquid64/golang-asm/blob/master/LICENSE)
* [codec](https://github.com/ugorji/go/codec) available under [license](https://github.com/ugorji/go/codec/blob/master/LICENSE)
* [tagparser](https://github.com/vmihailenco/tagparser/v2) available under [license](https://github.com/vmihailenco/tagparser/v2/blob/master/LICENSE)
* [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.v2
gopkg.in/yaml.v3
* [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)

View File

@ -1,8 +1,273 @@
# Proton Mail Bridge and Import-Export app Changelog
# Proton Mail Bridge Changelog
Changelog [format](http://keepachangelog.com/en/1.0.0/)
## [Bridge 3.1.2] Quebec
### Changed
* GODT-2582 Dedup recovered messages folder.
## [Bridge 3.1.1] Quebec
### Fixed
* GODT-2500: Fix handler passing.
## [Bridge 3.1.0] Quebec
### Changed
* GODT-2523: Use software QML rendering backend by default on Windows.
* GODT-2500: Reorganise async methods.
* GODT-2500: Add panic handlers everywhere.
* GODT-2511: Add bridge-gui switches to permanently select the QML rendering backend.
* GODT-2509: Migrate TLS cert from v1/v2 location during upgrade to v3.
* GODT-2487: Add windows test job and worker.
* Update GPA to include detailed error messages.
* GODT-2479: Ensure messages always have a text body part.
* GODT-2482: More attachment to relevant exceptions.
* GODT-2224: Refactor bridge sync to use less memory.
* GODT-2448: Supported Answered flag.
* GODT-2382: Added bridge-gui settings file with 'UseSoftwareRenderer' value.
* GODT-2411: Allow qmake executable to be named qmake6.
* GODT-2273: Menu with "Close window" and "Quit Bridge" button in main window.
* GODT-2261: Sync progress in GUI.
* GODT-2385: Gluon cache fallback.
* GODT-2366: Handle failed message updates as creates.
* GODT-2201: Bump Gluon to use pure Go IMAP parser.
* GODT-2374: Import TLS certs via shell.
* GODT-2361: Bump GPA to use simple encrypter.
* GODT-1264: Constraint on Scheduled mailbox in connector + Integration tests.
* GODT-1264: Creation and visibility of the 'Scheduled' system label.
* GODT-2283: Limit max import size to 30MB (bump GPA to v0.4.0).
* GODT-2352: Only copy resource file when needed.
* GODT-2352: Use go-build-finalize macro to build vault-editor for both mac arch.
* GODT-2278: Properly override server_name for go.
* GODT-2255: Randomize the focus service port.
* GODT-2144: Handle IMAP/SMTP server errors via event stream.
* GODT-2144: Delay IMAP/SMTP server start until all users are loaded.
* GODT-2295: Notifications for IMAP login when signed out.
* GODT-2278: Improve sentry logs.
* GODT-2289: UIDValidity as Timestamp.
### Fixed
* GODT-2505: Show notification only for cases when user needs to do actions.
* GODT-2516: Log error when the vault key cannot be created/loaded from the keychain.
* GODT-2526: Fix high memory usage with fetch/search.
* GODT-2514: Apply Retry-After to 503.
* GODT-2524: Preserve old vault values.
* GODT-2508: Handle Address Updated for none-existing address.
* GODT-2504: Fix missing attachments in imported message.
* GODT-2513: Scanner Crash in Gluon.
* GODT-2512: Catch unhandled API errors.
* GODT-2507: Memory consumption bug.
* GODT-2497: Do not report EOF and network errors.
* GODT-2481: Fix DBUS Secert Service.
* GODT-2455: Upper limit for number of merged events.
* GODT-2480: Do not override X-Original-Date with invalid Date.
* GODT-2473: Fix handling of complex mime types.
* GODT-2469: Fix sentry revision hash for cmake on windows.
* GODT-2424: Sync Builder Message Split.
* GODT-2419: Use connector.ErrOperationNotAllowed.
* GODT-2418: Ensure child folders are updated when parent is.
* GODT-1945: Handle disabled addresses correctly.
* GODT-2390: Add reports for uncaught json and net.opErr.
* GODT-2393: Improved handling of unrecoverable error.
* GODT-2394: Bump Gluon for golang.org/x/text DoS risk.
* GODT-2387: Ensure vault can be unlocked after factory reset.
* GODT-2389: Close bridge on exception and add max termination wait time.
* GODT-2201: Add missing rfc5322.CharsetReader initialization.
* GODT-1804: Preserve MIME parameters when uploading attachments.
* GODT-2312: Used space is properly updated.
* GODT-2319: Seed the math/rand RNG on app startup.
* GODT-2272: Use shorter filename for gRPC file socket.
* GODT-2318: Remove gluon DB if label sync was incomplete.
* GODT-2326: Only run sync after addIMAPUser().
* GODT-2323: Fix Expunge not issued for move.
* GODT-2224: Properly handle context cancellation during sync.
* GODT-2328: Ignore labels that aren't part of user label set.
* GODT-2326: Fix potential Win32 API deadlock.
* GODT-1804: Only promote content headers if non-empty.
* GODT-2327: Remove unnecessary sync when changing address mode.
* GODT-2343: Only poll after send if sync is complete.
* GODT-2336: Recover from changed address order while bridge is down.
* GODT-2347: Prevent updates from being dropped if goroutine doesn't start fast.
* GODT-2351: Bump GPA to properly handle net.OpError and add tests.
* GODT-2351: Bump GPA to automatically retry on net.OpError.
* GODT-2365: Use predictable remote ID for placeholder mailboxes.
* GODT-2381: Unset draft flag on sent messages.
* GODT-2380: Only set external ID in header if non-empty.
## [Bridge 3.0.21] Perth Narrows
### Added
* GODT-2509: Migrate TLS cert from v1/v2 location during upgrade to v3.
### Changed
* GODT-2516: log error when the vault key cannot be created/loaded from the keychain.
### Fixed
* GODT-2501: Remove additional .desktop file.
* GODT-2513: Crash in scanner.
* GODT-2481: Fix DBUS Secert Service.
* GODT-2512: Catch unhandled API errors.
* GODT-2469: Fix sentry revision hash for cmake on windows.
## [Bridge 3.0.20] Perth Narrows
### Added
* GODT-2442: Allow user to re-sync DB without logout.
### Changed
* GODT-2419: Reduce sentry reports.
* GODT-2458: Wait for both bridge and bridge-gui to be ended before restarting on crash.
* GODT-2457: Include address if GetPublickKeys() error message.
* GODT-2446: Attach logs to sentry reports for relevant bridge-gui exceptions.
* GODT-2425: Out of sync messages and read status.
* GODT-2435: Group report exception by message if exception message looks corrupted.
* GODT-2356: Unify sentry release description and add more context to it.
* GODT-2357: Hide DSN_SENTRY and use single setting point for DSN_SENTRY.
* GODT-2444: Bad event info.
* GODT-2447: Don't assume timestamp exists in log filename.
* GODT-2333: Do not allow modifications to All Mail label.
* GODT-2429: Do not report context cancel to sentry.
### Fixed
* GODT-2467: elide long email adresses in 'bad event' QML notification dialog.
* GODT-2449: fix bug in Bridge-GUI's Exception::what().
* GODT-2427: Parsing header issues.
* GODT-2426: Fix crash on user delete.
* GODT-2417: Do not request gluon recovered message from API.
## [Bridge 3.0.19] Perth Narrows
### Fixed
* GODT-2364: Wait and retry once if the gRPC service config file exists but cannot be opened.
* GODT-2364: Added optional details to C++ exceptions.
* GODT-2413: Use qEnvironmentVariable() instead of qgetenv().
* GODT-2412: Don't treat context cancellation as BadEvent.
* GODT-2404: Handle unexpected EOF.
* GODT-2400: Allow state updates to be applied if command fails.
* GODT-2399: Fix immediate message deletion during updates.
* GODT-2390: Missing changes from pervious commit.
* GODT-2390: Add reports for uncaught json and net.opErr.
* GODT-2414: Multiple deletion bug in WriteControlledStore.
## [Bridge 3.0.18] Perth Narrows
### Fixed
* GODT-2392: Create message if gluon updateMessage returns `no such message`.
* GODT-2391: Create draft if missing during message update on gluon side.
## [Bridge 3.0.16/17] Perth Narrows
### Fixed
* GODT-2371: Continue, not return, when handling draft.
## [Bridge 3.0.15] Perth Narrows
### Changed
* GODT-2355: Improve wording and actions on bad event.
### Fixed
* GODT-2354: Report failed load users.
* GODT-2353: Show popup only after 3.0.16.
* GODT-2351: Bump GPA to better handle net.OpError.
## [Bridge 3.0.14] Perth Narrows
### Fixed
* GODT-2323: Fix Expunge not issued for move.
* GODT-2341: Handle URL error.
* GODT-2340: Improve logging.
* GODT-2278: Improve sentry logs.
* GODT-2327: Sync issues when migrating DB.
* GODT-2318: Remove gluon DB if label sync was incomplete.
* GODT-1804: Only promote content headers if non-empty.
* GODT-2343: Only poll after send if sync is complete.
* GODT-2336: Recover from changed address order while bridge is down.
## [Bridge 3.0.13] Perth Narrows
### Fixed
GODT-2328: Ignore labels that aren't part of user label set.
GODT-2326: Sync issue on missing fresh DB file.
GODT-2319: Seed the math/rand RNG on app startup.
GODT-1804: Preserve MIME parameters when uploading attachments.
## [Bridge 3.0.12] Perth Narrows
### Added
* GODT-2210: v3.0 splash screen.
* GODT-1770: handle UserBadEvent in CLI and gRPC.
### Changed
* GODT-2311: Fix missing headers in re-downloaded Gluon messages.
* GODT-1453: clicking 'Sign in' from status window now selects the right account.
* GODT-2297: More significantly improve GPA's paging algorithm.
* GODT-2145: Fix button spacing w/ Qt 6.4.
* GODT-2223: Improve event handling.
* GODT-2305: Detect missing gluon DB.
* GODT-2291: Change gluon store default location from Cache to Data.
* Other: Disable dialer test until badssl cert is bumbed.
* GODT-2292: Updated BUILDS.md doc.
* GODT-2258: suggest email as login when signing in via status window.
* Other: Report corrupt and/or insecure vaults to sentry.
* Other: Better user load logs.
* GODT-2253: Restart Launcher from the gui when GUI crashes.
* Other(test): Make All Mail copy test more robust.
* Other(CI): Make race checks manual.
* Other: Remove old cert/key file location handling.
* GODT-2271: Update README with new system files path.
### Fixed
* GODT-2210: Fix splash screen always showing on CentOS and Ubuntu.
* GODT-2296: Log error rather than fail if cannot get parent ID.
* GODT-2266: Pause event stream while sending.
* GODT-2266: Add test for sent message flags.
* Other(test): Fix some more integration test placeholders.
* GODT-2177: Use correct attachment disposition when content ID is set.
* GODT-1556: If no references, use the in-reply-to header as ParentID.
* Other: make GUI Tester more resilient to Bridge abrupt termination.
* GODT-2275: fixed location of bridge-gui log files.
* Other: Ensure SMTP debug dump works on windows.
* Other: Fix MaxLogs off-by-one limit and bump limit to 10.
* Other: fix path of temp folder in README.
* Other(debug): Dump raw SMTP input to user's home dir.
## [Bridge 3.0.11] Perth Narrows
### Changed
* GODT-2252: Recover from deleted cached messages.
* GODT-2258: change login label and suggest email instead of username.
* Other: Don't clean settings path on teardown.
* Other: Bump GPA to v0.3.0.
* Other: added user's primary email address to the vault.
* GODT-2251: gluon store and DB separated.
* GODT-2093: use the primary email address in the account view and status view.
* GODT-2202: Report update errors from Gluon.
* GODT-2229: Own the full path for gluon and do not change Database path.
* GODT-1797: copyright notice shows a date range with the build year.
### Fixed
* GODT-2223: Handle bad events by logging user out.
* GODT-2165: Reduce UTF8 parsing errors from TLS header input.
* Others: chores fix a QML warning when no account is present* and a few typos in QML.
* Other(test): Fix integration test steps.
* GODT-2226: Fix moving drafts to trash.
* GODT-2246: do not report API error 422 when using an invalid email address.
## [Bridge 3.0.10] Perth Narrows
### Changed

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.0.10+git
BRIDGE_APP_VERSION?=3.1.2+git
APP_VERSION:=${BRIDGE_APP_VERSION}
APP_FULL_NAME:=Proton Mail Bridge
APP_VENDOR:=Proton AG
@ -23,16 +23,21 @@ REVISION:=$(shell git rev-parse --short=10 HEAD)
BUILD_TIME:=$(shell date +%FT%T%z)
MACOS_MIN_VERSION_ARM64=11.0
MACOS_MIN_VERSION_AMD64=10.15
BUILD_ENV?=dev
BUILD_FLAGS:=-tags='${BUILD_TAGS}'
BUILD_FLAGS_LAUNCHER:=${BUILD_FLAGS}
BUILD_FLAGS_GUI:=-tags='${BUILD_TAGS} build_qt'
GO_LDFLAGS:=$(addprefix -X github.com/ProtonMail/proton-bridge/v3/internal/constants., Version=${APP_VERSION} Revision=${REVISION} BuildTime=${BUILD_TIME})
GO_LDFLAGS+=-X "github.com/ProtonMail/proton-bridge/v3/internal/constants.FullAppName=${APP_FULL_NAME}"
ifneq "${BUILD_LDFLAGS}" ""
GO_LDFLAGS+=${BUILD_LDFLAGS}
ifneq "${DSN_SENTRY}" ""
GO_LDFLAGS+=-X github.com/ProtonMail/proton-bridge/v3/internal/constants.DSNSentry=${DSN_SENTRY}
endif
ifneq "${BUILD_ENV}" ""
GO_LDFLAGS+=-X github.com/ProtonMail/proton-bridge/v3/internal/constants.BuildEnv=${BUILD_ENV}
endif
GO_LDFLAGS_LAUNCHER:=${GO_LDFLAGS}
ifeq "${TARGET_OS}" "windows"
#GO_LDFLAGS+=-H=windowsgui # Disabled so we can inspect trace logs from the bridge for debugging.
@ -40,7 +45,6 @@ ifeq "${TARGET_OS}" "windows"
endif
BUILD_FLAGS+=-ldflags '${GO_LDFLAGS}'
BUILD_FLAGS_GUI+=-ldflags "${GO_LDFLAGS}"
BUILD_FLAGS_LAUNCHER+=-ldflags '${GO_LDFLAGS_LAUNCHER}'
DEPLOY_DIR:=cmd/${TARGET_CMD}/deploy
DIRNAME:=$(shell basename ${CURDIR})
@ -96,9 +100,9 @@ endif
ifeq "${GOOS}" "windows"
go-build-finalize= \
powershell Copy-Item ${ROOT_DIR}/${RESOURCE_FILE} ${4} && \
$(call go-build,$(1),$(2),$(3)) && \
powershell Remove-Item ${4} -Force
$(if $(4),powershell Copy-Item ${ROOT_DIR}/${RESOURCE_FILE} ${4} &&,) \
$(call go-build,$(1),$(2),$(3)) \
$(if $(4), && powershell Remove-Item ${4} -Force,)
endif
${EXE_NAME}: gofiles ${RESOURCE_FILE}
@ -112,7 +116,7 @@ versioner:
go build ${BUILD_FLAGS} -o versioner utils/versioner/main.go
vault-editor:
go build -tags debug -o vault-editor utils/vault-editor/main.go
$(call go-build-finalize,"-tags=debug","vault-editor","./utils/vault-editor/main.go")
hasher:
go build -o hasher utils/hasher/main.go
@ -154,8 +158,10 @@ ${EXE_TARGET}: check-build-essentials ${EXE_NAME}
BRIDGE_VENDOR="${APP_VENDOR}" \
BRIDGE_APP_VERSION=${APP_VERSION} \
BRIDGE_REVISION=${REVISION} \
BRIDGE_BUILD_TIME=${BUILD_TIME} \
BRIDGE_DSN_SENTRY=${DSN_SENTRY} \
BRIDGE_BUILD_TIME=${BUILD_TIME} \
BRIDGE_GUI_BUILD_CONFIG=Release \
BRIDGE_BUILD_ENV=${BUILD_ENV} \
BRIDGE_INSTALL_PATH=${ROOT_DIR}/${DEPLOY_DIR}/${GOOS} \
./build.sh install
mv "${ROOT_DIR}/${BRIDGE_EXE}" "$(ROOT_DIR)/${EXE_TARGET}"
@ -222,13 +228,13 @@ change-copyright-year:
./utils/missing_license.sh change-year
test: gofiles
go test -v -timeout=5m -p=1 -count=1 -coverprofile=/tmp/coverage.out -run=${TESTRUN} ./internal/... ./pkg/...
go test -v -timeout=10m -p=1 -count=1 -coverprofile=/tmp/coverage.out -run=${TESTRUN} ./internal/... ./pkg/...
test-race: gofiles
go test -v -timeout=30m -p=1 -count=1 -race -failfast -run=${TESTRUN} ./internal/... ./pkg/...
test-integration: gofiles
go test -v -timeout=10m -p=1 -count=1 github.com/ProtonMail/proton-bridge/v3/tests
go test -v -timeout=20m -p=1 -count=1 github.com/ProtonMail/proton-bridge/v3/tests
test-integration-debug: gofiles
dlv test github.com/ProtonMail/proton-bridge/v3/tests -- -test.v -test.timeout=10m -test.parallel=1 -test.count=1
@ -247,7 +253,7 @@ coverage: test
mocks:
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/bridge TLSReporter,ProxyController,Autostarter > tmp
mv tmp internal/bridge/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/async PanicHandler > internal/bridge/mocks/async_mocks.go
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
@ -294,7 +300,7 @@ gofiles: ./internal/bridge/credits.go
cd ./utils/ && ./credits.sh bridge
## Run and debug
.PHONY: run run-qt run-qt-cli run-nogui run-cli run-noninteractive run-debug run-qml-preview clean-vendor clean-frontend-qt clean-frontend-qt-common clean
.PHONY: run run-qt run-qt-cli run-nogui run-cli run-noninteractive run-debug run-gui-tester clean-vendor clean-frontend-qt clean-frontend-qt-common clean
LOG?=debug
LOG_IMAP?=client # client/server/all, or empty to turn it off
@ -319,14 +325,32 @@ run-nogui: build-nogui clean-vendor gofiles
PROTONMAIL_ENV=dev ./${LAUNCHER_EXE} ${RUN_FLAGS} -c
run-debug:
dlv debug ./cmd/Desktop-Bridge/main.go -- -l=debug
dlv debug \
--build-flags "-ldflags '-X github.com/ProtonMail/proton-bridge/v3/internal/constants.Version=3.1.0+git'" \
./cmd/Desktop-Bridge/main.go \
-- \
-n -l=trace
ifeq "${TARGET_OS}" "windows"
EXE_SUFFIX=.exe
endif
bridge-gui-tester: build-gui
cp ./cmd/Desktop-Bridge/deploy/${TARGET_OS}/bridge-gui${EXE_SUFFIX} .
cd ./internal/frontend/bridge-gui/bridge-gui-tester && cmake . && make
run-gui-tester: bridge-gui-tester
# copying tester as bridge so bridge-gui will start it and connect to it automatically
cp ./internal/frontend/bridge-gui/bridge-gui-tester/bridge-gui-tester${EXE_SUFFIX} bridge${EXE_SUFFIX}
./bridge-gui${EXE_SUFFIX}
clean-vendor:
rm -rf ./vendor
clean-gui:
cd internal/frontend/bridge-gui/ && \
rm -f Version.h && \
rm -f BuildConfig.h && \
rm -rf cmake-build-*/
clean-vcpkg:
@ -349,6 +373,6 @@ clean: clean-vendor clean-gui clean-vcpkg
.PHONY: generate
generate:
go generate ./...
$(MAKE) add-license
$(MAKE) build
.FORCE:

View File

@ -1,5 +1,5 @@
# Proton Mail Bridge and Import Export app
Copyright (c) 2022 Proton AG
Copyright (c) 2023 Proton AG
This repository holds the Proton Mail Bridge and the Proton Mail Import-Export applications.
For a detailed build information see [BUILDS](./BUILDS.md).
@ -48,9 +48,6 @@ major problems.
## Environment Variables
### Bridge application
- `BRIDGESTRICTMODE`: tells bridge to turn on `bbolt`'s "strict mode" which checks the database after every `Commit`. Set to `1` to enable.
### Dev build or run
- `APP_VERSION`: set the bridge app version used during testing or building
- `PROTONMAIL_ENV`: when set to `dev` it is not using Sentry to report crashes
@ -62,35 +59,34 @@ major problems.
- `TAGS`: set build tags for tests
- `FEATURES`: set feature dir, file or scenario to test
## Folders
There are now three types of system folders which Bridge recognises:
| | Windows | Mac | Linux | Linux (XDG) |
|--------|-------------------------------------|-----------------------------------------------------|-------------------------------------|---------------------------------------|
| config | %APPDATA%\protonmail\bridge-v3 | ~/Library/Application Support/protonmail/bridge-v3 | ~/.config/protonmail/bridge-v3 | $XDG_CONFIG_HOME/protonmail/bridge-v3 |
| cache | %LOCALAPPDATA%\protonmail\bridge-v3 | ~/Library/Caches/protonmail/bridge-v3 | ~/.cache/protonmail/bridge-v3 | $XDG_CACHE_HOME/protonmail/bridge-v3 |
| data | %APPDATA%\protonmail\bridge-v3 | ~/Library/Application Support/protonmail/bridge-v3 | ~/.local/share/protonmail/bridge-v3 | $XDG_DATA_HOME/protonmail/bridge-v3 |
| temp | %LOCALAPPDATA%\Temp | $TMPDIR if non-empty, else /tmp | $TMPDIR if non-empty, else /tmp | $TMPDIR if non-empty, else /tmp |
## Files
### Database
The database stores metadata necessary for presenting messages and mailboxes to an email client:
- Linux: `~/.cache/protonmail/bridge/<cacheVersion>/mailbox-<userID>.db` (unless `XDG_CACHE_HOME` is set, in which case that is used as your `~`)
- macOS: `~/Library/Caches/protonmail/bridge/<cacheVersion>/mailbox-<userID>.db`
- Windows: `%LOCALAPPDATA%\protonmail\bridge\<cacheVersion>\mailbox-<userID>.db`
### Preferences
User preferences are stored in json at the following location:
- Linux: `~/.config/protonmail/bridge/prefs.json`
- macOS: `~/Library/ApplicationSupport/protonmail/bridge/prefs.json`
- Windows: `%APPDATA%\protonmail\bridge\prefs.json`
| | Base Dir | Path |
|------------------------|----------|----------------------------|
| bridge lock file | cache | bridge.lock |
| bridge-gui lock file | cache | bridge-gui.lock |
| vault | config | vault.enc |
| gRPC server json | config | grpcServerConfig.json |
| gRPC client json | config | grpcClientConfig_<id>.json |
| gRPC Focus server json | config | grpcFocusServerConfig.json |
| Logs | data | logs |
| gluon DB | data | gluon/backend/db |
| gluon messages | data | gluon/backend/store |
| Update files | data | updates |
| sentry cache | data | sentry_cache |
| Mac/Linux File Socket | temp | bridge{4_DIGITS} |
### IMAP Cache
The currently subscribed mailboxes are held in a json file:
- Linux: `~/.cache/protonmail/bridge/<cacheVersion>/user_info.json` (unless `XDG_CACHE_HOME` is set, in which case that is used as your `~`)
- macOS: `~/Library/Caches/protonmail/bridge/<cacheVersion>/user_info.json`
- Windows: `%LOCALAPPDATA%\protonmail\bridge\<cacheVersion>\user_info.json`
### Lock file
Bridge utilises an on-disk lock to ensure only one instance is run at once. The lock file is here:
- Linux: `~/.cache/protonmail/bridge/<cacheVersion>/bridge.lock` (unless `XDG_CACHE_HOME` is set, in which case that is used as your `~`)
- macOS: `~/Library/Caches/protonmail/bridge/<cacheVersion>/bridge.lock`
- Windows: `%LOCALAPPDATA%\protonmail\bridge\<cacheVersion>\bridge.lock`
### TLS Certificate and Key
When bridge first starts, it generates a unique TLS certificate and key file at the following locations:
- Linux: `~/.config/protonmail/bridge/{cert,key}.pem` (unless `XDG_CONFIG_HOME` is set, in which case that is used as your `~/.config`)
- macOS: `~/Library/ApplicationSupport/protonmail/bridge/{cert,key}.pem`
- Windows: `%APPDATA%\protonmail\bridge\{cert,key}.pem`

View File

@ -25,6 +25,7 @@ import (
"time"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/crash"
@ -59,10 +60,10 @@ func main() { //nolint:funlen
logrus.SetLevel(logrus.DebugLevel)
l := logrus.WithField("launcher_version", constants.Version)
reporter := sentry.NewReporter(appName, constants.Version, useragent.New())
reporter := sentry.NewReporter(appName, useragent.New())
crashHandler := crash.NewHandler(reporter.ReportException)
defer crashHandler.HandlePanic()
defer async.HandlePanic(crashHandler)
locationsProvider, err := locations.NewDefaultProvider(filepath.Join(constants.VendorName, constants.ConfigName))
if err != nil {
@ -127,9 +128,11 @@ func main() { //nolint:funlen
l = l.WithField("exe_path", exe)
args, wait, mainExe := findAndStripWait(args)
args, wait, mainExes := findAndStripWait(args)
if wait {
waitForProcessToFinish(mainExe)
for _, mainExe := range mainExes {
waitForProcessToFinish(mainExe)
}
}
cmd := execabs.Command(exe, appendLauncherPath(launcher, args)...) //nolint:gosec
@ -186,12 +189,11 @@ func findAndStrip[T comparable](slice []T, v T) (strippedList []T, found bool) {
}
// findAndStripWait Check for waiter flag get its value and clean them both.
func findAndStripWait(args []string) ([]string, bool, string) {
func findAndStripWait(args []string) ([]string, bool, []string) {
res := append([]string{}, args...)
hasFlag := false
var value string
values := make([]string, 0)
for k, v := range res {
if v != FlagWait {
continue
@ -200,14 +202,16 @@ func findAndStripWait(args []string) ([]string, bool, string) {
continue
}
hasFlag = true
value = res[k+1]
values = append(values, res[k+1])
}
if hasFlag {
res, _ = findAndStrip(res, FlagWait)
res, _ = findAndStrip(res, value)
for _, v := range values {
res, _ = findAndStrip(res, v)
}
}
return res, hasFlag, value
return res, hasFlag, values
}
func getPathToUpdatedExecutable(

View File

@ -56,3 +56,25 @@ func TestFindAndStrip(t *testing.T) {
assert.False(t, found)
assert.True(t, xslices.Equal(result, []string{}))
}
func TestFindAndStripWait(t *testing.T) {
result, found, values := findAndStripWait([]string{"a", "b", "c"})
assert.False(t, found)
assert.True(t, xslices.Equal(result, []string{"a", "b", "c"}))
assert.True(t, xslices.Equal(values, []string{}))
result, found, values = findAndStripWait([]string{"a", "--wait", "b"})
assert.True(t, found)
assert.True(t, xslices.Equal(result, []string{"a"}))
assert.True(t, xslices.Equal(values, []string{"b"}))
result, found, values = findAndStripWait([]string{"a", "--wait", "b", "--wait", "c"})
assert.True(t, found)
assert.True(t, xslices.Equal(result, []string{"a"}))
assert.True(t, xslices.Equal(values, []string{"b", "c"}))
result, found, values = findAndStripWait([]string{"a", "--wait", "b", "--wait", "c", "--wait", "d"})
assert.True(t, found)
assert.True(t, xslices.Equal(result, []string{"a"}))
assert.True(t, xslices.Equal(values, []string{"b", "c", "d"}))
}

86
go.mod
View File

@ -4,21 +4,20 @@ go 1.18
require (
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
github.com/Masterminds/semver/v3 v3.1.1
github.com/ProtonMail/gluon v0.14.2-0.20230106095250-7e99ea4da61e
github.com/Masterminds/semver/v3 v3.2.0
github.com/ProtonMail/gluon v0.15.1-0.20230426114936-a356ef7f2753
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
github.com/ProtonMail/go-proton-api v0.2.4-0.20230109143101-f8fd857ee5b4
github.com/ProtonMail/go-rfc5322 v0.11.0
github.com/ProtonMail/gopenpgp/v2 v2.4.10
github.com/PuerkitoBio/goquery v1.8.0
github.com/ProtonMail/go-proton-api v0.4.1-0.20230406143739-c7596e170799
github.com/ProtonMail/gopenpgp/v2 v2.5.2
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.8.0
github.com/bradenaw/juniper v0.10.2
github.com/cucumber/godog v0.12.5
github.com/cucumber/messages-go/v16 v16.0.1
github.com/docker/docker-credential-helpers v0.6.3
github.com/elastic/go-sysinfo v1.8.1
github.com/emersion/go-imap v1.2.1-0.20220429085312-746087b7a317
github.com/emersion/go-imap v1.2.1
github.com/emersion/go-imap-id v0.0.0-20190926060100-f94a56b9ecde
github.com/emersion/go-message v0.16.0
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead
@ -26,7 +25,7 @@ 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.9.11
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
@ -35,36 +34,38 @@ require (
github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba
github.com/keybase/go-keychain v0.0.0
github.com/miekg/dns v1.1.50
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58
github.com/pkg/errors v0.9.1
github.com/pkg/profile v1.6.0
github.com/pkg/profile v1.7.0
github.com/sirupsen/logrus v1.9.0
github.com/stretchr/testify v1.8.0
github.com/urfave/cli/v2 v2.20.3
github.com/stretchr/testify v1.8.1
github.com/urfave/cli/v2 v2.24.4
github.com/vmihailenco/msgpack/v5 v5.3.5
go.uber.org/goleak v1.2.0
golang.org/x/exp v0.0.0-20221023144134-a1e5550cf13e
golang.org/x/net v0.1.0
golang.org/x/sys v0.1.0
golang.org/x/text v0.4.0
google.golang.org/grpc v1.50.1
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
google.golang.org/grpc v1.53.0
google.golang.org/protobuf v1.28.1
howett.net/plist v1.0.0
)
require (
ariga.io/atlas v0.7.0 // indirect
entgo.io/ent v0.11.2 // indirect
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-20220824120805-4b6e5c587895 // indirect
github.com/ProtonMail/go-mime v0.0.0-20220429130430-2192574d760f // 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/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/antlr/antlr4/runtime/Go/antlr v1.4.10 // indirect
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
github.com/bytedance/sonic v1.8.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.2.0 // indirect
github.com/cloudflare/circl v1.3.2 // 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,48 +74,53 @@ require (
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/felixge/fgprof v0.9.3 // indirect
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.8.1 // indirect
github.com/gin-gonic/gin v1.9.0 // indirect
github.com/go-openapi/inflect v0.19.0 // indirect
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.11.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/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
github.com/hashicorp/errwrap v1.1.0 // indirect
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.14.0 // 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/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/mattn/go-sqlite3 v1.14.15 // indirect
github.com/mattn/go-sqlite3 v1.14.16 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // 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.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // 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
github.com/rivo/uniseg v0.4.2 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/ugorji/go/codec v1.2.7 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.10 // 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.11.0 // indirect
golang.org/x/crypto v0.1.0 // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/sync v0.0.0-20220907140024-f12130a52804 // indirect
golang.org/x/tools v0.1.13-0.20220804200503-81c7dc4e4efa // indirect
google.golang.org/genproto v0.0.0-20220921223823-23cae91e6737 // indirect
gopkg.in/yaml.v2 v2.4.0 // 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/mod v0.8.0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/tools v0.3.1-0.20221202221704-aa9f4b2f3d57 // indirect
google.golang.org/genproto v0.0.0-20230221151758-ace64dc21148 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

223
go.sum
View File

@ -1,5 +1,5 @@
ariga.io/atlas v0.7.0 h1:daEFdUsyNm7EHyzcMfjWwq/fVv48fCfad+dIGyobY1k=
ariga.io/atlas v0.7.0/go.mod h1:ft47uSh5hWGDCmQC9DsztZg6Xk+KagM5Ts/mZYKb9JE=
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,44 +13,41 @@ 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.2 h1:UM2/BUhF2FfsxPHRxLjQbhqJNaDdVlOwNIAMLs2jyto=
entgo.io/ent v0.11.2/go.mod h1:YGHEQnmmIUgtD5b1ICD5vg74dS3npkNnmC5K+0J+IHU=
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.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
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=
github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug=
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
github.com/ProtonMail/gluon v0.14.2-0.20230106095250-7e99ea4da61e h1://xRNjGTAMXw2U91MtqPc4krUtxQmt2+4z1oYrBaOWU=
github.com/ProtonMail/gluon v0.14.2-0.20230106095250-7e99ea4da61e/go.mod h1:z2AxLIiBCT1K+0OBHyaDI7AEaO5qI6/BEC2TE42vs4Q=
github.com/ProtonMail/gluon v0.15.1-0.20230426114936-a356ef7f2753 h1:Ae/Ny7Fm+8RS/L+/9CiT6ntoPFSIJLyrLbD5bI77il4=
github.com/ProtonMail/gluon v0.15.1-0.20230426114936-a356ef7f2753/go.mod h1:yA4hk6CJw0BMo+YL8Y3ckCYs5L20sysu9xseshwY3QI=
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-20220822140716-1678d6eb0cbe/go.mod h1:UBYPn8k0D56RtnR8RFQMjmh4KrZzWJ5o7Z9SYjossQ8=
github.com/ProtonMail/go-crypto v0.0.0-20220824120805-4b6e5c587895 h1:NsReiLpErIPzRrnogAXYwSoU7txA977LjDGrbkewJbg=
github.com/ProtonMail/go-crypto v0.0.0-20220824120805-4b6e5c587895/go.mod h1:UBYPn8k0D56RtnR8RFQMjmh4KrZzWJ5o7Z9SYjossQ8=
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-20220302105931-303f85f7fe0f/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4=
github.com/ProtonMail/go-mime v0.0.0-20220429130430-2192574d760f h1:4IWzKjHzZxdrW9k4zl/qCwenOVHDbVDADPPHFLjs0Oc=
github.com/ProtonMail/go-mime v0.0.0-20220429130430-2192574d760f/go.mod h1:qRZgbeASl2a9OwmsV85aWwRqic0NHPh+9ewGAzb4cgM=
github.com/ProtonMail/go-proton-api v0.2.4-0.20230109143101-f8fd857ee5b4 h1:xCot3copmyPz0cDOwl1XVmYQDRJGi6EgJUKJ58Vn58U=
github.com/ProtonMail/go-proton-api v0.2.4-0.20230109143101-f8fd857ee5b4/go.mod h1:JUo5IQG0hNuPRuDpOUsCOvtee6UjTEHHF1QN2i8RSos=
github.com/ProtonMail/go-rfc5322 v0.11.0 h1:o5Obrm4DpmQEffvgsVqG6S4BKwC1Wat+hYwjIp2YcCY=
github.com/ProtonMail/go-rfc5322 v0.11.0/go.mod h1:6oOKr0jXvpoE6pwTx/HukigQpX2J9WUf6h0auplrFTw=
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.4.10 h1:EYgkxzwmQvsa6kxxkgP1AwzkFqKHscF2UINxaSn6rdI=
github.com/ProtonMail/gopenpgp/v2 v2.4.10/go.mod h1:CTRA7/toc/4DxDy5Du4hPDnIZnJvXSeQ8LsRTOUJoyc=
github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U=
github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI=
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/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=
@ -63,9 +60,6 @@ github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37 h1:2
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/antlr/antlr4/runtime/Go/antlr v0.0.0-20220816024939-bc8df83d7b9d/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY=
github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 h1:yL7+Jz0jTC6yykIK/Wh74gnTJnrGr5AyrNMXuA0gves=
github.com/antlr/antlr4/runtime/Go/antlr v1.4.10/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY=
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/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
@ -75,19 +69,27 @@ 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.8.0 h1:sdanLNdJbLjcLj993VYIwUHlUVkLzvgiD/x9O7cvvxk=
github.com/bradenaw/juniper v0.8.0/go.mod h1:Z2B7aJlQ7xbfWsnMLROj5t/5FQ94/MkIdKC30J4WvzI=
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/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/bwesterb/go-ristretto v1.2.1/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/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=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
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.2.0 h1:NheeISPSUcYftKlfrLuOo4T62FkmD4t4jviLfFFYaec=
github.com/cloudflare/circl v1.2.0/go.mod h1:Ch2UgYr6ti2KTtlejELlROl0YIYj7SLjAC8M+INXlMk=
github.com/cloudflare/circl v1.3.2 h1:VWp8dY3yH69fdM7lM6A1+NhhVoDu9vqK0jOgmkQHFWk=
github.com/cloudflare/circl v1.3.2/go.mod h1:+CauBF6R70Jqcyl8N2hC8pAXYbWkGIezuSbuGLtRhnw=
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=
@ -96,7 +98,6 @@ github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfc
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cronokirby/saferith v0.33.0 h1:TgoQlfsD4LIwx71+ChfRcIpjkw+RPOapDEVxa+LhwLo=
github.com/cronokirby/saferith v0.33.0/go.mod h1:QKJhjoqUtBsXCAVEjw38mFqoi7DebT7kthcD7UzbnoA=
github.com/cucumber/gherkin-go/v19 v19.0.3 h1:mMSKu1077ffLbTJULUfM5HPokgeBcIGboyeNUof1MdE=
@ -120,8 +121,8 @@ github.com/elastic/go-sysinfo v1.8.1 h1:4Yhj+HdV6WjbCRgGdZpPJ8lZQlXZLKDAeIkmQ/VR
github.com/elastic/go-sysinfo v1.8.1/go.mod h1:JfllUnzoQV/JRYymbH3dO1yggI3mV2oTKSXsDHM+uIM=
github.com/elastic/go-windows v1.0.1 h1:AlYZOldA+UJ0/2nBuqWdo90GFCgG9xuyw9SYzGUtJm0=
github.com/elastic/go-windows v1.0.1/go.mod h1:FoVvqWSun28vaDQPbj2Elfc0JahhPB7WQEGa3c814Ss=
github.com/emersion/go-imap v1.2.1-0.20220429085312-746087b7a317 h1:i0cBrdFLm8A/3hWEjn/BwdXLBplFJoZtu63p7bjrmaI=
github.com/emersion/go-imap v1.2.1-0.20220429085312-746087b7a317/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY=
github.com/emersion/go-imap v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA=
github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY=
github.com/emersion/go-imap-id v0.0.0-20190926060100-f94a56b9ecde h1:43mBoVwooyLm1+1YVf5nvn1pSFWhw7rOpcrp1Jg/qk0=
github.com/emersion/go-imap-id v0.0.0-20190926060100-f94a56b9ecde/go.mod h1:sPwp0FFboaK/bxsrUz1lNrDMUCsZUsKC5YuM4uRVRVs=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
@ -136,6 +137,8 @@ github.com/emersion/go-vcard v0.0.0-20220507122617-d4056df0ec4a/go.mod h1:HMJKR5
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=
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
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=
@ -144,8 +147,8 @@ github.com/getsentry/sentry-go v0.15.0/go.mod h1:RZPJKSw+adu8PBNygiri/A98FqVr2Ht
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.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8=
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
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/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=
@ -153,20 +156,19 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9
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.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ=
github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
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-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.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk=
github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
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/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=
@ -198,6 +200,8 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@ -239,12 +243,13 @@ 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.14.0 h1:jX6+Q38Ly9zaAJlAjnFVyeNSNCKKW8D0wvyg7vij5Wc=
github.com/hashicorp/hcl/v2 v2.14.0/go.mod h1:e4z5nxYlWNPdDSNYX+ph14EvWYMFm3eP0zIUqPc2jr0=
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=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba h1:QFQpJdgbON7I0jr2hYW7Bs+XV0qjc3d5tZoDnRFnqTg=
github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
@ -261,16 +266,17 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
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/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
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=
@ -282,13 +288,14 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
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 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
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-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.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
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/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=
@ -314,17 +321,20 @@ github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg=
github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas=
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/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=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.6.0 h1:hUDfIISABYI59DyeB3OTay/HxSRwTQ8rB/H83k6r5dM=
github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18=
github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
@ -344,9 +354,7 @@ 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.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
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=
@ -355,7 +363,6 @@ github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg
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.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
@ -376,24 +383,26 @@ github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cma
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
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/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/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/urfave/cli/v2 v2.20.3 h1:lOgGidH/N5loaigd9HjFsOIhXSTrzl7tBpHswZ428w4=
github.com/urfave/cli/v2 v2.20.3/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI=
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/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=
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
@ -402,16 +411,20 @@ github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/zclconf/go-cty v1.11.0 h1:726SxLdi2SDnjY+BStqB9J1hNp4+2WlzyXLuimibIe0=
github.com/zclconf/go-cty v1.11.0/go.mod h1:s9IfD1LK5ccNMSWCVFCE2rJfHiZgi7JijgeWIMfhLvA=
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=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
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/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=
@ -420,18 +433,16 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
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.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
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/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-20221023144134-a1e5550cf13e h1:SkwG94eNiiYJhbeDE018Grw09HIN/KB9NlRmZsrzfWs=
golang.org/x/exp v0.0.0-20221023144134-a1e5550cf13e/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
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/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=
@ -440,18 +451,16 @@ golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTk
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
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-20200801112145-973feb4309de/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4=
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.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -471,9 +480,9 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
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.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
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=
@ -483,8 +492,10 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220907140024-f12130a52804 h1:0SH2R3f1b1VmIMG7BXbEZCBUu2dKmHschSmjqGUrW8A=
golang.org/x/sync v0.0.0-20220907140024-f12130a52804/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/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=
@ -494,7 +505,6 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -508,15 +518,18 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
golang.org/x/sys v0.1.0/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/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/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=
@ -524,8 +537,8 @@ 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.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
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/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=
@ -546,11 +559,11 @@ golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
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.13-0.20220804200503-81c7dc4e4efa h1:uKcci2q7Qtp6nMTC/AAvfNUAldFtJuHWV9/5QWiypts=
golang.org/x/tools v0.1.13-0.20220804200503-81c7dc4e4efa/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
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/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=
@ -573,13 +586,13 @@ google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20220921223823-23cae91e6737 h1:K1zaaMdYBXRyX+cwFnxj7M6zwDyumLQMZ5xqwGvjreQ=
google.golang.org/genproto v0.0.0-20220921223823-23cae91e6737/go.mod h1:2r/26NEF3bFmT3eC3aZreahSal0C3Shl8Gi6vyDYqOQ=
google.golang.org/genproto v0.0.0-20230221151758-ace64dc21148 h1:muK+gVBJBfFb4SejshDBlN2/UgxCCOKH9Y34ljqEGOc=
google.golang.org/genproto v0.0.0-20230221151758-ace64dc21148/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.50.1 h1:DS/BukOZWp8s6p4Dt/tOaJaTQyPyOoCcrjroHuCeLzY=
google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
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=
@ -598,11 +611,8 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
@ -612,3 +622,4 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@ -19,13 +19,16 @@ package app
import (
"fmt"
"math/rand"
"net/http"
"net/http/cookiejar"
"os"
"path/filepath"
"runtime"
"time"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/cookies"
@ -79,7 +82,7 @@ const (
appUsage = "Proton Mail IMAP and SMTP Bridge"
)
func New() *cli.App { //nolint:funlen
func New() *cli.App {
app := cli.NewApp()
app.Name = constants.FullAppName
@ -154,7 +157,10 @@ func New() *cli.App { //nolint:funlen
return app
}
func run(c *cli.Context) error { //nolint:funlen
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 {
@ -165,7 +171,7 @@ func run(c *cli.Context) error { //nolint:funlen
identifier := useragent.New()
// Create a new Sentry client that will be used to report crashes etc.
reporter := sentry.NewReporter(constants.FullAppName, constants.Version, identifier)
reporter := sentry.NewReporter(constants.FullAppName, identifier)
// Determine the exe that should be used to restart/autostart the app.
// By default, this is the launcher, if used. Otherwise, we try to get
@ -180,14 +186,14 @@ func run(c *cli.Context) error { //nolint:funlen
exe = os.Args[0]
}
migrationErr := migrateOldVersions()
// Restart the app if requested.
return withRestarter(exe, func(restarter *restarter.Restarter) error {
// Handle crashes with various actions.
return withCrashHandler(restarter, reporter, func(crashHandler *crash.Handler, quitCh <-chan struct{}) error {
migrationErr := migrateOldVersions()
// Run with profiling if requested.
return withProfiler(c, func() error {
// Restart the app if requested.
return withRestarter(exe, func(restarter *restarter.Restarter) error {
// Handle crashes with various actions.
return withCrashHandler(restarter, reporter, func(crashHandler *crash.Handler, quitCh <-chan struct{}) error {
// Run with profiling if requested.
return withProfiler(c, func() error {
// Load the locations where we store our files.
return WithLocations(func(locations *locations.Locations) error {
// Migrate the keychain helper.
@ -203,30 +209,45 @@ func run(c *cli.Context) error { //nolint:funlen
}
// Ensure we are the only instance running.
return withSingleInstance(locations, version, func() error {
settings, err := locations.ProvideSettingsPath()
if err != nil {
logrus.WithError(err).Error("Failed to get settings path")
}
return withSingleInstance(settings, locations.GetLockFile(), version, func() error {
// Unlock the encrypted vault.
return WithVault(locations, func(vault *vault.Vault, insecure, corrupt bool) error {
if !vault.Migrated() {
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(vault); err != nil {
if err := migrateOldSettings(v); err != nil {
logrus.WithError(err).Error("Failed to migrate old settings")
}
// Migrate old accounts into the vault.
if err := migrateOldAccounts(locations, vault); err != nil {
if err := migrateOldAccounts(locations, v); err != nil {
logrus.WithError(err).Error("Failed to migrate old accounts")
}
// The vault has been migrated.
if err := vault.SetMigrated(); err != nil {
if err := v.SetMigrated(); err != nil {
logrus.WithError(err).Error("Failed to mark vault as migrated")
}
}
// Load the cookies from the vault.
return withCookieJar(vault, func(cookieJar http.CookieJar) error {
return withCookieJar(v, func(cookieJar http.CookieJar) error {
// Create a new bridge instance.
return withBridge(c, exe, locations, version, identifier, crashHandler, reporter, vault, cookieJar, func(b *bridge.Bridge, eventCh <-chan events.Event) error {
return withBridge(c, exe, locations, version, identifier, crashHandler, reporter, v, cookieJar, func(b *bridge.Bridge, eventCh <-chan events.Event) error {
if insecure {
logrus.Warn("The vault key could not be retrieved; the vault will not be encrypted")
b.PushError(bridge.ErrVaultInsecure)
@ -251,15 +272,15 @@ func run(c *cli.Context) error { //nolint:funlen
}
// If there's another instance already running, try to raise it and exit.
func withSingleInstance(locations *locations.Locations, version *semver.Version, fn func() error) error {
func withSingleInstance(settingPath, lockFile string, version *semver.Version, fn func() error) error {
logrus.Debug("Checking for other instances")
defer logrus.Debug("Single instance stopped")
lock, err := checkSingleInstance(locations.GetLockFile(), version)
lock, err := checkSingleInstance(settingPath, lockFile, version)
if err != nil {
logrus.Info("Another instance is already running; raising it")
if ok := focus.TryRaise(); !ok {
if ok := focus.TryRaise(settingPath); !ok {
return fmt.Errorf("another instance is already running but it could not be raised")
}
@ -322,14 +343,7 @@ func WithLocations(fn func(*locations.Locations) error) error {
}
// Create a new locations object that will be used to provide paths to store files.
locations := locations.New(provider, constants.ConfigName)
defer func() {
if err := locations.Clean(); err != nil {
logrus.WithError(err).Error("Failed to clean locations")
}
}()
return fn(locations)
return fn(locations.New(provider, constants.ConfigName))
}
// Start profiling if requested.
@ -366,7 +380,7 @@ func withCrashHandler(restarter *restarter.Restarter, reporter *sentry.Reporter,
defer logrus.Debug("Crash handler stopped")
crashHandler := crash.NewHandler(crash.ShowErrorNotification(constants.FullAppName))
defer crashHandler.HandlePanic()
defer async.HandlePanic(crashHandler)
// On crash, send crash report to Sentry.
crashHandler.AddRecoveryAction(reporter.ReportException)

View File

@ -23,6 +23,7 @@ import (
"runtime"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/gluon/imap"
"github.com/ProtonMail/go-autostart"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
@ -40,13 +41,11 @@ import (
"github.com/urfave/cli/v2"
)
const vaultSecretName = "bridge-vault-key"
// 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.
func withBridge( //nolint:funlen
func withBridge(
c *cli.Context,
exe string,
locations *locations.Locations,
@ -79,7 +78,7 @@ func withBridge( //nolint:funlen
)
// Create a proxy dialer which switches to a proxy if the request fails.
proxyDialer := dialer.NewProxyTLSDialer(pinningDialer, constants.APIHost)
proxyDialer := dialer.NewProxyTLSDialer(pinningDialer, constants.APIHost, crashHandler)
// Create the autostarter.
autostarter := newAutostarter(exe)
@ -110,6 +109,7 @@ func withBridge( //nolint:funlen
// Crash and report stuff
crashHandler,
reporter,
imap.DefaultEpochUIDValidityGenerator(),
// The logging stuff.
c.String(flagLogIMAP) == "client" || c.String(flagLogIMAP) == "all",

View File

@ -46,10 +46,11 @@ func runFrontend(
switch {
case c.Bool(flagCLI):
return bridgeCLI.New(bridge, restarter, eventCh).Loop()
return bridgeCLI.New(bridge, restarter, eventCh, crashHandler, quitCh).Loop()
case c.Bool(flagNonInteractive):
select {}
<-quitCh
return nil
case c.Bool(flagGRPC):
service, err := grpc.NewService(crashHandler, restarter, locations, bridge, eventCh, quitCh, !c.Bool(flagNoWindow), parentPID)

View File

@ -87,6 +87,11 @@ func migrateOldSettings(v *vault.Vault) error {
return fmt.Errorf("failed to get user config dir: %w", err)
}
return migrateOldSettingsWithDir(configDir, v)
}
// nolint:gosec
func migrateOldSettingsWithDir(configDir string, v *vault.Vault) error {
b, err := os.ReadFile(filepath.Join(configDir, "protonmail", "bridge", "prefs.json"))
if errors.Is(err, fs.ErrNotExist) {
return nil
@ -94,7 +99,27 @@ func migrateOldSettings(v *vault.Vault) error {
return fmt.Errorf("failed to read old prefs file: %w", err)
}
return migratePrefsToVault(v, b)
if err := migratePrefsToVault(v, b); err != nil {
return fmt.Errorf("failed to migrate prefs to vault: %w", err)
}
logrus.Info("Migrating TLS certificate")
certPEM, err := os.ReadFile(filepath.Join(configDir, "protonmail", "bridge", "cert.pem"))
if errors.Is(err, fs.ErrNotExist) {
return nil
} else if err != nil {
return fmt.Errorf("failed to read old cert file: %w", err)
}
keyPEM, err := os.ReadFile(filepath.Join(configDir, "protonmail", "bridge", "key.pem"))
if errors.Is(err, fs.ErrNotExist) {
return nil
} else if err != nil {
return fmt.Errorf("failed to read old key file: %w", err)
}
return v.SetBridgeTLSCertKey(certPEM, keyPEM)
}
func migrateOldAccounts(locations *locations.Locations, v *vault.Vault) error {
@ -147,7 +172,12 @@ func migrateOldAccount(userID string, store *credentials.Store, v *vault.Vault)
return fmt.Errorf("failed to split api token for user %q: %w", userID, err)
}
user, err := v.AddUser(creds.UserID, creds.Name, authUID, authRef, creds.MailboxPassword)
var primaryEmail string
if len(creds.EmailList()) > 0 {
primaryEmail = creds.EmailList()[0]
}
user, err := v.AddUser(creds.UserID, creds.Name, primaryEmail, authUID, authRef, creds.MailboxPassword)
if err != nil {
return fmt.Errorf("failed to add user %q: %w", userID, err)
}
@ -182,7 +212,6 @@ func migrateOldAccount(userID string, store *credentials.Store, v *vault.Vault)
return nil
}
// nolint:funlen
func migratePrefsToVault(vault *vault.Vault, b []byte) error {
var prefs struct {
IMAPPort int `json:"user_port_imap,,string"`
@ -193,11 +222,10 @@ func migratePrefsToVault(vault *vault.Vault, b []byte) error {
UpdateChannel updater.Channel `json:"update_channel"`
UpdateRollout float64 `json:"rollout,,string"`
FirstStart bool `json:"first_time_start,,string"`
FirstStartGUI bool `json:"first_time_start_gui,,string"`
ColorScheme string `json:"color_scheme"`
LastVersion *semver.Version `json:"last_used_version"`
Autostart bool `json:"autostart,,string"`
FirstStart bool `json:"first_time_start,,string"`
ColorScheme string `json:"color_scheme"`
LastVersion *semver.Version `json:"last_used_version"`
Autostart bool `json:"autostart,,string"`
AllowProxy bool `json:"allow_proxy,,string"`
FetchWorkers int `json:"fetch_workers,,string"`
@ -241,10 +269,6 @@ func migratePrefsToVault(vault *vault.Vault, b []byte) error {
errs = multierror.Append(errs, fmt.Errorf("failed to migrate first start: %w", err))
}
if err := vault.SetFirstStartGUI(prefs.FirstStartGUI); err != nil {
errs = multierror.Append(errs, fmt.Errorf("failed to migrate first start GUI: %w", err))
}
if err := vault.SetColorScheme(prefs.ColorScheme); err != nil {
errs = multierror.Append(errs, fmt.Errorf("failed to migrate color scheme: %w", err))
}
@ -265,14 +289,6 @@ func migratePrefsToVault(vault *vault.Vault, b []byte) error {
errs = multierror.Append(errs, fmt.Errorf("failed to migrate show all mail: %w", err))
}
if err := vault.SetSyncWorkers(prefs.FetchWorkers); err != nil {
errs = multierror.Append(errs, fmt.Errorf("failed to migrate sync workers: %w", err))
}
if err := vault.SetSyncAttPool(prefs.AttachmentWorkers); err != nil {
errs = multierror.Append(errs, fmt.Errorf("failed to migrate sync attachment pool: %w", err))
}
if err := vault.SetCookies([]byte(prefs.Cookies)); err != nil {
errs = multierror.Append(errs, fmt.Errorf("failed to migrate cookies: %w", err))
}

View File

@ -25,6 +25,7 @@ import (
"runtime"
"testing"
"github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
"github.com/ProtonMail/proton-bridge/v3/internal/cookies"
@ -38,54 +39,49 @@ import (
"github.com/stretchr/testify/require"
)
func TestMigratePrefsToVault(t *testing.T) {
func TestMigratePrefsToVaultWithKeys(t *testing.T) {
// Create a new vault.
vault, corrupt, err := vault.New(t.TempDir(), t.TempDir(), []byte("my secret key"))
vault, corrupt, err := vault.New(t.TempDir(), t.TempDir(), []byte("my secret key"), async.NoopPanicHandler{})
require.NoError(t, err)
require.False(t, corrupt)
// load the old prefs file.
b, err := os.ReadFile(filepath.Join("testdata", "prefs.json"))
require.NoError(t, err)
configDir := filepath.Join("testdata", "with_keys")
// Migrate the old prefs file to the new vault.
require.NoError(t, migratePrefsToVault(vault, b))
require.NoError(t, migrateOldSettingsWithDir(configDir, vault))
// Check that the IMAP and SMTP prefs are migrated.
require.Equal(t, 2143, vault.GetIMAPPort())
require.Equal(t, 2025, vault.GetSMTPPort())
require.True(t, vault.GetSMTPSSL())
// Check Json Settings
validateJSONPrefs(t, vault)
// Check that the update channel is migrated.
require.True(t, vault.GetAutoUpdate())
require.Equal(t, updater.EarlyChannel, vault.GetUpdateChannel())
require.Equal(t, 0.4849529004202015, vault.GetUpdateRollout())
cert, key := vault.GetBridgeTLSCert()
// Check the keys were found and collected.
require.Equal(t, "-----BEGIN CERTIFICATE-----", string(cert))
require.Equal(t, "-----BEGIN RSA PRIVATE KEY-----", string(key))
}
// Check that the app settings have been migrated.
require.False(t, vault.GetFirstStart())
require.True(t, vault.GetFirstStartGUI())
require.Equal(t, "blablabla", vault.GetColorScheme())
require.Equal(t, "2.3.0+git", vault.GetLastVersion().String())
require.True(t, vault.GetAutostart())
// Check that the other app settings have been migrated.
require.Equal(t, 16, vault.SyncWorkers())
require.Equal(t, 16, vault.SyncAttPool())
require.False(t, vault.GetProxyAllowed())
require.False(t, vault.GetShowAllMail())
// Check that the cookies have been migrated.
jar, err := cookiejar.New(nil)
func TestMigratePrefsToVaultWithoutKeys(t *testing.T) {
// Create a new vault.
vault, corrupt, err := vault.New(t.TempDir(), t.TempDir(), []byte("my secret key"), async.NoopPanicHandler{})
require.NoError(t, err)
require.False(t, corrupt)
cookies, err := cookies.NewCookieJar(jar, vault)
require.NoError(t, err)
// load the old prefs file.
configDir := filepath.Join("testdata", "without_keys")
url, err := url.Parse("https://api.protonmail.ch")
require.NoError(t, err)
// Migrate the old prefs file to the new vault.
require.NoError(t, migrateOldSettingsWithDir(configDir, vault))
// There should be a cookie for the API.
require.NotEmpty(t, cookies.Cookies(url))
// Migrate the old prefs file to the new vault.
require.NoError(t, migrateOldSettingsWithDir(configDir, vault))
// Check Json Settings
validateJSONPrefs(t, vault)
// Check the keys were found and collected.
cert, key := vault.GetBridgeTLSCert()
require.NotEqual(t, []byte("-----BEGIN CERTIFICATE-----"), cert)
require.NotEqual(t, []byte("-----BEGIN RSA PRIVATE KEY-----"), key)
}
func TestKeychainMigration(t *testing.T) {
@ -102,7 +98,7 @@ func TestKeychainMigration(t *testing.T) {
oldCacheDir := filepath.Join(tmpDir, "protonmail", "bridge")
require.NoError(t, os.MkdirAll(oldCacheDir, 0o700))
oldPrefs, err := os.ReadFile(filepath.Join("testdata", "prefs.json"))
oldPrefs, err := os.ReadFile(filepath.Join("testdata", "without_keys", "protonmail", "bridge", "prefs.json"))
require.NoError(t, err)
require.NoError(t, os.WriteFile(
@ -178,7 +174,7 @@ func TestUserMigration(t *testing.T) {
token, err := crypto.RandomToken(32)
require.NoError(t, err)
v, corrupt, err := vault.New(settingsFolder, settingsFolder, token)
v, corrupt, err := vault.New(settingsFolder, settingsFolder, token, async.NoopPanicHandler{})
require.NoError(t, err)
require.False(t, corrupt)
@ -197,3 +193,38 @@ func TestUserMigration(t *testing.T) {
require.Equal(t, vault.CombinedMode, u.AddressMode())
}))
}
func validateJSONPrefs(t *testing.T, vault *vault.Vault) {
// Check that the IMAP and SMTP prefs are migrated.
require.Equal(t, 2143, vault.GetIMAPPort())
require.Equal(t, 2025, vault.GetSMTPPort())
require.True(t, vault.GetSMTPSSL())
// Check that the update channel is migrated.
require.True(t, vault.GetAutoUpdate())
require.Equal(t, updater.EarlyChannel, vault.GetUpdateChannel())
require.Equal(t, 0.4849529004202015, vault.GetUpdateRollout())
// Check that the app settings have been migrated.
require.False(t, vault.GetFirstStart())
require.Equal(t, "blablabla", vault.GetColorScheme())
require.Equal(t, "2.3.0+git", vault.GetLastVersion().String())
require.True(t, vault.GetAutostart())
// Check that the other app settings have been migrated.
require.False(t, vault.GetProxyAllowed())
require.False(t, vault.GetShowAllMail())
// Check that the cookies have been migrated.
jar, err := cookiejar.New(nil)
require.NoError(t, err)
cookies, err := cookies.NewCookieJar(jar, vault)
require.NoError(t, err)
url, err := url.Parse("https://api.protonmail.ch")
require.NoError(t, err)
// There should be a cookie for the API.
require.NotEmpty(t, cookies.Cookies(url))
}

View File

@ -34,17 +34,17 @@ import (
//
// For macOS and Linux when already running version is older than this instance
// it will kill old and continue with this new bridge (i.e. no error returned).
func checkSingleInstance(lockFilePath string, curVersion *semver.Version) (*os.File, error) {
func checkSingleInstance(settingPath, lockFilePath string, curVersion *semver.Version) (*os.File, error) {
if lock, err := singleinstance.CreateLockFile(lockFilePath); err == nil {
logrus.WithField("path", lockFilePath).Debug("Created lock file; no other instance is running")
return lock, nil
}
logrus.Debug("Failed to create lock file; another instance is running")
logrus.Warn("Failed to create lock file; another instance is running")
// We couldn't create the lock file, so another instance is probably running.
// Check if it's an older version of the app.
lastVersion, ok := focus.TryVersion()
lastVersion, ok := focus.TryVersion(settingPath)
if !ok {
return nil, fmt.Errorf("failed to determine version of running instance")
}

View File

@ -0,0 +1 @@
-----BEGIN CERTIFICATE-----

View File

@ -0,0 +1 @@
-----BEGIN RSA PRIVATE KEY-----

View File

@ -0,0 +1,31 @@
{
"allow_proxy": "false",
"attachment_workers": "16",
"autostart": "true",
"autoupdate": "true",
"cache_compression": "true",
"cache_concurrent_read": "16",
"cache_concurrent_write": "16",
"cache_enabled": "true",
"cache_location": "/home/user/.config/protonmail/bridge/cache/c11/messages",
"cache_min_free_abs": "250000000",
"cache_min_free_rat": "",
"color_scheme": "blablabla",
"cookies": "{\"https://api.protonmail.ch\":[{\"Name\":\"Session-Id\",\"Value\":\"blablablablablablablablabla\",\"Path\":\"/\",\"Domain\":\"protonmail.ch\",\"Expires\":\"2023-02-19T00:20:40.269424437+01:00\",\"RawExpires\":\"\",\"MaxAge\":7776000,\"Secure\":true,\"HttpOnly\":true,\"SameSite\":0,\"Raw\":\"Session-Id=blablablablablablablablabla; Domain=protonmail.ch; Path=/; HttpOnly; Secure; Max-Age=7776000\",\"Unparsed\":null},{\"Name\":\"Tag\",\"Value\":\"default\",\"Path\":\"/\",\"Domain\":\"\",\"Expires\":\"2023-02-19T00:20:40.269428627+01:00\",\"RawExpires\":\"\",\"MaxAge\":7776000,\"Secure\":true,\"HttpOnly\":false,\"SameSite\":0,\"Raw\":\"Tag=default; Path=/; Secure; Max-Age=7776000\",\"Unparsed\":null}],\"https://protonmail.com\":[{\"Name\":\"Session-Id\",\"Value\":\"blablablablablablablablabla\",\"Path\":\"/\",\"Domain\":\"protonmail.com\",\"Expires\":\"2023-02-19T00:20:18.315084712+01:00\",\"RawExpires\":\"\",\"MaxAge\":7776000,\"Secure\":true,\"HttpOnly\":true,\"SameSite\":0,\"Raw\":\"Session-Id=Y3q2Mh-ClvqL6LWeYdfyPgAAABI; Domain=protonmail.com; Path=/; HttpOnly; Secure; Max-Age=7776000\",\"Unparsed\":null},{\"Name\":\"Tag\",\"Value\":\"redirect\",\"Path\":\"/\",\"Domain\":\"\",\"Expires\":\"2023-02-19T00:20:18.315087646+01:00\",\"RawExpires\":\"\",\"MaxAge\":7776000,\"Secure\":true,\"HttpOnly\":false,\"SameSite\":0,\"Raw\":\"Tag=redirect; Path=/; Secure; Max-Age=7776000\",\"Unparsed\":null}]}",
"fetch_workers": "16",
"first_time_start": "false",
"first_time_start_gui": "true",
"imap_workers": "16",
"is_all_mail_visible": "false",
"last_heartbeat": "325",
"last_used_version": "2.3.0+git",
"preferred_keychain": "secret-service",
"rebranding_migrated": "true",
"report_outgoing_email_without_encryption": "false",
"rollout": "0.4849529004202015",
"user_port_api": "1042",
"update_channel": "early",
"user_port_imap": "2143",
"user_port_smtp": "2025",
"user_ssl_smtp": "true"
}

View File

@ -18,26 +18,24 @@
package app
import (
"encoding/base64"
"fmt"
"path"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/proton-bridge/v3/internal/certs"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
"github.com/sirupsen/logrus"
"golang.org/x/exp/slices"
)
func WithVault(locations *locations.Locations, fn func(*vault.Vault, bool, bool) error) error {
func WithVault(locations *locations.Locations, panicHandler async.PanicHandler, fn func(*vault.Vault, bool, bool) error) error {
logrus.Debug("Creating vault")
defer logrus.Debug("Vault stopped")
// Create the encVault.
encVault, insecure, corrupt, err := newVault(locations)
encVault, insecure, corrupt, err := newVault(locations, panicHandler)
if err != nil {
return fmt.Errorf("could not create vault: %w", err)
}
@ -51,7 +49,9 @@ func WithVault(locations *locations.Locations, fn func(*vault.Vault, bool, bool)
if installed := encVault.GetCertsInstalled(); !installed {
logrus.Debug("Installing certificates")
if err := certs.NewInstaller().InstallCert(encVault.GetBridgeTLSCert()); err != nil {
certPEM, _ := encVault.GetBridgeTLSCert()
if err := certs.NewInstaller().InstallCert(certPEM); err != nil {
return fmt.Errorf("failed to install certs: %w", err)
}
@ -67,7 +67,7 @@ func WithVault(locations *locations.Locations, fn func(*vault.Vault, bool, bool)
return fn(encVault, insecure, corrupt)
}
func newVault(locations *locations.Locations) (*vault.Vault, bool, bool, error) {
func newVault(locations *locations.Locations, panicHandler async.PanicHandler) (*vault.Vault, bool, bool, error) {
vaultDir, err := locations.ProvideSettingsPath()
if err != nil {
return nil, false, false, fmt.Errorf("could not get vault dir: %w", err)
@ -80,7 +80,8 @@ func newVault(locations *locations.Locations) (*vault.Vault, bool, bool, error)
insecure bool
)
if key, err := getVaultKey(vaultDir); err != nil {
if key, err := loadVaultKey(vaultDir); err != nil {
logrus.WithError(err).Error("Could not load/create vault key")
insecure = true
// We store the insecure vault in a separate directory
@ -89,12 +90,12 @@ func newVault(locations *locations.Locations) (*vault.Vault, bool, bool, error)
vaultKey = key
}
gluonDir, err := locations.ProvideGluonPath()
gluonCacheDir, err := locations.ProvideGluonCachePath()
if err != nil {
return nil, false, false, fmt.Errorf("could not provide gluon path: %w", err)
}
vault, corrupt, err := vault.New(vaultDir, gluonDir, vaultKey)
vault, corrupt, err := vault.New(vaultDir, gluonCacheDir, vaultKey, panicHandler)
if err != nil {
return nil, false, false, fmt.Errorf("could not create vault: %w", err)
}
@ -102,42 +103,25 @@ func newVault(locations *locations.Locations) (*vault.Vault, bool, bool, error)
return vault, insecure, corrupt, nil
}
func getVaultKey(vaultDir string) ([]byte, error) {
func loadVaultKey(vaultDir string) ([]byte, error) {
helper, err := vault.GetHelper(vaultDir)
if err != nil {
return nil, fmt.Errorf("could not get keychain helper: %w", err)
}
keychain, err := keychain.NewKeychain(helper, constants.KeyChainName)
kc, err := keychain.NewKeychain(helper, constants.KeyChainName)
if err != nil {
return nil, fmt.Errorf("could not create keychain: %w", err)
}
secrets, err := keychain.List()
has, err := vault.HasVaultKey(kc)
if err != nil {
return nil, fmt.Errorf("could not list keychain: %w", err)
return nil, fmt.Errorf("could not check for vault key: %w", err)
}
if !slices.Contains(secrets, vaultSecretName) {
tok, err := crypto.RandomToken(32)
if err != nil {
return nil, fmt.Errorf("could not generate random token: %w", err)
}
if err := keychain.Put(vaultSecretName, base64.StdEncoding.EncodeToString(tok)); err != nil {
return nil, fmt.Errorf("could not put keychain item: %w", err)
}
if has {
return vault.GetVaultKey(kc)
}
_, keyEnc, err := keychain.Get(vaultSecretName)
if err != nil {
return nil, fmt.Errorf("could not get keychain item: %w", err)
}
keyDec, err := base64.StdEncoding.DecodeString(keyEnc)
if err != nil {
return nil, fmt.Errorf("could not decode keychain item: %w", err)
}
return keyDec, nil
return vault.NewVaultKey(kc)
}

View File

@ -1,82 +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/>.
package async
import (
"context"
"sync"
)
// Abortable collects groups of functions that can be aborted by calling Abort.
type Abortable struct {
abortFunc []context.CancelFunc
abortLock sync.RWMutex
}
func (a *Abortable) Do(ctx context.Context, fn func(context.Context)) {
fn(a.newCancelCtx(ctx))
}
func (a *Abortable) Abort() {
a.abortLock.RLock()
defer a.abortLock.RUnlock()
for _, fn := range a.abortFunc {
fn()
}
}
func (a *Abortable) newCancelCtx(ctx context.Context) context.Context {
a.abortLock.Lock()
defer a.abortLock.Unlock()
ctx, cancel := context.WithCancel(ctx)
a.abortFunc = append(a.abortFunc, cancel)
return ctx
}
// RangeContext iterates over the given channel until the context is canceled or the
// channel is closed.
func RangeContext[T any](ctx context.Context, ch <-chan T, fn func(T)) {
for {
select {
case v, ok := <-ch:
if !ok {
return
}
fn(v)
case <-ctx.Done():
return
}
}
}
// ForwardContext forwards all values from the src channel to the dst channel until the
// context is canceled or the src channel is closed.
func ForwardContext[T any](ctx context.Context, dst chan<- T, src <-chan T) {
RangeContext(ctx, src, func(v T) {
select {
case dst <- v:
case <-ctx.Done():
}
})
}

View File

@ -1,233 +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/>.
package async
import (
"context"
"math/rand"
"sync"
"time"
)
type PanicHandler interface {
HandlePanic()
}
// Group is forked and improved version of "github.com/bradenaw/juniper/xsync.Group".
//
// It manages a group of goroutines. The main change to original is posibility
// to wait passed function to finish without canceling it's context and adding
// PanicHandler.
type Group struct {
baseCtx context.Context
ctx context.Context
jobCtx context.Context
cancel context.CancelFunc
finish context.CancelFunc
wg sync.WaitGroup
panicHandler PanicHandler
}
// NewGroup returns a Group ready for use. The context passed to any of the f functions will be a
// descendant of ctx.
func NewGroup(ctx context.Context, panicHandler PanicHandler) *Group {
bgCtx, cancel := context.WithCancel(ctx)
jobCtx, finish := context.WithCancel(ctx)
return &Group{
baseCtx: ctx,
ctx: bgCtx,
jobCtx: jobCtx,
cancel: cancel,
finish: finish,
panicHandler: panicHandler,
}
}
// Once calls f once from another goroutine.
func (g *Group) Once(f func(ctx context.Context)) {
g.wg.Add(1)
go func() {
defer g.handlePanic()
f(g.ctx)
g.wg.Done()
}()
}
// jitterDuration returns a random duration in [d - jitter, d + jitter].
func jitterDuration(d time.Duration, jitter time.Duration) time.Duration {
return d + time.Duration(float64(jitter)*((rand.Float64()*2)-1)) //nolint:gosec
}
// Periodic spawns a goroutine that calls f once per interval +/- jitter.
func (g *Group) Periodic(
interval time.Duration,
jitter time.Duration,
f func(ctx context.Context),
) {
g.wg.Add(1)
go func() {
defer g.handlePanic()
defer g.wg.Done()
t := time.NewTimer(jitterDuration(interval, jitter))
defer t.Stop()
for {
if g.ctx.Err() != nil {
return
}
select {
case <-g.jobCtx.Done():
return
case <-t.C:
}
t.Reset(jitterDuration(interval, jitter))
f(g.ctx)
}
}()
}
// Trigger spawns a goroutine which calls f whenever the returned function is called. If f is
// already running when triggered, f will run again immediately when it finishes.
func (g *Group) Trigger(f func(ctx context.Context)) func() {
c := make(chan struct{}, 1)
g.wg.Add(1)
go func() {
defer g.handlePanic()
defer g.wg.Done()
for {
if g.ctx.Err() != nil {
return
}
select {
case <-g.jobCtx.Done():
return
case <-c:
}
f(g.ctx)
}
}()
return func() {
select {
case c <- struct{}{}:
default:
}
}
}
// PeriodicOrTrigger spawns a goroutine which calls f whenever the returned function is called. If
// f is already running when triggered, f will run again immediately when it finishes. Also calls f
// when it has been interval+/-jitter since the last trigger.
func (g *Group) PeriodicOrTrigger(
interval time.Duration,
jitter time.Duration,
f func(ctx context.Context),
) func() {
c := make(chan struct{}, 1)
g.wg.Add(1)
go func() {
defer g.handlePanic()
defer g.wg.Done()
t := time.NewTimer(jitterDuration(interval, jitter))
defer t.Stop()
for {
if g.ctx.Err() != nil {
return
}
select {
case <-g.jobCtx.Done():
return
case <-t.C:
t.Reset(jitterDuration(interval, jitter))
case <-c:
if !t.Stop() {
<-t.C
}
t.Reset(jitterDuration(interval, jitter))
}
f(g.ctx)
}
}()
return func() {
select {
case c <- struct{}{}:
default:
}
}
}
func (g *Group) resetCtx() {
g.jobCtx, g.finish = context.WithCancel(g.baseCtx)
g.ctx, g.cancel = context.WithCancel(g.baseCtx)
}
// Cancel is send to all of the spawn goroutines and ends periodic
// or trigger routines.
func (g *Group) Cancel() {
g.cancel()
g.finish()
g.resetCtx()
}
// Finish will ends all periodic or polls routines. It will let
// currently running functions to finish (cancel is not sent).
//
// It is not safe to call Wait concurrently with any other method on g.
func (g *Group) Finish() {
g.finish()
g.jobCtx, g.finish = context.WithCancel(g.baseCtx)
}
// CancelAndWait cancels the context passed to any of the spawned goroutines and waits for all spawned
// goroutines to exit.
//
// It is not safe to call Wait concurrently with any other method on g.
func (g *Group) CancelAndWait() {
g.finish()
g.cancel()
g.wg.Wait()
g.resetCtx()
}
// WaitToFinish will ends all periodic or polls routines. It will wait for
// currently running functions to finish (cancel is not sent).
//
// It is not safe to call Wait concurrently with any other method on g.
func (g *Group) WaitToFinish() {
g.finish()
g.wg.Wait()
g.jobCtx, g.finish = context.WithCancel(g.baseCtx)
}
func (g *Group) handlePanic() {
if g.panicHandler != nil {
g.panicHandler.HandlePanic()
}
}

View File

@ -21,6 +21,7 @@ import (
"net/http"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/sirupsen/logrus"
@ -32,14 +33,14 @@ func defaultAPIOptions(
version *semver.Version,
cookieJar http.CookieJar,
transport http.RoundTripper,
poolSize int,
panicHandler async.PanicHandler,
) []proton.Option {
return []proton.Option{
proton.WithHostURL(apiURL),
proton.WithAppVersion(constants.AppVersion(version.Original())),
proton.WithCookieJar(cookieJar),
proton.WithTransport(transport),
proton.WithAttPoolSize(poolSize),
proton.WithLogger(logrus.StandardLogger()),
proton.WithPanicHandler(panicHandler),
}
}

View File

@ -23,6 +23,7 @@ import (
"net/http"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/go-proton-api"
)
@ -32,7 +33,7 @@ func newAPIOptions(
version *semver.Version,
cookieJar http.CookieJar,
transport http.RoundTripper,
poolSize int,
panicHandler async.PanicHandler,
) []proton.Option {
return defaultAPIOptions(apiURL, version, cookieJar, transport, poolSize)
return defaultAPIOptions(apiURL, version, cookieJar, transport, panicHandler)
}

View File

@ -24,6 +24,7 @@ import (
"os"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/go-proton-api"
)
@ -33,9 +34,9 @@ func newAPIOptions(
version *semver.Version,
cookieJar http.CookieJar,
transport http.RoundTripper,
poolSize int,
panicHandler async.PanicHandler,
) []proton.Option {
opt := defaultAPIOptions(apiURL, version, cookieJar, transport, poolSize)
opt := defaultAPIOptions(apiURL, version, cookieJar, transport, panicHandler)
if host := os.Getenv("BRIDGE_API_HOST"); host != "" {
opt = append(opt, proton.WithHostURL(host))

View File

@ -21,6 +21,7 @@ package bridge
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net"
"net/http"
@ -29,15 +30,17 @@ import (
"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"
"github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/gluon/watcher"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/proton-bridge/v3/internal/async"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"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/user"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/bradenaw/juniper/xslices"
@ -90,8 +93,8 @@ type Bridge struct {
// locator is the bridge's locator.
locator Locator
// crashHandler
crashHandler async.PanicHandler
// panicHandler
panicHandler async.PanicHandler
// reporter
reporter reporter.Reporter
@ -108,6 +111,12 @@ type Bridge struct {
logIMAPServer bool
logSMTP bool
// These two variables keep track of the startup values for the two settings of the same name.
// They are updated in the vault on startup so that we're sure they're updated in case of kill/crash,
// but we need to keep their initial value for the current instance of bridge.
firstStart bool
lastVersion *semver.Version
// tasks manages the bridge's goroutines.
tasks *async.Group
@ -116,10 +125,12 @@ type Bridge struct {
// goUpdate triggers a check/install of updates.
goUpdate func()
uidValidityGenerator imap.UIDValidityGenerator
}
// New creates a new bridge.
func New( //nolint:funlen
func New(
locator Locator, // the locator to provide paths to store data
vault *vault.Vault, // the bridge's encrypted data store
autostarter Autostarter, // the autostarter to manage autostart settings
@ -132,17 +143,18 @@ func New( //nolint:funlen
tlsReporter TLSReporter, // the TLS reporter to report TLS errors
roundTripper http.RoundTripper, // the round tripper to use for API requests
proxyCtl ProxyController, // the DoH controller
crashHandler async.PanicHandler,
panicHandler async.PanicHandler,
reporter reporter.Reporter,
uidValidityGenerator imap.UIDValidityGenerator,
logIMAPClient, logIMAPServer bool, // whether to log IMAP client/server activity
logSMTP bool, // whether to log SMTP activity
) (*Bridge, <-chan events.Event, error) {
// api is the user's API manager.
api := proton.New(newAPIOptions(apiURL, curVersion, cookieJar, roundTripper, vault.SyncAttPool())...)
api := proton.New(newAPIOptions(apiURL, curVersion, cookieJar, roundTripper, panicHandler)...)
// tasks holds all the bridge's background tasks.
tasks := async.NewGroup(context.Background(), crashHandler)
tasks := async.NewGroup(context.Background(), panicHandler)
// imapEventCh forwards IMAP events from gluon instances to the bridge for processing.
imapEventCh := make(chan imapEvents.Event)
@ -157,12 +169,13 @@ func New( //nolint:funlen
autostarter,
updater,
curVersion,
crashHandler,
panicHandler,
reporter,
api,
identifier,
proxyCtl,
uidValidityGenerator,
logIMAPClient, logIMAPServer, logSMTP,
)
if err != nil {
@ -177,22 +190,9 @@ func New( //nolint:funlen
return nil, nil, fmt.Errorf("failed to initialize bridge: %w", err)
}
// Start serving IMAP.
if err := bridge.serveIMAP(); err != nil {
logrus.WithError(err).Error("IMAP error")
bridge.PushError(ErrServeIMAP)
}
// Start serving SMTP.
if err := bridge.serveSMTP(); err != nil {
logrus.WithError(err).Error("SMTP error")
bridge.PushError(ErrServeSMTP)
}
return bridge, eventCh, nil
}
// nolint:funlen
func newBridge(
tasks *async.Group,
imapEventCh chan imapEvents.Event,
@ -202,12 +202,13 @@ func newBridge(
autostarter Autostarter,
updater Updater,
curVersion *semver.Version,
crashHandler async.PanicHandler,
panicHandler async.PanicHandler,
reporter reporter.Reporter,
api *proton.Manager,
identifier Identifier,
proxyCtl ProxyController,
uidValidityGenerator imap.UIDValidityGenerator,
logIMAPClient, logIMAPServer, logSMTP bool,
) (*Bridge, error) {
@ -216,13 +217,29 @@ func newBridge(
return nil, fmt.Errorf("failed to load TLS config: %w", err)
}
gluonDir, err := getGluonDir(vault)
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)
}
lastVersion := vault.GetLastVersion()
if err := vault.SetLastVersion(curVersion); err != nil {
return nil, fmt.Errorf("failed to save last version indicator: %w", err)
}
imapServer, err := newIMAPServer(
gluonDir,
gluonCacheDir,
gluonDataDir,
curVersion,
tlsConfig,
reporter,
@ -230,12 +247,14 @@ func newBridge(
logIMAPServer,
imapEventCh,
tasks,
uidValidityGenerator,
panicHandler,
)
if err != nil {
return nil, fmt.Errorf("failed to create IMAP server: %w", err)
}
focusService, err := focus.NewService(curVersion)
focusService, err := focus.NewService(locator, curVersion, panicHandler)
if err != nil {
return nil, fmt.Errorf("failed to create focus service: %w", err)
}
@ -261,7 +280,7 @@ func newBridge(
newVersion: curVersion,
newVersionLock: safe.NewRWMutex(),
crashHandler: crashHandler,
panicHandler: panicHandler,
reporter: reporter,
focusService: focusService,
@ -272,7 +291,12 @@ func newBridge(
logIMAPServer: logIMAPServer,
logSMTP: logSMTP,
firstStart: firstStart,
lastVersion: lastVersion,
tasks: tasks,
uidValidityGenerator: uidValidityGenerator,
}
bridge.smtpServer = newSMTPServer(bridge, tlsConfig, logSMTP)
@ -280,7 +304,6 @@ func newBridge(
return bridge, nil
}
// nolint:funlen
func (bridge *Bridge) init(tlsReporter TLSReporter) error {
// Enable or disable the proxy at startup.
if bridge.vault.GetProxyAllowed() {
@ -349,15 +372,32 @@ func (bridge *Bridge) init(tlsReporter TLSReporter) error {
})
})
// Attempt to lazy load users when triggered.
bridge.goLoad = bridge.tasks.Trigger(func(ctx context.Context) {
logrus.Info("Loading users")
// 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 {
logrus.WithError(err).Error("Failed to load users")
} else {
bridge.publish(events.AllUsersLoaded{})
if netErr := new(proton.NetError); !errors.As(err, &netErr) {
sentry.ReportError(bridge.reporter, "Failed to load users", err)
}
return
}
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()
@ -435,11 +475,6 @@ func (bridge *Bridge) Close(ctx context.Context) {
}
bridge.watchers = nil
// Save the last version of bridge that was run.
if err := bridge.vault.SetLastVersion(bridge.curVersion); err != nil {
logrus.WithError(err).Error("Failed to save last version")
}
}
func (bridge *Bridge) publish(event events.Event) {
@ -461,7 +496,7 @@ func (bridge *Bridge) addWatcher(ofType ...events.Event) *watcher.Watcher[events
bridge.watchersLock.Lock()
defer bridge.watchersLock.Unlock()
watcher := watcher.New(ofType...)
watcher := watcher.New(bridge.panicHandler, ofType...)
bridge.watchers = append(bridge.watchers, watcher)
@ -522,7 +557,7 @@ func (bridge *Bridge) onStatusDown(ctx context.Context) {
}
func loadTLSConfig(vault *vault.Vault) (*tls.Config, error) {
cert, err := tls.X509KeyPair(vault.GetBridgeTLSCert(), vault.GetBridgeTLSKey())
cert, err := tls.X509KeyPair(vault.GetBridgeTLSCert())
if err != nil {
return nil, err
}

View File

@ -21,20 +21,24 @@ import (
"context"
"crypto/tls"
"fmt"
"net"
"net/http"
"os"
"runtime"
"path/filepath"
"sync"
"testing"
"time"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/gluon/imap"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/go-proton-api/server"
"github.com/ProtonMail/go-proton-api/server/backend"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
"github.com/ProtonMail/proton-bridge/v3/internal/certs"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/cookies"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/focus"
@ -45,6 +49,7 @@ 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"
"github.com/stretchr/testify/require"
)
@ -119,8 +124,11 @@ func TestBridge_Focus(t *testing.T) {
raiseCh, done := bridge.GetEvents(events.Raise{})
defer done()
settingsFolder, err := locator.ProvideSettingsPath()
require.NoError(t, err)
// Simulate a focus event.
focus.TryRaise()
focus.TryRaise(settingsFolder)
// Wait for the event.
require.IsType(t, events.Raise{}, <-raiseCh)
@ -349,7 +357,7 @@ func TestBridge_BadVaultKey(t *testing.T) {
})
}
func TestBridge_MissingGluonDir(t *testing.T) {
func TestBridge_MissingGluonStore(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
var gluonDir string
@ -361,13 +369,36 @@ func TestBridge_MissingGluonDir(t *testing.T) {
require.NoError(t, bridge.SetGluonDir(ctx, t.TempDir()))
// Get the gluon dir.
gluonDir = bridge.GetGluonDir()
gluonDir = bridge.GetGluonCacheDir()
})
// The user removes the gluon dir while bridge is not running.
require.NoError(t, os.RemoveAll(gluonDir))
// Bridge starts but can't find the gluon dir; there should be no error.
// Bridge starts but can't find the gluon store dir; there should be no error.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
// ...
})
})
}
func TestBridge_MissingGluonDatabase(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
var gluonDir string
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
_, err := bridge.LoginFull(context.Background(), username, password, nil, nil)
require.NoError(t, err)
// Get the gluon dir.
gluonDir, err = bridge.GetGluonDataDir()
require.NoError(t, err)
})
// The user removes the gluon dir while bridge is not running.
require.NoError(t, os.RemoveAll(gluonDir))
// Bridge starts but can't find the gluon database dir; there should be no error.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
// ...
})
@ -456,41 +487,158 @@ func TestBridge_FactoryReset(t *testing.T) {
})
}
func TestBridge_ChangeCacheDirectoryFailsBetweenDifferentVolumes(t *testing.T) {
if runtime.GOOS != "windows" {
t.Skip("Test only necessary on windows")
}
func TestBridge_InitGluonDirectory(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(b *bridge.Bridge, mocks *bridge.Mocks) {
configDir, err := b.GetGluonDataDir()
require.NoError(t, err)
_, err = os.ReadDir(bridge.ApplyGluonCachePathSuffix(b.GetGluonCacheDir()))
require.False(t, os.IsNotExist(err))
_, err = os.ReadDir(bridge.ApplyGluonConfigPathSuffix(configDir))
require.False(t, os.IsNotExist(err))
})
})
}
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) {
// Change directory
err := bridge.SetGluonDir(ctx, "XX:\\")
require.Error(t, err)
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())))
require.NoError(t, err)
require.Error(t, imapClient.Login("badUser", "badPass"))
require.Equal(t, "badUser", (<-failCh).Username)
})
})
}
func TestBridge_ChangeCacheDirectory(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) {
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, 10)
})
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
newCacheDir := t.TempDir()
currentCacheDir := bridge.GetGluonDir()
currentCacheDir := b.GetGluonCacheDir()
configDir, err := b.GetGluonDataDir()
require.NoError(t, err)
// Login the user.
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
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)
// The user is now connected.
require.Equal(t, []string{userID}, bridge.GetUserIDs())
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
require.Equal(t, []string{userID}, b.GetUserIDs())
require.Equal(t, []string{userID}, getConnectedUserIDs(t, b))
// Change directory
err = bridge.SetGluonDir(ctx, newCacheDir)
err = b.SetGluonDir(ctx, newCacheDir)
require.NoError(t, err)
_, err = os.ReadDir(currentCacheDir)
// Old store should no more exists.
_, err = os.ReadDir(bridge.ApplyGluonCachePathSuffix(currentCacheDir))
require.True(t, os.IsNotExist(err))
// Database should not have changed.
_, err = os.ReadDir(bridge.ApplyGluonConfigPathSuffix(configDir))
require.False(t, os.IsNotExist(err))
require.Equal(t, newCacheDir, bridge.GetGluonDir())
// New path should have Gluon sub-folder.
require.Equal(t, filepath.Join(newCacheDir, "gluon"), b.GetGluonCacheDir())
// And store should be inside it.
_, err = os.ReadDir(bridge.ApplyGluonCachePathSuffix(b.GetGluonCacheDir()))
require.False(t, os.IsNotExist(err))
// We should be able to fetch.
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.Equal(t, uint32(10), status.Messages)
})
})
}
func TestBridge_ChangeAddressOrder(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
// Create a user.
userID, addrID, err := s.CreateUser("imap", password)
require.NoError(t, err)
// Create a second address for the user.
aliasID, err := s.CreateAddress(userID, "alias@"+s.GetDomain(), password)
require.NoError(t, err)
// Create 10 messages for the user.
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 10)
})
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
// Log the user in with its first address.
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)
// We should see 10 messages in the inbox.
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(`Inbox`, false)
require.NoError(t, err)
require.Equal(t, uint32(10), status.Messages)
})
// Make the second address the primary one.
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
require.NoError(t, c.OrderAddresses(ctx, proton.OrderAddressesReq{AddressIDs: []string{aliasID, addrID}}))
})
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
// We should still see 10 messages in the inbox.
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() }()
require.Eventually(t, func() bool {
status, err := client.Select(`Inbox`, false)
require.NoError(t, err)
return status.Messages == 10
}, 5*time.Second, 100*time.Millisecond)
})
})
}
@ -522,20 +670,28 @@ func withEnv(t *testing.T, tests func(context.Context, *server.Server, *proton.N
tests(ctx, server, netCtl, locations, vaultKey)
}
// withMocks creates the mock objects used in the tests.
func withMocks(t *testing.T, tests func(*bridge.Mocks)) {
mocks := bridge.NewMocks(t, v2_3_0, v2_3_0)
defer mocks.Close()
tests(mocks)
}
// Needs to be global to survive bridge shutdown/startup in unit tests as they happen to fast.
var testUIDValidityGenerator = imap.DefaultEpochUIDValidityGenerator()
// withBridge creates a new bridge which points to the given API URL and uses the given keychain, and closes it when done.
func withBridge(
func withBridgeNoMocks(
ctx context.Context,
t *testing.T,
mocks *bridge.Mocks,
apiURL string,
netCtl *proton.NetCtl,
locator bridge.Locator,
vaultKey []byte,
tests func(*bridge.Bridge, *bridge.Mocks),
tests func(*bridge.Bridge),
) {
// Create the mock objects used in the tests.
mocks := bridge.NewMocks(t, v2_3_0, v2_3_0)
defer mocks.Close()
// Bridge will disable the proxy by default at startup.
mocks.ProxyCtl.EXPECT().DisallowProxy()
@ -544,7 +700,7 @@ func withBridge(
require.NoError(t, err)
// Create the vault.
vault, _, err := vault.New(vaultDir, t.TempDir(), vaultKey)
vault, _, err := vault.New(vaultDir, t.TempDir(), vaultKey, async.NoopPanicHandler{})
require.NoError(t, err)
// Create a new cookie jar.
@ -570,6 +726,7 @@ func withBridge(
mocks.ProxyCtl,
mocks.CrashHandler,
mocks.Reporter,
testUIDValidityGenerator,
// The logging stuff.
os.Getenv("BRIDGE_LOG_IMAP_CLIENT") == "1",
@ -581,6 +738,10 @@ func withBridge(
// 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))
@ -590,10 +751,27 @@ func withBridge(
defer bridge.Close(ctx)
// Use the bridge.
tests(bridge, mocks)
tests(bridge)
}
func waitForEvent[T any](t *testing.T, eventCh <-chan events.Event, wantEvent T) {
// withBridge creates a new bridge which points to the given API URL and uses the given keychain, and closes it when done.
func withBridge(
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)
})
})
}
func waitForEvent[T any](t *testing.T, eventCh <-chan events.Event, _ T) {
t.Helper()
for event := range eventCh {

View File

@ -37,7 +37,7 @@ const (
MaxCompressedFilesCount = 6
)
func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, description, username, email, client string, attachLogs bool) error { //nolint:funlen
func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, description, username, email, client string, attachLogs bool) error {
var account string
if info, err := bridge.QueryUserInfo(username); err == nil {

View File

@ -22,10 +22,7 @@ import "errors"
var (
ErrVaultInsecure = errors.New("the vault is insecure")
ErrVaultCorrupt = errors.New("the vault is corrupt")
ErrServeIMAP = errors.New("failed to serve IMAP")
ErrServeSMTP = errors.New("failed to serve SMTP")
ErrWatchUpdates = errors.New("failed to watch for updates")
ErrWatchUpdates = errors.New("failed to watch for updates")
ErrNoSuchUser = errors.New("no such user")
ErrUserAlreadyExists = errors.New("user already exists")

View File

@ -18,6 +18,8 @@
package bridge
import (
"fmt"
"io"
"os"
"path/filepath"
)
@ -62,3 +64,75 @@ func moveFile(from, to string) error {
return nil
}
func copyDir(from, to string) error {
entries, err := os.ReadDir(from)
if err != nil {
return err
}
if err := createIfNotExists(to, 0o700); err != nil {
return err
}
for _, entry := range entries {
sourcePath := filepath.Join(from, entry.Name())
destPath := filepath.Join(to, entry.Name())
if entry.IsDir() {
if err := copyDir(sourcePath, destPath); err != nil {
return err
}
} else {
if err := copyFile(sourcePath, destPath); err != nil {
return err
}
}
}
return nil
}
func copyFile(srcFile, dstFile string) error {
out, err := os.Create(filepath.Clean(dstFile))
defer func(out *os.File) {
_ = out.Close()
}(out)
if err != nil {
return err
}
in, err := os.Open(filepath.Clean(srcFile))
defer func(in *os.File) {
_ = in.Close()
}(in)
if err != nil {
return err
}
_, err = io.Copy(out, in)
if err != nil {
return err
}
return nil
}
func exists(filePath string) bool {
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return false
}
return true
}
func createIfNotExists(dir string, perm os.FileMode) error {
if exists(dir) {
return nil
}
if err := os.MkdirAll(dir, perm); err != nil {
return fmt.Errorf("failed to create directory: '%s', error: '%s'", dir, err.Error())
}
return nil
}

View File

@ -20,21 +20,21 @@ package bridge
import (
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"runtime"
"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"
"github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/gluon/store"
"github.com/ProtonMail/proton-bridge/v3/internal/async"
"github.com/ProtonMail/gluon/store/fallback_v0"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"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"
@ -47,26 +47,42 @@ const (
)
func (bridge *Bridge) serveIMAP() error {
if bridge.imapServer == nil {
return fmt.Errorf("no imap server instance running")
}
port, err := func() (int, error) {
if bridge.imapServer == nil {
return 0, fmt.Errorf("no IMAP server instance running")
}
logrus.Info("Starting IMAP server")
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
}()
imapListener, err := newListener(bridge.vault.GetIMAPPort(), bridge.vault.GetIMAPSSL(), bridge.tlsConfig)
if err != nil {
return fmt.Errorf("failed to create IMAP listener: %w", err)
bridge.publish(events.IMAPServerError{
Error: err,
})
return err
}
bridge.imapListener = imapListener
if err := bridge.imapServer.Serve(context.Background(), bridge.imapListener); err != nil {
return fmt.Errorf("failed to serve IMAP: %w", err)
}
if err := bridge.vault.SetIMAPPort(getPort(imapListener.Addr())); err != nil {
return fmt.Errorf("failed to store IMAP port in vault: %w", err)
}
bridge.publish(events.IMAPServerReady{
Port: port,
})
return nil
}
@ -78,6 +94,8 @@ func (bridge *Bridge) restartIMAP() error {
if err := bridge.imapListener.Close(); err != nil {
return fmt.Errorf("failed to close IMAP listener: %w", err)
}
bridge.publish(events.IMAPServerStopped{})
}
return bridge.serveIMAP()
@ -90,6 +108,7 @@ func (bridge *Bridge) closeIMAP(ctx context.Context) error {
if err := bridge.imapServer.Close(ctx); err != nil {
return fmt.Errorf("failed to close IMAP server: %w", err)
}
bridge.imapServer = nil
}
@ -99,6 +118,8 @@ func (bridge *Bridge) closeIMAP(ctx context.Context) error {
}
}
bridge.publish(events.IMAPServerStopped{})
return nil
}
@ -122,9 +143,53 @@ func (bridge *Bridge) addIMAPUser(ctx context.Context, user *user.User) error {
if gluonID, ok := user.GetGluonID(addrID); ok {
log.WithField("gluonID", gluonID).Info("Loading existing IMAP user")
if err := bridge.imapServer.LoadUser(ctx, imapConn, gluonID, user.GluonKey()); err != nil {
// 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")
@ -141,6 +206,9 @@ func (bridge *Bridge) addIMAPUser(ctx context.Context, user *user.User) error {
}
}
// Trigger a sync for the user, if needed.
user.TriggerSync()
return nil
}
@ -149,6 +217,7 @@ func (bridge *Bridge) removeIMAPUser(ctx context.Context, user *user.User, withD
if bridge.imapServer == nil {
return fmt.Errorf("no imap server instance running")
}
logrus.WithFields(logrus.Fields{
"userID": user.ID(),
"withData": withData,
@ -195,47 +264,52 @@ func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) {
if event.IMAPID.Name != "" && event.IMAPID.Version != "" {
bridge.identifier.SetClient(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")
bridge.publish(events.IMAPLoginFailed{Username: event.Username})
}
}
func getGluonDir(encVault *vault.Vault) (string, error) {
empty, exists, err := isEmpty(encVault.GetGluonDir())
if err != nil {
return "", fmt.Errorf("failed to check if gluon dir is empty: %w", err)
if err := os.MkdirAll(encVault.GetGluonCacheDir(), 0o700); err != nil {
return "", fmt.Errorf("failed to create gluon dir: %w", err)
}
if !exists {
if err := os.MkdirAll(encVault.GetGluonDir(), 0o700); err != nil {
return "", fmt.Errorf("failed to create gluon dir: %w", err)
}
}
if empty {
if err := encVault.ForUser(runtime.NumCPU(), func(user *vault.User) error {
return user.ClearSyncStatus()
}); err != nil {
return "", fmt.Errorf("failed to reset user sync status: %w", err)
}
}
return encVault.GetGluonDir(), nil
return encVault.GetGluonCacheDir(), nil
}
func ApplyGluonCachePathSuffix(basePath string) string {
return filepath.Join(basePath, "backend", "store")
}
func ApplyGluonConfigPathSuffix(basePath string) string {
return filepath.Join(basePath, "backend", "db")
}
// nolint:funlen
func newIMAPServer(
gluonDir string,
gluonCacheDir, gluonConfigDir string,
version *semver.Version,
tlsConfig *tls.Config,
reporter reporter.Reporter,
logClient, logServer bool,
eventCh chan<- imapEvents.Event,
tasks *async.Group,
uidValidityGenerator imap.UIDValidityGenerator,
panicHandler async.PanicHandler,
) (*gluon.Server, error) {
gluonCacheDir = ApplyGluonCachePathSuffix(gluonCacheDir)
gluonConfigDir = ApplyGluonConfigPathSuffix(gluonConfigDir)
logrus.WithFields(logrus.Fields{
"gluonDir": gluonDir,
"version": version,
"logClient": logClient,
"logServer": logServer,
"gluonStore": gluonCacheDir,
"gluonDB": gluonConfigDir,
"version": version,
"logClient": logClient,
"logServer": logServer,
}).Info("Creating IMAP server")
if logClient || logServer {
@ -263,11 +337,14 @@ func newIMAPServer(
imapServer, err := gluon.New(
gluon.WithTLS(tlsConfig),
gluon.WithDataDir(gluonDir),
gluon.WithDataDir(gluonCacheDir),
gluon.WithDatabaseDir(gluonConfigDir),
gluon.WithStoreBuilder(new(storeBuilder)),
gluon.WithLogger(imapClientLog, imapServerLog),
getGluonVersionInfo(version),
gluon.WithReporter(reporter),
gluon.WithUIDValidityGenerator(uidValidityGenerator),
gluon.WithPanicHandler(panicHandler),
)
if err != nil {
return nil, err
@ -297,32 +374,13 @@ func getGluonVersionInfo(version *semver.Version) gluon.Option {
)
}
// isEmpty returns whether the given directory is empty.
// If the directory does not exist, the second return value is false.
func isEmpty(dir string) (bool, bool, error) {
if _, err := os.Stat(dir); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return false, false, fmt.Errorf("failed to stat %s: %w", dir, err)
}
return true, false, nil
}
entries, err := os.ReadDir(dir)
if err != nil {
return false, false, fmt.Errorf("failed to read dir %s: %w", dir, err)
}
return len(entries) == 0, true, nil
}
type storeBuilder struct{}
func (*storeBuilder) New(path, userID string, passphrase []byte) (store.Store, error) {
return store.NewOnDiskStore(
filepath.Join(path, userID),
passphrase,
store.WithCompressor(new(store.GZipCompressor)),
store.WithFallback(fallback_v0.NewOnDiskStoreV0WithCompressor(&fallback_v0.GZipCompressor{})),
)
}

View File

@ -45,7 +45,7 @@ func NewMocks(tb testing.TB, version, minAuto *semver.Version) *Mocks {
mocks.TLSReporter.EXPECT().GetTLSIssueCh().Return(mocks.TLSIssueCh).AnyTimes()
// This is called at he end of any go-routine:
mocks.CrashHandler.EXPECT().HandlePanic().AnyTimes()
mocks.CrashHandler.EXPECT().HandlePanic(gomock.Any()).AnyTimes()
return mocks
}

View File

@ -1,5 +1,5 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/ProtonMail/proton-bridge/v3/internal/async (interfaces: PanicHandler)
// Source: github.com/ProtonMail/gluon/async (interfaces: PanicHandler)
// Package mocks is a generated GoMock package.
package mocks
@ -34,13 +34,13 @@ func (m *MockPanicHandler) EXPECT() *MockPanicHandlerMockRecorder {
}
// HandlePanic mocks base method.
func (m *MockPanicHandler) HandlePanic() {
func (m *MockPanicHandler) HandlePanic(arg0 interface{}) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "HandlePanic")
m.ctrl.Call(m, "HandlePanic", arg0)
}
// HandlePanic indicates an expected call of HandlePanic.
func (mr *MockPanicHandlerMockRecorder) HandlePanic() *gomock.Call {
func (mr *MockPanicHandlerMockRecorder) HandlePanic(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandlePanic", reflect.TypeOf((*MockPanicHandler)(nil).HandlePanic))
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandlePanic", reflect.TypeOf((*MockPanicHandler)(nil).HandlePanic), arg0)
}

View File

@ -57,6 +57,7 @@ func TestBridge_Refresh(t *testing.T) {
require.Equal(t, userID, (<-syncCh).UserID)
})
var uidValidities = make(map[string]uint32, len(names))
// If we then connect an IMAP client, it should see all the labels with UID validity of 1.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).AnyTimes()
@ -73,7 +74,7 @@ func TestBridge_Refresh(t *testing.T) {
for _, name := range names {
status, err := client.Select("Folders/"+name, false)
require.NoError(t, err)
require.Equal(t, uint32(1000), status.UidValidity)
uidValidities[name] = status.UidValidity
}
})
@ -106,7 +107,7 @@ func TestBridge_Refresh(t *testing.T) {
for _, name := range names {
status, err := client.Select("Folders/"+name, false)
require.NoError(t, err)
require.Equal(t, uint32(1001), status.UidValidity)
require.Greater(t, status.UidValidity, uidValidities[name])
}
})
})

View File

@ -18,10 +18,12 @@
package bridge_test
import (
"bytes"
"context"
"crypto/tls"
"fmt"
"net"
"os"
"strings"
"testing"
"time"
@ -30,6 +32,7 @@ import (
"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/emersion/go-imap"
"github.com/emersion/go-imap/client"
"github.com/emersion/go-sasl"
@ -42,7 +45,7 @@ func TestBridge_Send(t *testing.T) {
_, _, err := s.CreateUser("recipient", password)
require.NoError(t, err)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
senderUserID, err := bridge.LoginFull(ctx, username, password, nil, nil)
require.NoError(t, err)
@ -100,7 +103,7 @@ func TestBridge_Send(t *testing.T) {
defer recipientIMAPClient.Logout() //nolint:errcheck
// Sender should have 10 messages in the sent folder.
// Recipient should have 0 messages in inbox.
// Recipient should have 10 messages in inbox.
require.Eventually(t, func() bool {
sent, err := senderIMAPClient.Status(`Sent`, []imap.StatusItem{imap.StatusMessages})
require.NoError(t, err)
@ -113,3 +116,396 @@ func TestBridge_Send(t *testing.T) {
})
})
}
func TestBridge_SendDraftFlags(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
// Create a recipient user.
_, _, err := s.CreateUser("recipient", password)
require.NoError(t, err)
// The sender should be fully synced.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
defer done()
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
require.NoError(t, err)
require.Equal(t, userID, (<-syncCh).UserID)
})
// Start the bridge.
withBridge(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())))
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!`
// Save a draft.
require.NoError(t, imapClient.Append("Drafts", []string{imap.DraftFlag}, time.Now(), strings.NewReader(message)))
// Assert that the draft exists and is marked as a draft.
{
messages, err := clientFetch(imapClient, "Drafts")
require.NoError(t, err)
require.Len(t, messages, 1)
require.Contains(t, messages[0].Flags, imap.DraftFlag)
}
// Connect the SMTP client.
smtpClient, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
require.NoError(t, err)
defer smtpClient.Close() //nolint:errcheck
// Upgrade to TLS.
require.NoError(t, smtpClient.StartTLS(&tls.Config{InsecureSkipVerify: true}))
// Authorize with SASL PLAIN.
require.NoError(t, smtpClient.Auth(sasl.NewPlainClient(
userInfo.Addresses[0],
userInfo.Addresses[0],
string(userInfo.BridgePass)),
))
// Send the message.
require.NoError(t, smtpClient.SendMail(
userInfo.Addresses[0],
[]string{"recipient@" + s.GetDomain()},
strings.NewReader(message),
))
// Delete the draft: add the \Deleted flag and expunge.
{
status, err := imapClient.Select("Drafts", false)
require.NoError(t, err)
require.Equal(t, uint32(1), status.Messages)
// Add the \Deleted flag.
require.NoError(t, clientStore(imapClient, 1, 1, true, imap.FormatFlagsOp(imap.AddFlags, true), imap.DeletedFlag))
// Expunge.
require.NoError(t, imapClient.Expunge(nil))
}
// Assert that the draft is eventually gone.
require.Eventually(t, func() bool {
status, err := imapClient.Select("Drafts", false)
require.NoError(t, err)
return status.Messages == 0
}, 10*time.Second, 100*time.Millisecond)
// Assert that the message is eventually in the sent folder.
require.Eventually(t, func() bool {
messages, err := clientFetch(imapClient, "Sent")
require.NoError(t, err)
return len(messages) == 1
}, 10*time.Second, 100*time.Millisecond)
// Assert that the message is not marked as a draft.
{
messages, err := clientFetch(imapClient, "Sent")
require.NoError(t, err)
require.Len(t, messages, 1)
require.NotContains(t, messages[0].Flags, imap.DraftFlag)
}
})
})
}
func TestBridge_SendInvite(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
// Create a recipient user.
_, _, err := s.CreateUser("recipient", password)
require.NoError(t, err)
// Set "attach public keys" to true for the user.
withClient(ctx, t, s, username, password, func(ctx context.Context, client *proton.Client) {
settings, err := client.SetAttachPublicKey(ctx, proton.SetAttachPublicKeyReq{AttachPublicKey: true})
require.NoError(t, err)
require.Equal(t, proton.Bool(true), settings.AttachPublicKey)
})
// The sender should be fully synced.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
defer done()
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
require.NoError(t, err)
require.Equal(t, userID, (<-syncCh).UserID)
})
// Start the bridge.
withBridge(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())))
require.NoError(t, err)
require.NoError(t, imapClient.Login(userInfo.Addresses[0], string(userInfo.BridgePass)))
defer imapClient.Logout() //nolint:errcheck
// The message to send.
b, err := os.ReadFile("testdata/invite.eml")
require.NoError(t, err)
// Save a draft.
require.NoError(t, imapClient.Append("Drafts", []string{imap.DraftFlag}, time.Now(), bytes.NewReader(b)))
// Assert that the draft exists and is marked as a draft.
{
messages, err := clientFetch(imapClient, "Drafts")
require.NoError(t, err)
require.Len(t, messages, 1)
require.Contains(t, messages[0].Flags, imap.DraftFlag)
}
// Connect the SMTP client.
smtpClient, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
require.NoError(t, err)
defer smtpClient.Close() //nolint:errcheck
// Upgrade to TLS.
require.NoError(t, smtpClient.StartTLS(&tls.Config{InsecureSkipVerify: true}))
// Authorize with SASL PLAIN.
require.NoError(t, smtpClient.Auth(sasl.NewPlainClient(
userInfo.Addresses[0],
userInfo.Addresses[0],
string(userInfo.BridgePass)),
))
// Send the message.
require.NoError(t, smtpClient.SendMail(
userInfo.Addresses[0],
[]string{"recipient@" + s.GetDomain()},
bytes.NewReader(b),
))
// Delete the draft: add the \Deleted flag and expunge.
{
status, err := imapClient.Select("Drafts", false)
require.NoError(t, err)
require.Equal(t, uint32(1), status.Messages)
// Add the \Deleted flag.
require.NoError(t, clientStore(imapClient, 1, 1, true, imap.FormatFlagsOp(imap.AddFlags, true), imap.DeletedFlag))
// Expunge.
require.NoError(t, imapClient.Expunge(nil))
}
// Assert that the draft is eventually gone.
require.Eventually(t, func() bool {
status, err := imapClient.Select("Drafts", false)
require.NoError(t, err)
return status.Messages == 0
}, 10*time.Second, 100*time.Millisecond)
// Assert that the message is eventually in the sent folder.
require.Eventually(t, func() bool {
messages, err := clientFetch(imapClient, "Sent")
require.NoError(t, err)
return len(messages) == 1
}, 10*time.Second, 100*time.Millisecond)
// Assert that the message is not marked as a draft.
{
messages, err := clientFetch(imapClient, "Sent")
require.NoError(t, err)
require.Len(t, messages, 1)
require.NotContains(t, messages[0].Flags, imap.DraftFlag)
}
})
})
}
func TestBridge_SendAddTextBodyPartIfNotExists(t *testing.T) {
const messageMultipartWithoutText = `Content-Type: multipart/mixed;
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
Subject: A new message
Date: Mon, 13 Mar 2023 16:06:16 +0100
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
Content-Disposition: inline;
filename=Cat_August_2010-4.jpeg
Content-Type: image/jpeg;
name="Cat_August_2010-4.jpeg"
Content-Transfer-Encoding: base64
SGVsbG8gd29ybGQ=
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84--
`
const messageMultipartWithText = `Content-Type: multipart/mixed;
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
Subject: A new message Part2
Date: Mon, 13 Mar 2023 16:06:16 +0100
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
Content-Disposition: inline;
filename=Cat_August_2010-4.jpeg
Content-Type: image/jpeg;
name="Cat_August_2010-4.jpeg"
Content-Transfer-Encoding: base64
SGVsbG8gd29ybGQ=
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
Content-Type: text/html;charset=utf8
Content-Transfer-Encoding: quoted-printable
Hello world
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84--
`
const messageWithTextOnly = `Content-Type: text/plain;charset=utf8
Content-Transfer-Encoding: quoted-printable
Subject: A new message Part3
Date: Mon, 13 Mar 2023 16:06:16 +0100
Hello world
`
const messageMultipartWithoutTextWithTextAttachment = `Content-Type: multipart/mixed;
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
Subject: A new message Part4
Date: Mon, 13 Mar 2023 16:06:16 +0100
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
Content-Type: text/plain; charset=UTF-8; name="text.txt"
Content-Disposition: attachment; filename="text.txt"
Content-Transfer-Encoding: base64
SGVsbG8gd29ybGQK
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84--
`
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
_, _, err := s.CreateUser("recipient", password)
require.NoError(t, err)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
senderUserID, err := bridge.LoginFull(ctx, username, password, nil, nil)
require.NoError(t, err)
recipientUserID, err := bridge.LoginFull(ctx, "recipient", password, nil, nil)
require.NoError(t, err)
senderInfo, err := bridge.GetUserInfo(senderUserID)
require.NoError(t, err)
recipientInfo, err := bridge.GetUserInfo(recipientUserID)
require.NoError(t, err)
messages := []string{
messageMultipartWithoutText,
messageMultipartWithText,
messageWithTextOnly,
messageMultipartWithoutTextWithTextAttachment,
}
for _, m := range messages {
// Dial the server.
client, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
require.NoError(t, err)
defer client.Close() //nolint:errcheck
// Upgrade to TLS.
require.NoError(t, client.StartTLS(&tls.Config{InsecureSkipVerify: true}))
// Authorize with SASL LOGIN.
require.NoError(t, client.Auth(sasl.NewLoginClient(
senderInfo.Addresses[0],
string(senderInfo.BridgePass)),
))
// Send the message.
require.NoError(t, client.SendMail(
senderInfo.Addresses[0],
[]string{recipientInfo.Addresses[0]},
strings.NewReader(m),
))
}
// Connect the sender IMAP client.
senderIMAPClient, err := client.Dial(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())))
require.NoError(t, err)
require.NoError(t, recipientIMAPClient.Login(recipientInfo.Addresses[0], string(recipientInfo.BridgePass)))
defer recipientIMAPClient.Logout() //nolint:errcheck
require.Eventually(t, func() bool {
messages, err := clientFetch(senderIMAPClient, `Sent`, imap.FetchBodyStructure)
require.NoError(t, err)
require.Equal(t, 4, len(messages))
// messages may not be in order
for _, message := range messages {
switch {
case message.Envelope.Subject == "A new message":
// The message that was sent should now include an empty text/plain body part since there was none
// in the original message.
require.Equal(t, 2, len(message.BodyStructure.Parts))
require.Equal(t, "text", message.BodyStructure.Parts[0].MIMEType)
require.Equal(t, "plain", message.BodyStructure.Parts[0].MIMESubType)
require.Equal(t, uint32(0), message.BodyStructure.Parts[0].Size)
require.Equal(t, "image", message.BodyStructure.Parts[1].MIMEType)
require.Equal(t, "jpeg", message.BodyStructure.Parts[1].MIMESubType)
case message.Envelope.Subject == "A new message Part2":
// This message already has a text body, should be unchanged
require.Equal(t, 2, len(message.BodyStructure.Parts))
require.Equal(t, "image", message.BodyStructure.Parts[1].MIMEType)
require.Equal(t, "jpeg", message.BodyStructure.Parts[1].MIMESubType)
require.Equal(t, "text", message.BodyStructure.Parts[0].MIMEType)
require.Equal(t, "html", message.BodyStructure.Parts[0].MIMESubType)
case message.Envelope.Subject == "A new message Part3":
// This message already has a text body, should be unchanged
require.Equal(t, 0, len(message.BodyStructure.Parts))
require.Equal(t, "text", message.BodyStructure.MIMEType)
require.Equal(t, "plain", message.BodyStructure.MIMESubType)
case message.Envelope.Subject == "A new message Part4":
// The message that was sent should now include an empty text/plain body part since even though
// there was only a text/plain attachment in the original message.
require.Equal(t, 2, len(message.BodyStructure.Parts))
require.Equal(t, "text", message.BodyStructure.Parts[0].MIMEType)
require.Equal(t, "plain", message.BodyStructure.Parts[0].MIMESubType)
require.Equal(t, uint32(0), message.BodyStructure.Parts[0].Size)
require.Equal(t, "text", message.BodyStructure.Parts[1].MIMEType)
require.Equal(t, "plain", message.BodyStructure.Parts[1].MIMESubType)
require.Equal(t, "attachment", message.BodyStructure.Parts[1].Disposition)
}
}
return true
}, 10*time.Second, 100*time.Millisecond)
})
})
}

View File

@ -21,14 +21,13 @@ import (
"context"
"fmt"
"net"
"os"
"path/filepath"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
"github.com/sirupsen/logrus"
)
@ -114,38 +113,42 @@ func (bridge *Bridge) SetSMTPSSL(newSSL bool) error {
return bridge.restartSMTP()
}
func (bridge *Bridge) GetGluonDir() string {
return bridge.vault.GetGluonDir()
func (bridge *Bridge) GetGluonCacheDir() string {
return bridge.vault.GetGluonCacheDir()
}
func (bridge *Bridge) GetGluonDataDir() (string, error) {
return bridge.locator.ProvideGluonDataPath()
}
func (bridge *Bridge) SetGluonDir(ctx context.Context, newGluonDir string) error {
return safe.RLockRet(func() error {
currentGluonDir := bridge.GetGluonDir()
currentGluonDir := bridge.GetGluonCacheDir()
newGluonDir = filepath.Join(newGluonDir, "gluon")
if newGluonDir == currentGluonDir {
return fmt.Errorf("new gluon dir is the same as the old one")
}
currentVolumeName := filepath.VolumeName(currentGluonDir)
newVolumeName := filepath.VolumeName(newGluonDir)
if currentVolumeName != newVolumeName {
return fmt.Errorf("it's currently not possible to move the cache between different volumes")
}
if err := bridge.closeIMAP(context.Background()); err != nil {
return fmt.Errorf("failed to close IMAP: %w", err)
}
if err := moveDir(bridge.GetGluonDir(), newGluonDir); err != nil {
return fmt.Errorf("failed to move gluon dir: %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)
}
}
if err := bridge.vault.SetGluonDir(newGluonDir); err != nil {
return fmt.Errorf("failed to set new gluon dir: %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.GetGluonDir(),
bridge.vault.GetGluonCacheDir(),
gluonDataDir,
bridge.curVersion,
bridge.tlsConfig,
bridge.reporter,
@ -153,6 +156,8 @@ func (bridge *Bridge) SetGluonDir(ctx context.Context, newGluonDir string) error
bridge.logIMAPServer,
bridge.imapEventCh,
bridge.tasks,
bridge.uidValidityGenerator,
bridge.panicHandler,
)
if err != nil {
return fmt.Errorf("failed to create new IMAP server: %w", err)
@ -174,6 +179,23 @@ func (bridge *Bridge) SetGluonDir(ctx context.Context, newGluonDir string) error
}, bridge.usersLock)
}
func (bridge *Bridge) moveGluonCacheDir(oldGluonDir, newGluonDir string) error {
logrus.Infof("gluon cache moving from %s to %s", oldGluonDir, newGluonDir)
oldCacheDir := ApplyGluonCachePathSuffix(oldGluonDir)
if err := copyDir(oldCacheDir, ApplyGluonCachePathSuffix(newGluonDir)); err != nil {
return fmt.Errorf("failed to copy gluon dir: %w", err)
}
if err := bridge.vault.SetGluonDir(newGluonDir); err != nil {
return fmt.Errorf("failed to set new gluon cache dir: %w", err)
}
if err := os.RemoveAll(oldCacheDir); err != nil {
logrus.WithError(err).Error("failed to remove old gluon cache dir")
}
return nil
}
func (bridge *Bridge) GetProxyAllowed() bool {
return bridge.vault.GetProxyAllowed()
}
@ -272,23 +294,11 @@ func (bridge *Bridge) GetCurrentVersion() *semver.Version {
}
func (bridge *Bridge) GetLastVersion() *semver.Version {
return bridge.vault.GetLastVersion()
return bridge.lastVersion
}
func (bridge *Bridge) GetFirstStart() bool {
return bridge.vault.GetFirstStart()
}
func (bridge *Bridge) SetFirstStart(firstStart bool) error {
return bridge.vault.SetFirstStart(firstStart)
}
func (bridge *Bridge) GetFirstStartGUI() bool {
return bridge.vault.GetFirstStartGUI()
}
func (bridge *Bridge) SetFirstStartGUI(firstStart bool) error {
return bridge.vault.SetFirstStartGUI(firstStart)
return bridge.firstStart
}
func (bridge *Bridge) GetColorScheme() string {
@ -299,6 +309,9 @@ func (bridge *Bridge) SetColorScheme(colorScheme string) error {
return bridge.vault.SetColorScheme(colorScheme)
}
// FactoryReset deletes all users, wipes the vault, and deletes all files.
// 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) {
// Delete all the users.
safe.Lock(func() {
@ -308,29 +321,17 @@ func (bridge *Bridge) FactoryReset(ctx context.Context) {
}, bridge.usersLock)
// Wipe the vault.
gluonDir, err := bridge.locator.ProvideGluonPath()
gluonCacheDir, err := bridge.locator.ProvideGluonCachePath()
if err != nil {
logrus.WithError(err).Error("Failed to provide gluon dir")
} else if err := bridge.vault.Reset(gluonDir); err != nil {
} else if err := bridge.vault.Reset(gluonCacheDir); err != nil {
logrus.WithError(err).Error("Failed to reset vault")
}
// Then delete all files.
if err := bridge.locator.Clear(); err != nil {
// Lastly, delete all files except the vault.
if err := bridge.locator.Clear(bridge.vault.Path()); err != nil {
logrus.WithError(err).Error("Failed to clear data paths")
}
// Lastly clear the keychain.
vaultDir, err := bridge.locator.ProvideSettingsPath()
if err != nil {
logrus.WithError(err).Error("Failed to get vault dir")
} else if helper, err := vault.GetHelper(vaultDir); err != nil {
logrus.WithError(err).Error("Failed to get keychain helper")
} else if keychain, err := keychain.NewKeychain(helper, constants.KeyChainName); err != nil {
logrus.WithError(err).Error("Failed to get keychain")
} else if err := keychain.Clear(); err != nil {
logrus.WithError(err).Error("Failed to clear keychain")
}
}
func getPort(addr net.Addr) int {

View File

@ -162,26 +162,7 @@ func TestBridge_Settings_FirstStart(t *testing.T) {
// By default, first start is true.
require.True(t, bridge.GetFirstStart())
// Set first start to false.
require.NoError(t, bridge.SetFirstStart(false))
// Get the new setting.
require.False(t, bridge.GetFirstStart())
})
})
}
func TestBridge_Settings_FirstStartGUI(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) {
// By default, first start is true.
require.True(t, bridge.GetFirstStartGUI())
// Set first start to false.
require.NoError(t, bridge.SetFirstStartGUI(false))
// Get the new setting.
require.False(t, bridge.GetFirstStartGUI())
// the setting of the first start value is managed by bridge itself, so the setter is not exported.
})
})
}

View File

@ -22,6 +22,7 @@ import (
"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"
@ -31,25 +32,41 @@ import (
)
func (bridge *Bridge) serveSMTP() error {
logrus.Info("Starting SMTP server")
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 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")
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)
}
})
if err := bridge.vault.SetSMTPPort(getPort(smtpListener.Addr())); err != nil {
return fmt.Errorf("failed to store SMTP port in vault: %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
}
@ -60,6 +77,8 @@ func (bridge *Bridge) restartSMTP() error {
return fmt.Errorf("failed to close SMTP: %w", err)
}
bridge.publish(events.SMTPServerStopped{})
bridge.smtpServer = newSMTPServer(bridge, bridge.tlsConfig, bridge.logSMTP)
return bridge.serveSMTP()
@ -82,6 +101,8 @@ func (bridge *Bridge) closeSMTP() error {
logrus.WithError(err).Debug("Failed to close SMTP server (expected -- we close the listener ourselves)")
}
bridge.publish(events.SMTPServerStopped{})
return nil
}

View File

@ -28,6 +28,7 @@ import (
"testing"
"time"
"github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/gluon/rfc822"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/go-proton-api/server"
@ -351,7 +352,7 @@ func withClient(ctx context.Context, t *testing.T, s *server.Server, username st
fn(ctx, c)
}
func clientFetch(client *client.Client, mailbox string) ([]*imap.Message, error) { //nolint:unused
func clientFetch(client *client.Client, mailbox string, extraItems ...imap.FetchItem) ([]*imap.Message, error) {
status, err := client.Select(mailbox, false)
if err != nil {
return nil, err
@ -363,10 +364,13 @@ func clientFetch(client *client.Client, mailbox string) ([]*imap.Message, error)
resCh := make(chan *imap.Message)
fetchItems := []imap.FetchItem{imap.FetchFlags, imap.FetchEnvelope, imap.FetchUid, imap.FetchBodyStructure, "BODY.PEEK[]"}
fetchItems = append(fetchItems, extraItems...)
go func() {
if err := client.Fetch(
&imap.SeqSet{Set: []imap.Seq{{Start: 1, Stop: status.Messages}}},
[]imap.FetchItem{imap.FetchFlags, imap.FetchEnvelope, imap.FetchUid, "BODY.PEEK[]"},
fetchItems,
resCh,
); err != nil {
panic(err)
@ -376,6 +380,35 @@ func clientFetch(client *client.Client, mailbox string) ([]*imap.Message, error)
return iterator.Collect(iterator.Chan(resCh)), nil
}
func clientStore(client *client.Client, from, to int, isUID bool, item imap.StoreItem, flags ...string) error {
var storeFunc func(seqset *imap.SeqSet, item imap.StoreItem, value interface{}, ch chan *imap.Message) error
if isUID {
storeFunc = client.UidStore
} else {
storeFunc = client.Store
}
return storeFunc(
&imap.SeqSet{Set: []imap.Seq{{Start: uint32(from), Stop: uint32(to)}}},
item,
xslices.Map(flags, func(flag string) interface{} { return flag }),
nil,
)
}
func clientList(client *client.Client) []*imap.MailboxInfo {
resCh := make(chan *imap.MailboxInfo)
go func() {
if err := client.List("", "*", resCh); err != nil {
panic(err)
}
}()
return iterator.Collect(iterator.Chan(resCh))
}
func createNumMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID, labelID string, count int) []string {
literal, err := os.ReadFile(filepath.Join("testdata", "text-plain.eml"))
require.NoError(t, err)
@ -396,10 +429,13 @@ func createMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID,
keyPass, err := salt.SaltForKey(password, user.Keys.Primary().ID)
require.NoError(t, err)
_, addrKRs, err := proton.Unlock(user, addr, keyPass)
_, addrKRs, err := proton.Unlock(user, addr, keyPass, async.NoopPanicHandler{})
require.NoError(t, err)
res, err := stream.Collect(ctx, c.ImportMessages(
_, ok := addrKRs[addrID]
require.True(t, ok)
str, err := c.ImportMessages(
ctx,
addrKRs[addrID],
runtime.NumCPU(),
@ -414,7 +450,10 @@ func createMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID,
Message: message,
}
})...,
))
)
require.NoError(t, err)
res, err := stream.Collect(ctx, str)
require.NoError(t, err)
return xslices.Map(res, func(res proton.ImportRes) string {

85
internal/bridge/testdata/invite.eml vendored Normal file
View File

@ -0,0 +1,85 @@
From: <username@proton.local>
To: <recipient@proton.local>
Subject: Testing calendar invite
Date: Fri, 3 Feb 2023 01:04:32 +0100
Message-ID: <000001d93763$183b74e0$48b25ea0$@proton.local>
MIME-Version: 1.0
Content-Type: text/calendar; method=REQUEST;
charset="utf-8"
Content-Transfer-Encoding: 7bit
X-Mailer: Microsoft Outlook 16.0
Thread-Index: Adk3Yw5pLdgwsT46RviXb/nfvQlesQAAAmGA
Content-Language: en-gb
BEGIN:VCALENDAR
PRODID:-//Microsoft Corporation//Outlook 16.0 MIMEDIR//EN
VERSION:2.0
METHOD:REQUEST
X-MS-OLK-FORCEINSPECTOROPEN:TRUE
BEGIN:VTIMEZONE
TZID:Central European Standard Time
BEGIN:STANDARD
DTSTART:16011028T030000
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
END:STANDARD
BEGIN:DAYLIGHT
DTSTART:16010325T020000
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
END:DAYLIGHT
END:VTIMEZONE
BEGIN:VEVENT
ATTENDEE;CN=recipient@proton.local;RSVP=TRUE:mailto:recipient@proton.local
CLASS:PUBLIC
CREATED:20230203T000432Z
DESCRIPTION:qweqweqweqweqweqwe/gn\\n
DTEND;TZID="Central European Standard Time":20230203T020000
DTSTAMP:20230203T000432Z
DTSTART;TZID="Central European Standard Time":20230203T013000
LAST-MODIFIED:20230203T000432Z
LOCATION:qweqwe
ORGANIZER;CN=username@proton.local:mailto:username@proton.local
PRIORITY:5
SEQUENCE:0
SUMMARY;LANGUAGE=en-gb:Testing calendar invite
TRANSP:OPAQUE
UID:040000008200E00074C5B7101A82E008000000003080B2796B37D901000000000000000
0100000001236CD1CD93CA9449C6FF1AC4DEAC44E
X-ALT-DESC;FMTTYPE=text/html:<html xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:w="urn:schemas-mic
rosoft-com:office:word" xmlns:m="http://schemas.microsoft.com/office/2004/
12/omml" xmlns="http://www.w3.org/TR/REC-html40"><head><meta http-equiv=Co
ntent-Type content="text/html/g; charset=us-ascii"><meta name=Generator con
tent="Microsoft Word 15 (filtered medium)"><style><!--/gn/* Font Definition
s *//gn@font-face\\n {font-family:"Cambria Math"\\;\\n panose-1:2 4 5 3 5 4 6
3 2 4/g;}\\n@font-face\\n {font-family:Calibri\\;\\n panose-1:2 15 5 2 2 2 4 3
2 4/g;}\\n/* Style Definitions */\\np.MsoNormal\\, li.MsoNormal\\, div.MsoNorma
l/gn {margin:0cm\\;\\n font-size:11.0pt\\;\\n font-family:"Calibri"\\,sans-serif
/g;\\n mso-fareast-language:EN-US\\;}\\nspan.EmailStyle18\\n {mso-style-type:pe
rsonal-compose/g;\\n font-family:"Calibri"\\,sans-serif\\;\\n color:windowtext\\
;}/gn.MsoChpDefault\\n {mso-style-type:export-only\\;\\n font-size:10.0pt\\;}\\n
@page WordSection1/gn {size:612.0pt 792.0pt\\;\\n margin:72.0pt 72.0pt 72.0pt
72.0pt/g;}\\ndiv.WordSection1\\n {page:WordSection1\\;}\\n--></style><!--[if g
te mso 9]><xml>/gn<o:shapedefaults v:ext="edit" spidmax="1026" />\\n</xml><!
[endif]--><!--[if gte mso 9]><xml>/gn<o:shapelayout v:ext="edit">\\n<o:idmap
v:ext="edit" data="1" />/gn</o:shapelayout></xml><![endif]--></head><body
lang=EN-GB link="#0563C1" vlink="#954F72" style='word-wrap:break-word'><di
v class=WordSection1><p class=MsoNormal><span lang=EN-US>qweqweqweqweqweqw
e<o:p></o:p></span></p></div></body></html>
X-MICROSOFT-CDO-BUSYSTATUS:TENTATIVE
X-MICROSOFT-CDO-IMPORTANCE:1
X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY
X-MICROSOFT-DISALLOW-COUNTER:FALSE
X-MS-OLK-AUTOSTARTCHECK:FALSE
X-MS-OLK-CONFTYPE:0
BEGIN:VALARM
TRIGGER:-PT15M
ACTION:DISPLAY
DESCRIPTION:Reminder
END:VALARM
END:VEVENT
END:VCALENDAR

View File

@ -18,5 +18,9 @@
package bridge
func (bridge *Bridge) GetBridgeTLSCert() ([]byte, []byte) {
return bridge.vault.GetBridgeTLSCert(), bridge.vault.GetBridgeTLSKey()
return bridge.vault.GetBridgeTLSCert()
}
func (bridge *Bridge) SetBridgeTLSCertPath(certPath, keyPath string) error {
return bridge.vault.SetBridgeTLSCertPath(certPath, keyPath)
}

View File

@ -26,10 +26,11 @@ import (
type Locator interface {
ProvideSettingsPath() (string, error)
ProvideLogsPath() (string, error)
ProvideGluonPath() (string, error)
ProvideGluonCachePath() (string, error)
ProvideGluonDataPath() (string, error)
GetLicenseFilePath() string
GetDependencyLicensesLink() string
Clear() error
Clear(...string) error
}
type Identifier interface {

View File

@ -32,19 +32,7 @@ func (bridge *Bridge) CheckForUpdates() {
}
func (bridge *Bridge) InstallUpdate(version updater.VersionInfo) {
log := logrus.WithFields(logrus.Fields{
"version": version.Version,
"current": bridge.curVersion,
"channel": bridge.vault.GetUpdateChannel(),
})
select {
case bridge.installCh <- installJob{version: version, silent: false}:
log.Info("The update will be installed manually")
default:
log.Info("An update is already being installed")
}
bridge.installCh <- installJob{version: version, silent: false}
}
func (bridge *Bridge) handleUpdate(version updater.VersionInfo) {
@ -89,17 +77,7 @@ func (bridge *Bridge) handleUpdate(version updater.VersionInfo) {
default:
safe.RLock(func() {
if version.Version.GreaterThan(bridge.newVersion) {
log.Info("An update is available")
select {
case bridge.installCh <- installJob{version: version, silent: true}:
log.Info("The update will be installed silently")
default:
log.Info("An update is already being installed")
}
}
bridge.installCh <- installJob{version: version, silent: true}
}, bridge.newVersionLock)
}
}
@ -117,6 +95,12 @@ func (bridge *Bridge) installUpdate(ctx context.Context, job installJob) {
"channel": bridge.vault.GetUpdateChannel(),
})
if !job.version.Version.GreaterThan(bridge.newVersion) {
return
}
log.WithField("silent", job.silent).Info("An update is available")
bridge.publish(events.UpdateAvailable{
Version: job.version,
Compatible: true,
@ -142,6 +126,7 @@ func (bridge *Bridge) installUpdate(ctx context.Context, job installJob) {
Silent: job.silent,
Error: err,
})
default:
log.Info("The update was installed successfully")

View File

@ -23,9 +23,10 @@ import (
"fmt"
"runtime"
"github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/gluon/imap"
"github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/proton-bridge/v3/internal/async"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
@ -94,7 +95,7 @@ func (bridge *Bridge) GetUserInfo(userID string) (UserInfo, error) {
if len(user.AuthUID()) == 0 {
state = SignedOut
}
info = getUserInfo(user.UserID(), user.Username(), state, user.AddressMode())
info = getUserInfo(user.UserID(), user.Username(), user.PrimaryEmail(), state, user.AddressMode())
}); err != nil {
return UserInfo{}, fmt.Errorf("failed to get user info: %w", err)
}
@ -298,6 +299,59 @@ func (bridge *Bridge) SetAddressMode(ctx context.Context, userID string, mode va
}, bridge.usersLock)
}
// SendBadEventUserFeedback passes the feedback to the given user.
func (bridge *Bridge) SendBadEventUserFeedback(_ context.Context, userID string, doResync bool) error {
logrus.WithField("userID", userID).WithField("doResync", doResync).Info("Passing bad event feedback to user")
return safe.LockRet(func() error {
ctx := context.Background()
user, ok := bridge.users[userID]
if !ok {
if rerr := bridge.reporter.ReportMessageWithContext(
"Failed to handle event: feedback failed: no such user",
reporter.Context{"user_id": userID},
); rerr != nil {
logrus.WithError(rerr).Error("Failed to report feedback failure")
}
return ErrNoSuchUser
}
if doResync {
if rerr := bridge.reporter.ReportMessageWithContext(
"Failed to handle event: feedback resync",
reporter.Context{"user_id": userID},
); rerr != nil {
logrus.WithError(rerr).Error("Failed to report feedback failure")
}
if err := bridge.addIMAPUser(ctx, user); err != nil {
return fmt.Errorf("failed to add IMAP user: %w", err)
}
user.BadEventFeedbackResync(ctx)
return nil
}
if rerr := bridge.reporter.ReportMessageWithContext(
"Failed to handle event: feedback logout",
reporter.Context{"user_id": userID},
); rerr != nil {
logrus.WithError(rerr).Error("Failed to report feedback failure")
}
bridge.logoutUser(ctx, user, true, false)
bridge.publish(events.UserLoggedOut{
UserID: userID,
})
return nil
}, bridge.usersLock)
}
func (bridge *Bridge) loginUser(ctx context.Context, client *proton.Client, authUID, authRef string, keyPass []byte) (string, error) {
apiUser, err := client.GetUser(ctx)
if err != nil {
@ -329,30 +383,37 @@ func (bridge *Bridge) loginUser(ctx context.Context, client *proton.Client, auth
// loadUsers tries to load each user in the vault that isn't already loaded.
func (bridge *Bridge) loadUsers(ctx context.Context) error {
logrus.WithField("count", len(bridge.vault.GetUserIDs())).Info("Loading users")
defer logrus.Info("Finished loading users")
return bridge.vault.ForUser(runtime.NumCPU(), func(user *vault.User) error {
log := logrus.WithField("userID", user.UserID())
if user.AuthUID() == "" {
log.Info("User is not connected (skipping)")
return nil
}
if safe.RLockRet(func() bool { return mapHas(bridge.users, user.UserID()) }, bridge.usersLock) {
log.Info("User is already loaded (skipping)")
return nil
}
logrus.WithField("userID", user.UserID()).Info("Loading connected user")
log.Info("Loading connected user")
bridge.publish(events.UserLoading{
UserID: user.UserID(),
})
if err := bridge.loadUser(ctx, user); err != nil {
logrus.WithError(err).Error("Failed to load connected user")
log.WithError(err).Error("Failed to load connected user")
bridge.publish(events.UserLoadFail{
UserID: user.UserID(),
Error: err,
})
} else {
logrus.WithField("userID", user.UserID()).Info("Successfully loaded user")
log.Info("Successfully loaded connected user")
bridge.publish(events.UserLoadSuccess{
UserID: user.UserID(),
@ -367,12 +428,13 @@ func (bridge *Bridge) loadUsers(ctx context.Context) error {
func (bridge *Bridge) loadUser(ctx context.Context, user *vault.User) error {
client, auth, err := bridge.api.NewClientWithRefresh(ctx, user.AuthUID(), user.AuthRef())
if err != nil {
if apiErr := new(proton.Error); errors.As(err, &apiErr) && (apiErr.Code == proton.AuthRefreshTokenInvalid) {
if apiErr := new(proton.APIError); errors.As(err, &apiErr) && (apiErr.Code == proton.AuthRefreshTokenInvalid) {
// The session cannot be refreshed, we sign out the user by clearing his auth secrets.
if err := user.Clear(); err != nil {
logrus.WithError(err).Warn("Failed to clear user secrets")
}
}
return fmt.Errorf("failed to create API client: %w", err)
}
@ -389,6 +451,12 @@ func (bridge *Bridge) loadUser(ctx context.Context, user *vault.User) error {
return fmt.Errorf("failed to add user: %w", err)
}
if user.PrimaryEmail() != apiUser.Email {
if err := user.SetPrimaryEmail(apiUser.Email); err != nil {
return fmt.Errorf("failed to modify user primary email: %w", err)
}
}
return nil
}
@ -448,9 +516,9 @@ func (bridge *Bridge) addUserWithVault(
client,
bridge.reporter,
apiUser,
bridge.crashHandler,
bridge.vault.SyncWorkers(),
bridge.panicHandler,
bridge.vault.GetShowAllMail(),
bridge.vault.GetMaxSyncMemory(),
)
if err != nil {
return fmt.Errorf("failed to create user: %w", err)
@ -504,7 +572,7 @@ func (bridge *Bridge) newVaultUser(
saltedKeyPass []byte,
) (*vault.User, bool, error) {
if !bridge.vault.HasUser(apiUser.ID) {
user, err := bridge.vault.AddUser(apiUser.ID, apiUser.Name, authUID, authRef, saltedKeyPass)
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)
}
@ -550,11 +618,17 @@ func (bridge *Bridge) logoutUser(ctx context.Context, user *user.User, withAPI,
}
// getUserInfo returns information about a disconnected user.
func getUserInfo(userID, username string, state UserState, addressMode vault.AddressMode) UserInfo {
func getUserInfo(userID, username, primaryEmail string, state UserState, addressMode vault.AddressMode) UserInfo {
var addresses []string
if len(primaryEmail) > 0 {
addresses = []string{primaryEmail}
}
return UserInfo{
State: state,
UserID: userID,
Username: username,
Addresses: addresses,
AddressMode: addressMode,
}
}

View File

@ -0,0 +1,852 @@
// 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"
"net"
"net/http"
"net/mail"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/gluon/rfc822"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/go-proton-api/server"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"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/ProtonMail/proton-bridge/v3/internal/user"
"github.com/bradenaw/juniper/stream"
"github.com/bradenaw/juniper/xslices"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/client"
"github.com/golang/mock/gomock"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
)
func TestBridge_User_RefreshEvent(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
// Create a user.
userID, addrID, err := s.CreateUser("user", password)
require.NoError(t, err)
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
require.NoError(t, err)
var messageIDs []string
// Create 10 messages for the user.
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
messageIDs = createNumMessages(ctx, t, c, addrID, labelID, 10)
})
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
userLoginAndSync(ctx, t, bridge, "user", password)
})
// Remove a message
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
require.NoError(t, c.DeleteMessage(ctx, messageIDs[0]))
})
require.NoError(t, s.RefreshUser(userID, proton.RefreshMail))
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()
userContinueEventProcess(ctx, t, s, bridge)
})
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
createNumMessages(ctx, t, c, addrID, labelID, 10)
})
userContinueEventProcess(ctx, t, s, bridge)
})
})
}
func TestBridge_User_BadMessage_BadEvent(t *testing.T) {
t.Run("Resync", test_badMessage_badEvent(func(t *testing.T, ctx context.Context, bridge *bridge.Bridge, badUserID string) {
// User feedback is resync
require.NoError(t, bridge.SendBadEventUserFeedback(ctx, badUserID, true))
// Wait for sync to finish
syncCh, closeCh := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
require.Equal(t, badUserID, (<-syncCh).UserID)
closeCh()
}))
t.Run("LogoutAndLogin", test_badMessage_badEvent(func(t *testing.T, ctx context.Context, bridge *bridge.Bridge, badUserID string) {
logoutCh, closeCh := chToType[events.Event, events.UserLoggedOut](bridge.GetEvents(events.UserLoggedOut{}))
// User feedback is logout
require.NoError(t, bridge.SendBadEventUserFeedback(ctx, badUserID, false))
require.Equal(t, badUserID, (<-logoutCh).UserID)
closeCh()
// The user will eventually be logged out due to the bad request errors.
require.Eventually(t, func() bool {
return len(bridge.GetUserIDs()) == 1 && len(getConnectedUserIDs(t, bridge)) == 0
}, 100*user.EventPeriod, user.EventPeriod)
// Login again
_, err := bridge.LoginFull(ctx, "user", password, nil, nil)
require.NoError(t, err)
}))
}
func test_badMessage_badEvent(userFeedback func(t *testing.T, ctx context.Context, bridge *bridge.Bridge, badUserID string)) func(t *testing.T) {
return func(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
// Create a user.
userID, addrID, err := s.CreateUser("user", password)
require.NoError(t, err)
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
require.NoError(t, err)
// Create 10 messages for the user.
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
createNumMessages(ctx, t, c, addrID, labelID, 10)
})
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
userLoginAndSync(ctx, t, bridge, "user", password)
var messageIDs []string
// Create 10 more messages for the user, generating events.
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
messageIDs = createNumMessages(ctx, t, c, addrID, labelID, 10)
})
// If bridge attempts to sync the new messages, it should get a BadRequest error.
doBadRequest := true
s.AddStatusHook(func(req *http.Request) (int, bool) {
if !doBadRequest {
return 0, false
}
if xslices.Index(xslices.Map(messageIDs[0:5], func(messageID string) string {
return "/mail/v4/messages/" + messageID
}), req.URL.Path) < 0 {
return 0, false
}
return http.StatusBadRequest, true
})
badUserID := userReceivesBadError(t, bridge, mocks)
// Remove messages, make response OK again
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
require.NoError(t, c.DeleteMessage(ctx, messageIDs[0:5]...))
})
doBadRequest = false
userFeedback(t, ctx, bridge, badUserID)
userContinueEventProcess(ctx, t, s, bridge)
})
})
}
}
func TestBridge_User_BadMessage_NoBadEvent(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
// Create a user.
_, addrID, err := s.CreateUser("user", password)
require.NoError(t, err)
// Create 10 messages for the user.
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 10)
})
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
userLoginAndSync(ctx, t, bridge, "user", password)
var messageIDs []string
// Create 10 more messages for the user, generating events.
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
messageIDs = createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 10)
})
// If bridge attempts to sync the new messages, it should get a BadRequest error.
s.AddStatusHook(func(req *http.Request) (int, bool) {
if strings.Contains(req.URL.Path, "/mail/v4/messages/"+messageIDs[2]) {
return http.StatusUnprocessableEntity, true
}
return 0, false
})
// Remove messages
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
require.NoError(t, c.DeleteMessage(ctx, messageIDs...))
})
userContinueEventProcess(ctx, t, s, bridge)
})
})
}
func TestBridge_User_SameMessageLabelCreated_NoBadEvent(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
// Create a user.
userID, addrID, err := s.CreateUser("user", password)
require.NoError(t, err)
var messageIDs []string
// Create 10 messages for the user.
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
messageIDs = createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 10)
})
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
userLoginAndSync(ctx, t, bridge, "user", password)
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
require.NoError(t, err)
// Add NOOP events
require.NoError(t, s.AddLabelCreatedEvent(userID, labelID))
require.NoError(t, s.AddMessageCreatedEvent(userID, messageIDs[9]))
userContinueEventProcess(ctx, t, s, bridge)
})
})
}
func TestBridge_User_MessageLabelDeleted_NoBadEvent(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
// Create a user.
userID, addrID, err := s.CreateUser("user", password)
require.NoError(t, err)
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
require.NoError(t, err)
// Create 10 messages for the user.
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
createNumMessages(ctx, t, c, addrID, labelID, 10)
})
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
userLoginAndSync(ctx, t, bridge, "user", password)
// Create and delete 10 more messages for the user, generating delete events.
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
messageIDs := createNumMessages(ctx, t, c, addrID, labelID, 10)
require.NoError(t, c.DeleteMessage(ctx, messageIDs...))
})
// Create and delete 10 labels for the user, generating delete events.
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
for i := 0; i < 10; i++ {
label, err := c.CreateLabel(ctx, proton.CreateLabelReq{
Name: uuid.NewString(),
Color: "#f66",
Type: proton.LabelTypeLabel,
})
require.NoError(t, err)
require.NoError(t, c.DeleteLabel(ctx, label.ID))
}
})
userContinueEventProcess(ctx, t, s, bridge)
})
})
}
func TestBridge_User_AddressEvents_NoBadEvent(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
// Create a user.
userID, addrID, err := s.CreateUser("user", password)
require.NoError(t, err)
// Create 10 messages for the user.
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 10)
})
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
userLoginAndSync(ctx, t, bridge, "user", password)
addrID, err = s.CreateAddress(userID, "other@pm.me", password)
require.NoError(t, err)
userContinueEventProcess(ctx, t, s, bridge)
require.NoError(t, s.AddAddressCreatedEvent(userID, addrID))
userContinueEventProcess(ctx, t, s, bridge)
})
otherID, err := s.CreateAddress(userID, "another@pm.me", password)
require.NoError(t, err)
require.NoError(t, s.RemoveAddress(userID, otherID))
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
userContinueEventProcess(ctx, t, s, bridge)
require.NoError(t, s.CreateAddressKey(userID, addrID, password))
userContinueEventProcess(ctx, t, s, bridge)
require.NoError(t, s.RemoveAddress(userID, addrID))
userContinueEventProcess(ctx, t, s, bridge)
})
})
}
func TestBridge_User_AddressEventUpdatedForAddressThatDoesNotExist_NoBadEvent(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)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
userLoginAndSync(ctx, t, bridge, "user", password)
})
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
_, err := s.CreateAddressAsUpdate(userID, "another@pm.me", password)
require.NoError(t, err)
userContinueEventProcess(ctx, t, s, bridge)
})
})
}
func TestBridge_User_Network_NoBadEvents(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
retVal := int32(0)
setResponseAndWait := func(status int32) {
atomic.StoreInt32(&retVal, status)
time.Sleep(user.EventPeriod)
}
s.AddStatusHook(func(req *http.Request) (int, bool) {
status := atomic.LoadInt32(&retVal)
if strings.Contains(req.URL.Path, "/core/v4/events/") {
return int(status), status != 0
}
return 0, false
})
// Create a user.
_, addrID, err := s.CreateUser("user", password)
require.NoError(t, err)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
userLoginAndSync(ctx, t, bridge, "user", password)
// Create 10 more messages for the user, generating events.
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 10)
setResponseAndWait(http.StatusInternalServerError)
setResponseAndWait(http.StatusServiceUnavailable)
setResponseAndWait(http.StatusPaymentRequired)
setResponseAndWait(http.StatusForbidden)
setResponseAndWait(http.StatusBadRequest)
setResponseAndWait(http.StatusUnprocessableEntity)
setResponseAndWait(http.StatusTooManyRequests)
time.Sleep(10 * time.Second) // needs minimum of 10 seconds to retry
})
setResponseAndWait(0)
time.Sleep(10 * time.Second) // needs up to 20 seconds to retry
userContinueEventProcess(ctx, t, s, bridge)
})
})
}
func TestBridge_User_DropConn_NoBadEvent(t *testing.T) {
l, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
dropListener := proton.NewListener(l, proton.NewDropConn)
defer func() { _ = dropListener.Close() }()
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
// Create a user.
_, addrID, err := s.CreateUser("user", password)
require.NoError(t, err)
// Create 10 messages for the user.
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 10)
})
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
userLoginAndSync(ctx, t, bridge, "user", password)
mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).AnyTimes()
// Create 10 more messages for the user, generating events.
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
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()))
require.NoError(t, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }()
// The IMAP client will eventually see 20 messages.
require.Eventually(t, func() bool {
status, err := client.Status("INBOX", []imap.StatusItem{imap.StatusMessages})
return err == nil && status.Messages == 20
}, 10*time.Second, 100*time.Millisecond)
})
}, server.WithListener(dropListener))
}
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.
_, _, 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)
// 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).
require.NoError(t, getErr(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,
},
})))
// Import a message (generating a "create message" event).
str, err := c.ImportMessages(ctx, addrKRs[addrs[0].ID], 1, 1, proton.ImportReq{
Metadata: proton.ImportMetadata{
AddressID: addrs[0].ID,
Flags: proton.MessageFlagReceived,
},
Message: []byte("From: someone@example.com\r\nTo: blabla@example.com\r\n\r\nhello"),
})
require.NoError(t, err)
res, err := stream.Collect(ctx, str)
require.NoError(t, err)
// 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 imported message (generating an "update message" event).
require.NoError(t, c.MarkMessagesUnread(ctx, res[0].MessageID))
// Process those events.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
userContinueEventProcess(ctx, t, s, bridge)
})
})
})
}
func TestBridge_User_SendDraftRemoveDraftFlag(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",
ToList: []*mail.Address{{Address: addrs[0].Email}},
Sender: &mail.Address{Name: "sender", Address: addrs[0].Email},
Body: "body",
MIMEType: rfc822.TextPlain,
},
})
require.NoError(t, err)
// Process those events
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
userContinueEventProcess(ctx, t, s, bridge)
info, err := bridge.QueryUserInfo("user")
require.NoError(t, err)
client, err := client.Dial(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() }()
messages, err := clientFetch(client, "Drafts")
require.NoError(t, err)
require.Len(t, messages, 1)
require.Contains(t, messages[0].Flags, imap.DraftFlag)
})
// Send the draft (generating an "update message" event).
{
pubKeys, recType, err := c.GetPublicKeys(ctx, addrs[0].Email)
require.NoError(t, err)
require.Equal(t, recType, proton.RecipientTypeInternal)
var req proton.SendDraftReq
require.NoError(t, req.AddTextPackage(addrKRs[addrs[0].ID], "body", rfc822.TextPlain, map[string]proton.SendPreferences{
addrs[0].Email: {
Encrypt: true,
PubKey: must(crypto.NewKeyRing(must(crypto.NewKeyFromArmored(pubKeys[0].PublicKey)))),
SignatureType: proton.DetachedSignature,
EncryptionScheme: proton.InternalScheme,
MIMEType: rfc822.TextPlain,
},
}, nil))
require.NoError(t, getErr(c.SendDraft(ctx, draft.ID, req)))
}
// Process those events; the draft will move to the sent folder and lose the draft flag.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
userContinueEventProcess(ctx, t, s, bridge)
info, err := bridge.QueryUserInfo("user")
require.NoError(t, err)
client, err := client.Dial(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() }()
messages, err := clientFetch(client, "Sent")
require.NoError(t, err)
require.Len(t, messages, 1)
require.NotContains(t, messages[0].Flags, imap.DraftFlag)
})
})
})
}
func TestBridge_User_DisableEnableAddress(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)
// Create an additional address for the user.
aliasID, err := s.CreateAddress(userID, "alias@"+s.GetDomain(), password)
require.NoError(t, err)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
require.NoError(t, getErr(bridge.LoginFull(ctx, "user", password, nil, nil)))
// Initially we should list the address.
info, err := bridge.QueryUserInfo("user")
require.NoError(t, err)
require.Contains(t, info.Addresses, "alias@"+s.GetDomain())
})
// Disable the address.
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
require.NoError(t, c.DisableAddress(ctx, aliasID))
})
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
// Eventually we shouldn't list the address.
require.Eventually(t, func() bool {
info, err := bridge.QueryUserInfo("user")
require.NoError(t, err)
return xslices.Index(info.Addresses, "alias@"+s.GetDomain()) < 0
}, 5*time.Second, 100*time.Millisecond)
})
// Enable the address.
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
require.NoError(t, c.EnableAddress(ctx, aliasID))
})
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
// Eventually we should list the address.
require.Eventually(t, func() bool {
info, err := bridge.QueryUserInfo("user")
require.NoError(t, err)
return xslices.Index(info.Addresses, "alias@"+s.GetDomain()) >= 0
}, 5*time.Second, 100*time.Millisecond)
})
})
}
func TestBridge_User_CreateDisabledAddress(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)
// Create an additional address for the user.
aliasID, err := s.CreateAddress(userID, "alias@"+s.GetDomain(), password)
require.NoError(t, err)
// Immediately disable the address.
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
require.NoError(t, c.DisableAddress(ctx, aliasID))
})
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
require.NoError(t, getErr(bridge.LoginFull(ctx, "user", password, nil, nil)))
// Initially we shouldn't list the address.
info, err := bridge.QueryUserInfo("user")
require.NoError(t, err)
require.NotContains(t, info.Addresses, "alias@"+s.GetDomain())
})
})
}
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) {
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()))
require.NoError(t, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }()
withClient(ctx, t, s, username, password, func(ctx context.Context, c *proton.Client) {
parentName := uuid.NewString()
childName := uuid.NewString()
// Create a folder.
parentLabel, err := c.CreateLabel(ctx, proton.CreateLabelReq{
Name: parentName,
Type: proton.LabelTypeFolder,
Color: "#f66",
})
require.NoError(t, err)
// Wait for the parent folder to be created.
require.Eventually(t, func() bool {
return xslices.IndexFunc(clientList(client), func(mailbox *imap.MailboxInfo) bool {
return mailbox.Name == fmt.Sprintf("Folders/%v", parentName)
}) >= 0
}, 100*user.EventPeriod, user.EventPeriod)
// Create a subfolder.
childLabel, err := c.CreateLabel(ctx, proton.CreateLabelReq{
Name: childName,
Type: proton.LabelTypeFolder,
Color: "#f66",
ParentID: parentLabel.ID,
})
require.NoError(t, err)
require.Equal(t, parentLabel.ID, childLabel.ParentID)
// Wait for the parent folder to be created.
require.Eventually(t, func() bool {
return xslices.IndexFunc(clientList(client), func(mailbox *imap.MailboxInfo) bool {
return mailbox.Name == fmt.Sprintf("Folders/%v/%v", parentName, childName)
}) >= 0
}, 100*user.EventPeriod, user.EventPeriod)
newParentName := uuid.NewString()
// Rename the parent folder.
require.NoError(t, getErr(c.UpdateLabel(ctx, parentLabel.ID, proton.UpdateLabelReq{
Color: "#f66",
Name: newParentName,
})))
// Wait for the parent folder to be renamed.
require.Eventually(t, func() bool {
return xslices.IndexFunc(clientList(client), 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 mailbox.Name == fmt.Sprintf("Folders/%v/%v", newParentName, childName)
}) >= 0
}, 100*user.EventPeriod, user.EventPeriod)
})
})
})
}
// userLoginAndSync logs in user and waits until user is fully synced.
func userLoginAndSync(
ctx context.Context,
t *testing.T,
bridge *bridge.Bridge,
username string, password []byte, //nolint:unparam
) {
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
defer done()
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
require.NoError(t, err)
require.Equal(t, userID, (<-syncCh).UserID)
}
func userReceivesBadError(
t *testing.T,
bridge *bridge.Bridge,
mocks *bridge.Mocks,
) (userID string) {
badEventCh, closeCh := bridge.GetEvents(events.UserBadEvent{})
// The user will continue to process events and will receive bad request errors.
mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).MinTimes(1)
badEvent, ok := (<-badEventCh).(events.UserBadEvent)
require.True(t, ok)
closeCh()
return badEvent.UserID
}
func userContinueEventProcess(
ctx context.Context,
t *testing.T,
s *server.Server,
bridge *bridge.Bridge,
) {
info, err := bridge.QueryUserInfo("user")
require.NoError(t, err)
client, err := client.Dial(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() }()
randomLabel := uuid.NewString()
// Create a new label.
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
require.NoError(t, getErr(c.CreateLabel(ctx, proton.CreateLabelReq{
Name: randomLabel,
Color: "#f66",
Type: proton.LabelTypeLabel,
})))
})
// Wait for the label to be created.
require.Eventually(t, func() bool {
return xslices.IndexFunc(clientList(client), func(mailbox *imap.MailboxInfo) bool {
return mailbox.Name == "Labels/"+randomLabel
}) >= 0
}, 100*user.EventPeriod, user.EventPeriod)
}

View File

@ -21,10 +21,13 @@ import (
"context"
"fmt"
"github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/proton-bridge/v3/internal"
"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/internal/vault"
"github.com/sirupsen/logrus"
)
func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, event events.Event) error {
@ -34,9 +37,14 @@ func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, even
return fmt.Errorf("failed to handle user address created event: %w", err)
}
case events.UserAddressUpdated:
if err := bridge.handleUserAddressUpdated(ctx, user, event); err != nil {
return fmt.Errorf("failed to handle user address updated event: %w", err)
case events.UserAddressEnabled:
if err := bridge.handleUserAddressEnabled(ctx, user, event); err != nil {
return fmt.Errorf("failed to handle user address enabled event: %w", err)
}
case events.UserAddressDisabled:
if err := bridge.handleUserAddressDisabled(ctx, user, event); err != nil {
return fmt.Errorf("failed to handle user address disabled event: %w", err)
}
case events.UserAddressDeleted:
@ -45,74 +53,113 @@ func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, even
}
case events.UserRefreshed:
if err := bridge.handleUserRefreshed(ctx, user); err != nil {
if err := bridge.handleUserRefreshed(ctx, user, event); err != nil {
return fmt.Errorf("failed to handle user refreshed event: %w", err)
}
case events.UserDeauth:
bridge.handleUserDeauth(ctx, user)
case events.UserBadEvent:
bridge.handleUserBadEvent(ctx, user, event)
case events.UncategorizedEventError:
bridge.handleUncategorizedErrorEvent(event)
}
return nil
}
func (bridge *Bridge) handleUserAddressCreated(ctx context.Context, user *user.User, event events.UserAddressCreated) error {
if user.GetAddressMode() == vault.SplitMode {
if bridge.imapServer == nil {
return fmt.Errorf("no imap server instance running")
}
if user.GetAddressMode() == vault.CombinedMode {
return nil
}
gluonID, err := bridge.imapServer.AddUser(ctx, user.NewIMAPConnector(event.AddressID), user.GluonKey())
if err != nil {
return fmt.Errorf("failed to add user to IMAP server: %w", err)
}
if bridge.imapServer == nil {
return fmt.Errorf("no imap server instance running")
}
if err := user.SetGluonID(event.AddressID, gluonID); err != nil {
return fmt.Errorf("failed to set gluon ID: %w", err)
}
gluonID, err := bridge.imapServer.AddUser(ctx, user.NewIMAPConnector(event.AddressID), user.GluonKey())
if err != nil {
return fmt.Errorf("failed to add user to IMAP server: %w", err)
}
if err := user.SetGluonID(event.AddressID, gluonID); err != nil {
return fmt.Errorf("failed to set gluon ID: %w", err)
}
return nil
}
// GODT-1948: Handle addresses that have been disabled!
func (bridge *Bridge) handleUserAddressUpdated(_ context.Context, user *user.User, _ events.UserAddressUpdated) error {
switch user.GetAddressMode() {
case vault.CombinedMode:
return fmt.Errorf("not implemented")
func (bridge *Bridge) handleUserAddressEnabled(ctx context.Context, user *user.User, event events.UserAddressEnabled) error {
if user.GetAddressMode() == vault.CombinedMode {
return nil
}
case vault.SplitMode:
return fmt.Errorf("not implemented")
gluonID, err := bridge.imapServer.AddUser(ctx, user.NewIMAPConnector(event.AddressID), user.GluonKey())
if err != nil {
return fmt.Errorf("failed to add user to IMAP server: %w", err)
}
if err := user.SetGluonID(event.AddressID, gluonID); err != nil {
return fmt.Errorf("failed to set gluon ID: %w", err)
}
return nil
}
func (bridge *Bridge) handleUserAddressDisabled(ctx context.Context, user *user.User, event events.UserAddressDisabled) error {
if user.GetAddressMode() == vault.CombinedMode {
return nil
}
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 {
return fmt.Errorf("failed to remove user from IMAP server: %w", err)
}
if err := user.RemoveGluonID(event.AddressID, gluonID); err != nil {
return fmt.Errorf("failed to remove gluon ID for address: %w", err)
}
return nil
}
func (bridge *Bridge) handleUserAddressDeleted(ctx context.Context, user *user.User, event events.UserAddressDeleted) error {
if user.GetAddressMode() == vault.SplitMode {
if bridge.imapServer == nil {
return fmt.Errorf("no imap server instance running")
}
if user.GetAddressMode() == vault.CombinedMode {
return nil
}
gluonID, ok := user.GetGluonID(event.AddressID)
if !ok {
return fmt.Errorf("gluon ID not found for address %s", event.AddressID)
}
if bridge.imapServer == nil {
return fmt.Errorf("no imap server instance running")
}
if err := bridge.imapServer.RemoveUser(ctx, gluonID, true); err != nil {
return fmt.Errorf("failed to remove user from IMAP server: %w", err)
}
gluonID, ok := user.GetGluonID(event.AddressID)
if !ok {
return fmt.Errorf("gluon ID not found for address %s", event.AddressID)
}
if err := user.RemoveGluonID(event.AddressID, gluonID); err != nil {
return fmt.Errorf("failed to remove gluon ID for address: %w", err)
}
if err := bridge.imapServer.RemoveUser(ctx, gluonID, true); err != nil {
return fmt.Errorf("failed to remove user from IMAP server: %w", err)
}
if err := user.RemoveGluonID(event.AddressID, gluonID); err != nil {
return fmt.Errorf("failed to remove gluon ID for address: %w", err)
}
return nil
}
func (bridge *Bridge) handleUserRefreshed(ctx context.Context, user *user.User) error {
func (bridge *Bridge) handleUserRefreshed(ctx context.Context, user *user.User, event events.UserRefreshed) error {
return safe.RLockRet(func() error {
if event.CancelEventPool {
user.CancelSyncAndEventPoll()
}
if err := bridge.removeIMAPUser(ctx, user, true); err != nil {
return fmt.Errorf("failed to remove IMAP user: %w", err)
}
@ -130,3 +177,34 @@ func (bridge *Bridge) handleUserDeauth(ctx context.Context, user *user.User) {
bridge.logoutUser(ctx, user, false, false)
}, bridge.usersLock)
}
func (bridge *Bridge) handleUserBadEvent(_ context.Context, user *user.User, event events.UserBadEvent) {
safe.Lock(func() {
if rerr := bridge.reporter.ReportMessageWithContext("Failed to handle event", reporter.Context{
"user_id": user.ID(),
"old_event_id": event.OldEventID,
"new_event_id": event.NewEventID,
"event_info": event.EventInfo,
"error": event.Error,
"error_type": internal.ErrCauseType(event.Error),
}); rerr != nil {
logrus.WithError(rerr).Error("Failed to report failed event handling")
}
user.CancelSyncAndEventPoll()
// Disable IMAP user
if err := bridge.removeIMAPUser(context.Background(), user, false); err != nil {
logrus.WithError(err).Error("Failed to remove IMAP user")
}
}, bridge.usersLock)
}
func (bridge *Bridge) handleUncategorizedErrorEvent(event events.UncategorizedEventError) {
if rerr := bridge.reporter.ReportMessageWithContext("Failed to handle due to uncategorized error", reporter.Context{
"error_type": internal.ErrCauseType(event.Error),
"error": event.Error,
}); rerr != nil {
logrus.WithError(rerr).Error("Failed to report failed event handling")
}
}

View File

@ -20,6 +20,8 @@ package bridge_test
import (
"context"
"fmt"
"net"
"net/http"
"testing"
"time"
@ -61,6 +63,50 @@ func TestBridge_Login(t *testing.T) {
})
}
func TestBridge_Login_DropConn(t *testing.T) {
l, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
dropListener := proton.NewListener(l, proton.NewDropConn)
defer func() { _ = dropListener.Close() }()
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) {
// Login the user.
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
require.NoError(t, err)
// The user is now connected.
require.Equal(t, []string{userID}, bridge.GetUserIDs())
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
})
// Whether to allow the user to be created.
var allowUser bool
s.AddStatusHook(func(req *http.Request) (int, bool) {
// Drop any request to the users endpoint.
if !allowUser && req.URL.Path == "/core/v4/users" {
dropListener.DropAll()
}
// After the ping request, allow the user to be created.
if req.URL.Path == "/tests/ping" {
allowUser = true
}
return 0, false
})
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
// The user is eventually connected.
require.Eventually(t, func() bool {
return len(bridge.GetUserIDs()) == 1 && len(getConnectedUserIDs(t, bridge)) == 1
}, 5*time.Second, 100*time.Millisecond)
})
}, server.WithListener(dropListener))
}
func TestBridge_LoginTwice(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) {

View File

@ -98,6 +98,8 @@ func saveConfigTemporarily(mc *mobileconfig.Config) (fname string, err error) {
// Make sure the temporary file is deleted.
go func() {
defer recover() //nolint:errcheck
<-time.After(10 * time.Minute)
_ = os.RemoveAll(dir)
}()

View File

@ -44,6 +44,9 @@ var (
// DSNSentry client keys to be able to report crashes to Sentry.
DSNSentry = ""
// BuildEnv tags used at build time.
BuildEnv = ""
)
const (

View File

@ -38,14 +38,16 @@ func (h *Handler) AddRecoveryAction(action RecoveryAction) *Handler {
return h
}
func (h *Handler) HandlePanic() {
func (h *Handler) HandlePanic(r interface{}) {
sentry.SkipDuringUnwind()
if r := recover(); r != nil {
for _, action := range h.actions {
if err := action(r); err != nil {
logrus.WithError(err).Error("Failed to execute recovery action")
}
if r == nil {
return
}
for _, action := range h.actions {
if err := action(r); err != nil {
logrus.WithError(err).Error("Failed to execute recovery action")
}
}
}

View File

@ -21,38 +21,41 @@ import (
"fmt"
"testing"
"github.com/ProtonMail/gluon/async"
"github.com/stretchr/testify/assert"
)
func TestHandler(t *testing.T) {
var s string
assert.NotPanics(t, func() {
var s string
h := NewHandler(
func(r interface{}) error {
s += fmt.Sprintf("1: %v\n", r)
return nil
},
func(r interface{}) error {
s += fmt.Sprintf("2: %v\n", r)
return nil
},
)
h := NewHandler(
func(r interface{}) error {
s += fmt.Sprintf("1: %v\n", r)
return nil
},
func(r interface{}) error {
s += fmt.Sprintf("2: %v\n", r)
return nil
},
)
h.
AddRecoveryAction(func(r interface{}) error {
s += fmt.Sprintf("3: %v\n", r)
return nil
}).
AddRecoveryAction(func(r interface{}) error {
s += fmt.Sprintf("4: %v\n", r)
return nil
})
h.
AddRecoveryAction(func(r interface{}) error {
s += fmt.Sprintf("3: %v\n", r)
return nil
}).
AddRecoveryAction(func(r interface{}) error {
s += fmt.Sprintf("4: %v\n", r)
return nil
})
defer func() {
assert.Equal(t, "1: thing\n2: thing\n3: thing\n4: thing\n", s)
}()
defer func() {
assert.Equal(t, "1: thing\n2: thing\n3: thing\n4: thing\n", s)
}()
defer h.HandlePanic()
defer async.HandlePanic(h)
panic("thing")
panic("thing")
})
}

View File

@ -90,7 +90,8 @@ func TestTLSSignedCertWrongPublicKey(t *testing.T) {
r.Error(t, err, "expected dial to fail because of wrong public key")
}
func TestTLSSignedCertTrustedPublicKey(t *testing.T) {
// GODT-2293 bump badssl cert and re enable this.
func _TestTLSSignedCertTrustedPublicKey(t *testing.T) { //nolint:unused,deadcode
skipIfProxyIsSet(t)
_, dialer, _, checker, _ := createClientWithPinningDialer("")

View File

@ -24,6 +24,7 @@ import (
"sync"
"time"
"github.com/ProtonMail/gluon/async"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
@ -40,17 +41,20 @@ type ProxyTLSDialer struct {
allowProxy bool
proxyProvider *proxyProvider
proxyUseDuration time.Duration
panicHandler async.PanicHandler
}
// NewProxyTLSDialer constructs a dialer which provides a proxy-managing layer on top of an underlying dialer.
func NewProxyTLSDialer(dialer TLSDialer, hostURL string) *ProxyTLSDialer {
func NewProxyTLSDialer(dialer TLSDialer, hostURL string, panicHandler async.PanicHandler) *ProxyTLSDialer {
return &ProxyTLSDialer{
dialer: dialer,
locker: sync.RWMutex{},
directAddress: formatAsAddress(hostURL),
proxyAddress: formatAsAddress(hostURL),
proxyProvider: newProxyProvider(dialer, hostURL, DoHProviders),
proxyProvider: newProxyProvider(dialer, hostURL, DoHProviders, panicHandler),
proxyUseDuration: proxyUseDuration,
panicHandler: panicHandler,
}
}
@ -129,6 +133,8 @@ func (d *ProxyTLSDialer) switchToReachableServer() error {
// This means we want to disable it again in 24 hours.
if d.proxyAddress == d.directAddress {
go func() {
defer async.HandlePanic(d.panicHandler)
<-time.After(d.proxyUseDuration)
d.locker.Lock()

View File

@ -24,6 +24,7 @@ import (
"sync"
"time"
"github.com/ProtonMail/gluon/async"
"github.com/go-resty/resty/v2"
"github.com/miekg/dns"
"github.com/pkg/errors"
@ -67,11 +68,13 @@ type proxyProvider struct {
canReachTimeout time.Duration
lastLookup time.Time // The time at which we last attempted to find a proxy.
panicHandler async.PanicHandler
}
// newProxyProvider creates a new proxyProvider that queries the given DoH providers
// to retrieve DNS records for the given query string.
func newProxyProvider(dialer TLSDialer, hostURL string, providers []string) (p *proxyProvider) {
func newProxyProvider(dialer TLSDialer, hostURL string, providers []string, panicHandler async.PanicHandler) (p *proxyProvider) {
p = &proxyProvider{
dialer: dialer,
hostURL: hostURL,
@ -80,6 +83,7 @@ func newProxyProvider(dialer TLSDialer, hostURL string, providers []string) (p *
cacheRefreshTimeout: proxyCacheRefreshTimeout,
dohTimeout: proxyDoHTimeout,
canReachTimeout: proxyCanReachTimeout,
panicHandler: panicHandler,
}
// Use the default DNS lookup method; this can be overridden if necessary.
@ -109,11 +113,13 @@ func (p *proxyProvider) findReachableServer() (proxy string, err error) {
wg.Add(2)
go func() {
defer async.HandlePanic(p.panicHandler)
defer wg.Done()
apiReachable = p.canReach(p.hostURL)
}()
go func() {
defer async.HandlePanic(p.panicHandler)
defer wg.Done()
err = p.refreshProxyCache()
}()
@ -150,6 +156,8 @@ func (p *proxyProvider) refreshProxyCache() error {
resultChan := make(chan []string)
go func() {
defer async.HandlePanic(p.panicHandler)
for _, provider := range p.providers {
if proxies, err := p.dohLookup(ctx, p.query, provider); err == nil {
resultChan <- proxies
@ -203,6 +211,7 @@ func (p *proxyProvider) defaultDoHLookup(ctx context.Context, query, dohProvider
dataChan, errChan := make(chan []string), make(chan error)
go func() {
defer async.HandlePanic(p.panicHandler)
// Build new DNS request in RFC1035 format.
dnsRequest := new(dns.Msg).SetQuestion(dns.Fqdn(query), dns.TypeTXT)

View File

@ -23,6 +23,7 @@ import (
"testing"
"time"
"github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
r "github.com/stretchr/testify/require"
)
@ -31,7 +32,7 @@ func TestProxyProvider_FindProxy(t *testing.T) {
proxy := getTrustedServer()
defer closeServer(proxy)
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"not used"})
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"not used"}, async.NoopPanicHandler{})
p.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{proxy.URL}, nil }
url, err := p.findReachableServer()
@ -47,7 +48,7 @@ func TestProxyProvider_FindProxy_ChooseReachableProxy(t *testing.T) {
unreachableProxy := getTrustedServer()
closeServer(unreachableProxy)
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"not used"})
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"not used"}, async.NoopPanicHandler{})
p.dohLookup = func(ctx context.Context, q, p string) ([]string, error) {
return []string{reachableProxy.URL, unreachableProxy.URL}, nil
}
@ -68,7 +69,7 @@ func TestProxyProvider_FindProxy_ChooseTrustedProxy(t *testing.T) {
checker := NewTLSPinChecker(TrustedAPIPins)
dialer := NewPinningTLSDialer(NewBasicTLSDialer(""), reporter, checker)
p := newProxyProvider(dialer, "", []string{"not used"})
p := newProxyProvider(dialer, "", []string{"not used"}, async.NoopPanicHandler{})
p.dohLookup = func(ctx context.Context, q, p string) ([]string, error) {
return []string{untrustedProxy.URL, trustedProxy.URL}, nil
}
@ -85,7 +86,7 @@ func TestProxyProvider_FindProxy_FailIfNoneReachable(t *testing.T) {
unreachableProxy2 := getTrustedServer()
closeServer(unreachableProxy2)
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"not used"})
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"not used"}, async.NoopPanicHandler{})
p.dohLookup = func(ctx context.Context, q, p string) ([]string, error) {
return []string{unreachableProxy1.URL, unreachableProxy2.URL}, nil
}
@ -105,7 +106,7 @@ func TestProxyProvider_FindProxy_FailIfNoneTrusted(t *testing.T) {
checker := NewTLSPinChecker(TrustedAPIPins)
dialer := NewPinningTLSDialer(NewBasicTLSDialer(""), reporter, checker)
p := newProxyProvider(dialer, "", []string{"not used"})
p := newProxyProvider(dialer, "", []string{"not used"}, async.NoopPanicHandler{})
p.dohLookup = func(ctx context.Context, q, p string) ([]string, error) {
return []string{untrustedProxy1.URL, untrustedProxy2.URL}, nil
}
@ -115,7 +116,7 @@ func TestProxyProvider_FindProxy_FailIfNoneTrusted(t *testing.T) {
}
func TestProxyProvider_FindProxy_RefreshCacheTimeout(t *testing.T) {
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"not used"})
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"not used"}, async.NoopPanicHandler{})
p.cacheRefreshTimeout = 1 * time.Second
p.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { time.Sleep(2 * time.Second); return nil, nil }
@ -132,7 +133,7 @@ func TestProxyProvider_FindProxy_CanReachTimeout(t *testing.T) {
}))
defer closeServer(slowProxy)
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"not used"})
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"not used"}, async.NoopPanicHandler{})
p.canReachTimeout = 1 * time.Second
p.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{slowProxy.URL}, nil }
@ -144,7 +145,7 @@ func TestProxyProvider_FindProxy_CanReachTimeout(t *testing.T) {
}
func TestProxyProvider_DoHLookup_Quad9(t *testing.T) {
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{Quad9Provider, GoogleProvider})
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{Quad9Provider, GoogleProvider}, async.NoopPanicHandler{})
records, err := p.dohLookup(context.Background(), proxyQuery, Quad9Provider)
r.NoError(t, err)
@ -155,7 +156,7 @@ func TestProxyProvider_DoHLookup_Quad9(t *testing.T) {
// port filter. Basic functionality should be covered by other tests. Keeping
// code here to be able to run it locally if needed.
func DISABLEDTestProxyProviderDoHLookupQuad9Port(t *testing.T) {
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{Quad9Provider, GoogleProvider})
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{Quad9Provider, GoogleProvider}, async.NoopPanicHandler{})
records, err := p.dohLookup(context.Background(), proxyQuery, Quad9PortProvider)
r.NoError(t, err)
@ -163,7 +164,7 @@ func DISABLEDTestProxyProviderDoHLookupQuad9Port(t *testing.T) {
}
func TestProxyProvider_DoHLookup_Google(t *testing.T) {
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{Quad9Provider, GoogleProvider})
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{Quad9Provider, GoogleProvider}, async.NoopPanicHandler{})
records, err := p.dohLookup(context.Background(), proxyQuery, GoogleProvider)
r.NoError(t, err)
@ -173,7 +174,7 @@ func TestProxyProvider_DoHLookup_Google(t *testing.T) {
func TestProxyProvider_DoHLookup_FindProxy(t *testing.T) {
skipIfProxyIsSet(t)
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{Quad9Provider, GoogleProvider})
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{Quad9Provider, GoogleProvider}, async.NoopPanicHandler{})
url, err := p.findReachableServer()
r.NoError(t, err)
@ -183,7 +184,7 @@ func TestProxyProvider_DoHLookup_FindProxy(t *testing.T) {
func TestProxyProvider_DoHLookup_FindProxyFirstProviderUnreachable(t *testing.T) {
skipIfProxyIsSet(t)
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"https://unreachable", Quad9Provider, GoogleProvider})
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"https://unreachable", Quad9Provider, GoogleProvider}, async.NoopPanicHandler{})
url, err := p.findReachableServer()
r.NoError(t, err)

View File

@ -25,6 +25,7 @@ import (
"testing"
"time"
"github.com/ProtonMail/gluon/async"
"github.com/stretchr/testify/require"
)
@ -141,8 +142,8 @@ func TestProxyDialer_UseProxy(t *testing.T) {
trustedProxy := getTrustedServer()
defer closeServer(trustedProxy)
provider := newProxyProvider(NewBasicTLSDialer(""), "", DoHProviders)
d := NewProxyTLSDialer(NewBasicTLSDialer(""), "")
provider := newProxyProvider(NewBasicTLSDialer(""), "", DoHProviders, async.NoopPanicHandler{})
d := NewProxyTLSDialer(NewBasicTLSDialer(""), "", async.NoopPanicHandler{})
d.proxyProvider = provider
provider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{trustedProxy.URL}, nil }
@ -159,8 +160,8 @@ func TestProxyDialer_UseProxy_MultipleTimes(t *testing.T) {
proxy3 := getTrustedServer()
defer closeServer(proxy3)
provider := newProxyProvider(NewBasicTLSDialer(""), "", DoHProviders)
d := NewProxyTLSDialer(NewBasicTLSDialer(""), "")
provider := newProxyProvider(NewBasicTLSDialer(""), "", DoHProviders, async.NoopPanicHandler{})
d := NewProxyTLSDialer(NewBasicTLSDialer(""), "", async.NoopPanicHandler{})
d.proxyProvider = provider
provider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{proxy1.URL}, nil }
@ -189,8 +190,8 @@ func TestProxyDialer_UseProxy_RevertAfterTime(t *testing.T) {
trustedProxy := getTrustedServer()
defer closeServer(trustedProxy)
provider := newProxyProvider(NewBasicTLSDialer(""), "", DoHProviders)
d := NewProxyTLSDialer(NewBasicTLSDialer(""), "")
provider := newProxyProvider(NewBasicTLSDialer(""), "", DoHProviders, async.NoopPanicHandler{})
d := NewProxyTLSDialer(NewBasicTLSDialer(""), "", async.NoopPanicHandler{})
d.proxyProvider = provider
d.proxyUseDuration = time.Second
@ -212,8 +213,8 @@ func TestProxyDialer_UseProxy_RevertAfterTime(t *testing.T) {
func TestProxyDialer_UseProxy_RevertIfProxyStopsWorkingAndOriginalAPIIsReachable(t *testing.T) {
trustedProxy := getTrustedServer()
provider := newProxyProvider(NewBasicTLSDialer(""), "", DoHProviders)
d := NewProxyTLSDialer(NewBasicTLSDialer(""), "")
provider := newProxyProvider(NewBasicTLSDialer(""), "", DoHProviders, async.NoopPanicHandler{})
d := NewProxyTLSDialer(NewBasicTLSDialer(""), "", async.NoopPanicHandler{})
d.proxyProvider = provider
provider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{trustedProxy.URL}, nil }
@ -242,8 +243,8 @@ func TestProxyDialer_UseProxy_FindSecondAlternativeIfFirstFailsAndAPIIsStillBloc
proxy2 := getTrustedServer()
defer closeServer(proxy2)
provider := newProxyProvider(NewBasicTLSDialer(""), "", DoHProviders)
d := NewProxyTLSDialer(NewBasicTLSDialer(""), "")
provider := newProxyProvider(NewBasicTLSDialer(""), "", DoHProviders, async.NoopPanicHandler{})
d := NewProxyTLSDialer(NewBasicTLSDialer(""), "", async.NoopPanicHandler{})
d.proxyProvider = provider
provider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{proxy1.URL, proxy2.URL}, nil }

38
internal/errors.go Normal file
View File

@ -0,0 +1,38 @@
// 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 internal
import (
"errors"
"fmt"
)
// ErrCause returns the cause of the error, the inner-most error in the wrapped chain.
func ErrCause(err error) error {
cause := err
for errors.Unwrap(cause) != nil {
cause = errors.Unwrap(cause)
}
return cause
}
func ErrCauseType(err error) string {
return fmt.Sprintf("%T", ErrCause(err))
}

View File

@ -35,6 +35,30 @@ func (event UserAddressCreated) String() string {
return fmt.Sprintf("UserAddressCreated: UserID: %s, AddressID: %s, Email: %s", event.UserID, event.AddressID, logging.Sensitive(event.Email))
}
type UserAddressEnabled struct {
eventBase
UserID string
AddressID string
Email string
}
func (event UserAddressEnabled) String() string {
return fmt.Sprintf("UserAddressEnabled: UserID: %s, AddressID: %s, Email: %s", event.UserID, event.AddressID, logging.Sensitive(event.Email))
}
type UserAddressDisabled struct {
eventBase
UserID string
AddressID string
Email string
}
func (event UserAddressDisabled) String() string {
return fmt.Sprintf("UserAddressDisabled: UserID: %s, AddressID: %s, Email: %s", event.UserID, event.AddressID, logging.Sensitive(event.Email))
}
type UserAddressUpdated struct {
eventBase

View File

@ -52,9 +52,8 @@ type UserLabelDeleted struct {
UserID string
LabelID string
Name string
}
func (event UserLabelDeleted) String() string {
return fmt.Sprintf("UserLabelDeleted: UserID: %s, LabelID: %s, Name: %s", event.UserID, event.LabelID, logging.Sensitive(event.Name))
return fmt.Sprintf("UserLabelDeleted: UserID: %s, LabelID: %s", event.UserID, event.LabelID)
}

76
internal/events/serve.go Normal file
View File

@ -0,0 +1,76 @@
// 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 events
import "fmt"
type IMAPServerReady struct {
eventBase
Port int
}
func (event IMAPServerReady) String() string {
return fmt.Sprintf("IMAPServerReady: Port %d", event.Port)
}
type IMAPServerStopped struct {
eventBase
}
func (event IMAPServerStopped) String() string {
return "IMAPServerStopped"
}
type IMAPServerError struct {
eventBase
Error error
}
func (event IMAPServerError) String() string {
return fmt.Sprintf("IMAPServerError: %v", event.Error)
}
type SMTPServerReady struct {
eventBase
Port int
}
func (event SMTPServerReady) String() string {
return fmt.Sprintf("SMTPServerReady: Port %d", event.Port)
}
type SMTPServerStopped struct {
eventBase
}
func (event SMTPServerStopped) String() string {
return "SMTPServerStopped"
}
type SMTPServerError struct {
eventBase
Error error
}
func (event SMTPServerError) String() string {
return fmt.Sprintf("SMTPServerError: %v", event.Error)
}

View File

@ -23,6 +23,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
)
// AllUsersLoaded is emitted when all users have been loaded.
type AllUsersLoaded struct {
eventBase
}
@ -31,6 +32,7 @@ func (event AllUsersLoaded) String() string {
return "AllUsersLoaded"
}
// UserLoading is emitted when a user is being loaded.
type UserLoading struct {
eventBase
@ -41,6 +43,7 @@ func (event UserLoading) String() string {
return fmt.Sprintf("UserLoading: UserID: %s", event.UserID)
}
// UserLoadSuccess is emitted when a user has been loaded successfully.
type UserLoadSuccess struct {
eventBase
@ -51,6 +54,7 @@ func (event UserLoadSuccess) String() string {
return fmt.Sprintf("UserLoadSuccess: UserID: %s", event.UserID)
}
// UserLoadFail is emitted when a user has failed to load.
type UserLoadFail struct {
eventBase
@ -62,6 +66,7 @@ func (event UserLoadFail) String() string {
return fmt.Sprintf("UserLoadFail: UserID: %s, Error: %s", event.UserID, event.Error)
}
// UserLoggedIn is emitted when a user has logged in.
type UserLoggedIn struct {
eventBase
@ -72,6 +77,7 @@ func (event UserLoggedIn) String() string {
return fmt.Sprintf("UserLoggedIn: UserID: %s", event.UserID)
}
// UserLoggedOut is emitted when a user has logged out.
type UserLoggedOut struct {
eventBase
@ -82,6 +88,7 @@ func (event UserLoggedOut) String() string {
return fmt.Sprintf("UserLoggedOut: UserID: %s", event.UserID)
}
// UserDeauth is emitted when a user has lost its API authentication.
type UserDeauth struct {
eventBase
@ -92,6 +99,30 @@ func (event UserDeauth) String() string {
return fmt.Sprintf("UserDeauth: UserID: %s", event.UserID)
}
// UserBadEvent is emitted when a user cannot apply an event.
type UserBadEvent struct {
eventBase
UserID string
OldEventID string
NewEventID string
EventInfo string
Error error
}
func (event UserBadEvent) String() string {
return fmt.Sprintf(
"UserBadEvent: UserID: %s, OldEventID: %s, NewEventID: %s, EventInfo: %v, Error: %s",
event.UserID,
event.OldEventID,
event.NewEventID,
event.EventInfo,
event.Error,
)
}
// UserDeleted is emitted when a user has been deleted.
type UserDeleted struct {
eventBase
@ -102,6 +133,7 @@ func (event UserDeleted) String() string {
return fmt.Sprintf("UserDeleted: UserID: %s", event.UserID)
}
// UserChanged is emitted when a user's data has changed (name, email, etc.).
type UserChanged struct {
eventBase
@ -112,16 +144,19 @@ func (event UserChanged) String() string {
return fmt.Sprintf("UserChanged: UserID: %s", event.UserID)
}
// UserRefreshed is emitted when an API refresh was issued for a user.
type UserRefreshed struct {
eventBase
UserID string
UserID string
CancelEventPool bool
}
func (event UserRefreshed) String() string {
return fmt.Sprintf("UserRefreshed: UserID: %s", event.UserID)
}
// AddressModeChanged is emitted when a user's address mode has changed.
type AddressModeChanged struct {
eventBase
@ -133,3 +168,37 @@ type AddressModeChanged struct {
func (event AddressModeChanged) String() string {
return fmt.Sprintf("AddressModeChanged: UserID: %s, AddressMode: %s", event.UserID, event.AddressMode)
}
// UsedSpaceChanged is emitted when the storage space used by the user has changed.
type UsedSpaceChanged struct {
eventBase
UserID string
UsedSpace int
}
func (event UsedSpaceChanged) String() string {
return fmt.Sprintf("UsedSpaceChanged: UserID: %s, UsedSpace: %v", event.UserID, event.UsedSpace)
}
type IMAPLoginFailed struct {
eventBase
Username string
}
func (event IMAPLoginFailed) String() string {
return fmt.Sprintf("IMAPLoginFailed: Username: %s", event.Username)
}
type UncategorizedEventError struct {
eventBase
UserID string
Error error
}
func (event UncategorizedEventError) String() string {
return fmt.Sprintf("UncategorizedEventError: UserID: %s, Source:%T, Error: %s", event.UserID, event.Error, event.Error)
}

View File

@ -21,9 +21,11 @@ import (
"context"
"fmt"
"net"
"path/filepath"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/proton-bridge/v3/internal/focus/proto"
"github.com/ProtonMail/proton-bridge/v3/internal/service"
"github.com/sirupsen/logrus"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
@ -32,10 +34,10 @@ import (
// TryRaise tries to raise the application by dialing the focus service.
// It returns true if the service is running and the application was told to raise.
func TryRaise() bool {
func TryRaise(settingsPath string) bool {
var raised bool
if err := withClientConn(context.Background(), func(ctx context.Context, client proto.FocusClient) error {
if err := withClientConn(context.Background(), settingsPath, func(ctx context.Context, client proto.FocusClient) error {
if _, err := client.Raise(ctx, &emptypb.Empty{}); err != nil {
return fmt.Errorf("failed to call client.Raise: %w", err)
}
@ -53,10 +55,10 @@ func TryRaise() bool {
// TryVersion tries to determine the version of the running application instance.
// It returns the version and true if the version could be determined.
func TryVersion() (*semver.Version, bool) {
func TryVersion(settingsPath string) (*semver.Version, bool) {
var version *semver.Version
if err := withClientConn(context.Background(), func(ctx context.Context, client proto.FocusClient) error {
if err := withClientConn(context.Background(), settingsPath, func(ctx context.Context, client proto.FocusClient) error {
raw, err := client.Version(ctx, &emptypb.Empty{})
if err != nil {
return fmt.Errorf("failed to call client.Version: %w", err)
@ -78,10 +80,15 @@ func TryVersion() (*semver.Version, bool) {
return version, true
}
func withClientConn(ctx context.Context, fn func(context.Context, proto.FocusClient) error) error {
func withClientConn(ctx context.Context, settingsPath string, fn func(context.Context, proto.FocusClient) error) error {
var config = service.Config{}
err := config.Load(filepath.Join(settingsPath, serverConfigFileName))
if err != nil {
return err
}
cc, err := grpc.DialContext(
ctx,
net.JoinHostPort(Host, fmt.Sprint(Port)),
net.JoinHostPort(Host, fmt.Sprint(config.Port)),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {

View File

@ -18,19 +18,25 @@
package focus
import (
"os"
"testing"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
"github.com/stretchr/testify/require"
)
func TestFocus_Raise(t *testing.T) {
tmpDir := t.TempDir()
locations := locations.New(newTestLocationsProvider(tmpDir), "config-name")
// Start the focus service.
service, err := NewService(semver.MustParse("1.2.3"))
service, err := NewService(locations, semver.MustParse("1.2.3"), nil)
require.NoError(t, err)
settingsFolder, err := locations.ProvideSettingsPath()
require.NoError(t, err)
// Try to dial it, it should succeed.
require.True(t, TryRaise())
require.True(t, TryRaise(settingsFolder))
// The service should report a raise call.
<-service.GetRaiseCh()
@ -39,16 +45,60 @@ func TestFocus_Raise(t *testing.T) {
service.Close()
// Try to dial it, it should fail.
require.False(t, TryRaise())
require.False(t, TryRaise(settingsFolder))
}
func TestFocus_Version(t *testing.T) {
tmpDir := t.TempDir()
locations := locations.New(newTestLocationsProvider(tmpDir), "config-name")
// Start the focus service.
_, err := NewService(semver.MustParse("1.2.3"))
_, err := NewService(locations, semver.MustParse("1.2.3"), nil)
require.NoError(t, err)
settingsFolder, err := locations.ProvideSettingsPath()
require.NoError(t, err)
// Try to dial it, it should succeed.
version, ok := TryVersion()
version, ok := TryVersion(settingsFolder)
require.True(t, ok)
require.Equal(t, "1.2.3", version.String())
}
type TestLocationsProvider struct {
config, data, cache string
}
func newTestLocationsProvider(dir string) *TestLocationsProvider {
config, err := os.MkdirTemp(dir, "config")
if err != nil {
panic(err)
}
data, err := os.MkdirTemp(dir, "data")
if err != nil {
panic(err)
}
cache, err := os.MkdirTemp(dir, "cache")
if err != nil {
panic(err)
}
return &TestLocationsProvider{
config: config,
data: data,
cache: cache,
}
}
func (provider *TestLocationsProvider) UserConfig() string {
return provider.config
}
func (provider *TestLocationsProvider) UserData() string {
return provider.data
}
func (provider *TestLocationsProvider) UserCache() string {
return provider.cache
}

View File

@ -24,12 +24,11 @@
package proto
import (
reflect "reflect"
sync "sync"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
emptypb "google.golang.org/protobuf/types/known/emptypb"
reflect "reflect"
sync "sync"
)
const (
@ -105,7 +104,7 @@ var file_focus_proto_rawDesc = []byte{
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, 0x32, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x66,
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,
}

View File

@ -8,7 +8,6 @@ package proto
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"

View File

@ -24,17 +24,18 @@ import (
"net"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/proton-bridge/v3/internal/focus/proto"
"github.com/ProtonMail/proton-bridge/v3/internal/service"
"github.com/sirupsen/logrus"
"google.golang.org/grpc"
"google.golang.org/protobuf/types/known/emptypb"
)
// Host is the local host to listen on.
const Host = "127.0.0.1"
// Port is the port to listen on.
var Port = 1042 // nolint:gochecknoglobals
const (
Host = "127.0.0.1"
serverConfigFileName = "grpcFocusServerConfig.json"
)
// Service is a gRPC service that can be used to raise the application.
type Service struct {
@ -43,30 +44,48 @@ type Service struct {
server *grpc.Server
raiseCh chan struct{}
version *semver.Version
panicHandler async.PanicHandler
}
// NewService creates a new focus service.
// It listens on the local host and port 1042 (by default).
func NewService(version *semver.Version) (*Service, error) {
service := &Service{
server: grpc.NewServer(),
raiseCh: make(chan struct{}, 1),
version: version,
func NewService(locator service.Locator, version *semver.Version, panicHandler async.PanicHandler) (*Service, error) {
serv := &Service{
server: grpc.NewServer(),
raiseCh: make(chan struct{}, 1),
version: version,
panicHandler: panicHandler,
}
proto.RegisterFocusServer(service.server, service)
proto.RegisterFocusServer(serv.server, serv)
if listener, err := net.Listen("tcp", net.JoinHostPort(Host, fmt.Sprint(Port))); err != nil {
logrus.WithError(err).Warn("Failed to start focus service")
if listener, err := net.Listen("tcp", net.JoinHostPort(Host, fmt.Sprint(0))); err != nil {
logrus.WithError(err).Warn("Failed to start focus serv")
} else {
config := service.Config{}
// retrieve the port assigned by the system, so that we can put it in the config file.
address, ok := listener.Addr().(*net.TCPAddr)
if !ok {
return nil, fmt.Errorf("could not retrieve gRPC service listener address")
}
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")
} else {
logrus.WithField("path", path).Info("Successfully saved gRPC Focus service config file")
}
go func() {
if err := service.server.Serve(listener); err != nil {
defer async.HandlePanic(serv.panicHandler)
if err := serv.server.Serve(listener); err != nil {
fmt.Printf("failed to serve: %v", err)
}
}()
}
return service, nil
return serv, nil
}
// Raise implements the gRPC FocusService interface; it raises the application.
@ -90,6 +109,8 @@ func (service *Service) GetRaiseCh() <-chan struct{} {
// Close closes the service.
func (service *Service) Close() {
go func() {
defer async.HandlePanic(service.panicHandler)
// we do this in a goroutine, as on Windows, the gRPC shutdown may take minutes if something tries to
// interact with it in an invalid way (e.g. HTTP GET request from a Qt QNetworkManager instance).
service.server.Stop()

View File

@ -10,5 +10,5 @@ rcc_cgo_*.go
*.qmlc
# Generated file
bridge-gui/bridge-gui/Version.h
bridge-gui/bridge-gui/BuildConfig.h
bridge-gui/bridge-gui/Resources.rc

View File

@ -1,8 +1,8 @@
find_program(QMAKE_EXE "qmake")
find_program(QMAKE_EXE NAMES "qmake" "qmake6")
if (NOT QMAKE_EXE)
message(FATAL_ERROR "Could not locate qmake executable, make sur you have Qt 6 installed in that qmake is in your PATH environment variable.")
message(FATAL_ERROR "Could not locate qmake executable, make sure you have Qt 6 installed in that qmake is in your PATH environment variable.")
endif()
message(STATUS "Found qmake at ${QMAKE_EXE}")
execute_process(COMMAND "${QMAKE_EXE}" -query QT_INSTALL_PREFIX OUTPUT_VARIABLE QT_DIR OUTPUT_STRIP_TRAILING_WHITESPACE)
set(CMAKE_PREFIX_PATH ${QT_DIR} ${CMAKE_PREFIX_PATH})
set(CMAKE_PREFIX_PATH ${QT_DIR} ${CMAKE_PREFIX_PATH})

View File

@ -53,6 +53,7 @@ void GRPCQtProxy::connectSignals() {
connect(this, &GRPCQtProxy::logoutUserReceived, &usersTab, &UsersTab::logoutUser);
connect(this, &GRPCQtProxy::setUserSplitModeReceived, &usersTab, &UsersTab::setUserSplitMode);
connect(this, &GRPCQtProxy::configureUserAppleMailReceived, &usersTab, &UsersTab::configureUserAppleMail);
connect(this, &GRPCQtProxy::sendBadEventUserFeedbackReceived, &usersTab, &UsersTab::processBadEventUserFeedback);
}
@ -178,6 +179,15 @@ void GRPCQtProxy::setUserSplitMode(QString const &userID, bool makeItActive) {
}
//****************************************************************************************************************************************************
/// \param[in] userID The userID.
/// \param[in] doResync Did the user request a resync?
//****************************************************************************************************************************************************
void GRPCQtProxy::sendBadEventUserFeedback(QString const &userID, bool doResync) {
emit sendBadEventUserFeedbackReceived(userID, doResync);
}
//****************************************************************************************************************************************************
/// \param[in] userID The userID.
//****************************************************************************************************************************************************

View File

@ -52,6 +52,7 @@ public: // member functions.
void setDiskCachePath(QString const &path); ///< Forwards a setDiskCachePath call via a Qt signal.
void setIsAutomaticUpdateOn(bool on); ///< Forwards a SetIsAutomaticUpdateOn call via a Qt signal.
void setUserSplitMode(QString const &userID, bool makeItActive); ///< Forwards a setUserSplitMode call via a Qt signal.
void sendBadEventUserFeedback(QString const &userID, bool doResync); ///< Forwards a sendBadEventUserFeedback call via a Qt signal.
void logoutUser(QString const &userID); ///< Forwards a logoutUser call via a Qt signal.
void removeUser(QString const &userID); ///< Forwards a removeUser call via a Qt signal.
void configureUserAppleMail(QString const &userID, QString const &address); ///< Forwards a configureUserAppleMail call via a Qt signal.
@ -72,6 +73,7 @@ signals:
void setDiskCachePathReceived(QString const &path); ///< Signal for the setDiskCachePath gRPC call.
void setIsAutomaticUpdateOnReceived(bool on); ///< Signal for the SetIsAutomaticUpdateOn gRPC call.
void setUserSplitModeReceived(QString const &userID, bool makeItActive); ///< Signal for the SetUserSplitModeReceived gRPC call.
void sendBadEventUserFeedbackReceived(QString const &userID, bool doResync); ///< Signal for the SendBadEventUserFeedback gRPC call.
void logoutUserReceived(QString const &userID); ///< Signal for the LogoutUserReceived gRPC call.
void removeUserReceived(QString const &userID); ///< Signal for the RemoveUserReceived gRPC call.
void configureUserAppleMailReceived(QString const &userID, QString const &address); ///< Signal for the ConfigureAppleMail gRPC call.

View File

@ -20,6 +20,7 @@
#include "Cert.h"
#include "GRPCService.h"
#include <bridgepp/Exception/Exception.h>
#include <bridgepp/BridgeUtils.h>
#include <bridgepp/GRPC/GRPCUtils.h>
#include <bridgepp/GRPC/GRPCConfig.h>
@ -33,7 +34,6 @@ using namespace grpc;
//****************************************************************************************************************************************************
GRPCServerWorker::GRPCServerWorker(QObject *parent)
: Worker(parent) {
}
@ -81,7 +81,7 @@ void GRPCServerWorker::run() {
config.port = port;
QString err;
if (!config.save(grpcServerConfigPath(), &err)) {
if (!config.save(grpcServerConfigPath(bridgepp::userConfigDir()), &err)) {
throw Exception(QString("Could not save gRPC server config. %1").arg(err));
}

View File

@ -86,9 +86,10 @@ Status GRPCService::AddLogEntry(ServerContext *, AddLogEntryRequest const *reque
//****************************************************************************************************************************************************
/// \return The status for the call.
//****************************************************************************************************************************************************
Status GRPCService::GuiReady(ServerContext *, Empty const *, Empty *) {
Status GRPCService::GuiReady(ServerContext *, Empty const *, GuiReadyResponse *response) {
app().log().debug(__FUNCTION__);
app().mainWindow().settingsTab().setGUIReady(true);
response->set_showsplashscreen(app().mainWindow().settingsTab().showSplashScreen());
return Status::OK;
}
@ -124,28 +125,6 @@ Status GRPCService::ShowOnStartup(ServerContext *, Empty const *, BoolValue *res
}
//****************************************************************************************************************************************************
/// \param[out] response The response.
/// \return The status for the call.
//****************************************************************************************************************************************************
Status GRPCService::ShowSplashScreen(ServerContext *, Empty const *, BoolValue *response) {
app().log().debug(__FUNCTION__);
response->set_value(app().mainWindow().settingsTab().showSplashScreen());
return Status::OK;
}
//****************************************************************************************************************************************************
/// \param[out] response The response.
/// \return The status for the call.
//****************************************************************************************************************************************************
Status GRPCService::IsFirstGuiStart(ServerContext *, Empty const *, BoolValue *response) {
app().log().debug(__FUNCTION__);
response->set_value(app().mainWindow().settingsTab().isFirstGUIStart());
return Status::OK;
}
//****************************************************************************************************************************************************
/// \param[in] request The request.
/// \return The status for the call.
@ -213,17 +192,6 @@ Status GRPCService::IsAllMailVisible(ServerContext *, Empty const *request, Bool
}
//****************************************************************************************************************************************************
/// \param[out] response The response.
/// \return The status for the call.
//****************************************************************************************************************************************************
Status GRPCService::GoOs(ServerContext *, Empty const *, StringValue *response) {
app().log().debug(__FUNCTION__);
response->set_value(app().mainWindow().settingsTab().os().toStdString());
return Status::OK;
}
//****************************************************************************************************************************************************
/// \return The status for the call.
//****************************************************************************************************************************************************
@ -715,6 +683,17 @@ Status GRPCService::SetUserSplitMode(ServerContext *, UserSplitModeRequest const
}
//****************************************************************************************************************************************************
/// \param[in] request The request.
/// \return The status for the call.
//****************************************************************************************************************************************************
Status GRPCService::SendBadEventUserFeedback(ServerContext *, UserBadEventFeedbackRequest const *request, Empty *) {
app().log().debug(__FUNCTION__);
qtProxy_.sendBadEventUserFeedback(QString::fromStdString(request->userid()), request->doresync());
return Status::OK;
}
//****************************************************************************************************************************************************
/// \param[in] request The request.
/// \return The status for the call.
@ -752,7 +731,7 @@ Status GRPCService::ConfigureUserAppleMail(ServerContext *, ConfigureAppleMailRe
/// \param[in] writer The writer
/// \return The status for the call.
//****************************************************************************************************************************************************
Status GRPCService::RunEventStream(ServerContext *, EventStreamRequest const *request, ServerWriter<StreamEvent> *writer) {
Status GRPCService::RunEventStream(ServerContext *ctx, EventStreamRequest const *request, ServerWriter<StreamEvent> *writer) {
app().log().debug(__FUNCTION__);
{
QMutexLocker locker(&eventStreamMutex_);
@ -767,19 +746,19 @@ Status GRPCService::RunEventStream(ServerContext *, EventStreamRequest const *re
while (true) {
QMutexLocker locker(&eventStreamMutex_);
if (eventStreamShouldStop_) {
if (eventStreamShouldStop_ || ctx->IsCancelled()) {
qtProxy_.setIsStreaming(false);
qtProxy_.setClientPlatform(QString());
isStreaming_ = false;
return Status::OK;
}
if (eventQueue_.isEmpty()) {
locker.unlock();
QThread::msleep(100);
continue;
}
SPStreamEvent const event = eventQueue_.front();
eventQueue_.pop_front();
locker.unlock();

View File

@ -41,19 +41,16 @@ public: // member functions.
bool isStreaming() const; ///< Check if the service is currently streaming events.
grpc::Status CheckTokens(::grpc::ServerContext *context, ::google::protobuf::StringValue const *request, ::google::protobuf::StringValue *response) override;
grpc::Status AddLogEntry(::grpc::ServerContext *, ::grpc::AddLogEntryRequest const *request, ::google::protobuf::Empty *) override;
grpc::Status GuiReady(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::Empty *) override;
grpc::Status GuiReady(::grpc::ServerContext *, ::google::protobuf::Empty const *, grpc::GuiReadyResponse *response) override;
grpc::Status Quit(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::Empty *) override;
grpc::Status Restart(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::Empty *) override;
grpc::Status ShowOnStartup(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::BoolValue *response) override;
grpc::Status ShowSplashScreen(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::BoolValue *response) override;
grpc::Status IsFirstGuiStart(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::BoolValue *response) override;
grpc::Status SetIsAutostartOn(::grpc::ServerContext *, ::google::protobuf::BoolValue const *request, ::google::protobuf::Empty *) override;
grpc::Status IsAutostartOn(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::BoolValue *response) override;
grpc::Status SetIsBetaEnabled(::grpc::ServerContext *, ::google::protobuf::BoolValue const *request, ::google::protobuf::Empty *) override;
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 GoOs(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::StringValue *response) override;
grpc::Status TriggerReset(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::Empty *) override;
grpc::Status 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;
@ -90,12 +87,12 @@ public: // member functions.
grpc::Status GetUserList(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::grpc::UserListResponse *response) override;
grpc::Status GetUser(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::grpc::User *response) override;
grpc::Status SetUserSplitMode(::grpc::ServerContext *, ::grpc::UserSplitModeRequest const *request, ::google::protobuf::Empty *) override;
grpc::Status SendBadEventUserFeedback(::grpc::ServerContext *context, ::grpc::UserBadEventFeedbackRequest const *request, ::google::protobuf::Empty *response) override;
grpc::Status LogoutUser(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
grpc::Status RemoveUser(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
grpc::Status ConfigureUserAppleMail(::grpc::ServerContext *, ::grpc::ConfigureAppleMailRequest const *request, ::google::protobuf::Empty *) override;
grpc::Status RunEventStream(::grpc::ServerContext *, ::grpc::EventStreamRequest const *request, ::grpc::ServerWriter<::grpc::StreamEvent> *writer) override;
grpc::Status RunEventStream(::grpc::ServerContext *ctx, ::grpc::EventStreamRequest const *request, ::grpc::ServerWriter<::grpc::StreamEvent> *writer) override;
grpc::Status StopEventStream(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::Empty *) override;
bool sendEvent(bridgepp::SPStreamEvent const &event); ///< Queue an event for sending through the event stream.
private: // member functions

View File

@ -138,14 +138,6 @@ bool SettingsTab::showSplashScreen() const {
}
//****************************************************************************************************************************************************
/// \return true iff the 'Show Splash Screen' check box is checked.
//****************************************************************************************************************************************************
bool SettingsTab::isFirstGUIStart() const {
return ui_.checkIsFirstGUIStart->isChecked();
}
//****************************************************************************************************************************************************
/// \return true iff autosart is on.
//****************************************************************************************************************************************************
@ -450,7 +442,6 @@ void SettingsTab::resetUI() {
ui_.editCurrentEmailClient->setText("Thunderbird/102.0.3");
ui_.checkShowOnStartup->setChecked(true);
ui_.checkShowSplashScreen->setChecked(false);
ui_.checkIsFirstGUIStart->setChecked(false);
ui_.checkAutostart->setChecked(true);
ui_.checkBetaEnabled->setChecked(true);
ui_.checkAllMailVisible->setChecked(true);

View File

@ -42,7 +42,6 @@ public: // member functions.
void setGUIReady(bool ready); ///< Set the GUI as ready.
bool showOnStartup() const; ///< Get the value for the 'Show On Startup' check.
bool showSplashScreen() const; ///< Get the value for the 'Show Splash Screen' check.
bool isFirstGUIStart() const; ///< Get the value for the 'Is First GUI Start' check.
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.

View File

@ -124,16 +124,6 @@
</widget>
</item>
<item row="0" column="2">
<widget class="QCheckBox" name="checkIsFirstGUIStart">
<property name="text">
<string>Is FIrst GUI Start</string>
</property>
<property name="checked">
<bool>false</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="checkAutostart">
<property name="text">
<string>Autostart</string>
@ -143,7 +133,7 @@
</property>
</widget>
</item>
<item row="1" column="1">
<item row="1" column="0">
<widget class="QCheckBox" name="checkBetaEnabled">
<property name="text">
<string>Beta Enabled</string>
@ -153,14 +143,17 @@
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="checkAutomaticUpdate">
<item row="1" column="1">
<widget class="QCheckBox" name="checkAllMailVisible">
<property name="text">
<string>Automatic Update</string>
<string>Show 'All Mail'</string>
</property>
<property name="checked">
<bool>false</bool>
</property>
</widget>
</item>
<item row="2" column="0">
<item row="1" column="2">
<widget class="QCheckBox" name="checkDarkTheme">
<property name="text">
<string>Dark Theme</string>
@ -170,13 +163,10 @@
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QCheckBox" name="checkAllMailVisible">
<item row="2" column="0">
<widget class="QCheckBox" name="checkAutomaticUpdate">
<property name="text">
<string>Show 'All Mail'</string>
</property>
<property name="checked">
<bool>false</bool>
<string>Automatic Update</string>
</property>
</widget>
</item>
@ -901,12 +891,6 @@
<tabstop>editCurrentEmailClient</tabstop>
<tabstop>checkShowOnStartup</tabstop>
<tabstop>checkShowSplashScreen</tabstop>
<tabstop>checkIsFirstGUIStart</tabstop>
<tabstop>checkAutostart</tabstop>
<tabstop>checkBetaEnabled</tabstop>
<tabstop>checkAllMailVisible</tabstop>
<tabstop>checkDarkTheme</tabstop>
<tabstop>checkAutomaticUpdate</tabstop>
<tabstop>editHostname</tabstop>
<tabstop>spinPortIMAP</tabstop>
<tabstop>spinPortSMTP</tabstop>

View File

@ -51,7 +51,12 @@ UsersTab::UsersTab(QWidget *parent)
connect(ui_.buttonEditUser, &QPushButton::clicked, this, &UsersTab::onEditUserButton);
connect(ui_.tableUserList, &QTableView::doubleClicked, this, &UsersTab::onEditUserButton);
connect(ui_.buttonRemoveUser, &QPushButton::clicked, this, &UsersTab::onRemoveUserButton);
connect(ui_.buttonUserBadEvent, &QPushButton::clicked, this, &UsersTab::onSendUserBadEvent);
connect(ui_.buttonImapLoginFailed, &QPushButton::clicked, this, &UsersTab::onSendIMAPLoginFailedEvent);
connect(ui_.buttonUsedBytesChanged, &QPushButton::clicked, this, &UsersTab::onSendUsedBytesChangedEvent);
connect(ui_.checkUsernamePasswordError, &QCheckBox::toggled, this, &UsersTab::updateGUIState);
connect(ui_.checkSync, &QCheckBox::toggled, this, &UsersTab::onCheckSyncToggled);
connect(ui_.sliderSync, &QSlider::valueChanged, this, &UsersTab::onSliderSyncValueChanged);
users_.append(randomUser());
@ -96,6 +101,8 @@ void UsersTab::onEditUserButton() {
if (grpc.isStreaming()) {
grpc.sendEvent(newUserChangedEvent(user->id()));
}
this->updateGUIState();
}
@ -125,14 +132,103 @@ void UsersTab::onSelectionChanged(QItemSelection, QItemSelection) {
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void UsersTab::onSendUserBadEvent() {
SPUser const user = selectedUser();
int const index = this->selectedIndex();
if (!user) {
app().log().error(QString("%1 failed. Unkown user.").arg(__FUNCTION__));
return;
}
if (UserState::SignedOut == user->state()) {
app().log().error(QString("%1 failed. User is already signed out").arg(__FUNCTION__));
}
GRPCService &grpc = app().grpc();
if (grpc.isStreaming()) {
QString const userID = user->id();
grpc.sendEvent(newUserChangedEvent(userID));
grpc.sendEvent(newUserBadEvent(userID, ui_.editUserBadEvent->text()));
}
this->updateGUIState();
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void UsersTab::onSendUsedBytesChangedEvent() {
SPUser const user = selectedUser();
int const index = this->selectedIndex();
if (!user) {
app().log().error(QString("%1 failed. Unkown user.").arg(__FUNCTION__));
return;
}
if (UserState::Connected != user->state()) {
app().log().error(QString("%1 failed. User is not connected").arg(__FUNCTION__));
}
qint64 const usedBytes = qint64(ui_.spinUsedBytes->value());
user->setUsedBytes(usedBytes);
users_.touch(index);
GRPCService &grpc = app().grpc();
if (grpc.isStreaming()) {
QString const userID = user->id();
grpc.sendEvent(newUsedBytesChangedEvent(userID, usedBytes));
}
this->updateGUIState();
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void UsersTab::onSendIMAPLoginFailedEvent() {
GRPCService &grpc = app().grpc();
if (grpc.isStreaming()) {
grpc.sendEvent(newIMAPLoginFailedEvent(ui_.editIMAPLoginFailedUsername->text()));
}
this->updateGUIState();
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void UsersTab::updateGUIState() {
bool const hasSelectedUser = ui_.tableUserList->selectionModel()->hasSelection();
SPUser const user = selectedUser();
bool const hasSelectedUser = user.get();
UserState const state = user ? user->state() : UserState::SignedOut;
ui_.buttonEditUser->setEnabled(hasSelectedUser);
ui_.buttonRemoveUser->setEnabled(hasSelectedUser);
ui_.groupBoxBadEvent->setEnabled(hasSelectedUser && (UserState::SignedOut != state));
ui_.groupBoxUsedSpace->setEnabled(hasSelectedUser && (UserState::Connected == state));
ui_.editUsernamePasswordError->setEnabled(ui_.checkUsernamePasswordError->isChecked());
ui_.spinUsedBytes->setValue(user ? user->usedBytes() : 0.0);
ui_.groupboxSync->setEnabled(user.get());
if (user)
ui_.editIMAPLoginFailedUsername->setText(user->primaryEmailOrUsername());
QSignalBlocker b(ui_.checkSync);
bool const syncing = user && user->isSyncing();
ui_.checkSync->setChecked(syncing);
b = QSignalBlocker(ui_.sliderSync);
ui_.sliderSync->setEnabled(syncing);
qint32 const progressPercent = syncing ? qint32(user->syncProgress() * 100.0f) : 0;
ui_.sliderSync->setValue(progressPercent);
ui_.labelSync->setText(syncing ? QString("%1%").arg(progressPercent) : "" );
}
@ -309,5 +405,71 @@ void UsersTab::removeUser(QString const &userID) {
//****************************************************************************************************************************************************
void UsersTab::configureUserAppleMail(QString const &userID, QString const &address) {
app().log().info(QString("Apple mail configuration was requested for user %1, address %2").arg(userID, address));
}
//****************************************************************************************************************************************************
/// \param[in] userID The userID.
/// \param[in] doResync Did the user request a resync?
//****************************************************************************************************************************************************
void UsersTab::processBadEventUserFeedback(QString const &userID, bool doResync) {
app().log().info(QString("Feedback received for bad event: doResync = %1, userID = %2").arg(doResync ? "true" : "false", userID));
if (doResync) {
return; // we do not do any form of emulation for resync.
}
SPUser user = users_.userWithID(userID);
if (!user) {
app().log().error(QString("%1(): could not find user with id %1.").arg(__func__, userID));
}
user->setState(UserState::SignedOut);
users_.touch(userID);
GRPCService &grpc = app().grpc();
if (grpc.isStreaming()) {
grpc.sendEvent(newUserChangedEvent(userID));
}
this->updateGUIState();
}
//****************************************************************************************************************************************************
/// \param[in] checked Is the sync checkbox checked?
//****************************************************************************************************************************************************
void UsersTab::onCheckSyncToggled(bool checked) {
SPUser const user = this->selectedUser();
if ((!user) || (user->isSyncing() == checked)) {
return;
}
user->setIsSyncing(checked);
user->setSyncProgress(0.0);
GRPCService &grpc = app().grpc();
// we do not apply delay for these event.
if (checked) {
grpc.sendEvent(newSyncStartedEvent(user->id()));
grpc.sendEvent(newSyncProgressEvent(user->id(), 0.0, 1, 1));
} else {
grpc.sendEvent(newSyncFinishedEvent(user->id()));
}
this->updateGUIState();
}
//****************************************************************************************************************************************************
/// \param[in] value The value for the slider.
//****************************************************************************************************************************************************
void UsersTab::onSliderSyncValueChanged(int value) {
SPUser const user = this->selectedUser();
if ((!user) || (!user->isSyncing()) || user->syncProgress() == value) {
return;
}
double const progress = value / 100.0;
user->setSyncProgress(progress);
app().grpc().sendEvent(newSyncProgressEvent(user->id(), progress, 1, 1)); // we do not simulate elapsed & remaining.
this->updateGUIState();
}

View File

@ -54,12 +54,18 @@ public slots:
void logoutUser(QString const &userID); ///< slot for the logging out of a user.
void removeUser(QString const &userID); ///< Slot for the removal of a user.
void configureUserAppleMail(QString const &userID, QString const &address); ///< Slot for the configuration of Apple mail.
void processBadEventUserFeedback(QString const& userID, bool doResync); ///< Slot for the reception of a bad event user feedback.
private slots:
void onAddUserButton(); ///< Add a user to the user list.
void onEditUserButton(); ///< Edit the currently selected user.
void onRemoveUserButton(); ///< Remove the currently selected user.
void onSelectionChanged(QItemSelection, QItemSelection); ///< Slot for the change of the selection.
void onSendUserBadEvent(); ///< Slot for the 'Send Bad Event Error' button.
void onSendUsedBytesChangedEvent(); ///< Slot for the 'Send Used Bytes Changed Event' button.
void onSendIMAPLoginFailedEvent(); ///< Slot for the 'Send IMAP Login failure Event' button.
void onCheckSyncToggled(bool checked); ///< Slot for the 'Synchronizing' check box.
void onSliderSyncValueChanged(int value); ///< Slot for the sync 'Progress' slider.
void updateGUIState(); ///< Update the GUI state.
private: // member functions.

View File

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>1225</width>
<height>717</height>
<width>1221</width>
<height>894</height>
</rect>
</property>
<property name="windowTitle">
@ -67,7 +67,176 @@
</spacer>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<widget class="QGroupBox" name="groupboxSync">
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="title">
<string>Sync</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4" stretch="1,0">
<item>
<widget class="QCheckBox" name="checkSync">
<property name="text">
<string>Synchronizing</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="labelSync">
<property name="text">
<string>0%</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QSlider" name="sliderSync">
<property name="maximum">
<number>100</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="tickInterval">
<number>10</number>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBoxBadEvent">
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="title">
<string>Bad Event</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QLineEdit" name="editUserBadEvent">
<property name="minimumSize">
<size>
<width>200</width>
<height>0</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="placeholderText">
<string>error message</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="buttonUserBadEvent">
<property name="text">
<string>Send</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBoxUsedSpace">
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="title">
<string>Used Bytes Changed</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<layout class="QHBoxLayout" name="hBoxUsedBytes" stretch="1,0">
<item>
<widget class="QDoubleSpinBox" name="spinUsedBytes">
<property name="buttonSymbols">
<enum>QAbstractSpinBox::NoButtons</enum>
</property>
<property name="decimals">
<number>0</number>
</property>
<property name="maximum">
<double>1000000000000000.000000000000000</double>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="buttonUsedBytesChanged">
<property name="text">
<string>Send</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBoxIMAPLoginFailed">
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="title">
<string>IMAP Login Failure</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_8">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<widget class="QLineEdit" name="editIMAPLoginFailedUsername">
<property name="minimumSize">
<size>
<width>200</width>
<height>0</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="placeholderText">
<string>username or primary email</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="buttonImapLoginFailed">
<property name="text">
<string>Send</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBoxNextLogin">
<property name="minimumSize">
<size>
<width>0</width>

View File

@ -186,6 +186,14 @@ void UserTable::touch(qint32 index) {
}
//****************************************************************************************************************************************************
/// \param[in] userID The userID.
//****************************************************************************************************************************************************
void UserTable::touch(QString const &userID) {
this->touch(this->indexOfUser(userID));
}
//****************************************************************************************************************************************************
/// \param[in] index The index of the user in the list.
//****************************************************************************************************************************************************

View File

@ -43,6 +43,7 @@ public: // member functions.
bridgepp::SPUser userWithUsername(QString const &username); ///< Return the user with a given username.
qint32 indexOfUser(QString const &userID); ///< Return the index of a given User.
void touch(qint32 index); ///< touch the user at a given index (indicates it has been modified).
void touch(QString const& userID); ///< touch the user with the given userID (indicates it has been modified).
void remove(qint32 index); ///< Remove the user at a given index.
QList<bridgepp::SPUser> users() const; ///< Return a copy of the user list.

View File

@ -85,7 +85,10 @@ int main(int argc, char **argv) {
return exitCode;
}
catch (Exception const &e) {
QTextStream(stderr) << QString("A fatal error occurred: %1\n").arg(e.qwhat());
QString message = e.qwhat();
if (!e.details().isEmpty())
message += "\n\nDetails:\n" + e.details();
QTextStream(stderr) << QString("A fatal error occurred: %1\n").arg(message);
return EXIT_FAILURE;
}
}

View File

@ -19,6 +19,7 @@
#include "AppController.h"
#include "QMLBackend.h"
#include "SentryUtils.h"
#include "Settings.h"
#include <bridgepp/GRPC/GRPCClient.h>
#include <bridgepp/Exception/Exception.h>
#include <bridgepp/ProcessMonitor.h>
@ -28,6 +29,10 @@
using namespace bridgepp;
namespace {
QString const noWindowFlag = "--no-window"; ///< The no-window command-line flag.
}
//****************************************************************************************************************************************************
/// \return The AppController instance.
@ -44,10 +49,19 @@ AppController &app() {
AppController::AppController()
: backend_(std::make_unique<QMLBackend>())
, grpc_(std::make_unique<GRPCClient>())
, log_(std::make_unique<Log>()) {
, log_(std::make_unique<Log>())
, settings_(new Settings) {
}
//****************************************************************************************************************************************************
// The following is in the implementation file because of unique pointers with incomplete types in headers.
// See https://stackoverflow.com/questions/6012157/is-stdunique-ptrt-required-to-know-the-full-definition-of-t
//****************************************************************************************************************************************************
AppController::~AppController() = default;
//****************************************************************************************************************************************************
/// \return The bridge worker, which can be null if the application was run in 'attach' mode (-a command-line switch).
//****************************************************************************************************************************************************
@ -68,13 +82,42 @@ ProcessMonitor *AppController::bridgeMonitor() const {
//****************************************************************************************************************************************************
/// \param[in] function The function that caught the exception.
/// \param[in] message The error message.
/// \return A reference to the application settings.
//****************************************************************************************************************************************************
void AppController::onFatalError(QString const &function, QString const &message) {
QString const fullMessage = QString("%1(): %2").arg(function, message);
reportSentryException(SENTRY_LEVEL_ERROR, "AppController got notified of a fatal error", "Exception", fullMessage.toLocal8Bit());
QMessageBox::critical(nullptr, tr("Error"), message);
log().fatal(fullMessage);
Settings &AppController::settings() {
return *settings_;
}
//****************************************************************************************************************************************************
/// \param[in] exception The exception that triggered the fatal error.
//****************************************************************************************************************************************************
void AppController::onFatalError(Exception const &exception) {
sentry_uuid_t uuid = reportSentryException("AppController got notified of a fatal error", exception);
QMessageBox::critical(nullptr, tr("Error"), exception.what());
restart(true);
log().fatal(QString("reportID: %1 Captured exception: %2").arg(QByteArray(uuid.bytes, 16).toHex(), exception.detailedWhat()));
qApp->exit(EXIT_FAILURE);
}
void AppController::restart(bool isCrashing) {
if (!launcher_.isEmpty()) {
QProcess p;
log_->info(QString("Restarting - App : %1 - Args : %2").arg(launcher_, launcherArgs_.join(" ")));
QStringList args = launcherArgs_;
if (isCrashing) {
args.append(noWindowFlag);
}
p.startDetached(launcher_, args);
p.waitForStarted();
}
}
void AppController::setLauncherArgs(const QString &launcher, const QStringList &args) {
launcher_ = launcher;
launcherArgs_ = args;
}

View File

@ -20,21 +20,17 @@
#define BRIDGE_GUI_APP_CONTROLLER_H
//@formatter:off
class QMLBackend;
class Settings;
namespace bridgepp {
class Log;
class Overseer;
class GRPCClient;
class ProcessMonitor;
class Exception;
}
//@formatter:on
//****************************************************************************************************************************************************
@ -47,7 +43,7 @@ Q_OBJECT
public: // member functions.
AppController(AppController const &) = delete; ///< Disabled copy-constructor.
AppController(AppController &&) = delete; ///< Disabled assignment copy-constructor.
~AppController() override = default; ///< Destructor.
~AppController() override; ///< Destructor.
AppController &operator=(AppController const &) = delete; ///< Disabled assignment operator.
AppController &operator=(AppController &&) = delete; ///< Disabled move assignment operator.
QMLBackend &backend() { return *backend_; } ///< Return a reference to the backend.
@ -55,18 +51,24 @@ public: // member functions.
bridgepp::Log &log() { return *log_; } ///< Return a reference to the log.
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);
public slots:
void onFatalError(QString const &function, QString const &message); ///< Handle fatal errors.
void onFatalError(bridgepp::Exception const& e); ///< Handle fatal errors.
private: // member functions
AppController(); ///< Default constructor.
void restart(bool isCrashing = false); ///< Restart the app.
private: // data members
std::unique_ptr<QMLBackend> backend_; ///< The backend.
std::unique_ptr<bridgepp::GRPCClient> grpc_; ///< The RPC client.
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_;
};

View File

@ -0,0 +1,31 @@
// 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_VERSION_H
#define BRIDGE_GUI_VERSION_H
#define PROJECT_FULL_NAME "@BRIDGE_APP_FULL_NAME@"
#define PROJECT_VENDOR "@BRIDGE_VENDOR@"
#define PROJECT_VER "@BRIDGE_APP_VERSION@"
#define PROJECT_REVISION "@BRIDGE_REVISION@"
#define PROJECT_BUILD_TIME "@BRIDGE_BUILD_TIME@"
#define PROJECT_DSN_SENTRY "@BRIDGE_DSN_SENTRY@"
#define PROJECT_BUILD_ENV "@BRIDGE_BUILD_ENV@"
#define PROJECT_CRASHPAD_HANDLER_PATH "@BRIDGE_CRASHPAD_HANDLER_PATH@"
#endif // BRIDGE_GUI_VERSION_H

View File

@ -85,20 +85,11 @@ message(STATUS "Using Qt ${Qt6_VERSION}")
#*****************************************************************************************************************************************************
find_package(sentry CONFIG REQUIRED)
set(DSN_SENTRY "https://ea31dfe8574849108fb8ba044fec3620@api.protonmail.ch/core/v4/reports/sentry/7")
set(SENTRY_CONFIG_GENERATED_FILE_DIR ${CMAKE_CURRENT_BINARY_DIR}/sentry-generated)
set(SENTRY_CONFIG_FILE ${SENTRY_CONFIG_GENERATED_FILE_DIR}/project_sentry_config.h)
file(GENERATE OUTPUT ${SENTRY_CONFIG_FILE} CONTENT
"// AUTO GENERATED FILE, DO NOT MODIFY\n#pragma once\nconst char* SentryDNS=\"${DSN_SENTRY}\";\nconst char* SentryProductID=\"bridge-mail@${BRIDGE_APP_VERSION}\";\n"
)
#*****************************************************************************************************************************************************
# Source files and output
#*****************************************************************************************************************************************************
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/Version.h.in ${CMAKE_CURRENT_SOURCE_DIR}/Version.h)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/BuildConfig.h.in ${CMAKE_CURRENT_SOURCE_DIR}/BuildConfig.h)
if (NOT TARGET bridgepp)
add_subdirectory(../bridgepp bridgepp)
@ -118,23 +109,23 @@ add_executable(bridge-gui
Resources.qrc
AppController.cpp AppController.h
BridgeApp.cpp BridgeApp.h
BuildConfig.h
CommandLine.cpp CommandLine.h
EventStreamWorker.cpp EventStreamWorker.h
LogUtils.cpp LogUtils.h
main.cpp
Pch.h
Version.h
QMLBackend.cpp QMLBackend.h
UserList.cpp UserList.h
SentryUtils.cpp SentryUtils.h
Settings.cpp Settings.h
${DOCK_ICON_SRC_FILE} MacOS/DockIcon.h
)
if (APPLE)
target_sources(bridge-gui PRIVATE MacOS/SecondInstance.mm MacOS/SecondInstance.h)
endif(APPLE)
if (WIN32) # on Windows, we add a (non-Qt) resource file that contains the application icon and version information.
string(TIMESTAMP BRIDGE_BUILD_YEAR "%Y")
set(REGEX_NUMBER "[0123456789]") # CMake matches does not support \d.

View File

@ -18,6 +18,7 @@
#include "Pch.h"
#include "CommandLine.h"
#include "Settings.h"
using namespace bridgepp;
@ -28,7 +29,10 @@ namespace {
QString const launcherFlag = "--launcher"; ///< launcher flag parameter used for bridge.
QString const noWindowFlag = "--no-window"; ///< The no-window command-line flag.
QString const softwareRendererFlag = "--software-renderer"; ///< The 'software-renderer' command-line flag.
QString const softwareRendererFlag = "--software-renderer"; ///< The 'software-renderer' command-line flag. enable software rendering for a single execution
QString const setSoftwareRendererFlag = "--set-software-renderer"; ///< The 'set-software-renderer' command-line flag. Software rendering will be used for all subsequent executions of the application.
QString const setHardwareRendererFlag = "--set-hardware-renderer"; ///< The 'set-hardware-renderer' command-line flag. Hardware rendering will be used for all subsequent executions of the application.
//****************************************************************************************************************************************************
/// \brief parse a command-line string argument as expected by go's CLI package.
@ -98,8 +102,17 @@ CommandLineOptions parseCommandLine(int argc, char *argv[]) {
// we can't use QCommandLineParser here since it will fail on unknown options.
// Arguments may contain some bridge flags.
if (arg == softwareRendererFlag) {
options.bridgeGuiArgs.append(arg);
options.useSoftwareRenderer = true;
}
if (arg == setSoftwareRendererFlag) {
app().settings().setUseSoftwareRenderer(true);
continue; // setting is permanent. no need to keep/pass it to bridge for restart.
}
if (arg == setHardwareRendererFlag) {
app().settings().setUseSoftwareRenderer(false);
continue; // setting is permanent. no need to keep/pass it to bridge for restart.
}
if (arg == noWindowFlag) {
options.noWindow = true;
}
@ -113,10 +126,12 @@ CommandLineOptions parseCommandLine(int argc, char *argv[]) {
else if (arg == "--attach" || arg == "-a") {
// we don't keep the attach mode within the args since we don't need it for Bridge.
options.attach = true;
options.bridgeGuiArgs.append(arg);
}
#endif
else {
options.bridgeArgs.append(arg);
options.bridgeGuiArgs.append(arg);
}
}
if (!flagFound) {

View File

@ -28,6 +28,7 @@
//****************************************************************************************************************************************************
struct CommandLineOptions {
QStringList bridgeArgs; ///< The command-line arguments we will pass to bridge when launching it.
QStringList bridgeGuiArgs; ///< The command-line arguments we will pass to bridge when launching it.
QString launcher; ///< The path to the launcher.
bool attach { false }; ///< Is the application running in attached mode?
bridgepp::Log::Level logLevel { bridgepp::Log::defaultLevel }; ///< The log level

View File

@ -52,7 +52,7 @@ void EventStreamReader::run() {
emit finished();
}
catch (Exception const &e) {
reportSentryException(SENTRY_LEVEL_ERROR, "Error during event stream read", "Exception", e.what());
reportSentryException("Error during event stream read", e);
emit error(e.qwhat());
}
}

View File

@ -0,0 +1,58 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
#include "LogUtils.h"
#include "BuildConfig.h"
#include <bridgepp/Log/LogUtils.h>
#include <bridgepp/BridgeUtils.h>
using namespace bridgepp;
//****************************************************************************************************************************************************
/// \return A reference to the log.
//****************************************************************************************************************************************************
Log &initLog() {
Log &log = app().log();
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)) {
log.error(error);
}
log.info("bridge-gui starting");
QString const qtCompileTimeVersion = QT_VERSION_STR;
QString const qtRuntimeVersion = qVersion();
QString msg = QString("Using Qt %1").arg(qtRuntimeVersion);
if (qtRuntimeVersion != qtCompileTimeVersion) {
msg += QString(" (compiled against %1)").arg(qtCompileTimeVersion);
}
log.info(msg);
return log;
}

View File

@ -16,13 +16,14 @@
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
#ifndef BRIDGE_GUI_VERSION_H
#define BRIDGE_GUI_VERSION_H
#ifndef BRIDGE_GUI_LOG_UTILS_H
#define BRIDGE_GUI_LOG_UTILS_H
#define PROJECT_FULL_NAME "@BRIDGE_APP_FULL_NAME@"
#define PROJECT_VENDOR "@BRIDGE_VENDOR@"
#define PROJECT_VER "@BRIDGE_APP_VERSION@"
#define PROJECT_REVISION "@BRIDGE_REVISION@"
#define PROJECT_BUILD_TIME "@BRIDGE_BUILD_TIME@"
#endif // BRIDGE_GUI_VERSION_H
#include <bridgepp/Log/Log.h>
bridgepp::Log &initLog(); ///< Initialize the application log.
#endif //BRIDGE_GUI_LOG_UTILS_H

View File

@ -17,16 +17,19 @@
#include "QMLBackend.h"
#include "BuildConfig.h"
#include "EventStreamWorker.h"
#include "Version.h"
#include <bridgepp/GRPC/GRPCClient.h>
#include <bridgepp/BridgeUtils.h>
#include <bridgepp/Exception/Exception.h>
#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 } \
catch (Exception const &e) { emit fatalError(__func__, e.qwhat()); } \
catch (...) { emit fatalError(__func__, QString("An unknown exception occurred")); }
catch (Exception const &e) { emit fatalError(e); } \
catch (...) { emit fatalError(Exception("An unknown exception occurred", QString(), __func__)); }
#define HANDLE_EXCEPTION_RETURN_BOOL(x) HANDLE_EXCEPTION(x) return false;
#define HANDLE_EXCEPTION_RETURN_QSTRING(x) HANDLE_EXCEPTION(x) return QString();
#define HANDLE_EXCEPTION_RETURN_ZERO(x) HANDLE_EXCEPTION(x) return 0;
@ -56,12 +59,8 @@ void QMLBackend::init(GRPCConfig const &serviceConfig) {
app().grpc().setLog(&log);
this->connectGrpcEvents();
QString error;
if (app().grpc().connectToServer(serviceConfig, app().bridgeMonitor(), error)) {
app().log().info("Connected to backend via gRPC service.");
} else {
throw Exception(QString("Cannot connectToServer to go backend via gRPC: %1").arg(error));
}
app().grpc().connectToServer(bridgepp::userConfigDir(), serviceConfig, app().bridgeMonitor());
app().log().info("Connected to backend via gRPC service.");
QString bridgeVer;
app().grpc().version(bridgeVer);
@ -77,7 +76,6 @@ void QMLBackend::init(GRPCConfig const &serviceConfig) {
});
// Grab from bridge the value that will not change during the execution of this app (or that will only change locally).
app().grpc().showSplashScreen(showSplashScreen_);
app().grpc().goos(goos_);
app().grpc().logsPath(logsPath_);
app().grpc().licensePath(licensePath_);
@ -102,6 +100,14 @@ bool QMLBackend::waitForEventStreamReaderToFinish(qint32 timeoutMs) {
}
//****************************************************************************************************************************************************
/// \return The build year as a string (e.g. 2023)
//****************************************************************************************************************************************************
QString QMLBackend::buildYear() {
return QString(__DATE__).right(4);
}
//****************************************************************************************************************************************************
/// \return The position of the cursor.
//****************************************************************************************************************************************************
@ -173,16 +179,6 @@ void QMLBackend::setShowSplashScreen(bool show) {
}
//****************************************************************************************************************************************************
/// \return The value for the 'showSplashScreen' property.
//****************************************************************************************************************************************************
bool QMLBackend::showSplashScreen() const {
HANDLE_EXCEPTION_RETURN_BOOL(
return showSplashScreen_;
)
}
//****************************************************************************************************************************************************
/// \return The value for the 'GOOS' property.
//****************************************************************************************************************************************************
@ -193,6 +189,16 @@ QString QMLBackend::goos() const {
}
//****************************************************************************************************************************************************
/// \return The value for the 'showSplashScreen' property.
//****************************************************************************************************************************************************
bool QMLBackend::showSplashScreen() const {
HANDLE_EXCEPTION_RETURN_BOOL(
return showSplashScreen_;
)
}
//****************************************************************************************************************************************************
/// \return The value for the 'logsPath' property.
//****************************************************************************************************************************************************
@ -461,18 +467,6 @@ bool QMLBackend::isDoHEnabled() const {
}
//****************************************************************************************************************************************************
/// \return The value for the 'isFirstGUIStart' property.
//****************************************************************************************************************************************************
bool QMLBackend::isFirstGUIStart() const {
HANDLE_EXCEPTION_RETURN_BOOL(
bool v;
app().grpc().isFirstGUIStart(v);
return v;
)
}
//****************************************************************************************************************************************************
/// \return The value for the 'isAutomaticUpdateOn' property.
//****************************************************************************************************************************************************
@ -603,7 +597,8 @@ void QMLBackend::setDiskCachePath(QUrl const &path) const {
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");
throw Exception("User requested bridge-gui to crash by trying to log as coco@bandicoot",
"This error exists for test purposes and should be ignored.", __func__, tailOfLatestBridgeLog());
}
app().grpc().login(username, password);
)
@ -691,9 +686,11 @@ void QMLBackend::changeKeychain(QString const &keychain) {
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void QMLBackend::guiReady() const {
void QMLBackend::guiReady() {
HANDLE_EXCEPTION(
app().grpc().guiReady();
bool showSplashScreen;
app().grpc().guiReady(showSplashScreen);
this->setShowSplashScreen(showSplashScreen);
)
}
@ -821,6 +818,26 @@ void QMLBackend::setMailServerSettings(int imapPort, int smtpPort, bool useSSLFo
}
//****************************************************************************************************************************************************
/// \param[in] userID The userID.
/// \param[in] doResync Did the user request a resync.
//****************************************************************************************************************************************************
void QMLBackend::sendBadEventUserFeedback(QString const &userID, bool doResync) {
HANDLE_EXCEPTION(
app().grpc().sendBadEventUserFeedback(userID, doResync);
// Notification dialog has just been dismissed, we remove the userID from the queue, and if there are other events in the queue, we show
// the dialog again.
badEventDisplayQueue_.removeOne(userID);
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()); });
}
)
}
//****************************************************************************************************************************************************
/// \param[in] imapPort The IMAP port.
/// \param[in] smtpPort The SMTP port.
@ -872,6 +889,47 @@ void QMLBackend::onLoginAlreadyLoggedIn(QString const &userID) {
}
//****************************************************************************************************************************************************
/// \param[in] userID The userID.
//****************************************************************************************************************************************************
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.");
return;
}
SPUser const user = users_->getUserWithID(userID);
if (!user) {
app().log().error(QString("Received bad event for unknown user %1."));
}
badEventDisplayQueue_.append(userID);
if (badEventDisplayQueue_.size() == 1) { // there was no other item is the queue, we can display the dialog immediately.
this->displayBadEventDialog(userID);
}
)
}
//****************************************************************************************************************************************************
/// \param[in] username The username (or primary email address)
//****************************************************************************************************************************************************
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
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);
)
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
@ -979,5 +1037,27 @@ void QMLBackend::connectGrpcEvents() {
// user events
connect(client, &GRPCClient::userDisconnected, this, &QMLBackend::userDisconnected);
connect(client, &GRPCClient::userBadEvent, this, &QMLBackend::onUserBadEvent);
connect(client, &GRPCClient::imapLoginFailed, this, &QMLBackend::onIMAPLoginFailed);
users_->connectGRPCEvents();
}
//****************************************************************************************************************************************************
/// \param[in] userID The userID.
//****************************************************************************************************************************************************
void QMLBackend::displayBadEventDialog(QString const &userID) {
HANDLE_EXCEPTION(
SPUser const user = users_->getUserWithID(userID);
if (!user) {
return;
}
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 showMainWindow();
)
}

View File

@ -21,7 +21,7 @@
#include "MacOS/DockIcon.h"
#include "Version.h"
#include "BuildConfig.h"
#include "UserList.h"
#include <bridgepp/GRPC/GRPCClient.h>
#include <bridgepp/GRPC/GRPCUtils.h>
@ -45,6 +45,7 @@ public: // member functions.
bool waitForEventStreamReaderToFinish(qint32 timeoutMs); ///< Wait for the event stream reader to finish.
// 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.
Q_INVOKABLE QPoint getCursorPos() const; ///< Retrieve the cursor position.
Q_INVOKABLE bool isPortFree(int port) const; ///< Check if a given network port is available.
Q_INVOKABLE QString nativePath(QUrl const &url) const; ///< Retrieve the native path of a local URL.
@ -73,7 +74,6 @@ public: // Qt/QML properties. Note that the NOTIFY-er signal is required even fo
Q_PROPERTY(int imapPort READ imapPort WRITE setIMAPPort NOTIFY imapPortChanged)
Q_PROPERTY(int smtpPort READ smtpPort WRITE setSMTPPort NOTIFY smtpPortChanged)
Q_PROPERTY(bool isDoHEnabled READ isDoHEnabled NOTIFY isDoHEnabledChanged)
Q_PROPERTY(bool isFirstGUIStart READ isFirstGUIStart)
Q_PROPERTY(bool isAutomaticUpdateOn READ isAutomaticUpdateOn NOTIFY isAutomaticUpdateOnChanged)
Q_PROPERTY(QString currentEmailClient READ currentEmailClient NOTIFY currentEmailClientChanged)
Q_PROPERTY(QStringList availableKeychain READ availableKeychain NOTIFY availableKeychainChanged)
@ -110,7 +110,6 @@ public: // Qt/QML properties. Note that the NOTIFY-er signal is required even fo
void setSMTPPort(int port); ///< Setter for the 'smtpPort' property.
int smtpPort() const; ///< Getter for the 'smtpPort' property.
bool isDoHEnabled() const; ///< Getter for the 'isDoHEnabled' property.
bool isFirstGUIStart() const; ///< Getter for the 'isFirstGUIStart' property.
bool isAutomaticUpdateOn() const; ///< Getter for the 'isAutomaticUpdateOn' property.
QString currentEmailClient() const; ///< Getter for the 'currentEmail' property.
QStringList availableKeychain() const; ///< Getter for the 'availableKeychain' property.
@ -162,7 +161,7 @@ public slots: // slot for signals received from QML -> To be forwarded to Bridge
void toggleAutomaticUpdate(bool makeItActive); ///< Slot for the automatic update toggle
void updateCurrentMailClient(); ///< Slot for the change of the current mail client.
void changeKeychain(QString const &keychain); ///< Slot for the change of keychain.
void guiReady() const; ///< Slot for the GUI ready signal.
void guiReady(); ///< Slot for the GUI ready signal.
void quit() const; ///< Slot for the quit signal.
void restart() const; ///< Slot for the restart signal.
void forceLauncher(QString launcher) const; ///< Slot for the change of the launcher.
@ -174,12 +173,15 @@ public slots: // slot for signals received from QML -> To be forwarded to Bridge
void onResetFinished(); ///< Slot for the reset finish signal.
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.
public slots: // slot for signals received from gRPC that need transformation instead of simple forwarding
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.
void onLoginAlreadyLoggedIn(QString const &userID); ///< Slot for the LoginAlreadyLoggedIn gRPC event.
void onUserBadEvent(QString const& userID, QString const& errorMessage); ///< Slot for the userBadEvent gRPC event.
void onIMAPLoginFailed(QString const& username); ///< Slot the the imapLoginFailed event.
signals: // Signals received from the Go backend, to be forwarded to QML
void toggleAutostartFinished(); ///< Signal for the 'toggleAutostartFinished' gRPC stream event.
@ -222,6 +224,7 @@ signals: // Signals received from the Go backend, to be forwarded to QML
void addressChangedLogout(QString const &address); ///< Signal for the 'addressChangedLogout' gRPC stream event.
void apiCertIssue(); ///< Signal for the 'apiCertIssue' gRPC stream event.
void userDisconnected(QString const &username); ///< Signal for the 'userDisconnected' gRPC stream event.
void userBadEvent(QString const &userID, QString const &description); ///< Signal for the 'userBadEvent' gRPC stream event.
void internetOff(); ///< Signal for the 'internetOff' gRPC stream event.
void internetOn(); ///< Signal for the 'internetOn' gRPC stream event.
void resetFinished(); ///< Signal for the 'resetFinished' gRPC stream event.
@ -231,13 +234,16 @@ signals: // Signals received from the Go backend, to be forwarded to QML
void showMainWindow(); ///< Signal for the 'showMainWindow' gRPC stream event.
void hideMainWindow(); ///< Signal for the 'hideMainWindow' gRPC stream event.
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.
void fatalError(QString const &function, QString const &message) const; ///< Signal emitted when an fatal error occurs.
void fatalError(bridgepp::Exception const& e) const; ///< Signal emitted when an fatal error occurs.
private: // member functions
void retrieveUserList(); ///< Retrieve the list of users via gRPC.
void connectGrpcEvents(); ///< Connect gRPC that need to be forwarded to QML via backend signals
void displayBadEventDialog(QString const& userID); ///< Displays the bad event dialog for a user.
private: // data members
UserList *users_ { nullptr }; ///< The user list. Owned by backend.
@ -250,6 +256,7 @@ 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.
QList<QString> badEventDisplayQueue_; ///< THe queue for displaying 'bad event feedback request dialog'.
friend class AppController;
};

View File

@ -42,6 +42,7 @@
<file>qml/icons/ic-other-mail-clients.svg</file>
<file>qml/icons/ic-plus.svg</file>
<file>qml/icons/ic-question-circle.svg</file>
<file>qml/icons/ic-splash-check.svg</file>
<file>qml/icons/ic-success.svg</file>
<file>qml/icons/ic-three-dots-vertical.svg</file>
<file>qml/icons/ic-trash.svg</file>

View File

@ -16,19 +16,166 @@
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
#include "SentryUtils.h"
#include "BuildConfig.h"
#include <bridgepp/BridgeUtils.h>
#include <bridgepp/Exception/Exception.h>
using namespace bridgepp;
static constexpr const char *LoggerName = "bridge-gui";
void reportSentryEvent(sentry_level_t level, const char *message) {
auto event = sentry_value_new_message_event(level, LoggerName, message);
sentry_capture_event(event);
//****************************************************************************************************************************************************
/// \return The temporary file used for sentry attachment.
//****************************************************************************************************************************************************
QString sentryAttachmentFilePath() {
static QString path;
if (!path.isEmpty()) {
return path;
}
while (true) {
path = QDir::temp().absoluteFilePath(QUuid::createUuid().toString(QUuid::WithoutBraces) + ".txt"); // Sentry does not offer preview for .log files.
if (!QFileInfo::exists(path)) {
return path;
}
}
}
void reportSentryException(sentry_level_t level, const char *message, const char *exceptionType, const char *exception) {
//****************************************************************************************************************************************************
/// \brief Get a hash of the computer's host name
//****************************************************************************************************************************************************
QByteArray getProtectedHostname() {
QByteArray hostname = QCryptographicHash::hash(QSysInfo::machineHostName().toUtf8(), QCryptographicHash::Sha256);
return hostname.toHex();
}
//****************************************************************************************************************************************************
/// \return The OS String used by sentry
//****************************************************************************************************************************************************
QString getApiOS() {
switch (os()) {
case OS::MacOS:
return "macos";
case OS::Windows:
return "windows";
case OS::Linux:
default:
return "linux";
}
}
//****************************************************************************************************************************************************
/// \return The application version number.
//****************************************************************************************************************************************************
QString appVersion(const QString& version) {
return QString("%1-bridge@%2").arg(getApiOS(), version);
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void initSentry() {
sentry_options_t *sentryOptions = newSentryOptions(PROJECT_DSN_SENTRY, sentryCacheDir().toStdString().c_str());
if (!QString(PROJECT_CRASHPAD_HANDLER_PATH).isEmpty())
sentry_options_set_handler_path(sentryOptions, PROJECT_CRASHPAD_HANDLER_PATH);
if (sentry_init(sentryOptions) != 0) {
QTextStream(stderr) << "Failed to initialize sentry\n";
}
setSentryReportScope();
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void setSentryReportScope() {
sentry_set_tag("OS", bridgepp::goos().toUtf8());
sentry_set_tag("Client", PROJECT_FULL_NAME);
sentry_set_tag("Version", PROJECT_REVISION);
sentry_set_tag("HostArch", QSysInfo::currentCpuArchitecture().toUtf8());
sentry_set_tag("server_name", getProtectedHostname());
sentry_value_t user = sentry_value_new_object();
sentry_value_set_by_key(user, "id", sentry_value_new_string(getProtectedHostname()));
sentry_set_user(user);
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
sentry_options_t* newSentryOptions(const char *sentryDNS, const char *cacheDir) {
sentry_options_t *sentryOptions = sentry_options_new();
sentry_options_set_dsn(sentryOptions, sentryDNS);
sentry_options_set_database_path(sentryOptions, cacheDir);
sentry_options_set_release(sentryOptions, appVersion(PROJECT_VER).toUtf8());
sentry_options_set_max_breadcrumbs(sentryOptions, 50);
sentry_options_set_environment(sentryOptions, PROJECT_BUILD_ENV);
QByteArray const array = sentryAttachmentFilePath().toLocal8Bit();
sentry_options_add_attachment(sentryOptions, array.constData());
// Enable this for debugging sentry.
// sentry_options_set_debug(sentryOptions, 1);
return sentryOptions;
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
sentry_uuid_t reportSentryEvent(sentry_level_t level, const char *message) {
auto event = sentry_value_new_message_event(level, LoggerName, message);
return sentry_capture_event(event);
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
sentry_uuid_t reportSentryException(sentry_level_t level, const char *message, const char *exceptionType, const char *exception) {
auto event = sentry_value_new_message_event(level, LoggerName, message);
sentry_event_add_exception(event, sentry_value_new_exception(exceptionType, exception));
sentry_capture_event(event);
// reject exception content from the fingerprint if there is not enough content
if ( strlen(exception) < 5) {
sentry_value_t fingerprint = sentry_value_new_list();
sentry_value_append(fingerprint, sentry_value_new_string("level"));
sentry_value_append(fingerprint, sentry_value_new_string(message));
sentry_value_append(fingerprint, sentry_value_new_string(LoggerName));
sentry_value_set_by_key(event, "fingerprint", fingerprint);
}
return sentry_capture_event(event);
}
//****************************************************************************************************************************************************
/// \param[in] message The message for the exception.
/// \param[in] function The name of the function that triggered the exception.
/// \param[in] exception The exception.
/// \return The Sentry exception UUID.
//****************************************************************************************************************************************************
sentry_uuid_t reportSentryException(QString const &message, bridgepp::Exception const exception) {
QByteArray const attachment = exception.attachment();
QFile file(sentryAttachmentFilePath());
bool const hasAttachment = !attachment.isEmpty();
if (hasAttachment) {
if (file.open(QIODevice::Text | QIODevice::WriteOnly)) {
file.write(attachment);
file.close();
}
}
sentry_uuid_t const uuid = reportSentryException(SENTRY_LEVEL_ERROR, message.toLocal8Bit(), "Exception",
exception.detailedWhat().toLocal8Bit());
if (hasAttachment) {
file.remove();
}
return uuid;
}

View File

@ -21,8 +21,11 @@
#include <sentry.h>
void initSentry();
void setSentryReportScope();
sentry_options_t* newSentryOptions(const char * sentryDNS, const char * cacheDir);
sentry_uuid_t reportSentryEvent(sentry_level_t level, const char *message);
sentry_uuid_t reportSentryException(QString const& message, bridgepp::Exception const exception);
void reportSentryEvent(sentry_level_t level, const char *message);
void reportSentryException(sentry_level_t level, const char *message, const char *exceptionType, const char *exception);
#endif //BRIDGE_GUI_SENTRYUTILS_H

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/>.
#include "Settings.h"
#include <bridgepp/BridgeUtils.h>
using namespace bridgepp;
namespace {
QString const settingsFileName = "bridge-gui.ini"; ///< The name of the settings file.
QString const keyUseSoftwareRenderer = "UseSoftwareRenderer"; ///< The key for storing the 'Use software rendering' setting.
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
Settings::Settings()
: settings_(QDir(userConfigDir()).absoluteFilePath("bridge-gui.ini"), QSettings::Format::IniFormat) {
}
//****************************************************************************************************************************************************
/// \return The value for the 'Use software renderer' setting.
//****************************************************************************************************************************************************
bool Settings::useSoftwareRenderer() const {
return settings_.value(keyUseSoftwareRenderer, onWindows()).toBool();
}
//****************************************************************************************************************************************************
/// \param[in] value The value for the 'Use software renderer' setting.
//****************************************************************************************************************************************************
void Settings::setUseSoftwareRenderer(bool value) {
settings_.setValue(keyUseSoftwareRenderer, value);
}

View File

@ -0,0 +1,47 @@
// 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_SETTINGS_H
#define BRIDGE_GUI_SETTINGS_H
//****************************************************************************************************************************************************
/// \brief Application settings class
//****************************************************************************************************************************************************
class Settings {
public: // member functions.
Settings(Settings const&) = delete; ///< Disabled copy-constructor.
Settings(Settings&&) = delete; ///< Disabled assignment copy-constructor.
~Settings() = default; ///< Destructor.
Settings& operator=(Settings const&) = delete; ///< Disabled assignment operator.
Settings& operator=(Settings&&) = delete; ///< Disabled move assignment operator.
bool useSoftwareRenderer() const; ///< Get the 'Use software renderer' settings value.
void setUseSoftwareRenderer(bool value); ///< Set the 'Use software renderer' settings value.
private: // member functions.
Settings(); ///< Default constructor.
private: // data members.
QSettings settings_; ///< The settings.
friend class AppController;
};
#endif //BRIDGE_GUI_SETTINGS_H

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