Compare commits

...

1340 Commits

Author SHA1 Message Date
3210709810 chore: Wakato Bridge 3.7.1 changelog. 2023-11-20 11:56:03 +01:00
8fd988d7c5 fix(GODT-3054): Only delete drafts after message has been Sent
When editing a draft created by Apple Mail on the web client and then
later sending the draft with Apple Mail, we need to delete the draft
ourselves, or it will remain in the Draft folder.

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

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

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

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

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

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

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

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

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

After the account is added, the sync is automatically triggered and the
account state fixes itself.
2023-10-09 10:23:58 +01:00
cb0935be96 fix(GODT-3001): Only create system labels during system label sync 2023-10-06 10:09:10 +01:00
441b388f62 fix(GODT-2966): Add more test regarding quoted/unquoted filename in attachment. 2023-10-05 12:27:43 +00:00
cdbcd30d15 fix(GODT-2490): fix sync progress not being reset when toggling split mode. 2023-10-05 11:37:01 +02:00
acc7ca8d4a feat(GODT-2996): set password fields to hidden when resetting the login form. 2023-10-04 15:57:36 +02:00
42e1dd4c41 chore: Vasco da Gama Bridge 3.6.0 changelog. 2023-10-03 16:44:24 +02:00
4cbd3ca832 feat(GODT-2990): change runner tags 2023-10-03 13:49:45 +00:00
de0b6c0737 feat(GODT-2835): Bump GPA adding support for AsyncAttachments for BugReport +... 2023-10-03 13:43:16 +00:00
1c344211d1 fix(GODT-2992): fix link in 'no account view' in main window after 2FA or TOTP are cancelled. 2023-10-03 10:49:24 +02:00
c11a87c16a fix(GODT-2515): customized notification of unavailable keychain on macOS. 2023-10-02 17:02:39 +02:00
3bf4282037 feat(GODT-2940): allow 3 attempts for mailbox password. 2023-10-02 16:50:07 +02:00
0c212fbef4 chore: Vasco da Gama Bridge 3.6.0 changelog. 2023-10-02 16:31:07 +02:00
48d1ca1e72 fix(GODT-2989): allow to send bug report when no account connected. 2023-10-02 13:34:40 +00:00
52addb2582 feat(GODT-2960): replaced the account list with a button and label when no account is configured. 2023-09-29 17:36:23 +02:00
742d9eeef3 feat(GODT-2960): added content in empty view when there is no account. 2023-09-29 17:36:23 +02:00
55a9d4973c fix(GODT-2988): fix setup wizard KB links. 2023-09-29 15:25:30 +02:00
8402657108 fix(GODT-2968): use proper base64 encoded string even for bad password test. 2023-09-29 08:35:41 +00:00
8a6f96f9f2 fix(GODT-2965): fix multipart/mixed testdata + structure parsing steps related to this. 2023-09-29 07:08:10 +00:00
56c53e9188 fix(GODT-2932): fix syncing not being reported in GUI. 2023-09-28 12:39:24 +02:00
bb67d95669 fix(GODT-2967): tray menu entries close the setup wizard when needed. 2023-09-27 18:23:02 +02:00
50acc0dcfb feat(GODT-2725): Implement receive message step with expected structure exposed. 2023-09-27 14:17:51 +00:00
e9c73c2d0d chore: Umshiang Bridge 3.5.1 changelog. 2023-09-27 15:34:50 +02:00
07c03c6920 fix(GODT-2963): Use multi error to report file removal errors
Do not abort removing files on first error. Collect errors and try to
remove as many as possible. This would cause some state files to not be
removed on windows.
2023-09-27 11:30:46 +02:00
f4958b9b53 fix(GODT-2956): Restore old deletion rules
When unlabeling a message from trash we have to check if this message is
present in another folder before perma-deleting.
2023-09-26 13:47:09 +02:00
76f2e7fdb9 fix(GODT-2951): Negative WaitGroup Counter
Do not defer call to `wg.Done()` in `job.onJobFinished`. If there is an
error it will also call `wg.Done()`.
2023-09-26 09:45:27 +02:00
c0992e8801 fix(GODT-2590): Fix send on closed channel
Ensure periodic user tasks are terminated before the other user
services. The panic triggered due to the fact that the telemetry service
was shutdown before this periodic task.
2023-09-26 09:20:01 +02:00
cf3abaa96f fix(GODT-2949): Fix close of close channel in event service
This issue is triggered due to the `Service.Close()` call after the
go-routine for the event service exists. It is possible that during this
period a recently added subscriber with `pendingOpAdd` gets cancelled
and closed.

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

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

The original behavior is still available under `DecryptAndBuildRFC822`.
2023-09-18 14:40:07 +02:00
fa794a982b feat(GODT-2597): Implement contact specific settings in integration tests. 2023-09-15 10:53:58 +00:00
cab32d5d5a chore: update changelog. 2023-09-13 10:26:24 +02:00
8e5a892c45 feat(GODT-2664): trigger QA installer. 2023-09-12 08:45:02 +00:00
50dc5c4085 chore: Umshiang Bridge 3.5.0 changelog. 2023-09-12 08:00:12 +02:00
3b58078595 fix(GODT-2929): Message dedup with different text transfer encoding
https://github.com/ProtonMail/gluon/pull/396
2023-09-11 15:44:11 +02:00
7689139cb3 fix(GODT-2909): Remove Timeout on event publish
While good intentioned, this change causes issues when the computer goes
to sleep and a user resumes after the timeout interval.
2023-09-11 10:03:00 +02:00
6269b1ab88 fix(GODT-2913): Reduce the number of configuration failure detected. 2023-09-08 11:53:24 +00:00
79524185a8 feat(GODT-2734): Add testing steps to modify account settings. 2023-09-04 16:48:59 +02:00
635b81314a test(GODT-2746): polish the test code. 2023-09-01 07:17:21 +02:00
4c76e35a2d test(GODT-2746): Added tags to certain test scenarios
- Added tags to certain test scenarios so they are not ran
on each MR, just on the nightly.

GODT-2746
2023-09-01 04:52:42 +00:00
b0ac20425e test(GODT-2746): Updated test scenario
GODT-2746
2023-09-01 04:52:42 +00:00
21a56a0725 test(GODT-2746): Add integration test for logged out user
GODT-2746
2023-09-01 04:52:42 +00:00
c57b52ef23 test(GODT-2746): Integration tests for reporting a problem
- Add function for checking header in form-data request

Bug reports are sent with multipart/form-data, and the function parses this,
then compares the needed field with the wanted
Also, added the step definition.

- Add functions for reporting a bug with changes

Be able to report a bug by changing the value of a single field, or multiple fields through a JSON format

- Add integration tests for reporting a problem
2023-09-01 04:52:42 +00:00
44e103edd5 chore: Umshiang Bridge 3.5.0 changelog. 2023-08-31 16:39:13 +02:00
970de4c205 fix(GODT-2828): Fix negative report time 2023-08-31 11:52:48 +00:00
a189c35899 fix(GODT-2828): Fix sync progress report after restart
Number of already synced messages also needs to be multiplied by the
number of sync stages to have accurate progress reporting.
2023-08-31 11:52:48 +00:00
c9323a40c8 fix(GODT-2867): do not crash on timeout or context cancel. 2023-08-31 11:14:44 +00:00
7a192d50db feat(GODT-2891): Allow message create & delete during sync
Incoming messages which arrive into labels we know during sync are now
presented to the IMAP clients.

We also allow messages to be deleted while syncing if deleted on other
clients.

Other operations such as moving, marking messages as read and label
operations need to be considered in a follow up patch as they are far
more complex.
2023-08-31 10:47:47 +02:00
a5a9bd762d feat(GODT-2848): Decouple IMAP service from Event Loop
The IMAP service no longer blocks the event loop from progressing. When
syncing the event loop continues as usual, allow other parts of the
bridge to continue updating.

Once the sync is complete, it resets the event id back to the last
handled id before sync started and then we play catch up.
2023-08-30 14:10:36 +02:00
dd7db00f74 chore: Umshiang Bridge 3.5.0 changelog. 2023-08-29 14:35:49 +02:00
a6c20f698c feat(GODT-2828): Increase sync progress report frequency
We now report sync progress after a batch completes each stage.
2023-08-29 11:50:50 +00:00
f7252ed40e fix(GODT-2693): Duplicate messages in sent folder
* Only call `RemoveOnFail` on error. It was also called on success.
* Add some logging to track send times.
* Fix non-deterministic hash from `rfc8222.GetMessageHash`.
    https://github.com/ProtonMail/gluon/pull/395
2023-08-29 11:50:50 +00:00
66c30716ea fix(GODT-2867): get attachment returns API error on network problem. 2023-08-29 13:20:45 +02:00
06d7fdf26f fix(GODT-2805): Ignore Contact Group Labels 2023-08-29 07:25:00 +00:00
8aee732fe8 fix(GODT-2866): Add 429/5xx Retry to Event Service 2023-08-29 07:25:00 +00:00
47233752f5 fix(GODT-2855): fix for text overlapping in settings view. 2023-08-28 13:00:33 +00:00
cb6e51531b test: Fix TestBridge_SyncWithOnGoingEvents
Checking for the initial status of the original check as this is not
required to verify the intended behavior. The original check is also
prone to issues related to event processing order.
2023-08-28 13:55:14 +02:00
0b9b886039 fix(GODT-2829): Fix new sync service bugs
* Fix wrong context use for message downloads
* Fix delete of sync data failing due ErrNotFound
* Pre-allocate attachment data buffer before download
* Fix calculation of progress if message count is higher than total
2023-08-28 13:37:55 +02:00
1fa0d77b10 chore: Add trace profiling option 2023-08-28 11:23:41 +02:00
efbe84964f feat(GODT-2829): Integrate new sync service
Update imap service to use the new sync service.

The new sync state is stored as simple file on disk to avoid contention
with concurrent vault writes.
2023-08-25 15:21:00 +02:00
aa77a67a1c fix(GODT-2829): Sync Service fixes
Fix tracking of child jobs. The build stage splits the incoming work
even further, but this was not reflected in the wait group counter. This
also fixes an issue where the cache was cleared to late.

Add more debug info for analysis.

Refactor sync state interface in order to have persistent sync rate.
2023-08-25 15:03:27 +02:00
78f7cbdc79 feat(GODT-2829): New Sync Service
Implementation of the new sync service that interleaves syncing jobs for
all active users.

It also includes improvements to the message downloader. The download
will now auto rate limit the parallel workers based on the server
responses.

Additionally each of the stages is now tested in isolation to ensure the
behavior matches the expectations.

Finally, this patch does not replace the existing IMAP sync. A follow up
patch is necessary to integrate the IMAP bits into the interfaces
required by these changes.
2023-08-23 16:12:19 +02:00
a731237701 test(GODT-2871): tests for new telemetry logic. 2023-08-22 11:15:47 +02:00
f557666b4d feat(GODT-2871): is telemetry enabled as service. 2023-08-21 14:08:47 +02:00
5f89e85139 chore: Under Bridge 3.5.0 changelog. 2023-08-17 12:39:31 +02:00
9fb922d18f chore: merge master to devel 2023-08-17 12:04:17 +02:00
884c6ed932 fix(GODT-2865): add error on failed unlock. 2023-08-17 08:19:25 +00:00
db77bd4983 test(GODT-2873): Wait for Gluon Watcher to finish
Avoids go-routine leak checker entry.

https://github.com/ProtonMail/gluon/pull/393
2023-08-17 06:31:25 +00:00
85bfcf7158 test(GODT-2744): Add integration tests for moving messages (with MOVE support) 2023-08-17 06:22:12 +00:00
09a5f8ac0f test(GODT-2872): Fix nightly job 2023-08-16 12:56:40 +00:00
81f81a63e8 test(GODT-2742): Add more integration tests regarding drafts 2023-08-16 10:56:23 +00:00
e724fafe2f chore: checkout features from trift to devel 2023-08-15 15:40:53 +02:00
cba73997cd fix(GODT-2802): Fix missing key pass input for keyring unlock 2023-08-14 13:09:22 +02:00
29f44fc312 fix(GODT-2814): Server Manager shutdown task cancel 2023-08-14 11:04:53 +02:00
41c125f65e feat(GODT-2803): Gluon IMAP State access
Update to latest Gluon to allow access to the database for bridge. The
cache is placed in a `SharedCache` type to ensure that we respect calls
to `Connector.Close`.

In theory, this should never trigger an error, but this way we can catch
it if it happens.

https://github.com/ProtonMail/gluon/pull/391
2023-08-14 09:51:49 +02:00
0a555bf767 chore: Trift Bridge 3.4.1 changelog. 2023-08-14 08:33:51 +02:00
24331f9715 fix(GODT-2803): Separate conditions to pause event loop for IMAP
Add two separate toggles to control event loop pausing. This is required
to prevent cases where the bridge requests the event loop to be paused
but a sync process completes and resumes the event loop.

For the loop to resume now both states need to be set to false. This
will be removed once GODT-2848 is implemented.
2023-08-11 14:32:18 +02:00
a5500629e5 feat(GODT-2803): Add Event Poll Waiter
Wait for the current event poll to finish publishing events after a
request to pause the event loop. This is required to change the gluon
cache directory.
2023-08-11 14:32:18 +02:00
a46533dcf2 feat(GODT-2787): Force Scrollview to top when re-opening questions set. 2023-08-11 08:43:23 +02:00
12183fbf05 feat(GODT-2787): Tweaking Bug Report form with last Review. 2023-08-11 05:39:34 +00:00
c90248920a ci(GODT-2717): Create a job that will run on schedule 2023-08-10 12:19:59 +00:00
ccb9b7f81c feat(GODT-2787): Fix vertical alignement on CategoryItem. 2023-08-10 08:43:37 +00:00
78c0651661 feat(GODT-2842): Implement Bug Report Fallback notification. 2023-08-10 09:46:12 +02:00
d72980e443 fix(GODT-2859): Fix scope of the function + rename properly. 2023-08-10 08:32:54 +02:00
b24937b666 fix(GODT-2859): Fix lint. 2023-08-10 08:30:46 +02:00
5ca9ec6674 fix(GODT-2859): Trigger user resync while updating from 3.4.0 to 3.4.1. 2023-08-10 08:28:39 +02:00
4ee6da4baa chore(GODT-2848): Simplify User Event Service
Rather than having the services subscribe to each individual event type,
have only one subscriber for the whole event object. This spreads the
logic to the service through the `EventHandler` type.

This is important for the next stage where the IMAP service will be
required to apply events from the past.
2023-08-09 16:11:09 +02:00
e8ce8942be feat(GODT-2788): Add preview to bug report validation. 2023-08-09 11:39:08 +00:00
9d0d3708af feat(GODT-2808): Apply comment from Bug Report content review. 2023-08-09 11:07:15 +00:00
4c908aac7c fix(GODT-2857): Do not check changed values in clear recent flag
https://github.com/ProtonMail/gluon/pull/390
2023-08-09 10:18:30 +02:00
6eb1878f66 fix(GODT-2827): Restore ticker to event poller 2023-08-09 10:17:41 +02:00
826dc2e5c3 test(GODT-2743): Sync high number of messages 2023-08-08 14:37:00 +00:00
e6ab874308 chore: Trift Bridge 3.4.1 changelog. 2023-08-08 15:06:16 +02:00
20b188368a feat(GODT-2799): SMTP service interacts directly with Server Manager
Bridge no longer needs to manually add and remove accounts from the
service.
2023-08-08 14:14:52 +02:00
ded4f370dc fix(GODT-2759): Use examine rather than select for fetching
When fetching messages in the debug mailbox state command, use read only
mode to avoid modifying the mailbox state.
2023-08-08 13:05:39 +02:00
519a6ecdb7 fix(GODT-2833): Migration of Message Flags
https://github.com/ProtonMail/gluon/pull/388
2023-08-08 12:38:54 +02:00
c35344d6f1 fix(GODT-2833): Fix migration of message flags
Migration of message flags was incomplete, leading to incorrect state
after migration.

https://github.com/ProtonMail/gluon/pull/388
2023-08-08 12:36:19 +02:00
a9865976a3 fix(GODT-2759): Use examine rather than select for fetching
When fetching messages in the debug mailbox state command, use read only
mode to avoid modifying the mailbox state.
2023-08-08 09:51:59 +02:00
9a96588afb feat(GODT-2814): Standalone Server Manager
Convert ServerManger into a standalone service so that it can become a
self contained module.
2023-08-07 16:47:41 +02:00
1f2c573803 feat(GODT-2782): Apply content modification after review. 2023-08-03 18:05:33 +02:00
cc33423c1f feat(GODT-2782): Add category name in decribe issue view + apply review comments. 2023-08-03 17:51:37 +02:00
1b95c290f1 feat(GODT-2808): Initial list of categories and questions. 2023-08-01 14:49:53 +00:00
9b88778c43 feat(GODT-2788): Add JSON validator file. 2023-08-01 14:49:53 +00:00
ae4705ba70 feat(GODT-2787): Replace the PathTracker by a more visual NavigationIndicator. 2023-08-01 14:49:53 +00:00
243ddf47ab feat(GODT-2816): Handle maxChar even for non mandatory field + make it customisable from JSON. 2023-08-01 14:49:53 +00:00
80d729e3e5 feat(GODT-2816): Wait until mandatory fields are filled then fill body and title. 2023-08-01 14:49:53 +00:00
3d64c5f894 feat(GODT-2794): Clear cached answers when report is sent. 2023-08-01 14:49:53 +00:00
c4bcc38c53 feat(GODT-2793): Feed the bug report body with the answered questions. 2023-08-01 14:49:53 +00:00
2c2f816f3a feat(GODT-2791): Add Unitary test. 2023-08-01 14:49:53 +00:00
86e115b2f3 feat(GODT-2791): Parse the Bug Report Flow description file and ensure forward compatibility (GODT-2789). 2023-08-01 14:49:53 +00:00
1a2783a63b feat(GODT-2821): Display questions in one page. 2023-08-01 14:49:53 +00:00
cbbab71f5c feat(GODT-2786): Init bug report flow description file. 2023-08-01 14:49:53 +00:00
80add80be2 feat(GODT-2792): Implement display of question set for bug report. 2023-08-01 14:49:53 +00:00
84adbbc461 chore: Trift Bridge 3.4.0 changelog. 2023-08-01 07:54:57 +02:00
75811d22e8 fix(GODT-2822): rename funcs 2023-07-31 15:21:53 +02:00
e26c7683d2 fix(GODT-2822): event loop behaviour on 429. 2023-07-31 13:52:39 +02:00
f0e2688a8e fix(GODT-2800): Ensure 429 does not cause bad event 2023-07-31 13:15:26 +02:00
06639ff6cd fix(GODT-2800): Handle subscribe followed by unsubscribe properly 2023-07-31 13:12:29 +02:00
716de7f45a test: Fix event registration in TestBridge_SyncWithOngoingEvents
We need to register the event subscriber earlier to avoid missing the
event later.
2023-07-31 12:36:06 +02:00
750de0cd31 test: TestBridge_SendAddTextBodyPartIfNotExists eventually fix
It may take a couple of fetch requests until the pending state updates
get applied.
2023-07-31 12:36:06 +02:00
823ca4d207 feat(GODT-2822): Integrate and activate all service
The bridge now runs on the new architecture.
2023-07-31 12:36:03 +02:00
a187747c7c feat(GODT-2802): IMAP Serivce
Handles all IMAP related tasks. Unlike the previous iteration, this new
service automatically adds and removes users from Gluon by interfacing
with server manager.
2023-07-31 11:06:47 +02:00
11ebb16933 fix(GODT-2822): Update smtp and events service to use ordered cancel 2023-07-31 11:06:47 +02:00
0048767022 fix(GODT-28022): Missing user identity service methods
Add methods to access user identity state from the outside.
2023-07-31 11:06:47 +02:00
c4b75c6f34 fix(GODT-2802): Ensure cpc reply does not block if never read 2023-07-31 11:06:43 +02:00
32448063dc test: Verify leaks at end of WithEnv
Helps track down the individual test leaks.
2023-07-28 15:00:23 +02:00
86bde91958 feat(GODT-2802): Ordered task cancellation
Add `OrderedCancelGroup` type to enforce the cancellation of go routines
in a reverse order. Additionally this type waits for the go-routine to
finish before proceeding to the next one.

We need this to guarantee that all the user services shut down properly
without deadlocking.
2023-07-28 14:50:42 +02:00
90c34406ba test: Fix deadlock in chToType
Make sure the go routine is cancelled if there is a new event, but no
one is reading it after a call to done.
2023-07-28 13:32:28 +02:00
d7b71aceda fix(GODT-2822): retry 429 for metadata and exponential cooldown GODT-2823. 2023-07-28 07:58:05 +02:00
25a787529b fix(GODT-2822): Retry on 429 during Message ID fetch 2023-07-27 17:59:13 +02:00
f82965b825 fix(GODT-2812): Fix rare sync deadlock
Copy data rather than hold onto the locks while sync is ongoing. The
data in question does not change while the sync is ongoing and holding
on to the locks during a very long sync can create a deadlock with
due to some IMAP operation that needs to acquire one of those locks with
write access.
2023-07-27 17:48:55 +02:00
f1cf4ee194 fix(GODT-2822): Sync Cache
When the sync fail, store the previously downloaded data in memory so
that on next retry we don't have to re-download everything.
2023-07-27 17:24:20 +02:00
5136919c36 fix(GODT-2822): Handle 429 during message download
When we run into 429 during a message download, do not cancel the whole
batch and switch to a sequential downloader to avoid API overload.
2023-07-27 16:55:18 +02:00
334a256638 fix(GODT-2802): Remove CPC request from Event Service
Prevents deadlocks if the service needs to be paused during an event
loop.
2023-07-26 10:03:48 +02:00
da528f2d9b feat(GODT-2801): Add Key Decryption helpers to identityservice.State 2023-07-25 09:59:26 +02:00
1b0f930471 fix(GODT-2802): Consolidate Address Mode 2023-07-25 09:15:52 +02:00
09c523e2d2 feat(GODT-2801): User Service Integration
Enable the User Event, User Identity and User SMTP service. The services
don't persist any data at the moment to avoid conflict with the existing
event loop.
2023-07-24 17:10:05 +02:00
0b35f41ffd feat(GODT-2799): Integrate SMTP service with User Identity Service 2023-07-24 17:10:05 +02:00
7be1a8ae8a feat(GODT-2801): Identity State Cloning & Auth Check
This patch moves the `user.User.CheckAuth` function into the `State`
type as it contains all the necessary information to perform this check.

A couple of more interfaces are added to abstract the retrieval of the
Bridge and the User's key passwords, as well as telemetry reports.

Finally, adds `State.OnAddressEvents` which other services should use to
update the state.
2023-07-24 17:10:05 +02:00
bdbf1bdd76 fix(GODT-2800): Pending Subscriptions Cleanup
Ensure pending subscriptions are cleaned up with `Service.Close()` or
we can leak go routines.
2023-07-24 17:10:05 +02:00
776976a8a2 feat(GODT-2801): Debug names for QueuedChannels
https://github.com/ProtonMail/gluon/pull/385
https://github.com/ProtonMail/go-proton-api/pull/90
2023-07-24 16:38:01 +02:00
040ddadb7a feat(GODT-2801): Identity Service
Identity Service contains all the information related to user state,
addresses and keys.

This patch also introduces the `State` type which can be used by other
services to maintain their own copy of this state to avoid lock
contention.

Finally, there are currently no external facing methods via a CPC
interface. Those will added as needed once the refactoring of the
architecture is complete.
2023-07-24 11:22:50 +02:00
11f6f84dd6 fix(GODT-2800): ChanneledSubcriber types 2023-07-21 15:18:49 +02:00
82efa16d65 feat(GODT-2800): User Event Service
This patch adds the User Event Service which is meant to replace the
current event polling flow.

Each user interested in receiving events should register a new
subscriber using the `Service.Subscribe` function and then react on
the incoming events.

The current patch does not hook this up Bridge user as there are no
existing consumers, but it does provide extensive testing for the
expected behavior.
2023-07-21 11:47:43 +02:00
110286b81c fix(GODT-2813): Write new vault to temporary file first
Do not immediately overwrite the old vault with new data. If something
goes wrong, we at least maintain on previously valid state.
2023-07-19 09:12:36 +02:00
bc66841cdc chore(GODT-2799): Move SMTP backend to SMTP service module 2023-07-19 09:12:33 +02:00
8d028966c7 chore(GODT-2799): Move Identifier interface to separate module
Required so we can move the smtp backend into the smtp service module.
2023-07-19 09:07:02 +02:00
d120bbeffc chore(GODT-2799): Separate account states for SMTP Backend
Rather than accessing the Bridge user list, each user register their
individual SMTP service with the server manager.

Note that some dependencies on the user data are hidden behind the
`UserInterface`. These will be removed in a future patch.
2023-07-19 09:07:02 +02:00
eda49483e2 chore: use qmlformat on qml files, and removed deprecated tests. 2023-07-18 18:12:29 +02:00
3ef526333a feat(GODT-2799): SMTP Service
Refactor code to isolate the SMTP functionality in a dedicated SMTP
service for each user as discussed in the Bridge Service Architecture
RFC.

Some shared types have been moved from `user` to `usertypes` so that
they can be shared with Service and User Code.

Finally due to lack of recursive imports, the user data SMTP needs
access to is hidden behind an interface until the User Identity service
is implemented.
2023-07-18 11:49:18 +02:00
f454f1a74f chore: Enable fuzzing in CI 2023-07-18 09:13:20 +02:00
e97a4d8847 chore: update Makefile to add fuzz
Signed-off-by: Arjun Singh <ajsinghyadav00@gmail.com>
2023-07-18 09:13:17 +02:00
cb8174dbfd test: oss-fuzz support for fuzzing
Signed-off-by: Arjun Singh <ajsinghyadav00@gmail.com>
2023-07-18 09:13:11 +02:00
cfca429067 fix(GODT-2807): fix issue where sessionID would not be removed from command-line on restart by bridge-gui. 2023-07-17 12:48:10 +02:00
1ffb9089ba fix(GODT-2687): Tabs after header field colon
https://github.com/ProtonMail/gluon/pull/384
2023-07-17 11:26:00 +02:00
c0fc23d7cd chore: merge "release/trift" to devel 2023-07-13 08:21:49 +02:00
4f8ecd598f chore: Trift Bridge 3.4.0 changelog. 2023-07-13 08:14:44 +02:00
65365281eb chore: merge release/stone with release/trift. 2023-07-13 07:51:19 +02:00
c05dfb36d3 chore: Stone Bridge 3.3.2 changelog. 2023-07-11 17:02:38 +02:00
42178806d1 Revert "feat(GODT-2749): manual windows-test."
This reverts commit 650158ea8a.

The issue has that was causing the problem has been addressed.
2023-07-11 16:07:11 +02:00
e34050282e fix(GODT-2764): Allow perma-delete for messages which still have labels
Rather than match the web client behavior (removing labels) when
messages are appended to trash, we simply omit the check to see if the
message in trash is present in other labels.

This will produce the same end result and will resolve the issue of
users not being able to fully delete their messages.
2023-07-11 16:04:36 +02:00
b2830b39e0 fix(GODT-2782): Filter all labels when doing perma delete check
Previously we were not filtering out labels we ignored from the
perma-delete check. The introduction of new system label types could
break this check leading to user never being able to perma-delete
messages.
2023-07-11 15:25:46 +02:00
7997ad2b93 fix(GODT-2782): Filter all labels when doing perma delete check
Previously we were not filtering out labels we ignored from the
perma-delete check. The introduction of new system label types could
break this check leading to user never being able to perma-delete
messages.
2023-07-11 15:17:25 +02:00
efb6ba0f1b fix(GODT-2782): Filter all labels when doing perma delete check
Previously we were not filtering out labels we ignored from the
perma-delete check. The introduction of new system label types could
break this check leading to user never being able to perma-delete
messages.
2023-07-11 13:41:21 +02:00
80194ad797 fix(GODT-2693): Fix message appearing twice after sent
Previous attempt at fixing a bug in the send recorder (GODT-2627)
introduced a new problem where if the same message is sent multiple
times with different recipients it is possible to trigger a case where
the incorrect wait channel is chosen. This in turn led to IMAP client
not recognizing that message has been successfully submitted. This
case is represented by
`TestSendHashed_SameMessageWIthDifferentToListShouldWaitSuccessfullyAfterSend`
but could potentially happen over time or due some other
concurrency/scheduling wake up order.

To prevent this from happening every send recorder request now requires
that the full list of addresses be presented. This is necessary for us
to locate the correct entry and its respective wait channel.

Finally each unique send recorder request is assigned an ID, in order
to ensure make sure that if we ever need to cancel a request,
we don't accidentally cancel a similar request if the original was
removed from scope due to expiration.
2023-07-11 07:33:32 +00:00
cda6b2a728 fix(GODT-2781): try to remove stale lock file before failing in checkSingleInstance. 2023-07-11 09:03:45 +02:00
44bde86fde fix(GODT-2780): fix 'QSystemTrayIcon::setVisible: No Icon set' warning in bridge-gui log on startup. 2023-07-10 10:47:10 +02:00
6cb52cacc9 fix(GODT-2778): fix login screen being disabled after an 'already logged in' error. 2023-07-10 09:44:18 +02:00
8248491833 chore: Merge branch 'release-notes' into devel 2023-07-07 16:00:56 +02:00
fbdede28f0 chore: fix typos found by codespell in Changelog.md 2023-07-07 15:31:34 +02:00
66bc911652 chore: fix typos found by codespell 2023-07-07 15:31:14 +02:00
c860741ffa doc: Update 3.3.1 release notes 2023-07-07 11:02:48 +02:00
80d743afec chore: Trift Bridge 3.4.0 changelog. 2023-07-06 16:15:55 +02:00
ac9857a965 chore: Merge remote-tracking branch 'origin/devel' into release/trift 2023-07-06 16:13:25 +02:00
0e9fd46a5c chore: Stone Bridge 3.3.1 changelog. 2023-07-06 15:50:18 +02:00
305d180a5f fix(GODT-2774): only check telemetry availability for the current user. 2023-07-06 15:28:24 +02:00
c4f80103b6 fix(GODT-2774): Add external context to telemetry tasks
This ensures they get cancelled if the parent context becomes invalid
2023-07-06 15:28:05 +02:00
0d57e3645a test: Add require.Eventually to TestBridge_UserAgentFromSMTPClient 2023-07-06 14:40:17 +02:00
0afdc31f96 test: Add smtp-send utility
Small program to simulate external SMTP send to Bridge.
2023-07-06 14:06:28 +02:00
91de6e001e fix(GODT-2763): Missing Answered flag on Sync and Message Create
Ensure we are using the same flag conversion code for all IMAP updates.
2023-07-06 14:02:05 +02:00
908ed3e723 feat(GODT-2759): Check for oprhan messages
Orphan messages are messages which are only in AllMail, AllDrafts or
AllSent.
2023-07-06 14:02:05 +02:00
7411073c08 feat(GODT-2759): Add prompt to download missing messages for analysis
This will download the missing messages into a temporary directory and
decrypt them along with the metadata so we can attempt analyze them once
submitted to see what is going wrong.
2023-07-06 14:02:05 +02:00
7d838375bb feat(GODT-2759): CLI debug commands
Add debug commands to CLI to diagnose potential bride problems.
Currently we only have a command which validates whether the state of
all the mailboxes reported by IMAP matches what is currently available
on the proton servers.
2023-07-06 14:02:05 +02:00
f545f30ec0 fix(GODT-2774): only check telemetry availability for the current user. 2023-07-06 13:38:06 +02:00
6579cdfc7f fix(GODT-2774): Add context to Authorize in gluon.Connector
Backported from: https://github.com/ProtonMail/gluon/pull/379
2023-07-06 13:28:58 +02:00
40c48ba804 fix(GODT-2774): Add external context to telemetry tasks
This ensures they get cancelled if the parent context becomes invalid
2023-07-06 13:09:35 +02:00
1c2cb4f439 chore: changelog fix. 2023-07-06 08:45:35 +02:00
650dac37a7 chore: changelog fix. 2023-07-05 12:14:49 +02:00
55275b23ee chore: Trift Bridge 3.4.0 changelog. 2023-07-05 12:13:20 +02:00
bce69e1a1b chore: Merge remote-tracking branch 'origin/devel' into release/trift 2023-07-05 12:07:37 +02:00
f1917ad0de fix(GODT-2758): Fix panic in SetFlagsOnMessages
https://github.com/ProtonMail/gluon/pull/377
2023-07-05 10:57:53 +02:00
300d243331 doc: Added 3.3.1 release notes 2023-07-04 20:35:54 +00:00
6ea6d54af6 chore: Trift Bridge 3.4.0 changelog. 2023-07-04 13:31:16 +02:00
c1b486a7eb chore: Merge remote-tracking branch 'origin/devel' into release/trift 2023-07-04 13:26:46 +02:00
454c9e1534 chore: Stone Bridge 3.3.1 changelog. 2023-07-04 13:09:51 +02:00
eaa673c4e4 fix(GODT-2708): fix dimensions event format + handling of ReportClicked event. 2023-07-04 13:04:30 +02:00
9c389e3007 fix(GODT-2708): fix dimensions event format + handling of ReportClicked event. 2023-07-04 09:59:43 +00:00
7e9a5934c5 feat(GODT-2707): set bridge-gui default log level to 'debug'.
(cherry picked from commit cc1d0e803b)
2023-07-04 11:14:40 +02:00
cc17366c1c fix(GODT-2578): Refresh literals appended to Sent folder
Whenever a message gets moved to the sent folder we should retrieve the
new literal in order to guarantee that, if another client modifies and
sends the message, we always see the latest version of the message and
not a previous state stored in the Gluon cache.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Finally, remove unnecessary attributes.
2023-03-16 11:48:41 +01:00
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
b4ea667858 doc: Added 3.0.20 release notes 2023-03-09 15:47:32 +00:00
8f45fe823a doc: Add 3.0.20 release notes 2023-03-09 15:46:54 +00: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 4153a69253)
2023-02-28 09:05:50 +01:00
3b1c3b9d44 doc: Add 3.0.19 release 2023-02-28 07:21:55 +00:00
60256fd076 doc: Add 3.0.19 release 2023-02-28 07:21:01 +00: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
9c1b9e8df2 doc: Add 3.0.18 and remove non-beta 3.0.17 2023-02-24 08:36:02 +00: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
fb44de4f18 doc: Add 3.0.18 release notes 2023-02-23 16:16:26 +00: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
c821d02f67 doc: Reduce the technical terms and do not show v3 development related changes for users who have only seen v2 2023-02-21 07:01:32 +00: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
ec2a4f9111 doc: release notes v3 live 2023-02-20 05:59:57 +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
968a01053f chore: release notes 2023-02-17 12:02:44 +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
6f914a4973 chore: early 3.0.15 release notes 2023-02-14 12:24:50 +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
76bdc21fef doc: Add 3.0.14 notes 2023-02-09 09:56:45 +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
dd29ff4731 doc: Remove details 2023-02-01 11:31:57 +00:00
8b94a28e00 doc: 3.0.12 notes 2023-02-01 12:28:08 +01:00
60100ad7f0 chore: update release notes wording. 2023-02-01 11:48:21 +01:00
62a50fd7fc chore: release notes 2023-02-01 11:21:12 +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
777ad369a2 Other: Bridge Perth Narrows v3.0.10, scope change 2023-01-11 10:24:25 +01:00
715efaa087 Revert "GODT-2229: Allow changing cache folder to a non-empty folder."
This reverts commit b19e16e4b8.
2023-01-11 10:19:38 +01:00
606a8f134d Revert "Other: Update Gluon"
This reverts commit 761b98f02f.
2023-01-11 10:19:16 +01:00
84e92ca69f Other: Bridge Perth Narrows v3.0.10 2023-01-11 09:42:31 +01:00
0f0f8b3461 GODT-2205: use lock file in bridge-gui to detect orphan bridge. 2023-01-11 08:22:46 +01:00
761b98f02f Other: Update Gluon
Includes fix for not panicking on out of or UID insertion.
2023-01-10 17:54:53 +01:00
b19e16e4b8 GODT-2229: Allow changing cache folder to a non-empty folder. 2023-01-10 16:40:52 +00:00
407c9fe1a6 GODT-2181: Empty but not nil address from API 2023-01-10 14:54:29 +00:00
0b61f8f146 GODT-2242: Bump GPA - Don't send any 2fa information if not needed. 2023-01-10 13:23:17 +00:00
06eee89479 GODT-1817: Port old user feature tests 2023-01-10 11:47:05 +01:00
e3a43e4ca8 GODT-2179: added handler for exceptions in QML backend methods.
GODT-2179: added custom QApplication class to handle exceptions.
GODT-2179: wired sentry report in AppController error handler.
2023-01-10 08:33:42 +01:00
f876ffab52 GODT-1817: Add missing IMAP auth tests with disabled & secondary accounts 2023-01-09 15:10:39 +00:00
0dcd4ca133 GODT-1817: Restore missing SMTP feature tests
Requires update to GPA to set disabled state on addresses.
2023-01-09 15:10:39 +00:00
2562d1e77d GODT-1817: Do not allow authentication of disabled accounts 2023-01-09 15:10:39 +00:00
e1531c200c GODT-1817: Delete old smtp send feature tests
All these tests have already been ported.
2023-01-09 15:10:39 +00:00
c09bc742d8 GODT-1817: Delete on update and spam test features
These are handled by Gluon and the update_spam feature is not compatible
with the current architecture. Do note messages that IMAP client move
messages to the Folder with the \Junk attribute, which is correctly
mapped into Gluon.
2023-01-09 15:10:39 +00:00
29e8d07693 GODT-1817: Delete old tests that are already ported or handled in Gluon 2023-01-09 15:10:39 +00:00
4fd4e8a16e GODT-2181: Add env proxy support for integration tests. 2023-01-09 13:26:08 +01:00
30d627c2be Other: reorganised QMLBackend class code. 2023-01-09 10:15:21 +01:00
9390cb64b4 GODT-1817: Restore move related feature tests
Gluon updated to latest dev commit, required for feature. Checks from
move_local_folder.feature are implemented in Gluon.
2023-01-06 10:58:07 +01:00
d720feaa6d Other: Flag messages imported into "Sent" mailbox as Sent 2023-01-06 10:58:07 +01:00
9f7cda3b69 Other: Fix testCtx.getMBoxID()
Ensure we always translate the labels to their full name so they match
properly on all commands.
2023-01-06 10:58:07 +01:00
878f67a051 GODT-1817: Delete old fetch test
These are tested in Gluon instead.
2023-01-06 10:58:07 +01:00
7fb8550c97 GODT-1817: Port missing import feature tests 2023-01-06 10:58:07 +01:00
700836aea0 Other: fIxed GUI Tester to comply with latest gRPC changes. 2023-01-06 08:24:34 +01:00
16aaa1b050 GODT-2010: add Cocoa app delegate handler for second application instance. 2023-01-05 17:12:02 +01:00
8790d3cfcf Other: C++ Code reformat. 2023-01-05 08:37:38 +01:00
bb07138fb0 GODT-2236: add log entry when SMTP / IMAP serve method fails. 2023-01-04 16:45:34 +01:00
37c650e490 GODT-1817: Remove deleted check from copy.feature message tests
This check is only possible if the messages are imported via imap APPEND
commands and not through the API client. The latter has no way to
express this state.
2023-01-04 13:37:28 +01:00
272e3895fd GODT-1817: Restore old date message feature test + fix
This patch also fixes the message builder to not override other headers
that already exist to avoid overriding sanitized header entries.
2023-01-04 13:37:28 +01:00
6e7f374b0d GODT-1817: Remove old Drafts and Delete tests.
Test have been ported and other features are validated in Gluon.
2023-01-04 13:37:28 +01:00
3743e45566 GODT-2221: Set DOH off by default. 2023-01-04 12:08:06 +00:00
b10e8abde0 GODT-2234: added command-line switch to force Qt to use software rendering for QML. 2023-01-03 17:54:57 +01:00
5dab4422e9 Other: added C/C++ header template file (*.h.in) type to missing_license.sh script. 2023-01-03 17:42:53 +01:00
82b6037a00 GODT-1817: Add create check to validate mailbox creation
Only allow mailboxes with the "Folder" or "Label/" prefix.
2023-01-03 10:19:11 +01:00
1bdb8b2724 GODT-1817: Add tests skips reporter checks to feature tests
Some tests on failure will produce sentry reports. Add a way to skip
the check to see if any reports are produce when we know they will be
triggered.
2023-01-03 10:19:05 +01:00
8c905e4f42 GODT-1817: Port missing IMAP create feature test 2023-01-02 13:37:40 +01:00
e9e59a2704 GODT-1817: Port over missing IMAP copy feature test 2023-01-02 13:37:40 +01:00
e3a1482b8f Other: Fix double close on event channels 2023-01-02 13:37:40 +01:00
9539b24d64 GODT-1817: Delete old feature test files
All these feature test have either been ported or are already tested in
Gluon.
2023-01-02 13:37:40 +01:00
87caeef0af GODT-1817: Delete unnecessary IDLE tests
Gluon tests already cover this.
2023-01-02 13:37:40 +01:00
757e8a02ec GODT-2233: Fix sub folder creation bug
Sub folders with more than 2 levels of depth (e.g.: Folders/first/second)
could not be created since we did not update the known label list we use
to validate the request.
2023-01-02 11:41:49 +01:00
6d0a128111 Other: Update copyright year 2023-01-02 11:09:11 +01:00
28b36d379b GODT-1817: Update IMAP commands to push errors to error stack 2022-12-21 14:29:42 +01:00
038b5d1437 GODT-2222: Dot not error on unknown Address Events
Prevent infinite error loop in event parsing by not returning errors if
we already have or do not have a given address. This occurs since we
sync the latest state at Bridge startup but still receive the events
which contain these changes later.
2022-12-21 10:16:02 +01:00
038e1794eb GODT-2218: Fix invalid UID ranges
Fix applied in Gluon
2022-12-21 09:15:54 +01:00
663b2cd888 Other: Bridge Perth Narrows v3.0.9 2022-12-20 14:18:50 +01:00
23f14e5799 Other: Bridge Perth Narrows v3.0.9 2022-12-20 14:05:38 +01:00
55572acdc8 Other: Fix TOTP login (bump go-proton-api) 2022-12-20 13:06:30 +01:00
08125e9281 Merge branch 'release/perth_narrows' into devel 2022-12-20 09:00:26 +01:00
e8ee9de5b9 Other: Bridge Perth Narrows v3.0.8 2022-12-19 15:53:49 +01:00
91aea0e968 Other: Update go-proton-api to v0.2.2
Fixes crash on invalid response object access.
2022-12-19 15:25:18 +01:00
4cba009ac8 GODT-2188: Do not fail append with invalid mime-type
Requires gluon update where the fix was applied.

Disable TestBridge_Sync_BadMessage as it is no longer valid with the
latest Gluon fixes. Traked as GODT-2215.
2022-12-19 15:24:35 +01:00
47ea4b226a Other: Add sentry reports for event processing failures 2022-12-19 14:38:01 +01:00
00059e6754 Other: Do not fail on label events
Do not treat unknown label creation/deletion/update or deletion in Bridge
as an error as the Gluon cache still needs to receive these events to
correct its internal state.
2022-12-19 14:24:12 +01:00
e4b81063cb GODT-2213: Don't unnecessarily enable/disable autostart 2022-12-19 08:29:57 +00:00
3499fbd758 Other: Do not decode message body during send record hashing
When calculating the hash for the body to match against sent email to
avoid duplicate addition to the sent folder, do not decode the actual
contents of the body.

It is possible that certain attachments are not formed correctly but
can still accepted by the backend. Trimming spaces and \r characters is
enough to hash the message and match it later on.

This also speeds the process up as we no longer have to perform
encoding conversions.
2022-12-16 14:26:59 +01:00
4b3d4690e8 GODT-2196: Do not generate message updates for unknown labels
During sync a user may continue to perform operations on the server it
is possible we run into a message which has a labelID we are not aware
of. To counter this we issue `CreateMessage` updates with
`IgnoreUnknownMailboxIDs` set to true. Eventually, after sync the state
will resolve itself with events.
2022-12-15 09:37:22 +01:00
48480bc839 Merge branch 'release/perth_narrows' into devel 2022-12-14 13:56:00 +01:00
031ed9c203 Other: Update Gluon to latest to revert mailbox subscription bug
Includes fix to remove incomplete feature from Gluon related to mailbox
subscription.
2022-12-14 13:25:56 +01:00
f551732a17 Other: Add SMTP debug dump to disk 2022-12-14 10:27:12 +00:00
7a814faed2 Other: Update release notes. 2022-12-14 11:08:34 +01:00
792317e945 Other: Prevent double login 2022-12-14 10:15:40 +01:00
9c10e06aac Other: Improve migration logging, prefer username over primary address 2022-12-14 08:16:29 +01:00
c39108043b Merge branch 'release/perth_narrows' into devel (3.0.7) 2022-12-13 19:37:12 +01:00
30bf941979 Other: Bridge Perth Narrows v3.0.7 2022-12-13 19:21:07 +01:00
55ee6a9d13 Other: default UIDVALIDITY 2022-12-13 16:16:54 +01:00
2b25fe1fa4 GODT-2173: fix: Migrate Bridge password from v2.X. 2022-12-13 14:25:39 +00:00
57d563d488 GODT-2173: fix: do not migrate keychain once migrated 2022-12-13 14:25:39 +00:00
2ca9ca3cb6 GODT-2181(test): Linter fixes 2022-12-13 15:05:09 +01:00
ebb04d8a14 GODT-2207: Fix encoding of non utf7 mailbox names
Fix was applied in Gluon. Bumping Gluon to match that version.

Fixes: #318
2022-12-13 13:38:04 +01:00
3c24ac26d5 Other: Sneaky worker count bump (*2 -> *4) 2022-12-13 10:35:55 +01:00
87ce5a6d82 GODT-2181(test): Use [user:NAME] for more test user names 2022-12-13 10:28:59 +01:00
9623e2de6f GODT-2181(test): Basic ATLAS test in test context 2022-12-13 10:28:59 +01:00
b9b4c1c38d GODT-2181(test): Use [user:NAME] for test user name 2022-12-13 10:28:59 +01:00
688cb30d4a GODT-2181(test): use [domain] for test server domain 2022-12-13 10:28:59 +01:00
1aca2cde71 GODT-2181(test): Refactor integration test setup a bit 2022-12-13 10:28:59 +01:00
49fa451cc3 Other(test): Prefer native API revoke rather than fake server method 2022-12-12 10:47:06 +01:00
5f1389f824 Other: Sneaky worker count bump (*2 -> *4) 2022-12-07 19:37:31 +01:00
a90693e488 GODT-2190: Unify crashpad_handler for darwin. 2022-12-07 13:05:33 +00:00
ebeec056cd Other(test): Add test that we skip and report bad messages during sync 2022-12-07 12:10:02 +01:00
49d65292c0 Other: catalina build.
Other: fix intel build of bridge-gui.
2022-12-07 09:56:21 +01:00
6c30a04ac0 Merge branch 'release/perth_narrows' into devel 2022-12-07 09:26:59 +01:00
f070314524 Other: Bridge Perth Narrows v3.0.6 2022-12-07 07:38:47 +01:00
75c88eaa55 GODT-2187: Handle unbuildable messages in event loop 2022-12-06 19:27:55 +01:00
bd6ae2ac2b GODT-2187: Placeholder for unbuildable messages 2022-12-06 16:35:32 +01:00
58d04f9693 GODT-2187: Skip messages during sync that fail to build/parse 2022-12-06 14:07:13 +00:00
01c12655b8 Other: Update Gluon to latest version
Fixes: #316
2022-12-06 11:49:39 +01:00
d4198737a6 Other: Bridge Perth Narrows v3.0.5 2022-12-05 15:42:49 +01:00
04881b9b78 GODT-2178: Bump go-proton-api to fix drafts 2022-12-05 15:14:30 +01:00
990b8cda96 GODT-2180: Allow login with FIDO2
The API docs didn't specify what the "integer" meant. Turns out it's a
bitfield; we can't compare with equality.
2022-12-05 14:22:38 +01:00
27889b8085 Other: Bridge Perth Narrows v3.0.4 2022-12-02 15:42:11 +01:00
2cd7735468 Other: Do not list \Deleted flag for All Mail 2022-12-02 14:59:52 +01:00
8990f2d1d6 Other: Ensure expunge feature test pushes to error stack 2022-12-02 14:59:52 +01:00
7bc608ce6c GODT-2170: Use client-side draft update in integration tests 2022-12-02 13:27:19 +00:00
01c7daaba7 Other: Update gluon to latest version 2022-12-02 13:27:19 +00:00
8408a5fdc0 GODT-2170: Improving test server behaviour. 2022-12-02 13:27:19 +00:00
828fe0e86e GODT-2170: Update draft event means delete old and create new message. 2022-12-02 13:27:19 +00:00
5c3179df48 GODT-2170: User create draft rounte: first steps. 2022-12-02 13:27:19 +00:00
618cb27ac1 Other: Disable perma-delete for expunge on Spam folder 2022-12-02 13:43:53 +01:00
4003e0a2ab GODT-2042: fix setup guide not always showing on first login. 2022-12-02 11:36:31 +01:00
e87db5b2ab Other: updated GUI tester for new gRPC calls. 2022-12-01 15:40:20 +01:00
5b9c28e6f0 GODT-1847: add option to export TLS Certificates in GUI. 2022-12-01 13:08:04 +01:00
4375d77a98 GODT-2152: Sign-in dialog validate email and password only when button is pressed. 2022-12-01 07:54:21 +00:00
83a569b366 Other: Bridge Perth Narrows v3.0.3 2022-12-01 08:42:24 +01:00
842c9c8ecd GODT-1556: Add unit test for in-reply-to header without references. 2022-12-01 08:27:10 +01:00
70244071ea Other: Bump go-proton-api to v0.1.4 2022-12-01 08:19:16 +01:00
f3cc19b09c GODT-2150: fixed initial implementation that filtered --no-window in gui instead of bridge. 2022-11-30 19:05:43 +01:00
6b8faf0ecf GODT-2167: bind sign-in buttons availability to loading state. 2022-11-30 16:41:43 +00:00
71ad1e9939 Other: Only send to necessary update channel 2022-11-30 13:52:42 +00:00
f355cb4d38 GODT-1804: Add parsing ics attachment test. 2022-11-30 12:32:05 +01:00
5ae8d274c0 Other: fix Warning introduced by connecting check timer. 2022-11-30 08:14:30 +01:00
6402894096 Other: Bump Gluon to lastet dev version 2022-11-29 16:05:47 +00:00
2bb0008eb4 Other: fix changelog [skip-ci] 2022-11-29 17:05:05 +01:00
906141e2ae Other: bridge Perth Narrows 3.0.2 2022-11-29 16:56:58 +01:00
6ac8a4c0bc GODT-2160: Ensure we can safely move cache file
It's currently impossible to wait until all SQLite write finish to disk.
This is not guaranteed when closing the ent DB client.

The existing code to move the cache handles the case where the
new location is on a new drive. However, due to the above issue this can
now lead to database corruption.

To avoid database corruption we now use the `os.Rename` function and
prevent moving the cache between drives until a better solution can be
implemented.
2022-11-29 16:33:53 +01:00
0827d81617 Other: Bump gluon version to drop non-UTF-8 commands 2022-11-29 16:20:16 +01:00
e71e56f7fe Other: Ensure context is string in sentry reports 2022-11-29 14:58:29 +00:00
7510ba2541 Other: include sentry dll for Windows deploy. 2022-11-29 14:26:09 +00:00
a78b2dee46 Other: GUI Tester supports the 3 states of user (Signed out/Locked/Connected). 2022-11-29 13:35:47 +00:00
2cce1c7b2a Other: setMailServerSettings is async as it should. 2022-11-29 13:31:12 +01:00
7533dc952d Other: update gui tester to support latest changes in gRPC implementation. 2022-11-29 11:57:07 +01:00
3408e8427d Other: Fix Changelog lint. 2022-11-29 11:29:00 +01:00
5795a6a2f0 Other: Bridge Perth Narrows v3.0.2 2022-11-29 10:30:30 +01:00
9f64e8a6fa Other: Wipe vault properly on factory reset
Deleting the file isn't enough because it's still held in memory
and is written back to disk on the next write (SetLastVersion during
bridge teardown).
2022-11-29 09:59:30 +01:00
f176174fca Other: Remove sentry test code 2022-11-29 09:41:25 +01:00
2747f3b492 GODT-2157: Add Sentry to Bridge-Gui 2022-11-29 08:05:48 +00:00
1c374b59d3 GODT-2160: Prevent double closing of bridge if restart fails
Set imapServer instance to nil once the server is no longer running to
prevent multiple calls to close on shutdown.
2022-11-29 08:38:21 +01:00
ae7ae2886f GODT-2041: Crash after factory reset
I forgot to remove the user from the users map during factory reset.
This meant the (deleted) would attempt to be closed during teardown.
2022-11-28 19:58:10 +01:00
b902f1490f GODT-2114: sanitize attachment disposition. 2022-11-28 18:20:48 +00:00
8e5040a357 GODT-1910: fix GUI not being notified of SMTP SSL being turned on by ConfigureAppleMail. 2022-11-28 17:55:55 +01:00
9881011043 GODT-1910: Fix save button state not being updated after being clicked once. 2022-11-28 17:16:16 +01:00
d4b8f3e1c2 GODT-2153: use file socket for bridge gRPC on linux & macOS.
Other: fix integration tests.
2022-11-28 16:51:13 +01:00
b7fff07197 GODT-2159: improve 429 retry. 2022-11-28 15:56:52 +01:00
7fa81a7aca GODT-2150: Do not forward --no-window flag. 2022-11-25 14:55:51 +00:00
e0d1e67d4b Other: Upgrade Gluon to v0.14.1 2022-11-25 15:23:25 +01:00
8049c47aa8 GODT-1989: Handle Move with Append and Expunge
Resurrect Bridge feature test for move with append. Only for drafts, as
it has been established that moving/appending to All Mail is no longer
valid.
2022-11-25 12:59:27 +01:00
37a46465ba GODT-2154: Allow noninteractive mode from launcher. 2022-11-25 09:47:55 +00:00
ece6d7b2d7 Merge remote-tracking branch 'origin/v3' into devel 2022-11-25 10:04:16 +01:00
3d4c73f8af Other: Bump Gluon to v0.14.0 2022-11-25 08:29:35 +01:00
ed36273755 Other: Bridge Perth Narrows v3.0.1 2022-11-25 07:50:48 +01:00
b7be599769 GODT-2151: Sync backwards to please product people 2022-11-24 13:04:00 +01:00
c3484dc062 GODT-2149: Sort logs by timestamp when clearing 2022-11-23 16:12:46 +00:00
578a12529c GODT-2137: set sentry sync transport. 2022-11-23 16:41:58 +01:00
e601245f01 Other(chore): Bump major version to v3 2022-11-23 16:08:27 +01:00
ad1fb47b0d Other: Switch from liteapi to go-proton-api 2022-11-23 15:17:56 +01:00
e852c5a22f Other: Bridge Tacoma v2.5.14 2022-11-23 14:11:54 +01:00
61287d05bf Other: Retry sync after cooldown if it fails 2022-11-23 12:47:16 +01:00
555453bc1a GODT-2142: Also permit split by comma in References header 2022-11-22 19:08:03 +01:00
cb81175fa0 Other: missing import 2022-11-22 17:18:30 +01:00
627bf25791 Other: Bridge Tacoma 2.5.13 2022-11-22 16:49:05 +01:00
8d1015caba GODT-2104: reverted go exe linker flags windowsgui for launcher. 2022-11-22 15:15:35 +00:00
f2db2b9b1d GODT-2085: Use time.Since, structured logging 2022-11-22 16:14:25 +01:00
4b6d0d035e GODT-2085: Ensure minimum sync worker count
Make sure that we are at least using 16 workers for sync, otherwise
multiply the current sync worker count by 2.

Finally, this patch also logs the duration of the time it takes to
transfer all the messages from the server.
2022-11-22 15:02:52 +00:00
57e9310510 Other: Use API call rather than server-specific method in test code 2022-11-22 16:01:01 +01:00
fd09769ccc GODT-2127: Bump gluon to fix flags store 2022-11-22 15:59:59 +01:00
029c798eff Other: Switch to mail-api.proton.me 2022-11-22 15:59:36 +01:00
b81fa5ed39 GODT-2139: Validate key pass during login 2022-11-22 14:51:31 +00:00
1375f42869 Other: Clean up gRPC shutdown goroutine 2022-11-22 14:20:30 +00:00
7cb9d62f0c Other: Don't forward stdin/stdout/stderr 2022-11-22 14:20:30 +00:00
6bd8c6ceb6 Other: Stop gRPC server on crash 2022-11-22 14:20:30 +00:00
f954f89747 GODT-2111: Fix restart on macOS 2022-11-22 14:20:30 +00:00
82788e39f0 GODT-2111: Use cmd.Start for restart command for windows as well. 2022-11-22 14:20:30 +00:00
0df4f41269 Other: Remove unused SyncBuffers setting 2022-11-22 13:59:06 +01:00
a2ab5df7ce GODT-2085: Revise sync algorithm
Revise syncing work distribution. Sync time can be reduced by up to 50%.

Rework the sync so that it pipelines better with bigger batch counts at
each stage. We now use 3 separate stages: Download, Updates and Sync.

The Download stage downloads messages in maxBatchSize intervals using
1.5x syncWorkers. Once the current batch has finished downloading it's
forwarded to the Updates stage and we proceed to download the next
batch.

The Update stage converts everything into gluon updates and prepares a
collection of noops that the sync stage can wait on for termination.

Finally the sync stage waits until the updates have been applied in
Gluon so that the vault information can be updated. We allow up to 4
pending wait operations to be queued currently to not block the
pipeline.
2022-11-22 12:32:47 +00:00
1395f1c990 GODT-2138: fix UserDataDir for Windows and mac. 2022-11-22 12:06:21 +00:00
c473e987f4 GODT-2134: fix dock icon on macOS when launched with '--no-window'. 2022-11-22 11:49:52 +00:00
b97ffc16ea Other: Don't migrate if prefs doesn't exist 2022-11-22 10:15:39 +00:00
355ae5f046 Other: bridge Tacoma 2.5.12 2022-11-22 08:43:14 +01:00
1abda7555d GODT-2131: if refresh token is revoked, user gets signed out. 2022-11-22 07:23:07 +00:00
520361f7f3 GODT-2111: properly name bridge-gui lock 2022-11-21 20:33:00 +01:00
c8c9e911f6 Other: chores: removed comments in bridge-gui that were used for the transition Go -> C++ of the Qt code. 2022-11-21 17:31:22 +00:00
eb62056755 GODT-2119: Only show supported label IDs to clients 2022-11-21 17:15:51 +00:00
294d1edfee GODT-2120: Encrypt gluon store with gzip 2022-11-21 17:37:56 +01:00
febab47124 Other: Don't show corrupt vault as "no keychain" error 2022-11-21 17:15:28 +01:00
8160fe5448 Other: Update liteapi to v0.43.0 2022-11-21 13:16:50 +01:00
9169499087 Other: bridge Tacoma v2.5.11 2022-11-21 13:06:44 +01:00
81facfd05f GODT-2111: Give the child process its own group ID 2022-11-21 11:32:26 +01:00
054d9b3f09 GODT-2111: Properly reset crash counter + remove additional Quit call. 2022-11-21 11:32:25 +01:00
a95eb759ca GODT-1910: use a single view for IMAP & SMTP SSL options. 2022-11-21 10:22:03 +00:00
d1f140ebcb Other: More descriptive event poll name 2022-11-21 10:20:28 +00:00
721cd9f319 GODT-2002: Wait for API events to be applied after send
When we send a message, the send recorder records the sent message.
When the client then appends an identical message to the sent folder,
the deduplication works and instead returns the message ID of the
existing proton message, rather than creating a new message. Gluon is
expected to notice that it already has this message ID and perform
some deduplication stuff internally.

However, it can happen that gluon doesn't yet have this message ID,
because we haven't yet received the "Message Created" event from the
API. To prevent this, we poll the events after send and wait for all
new events to be applied.

There's still a chance that the event wasn't generated yet on the API
side. Not sure what we can do about this.
2022-11-21 10:20:28 +00:00
e6a780ebd4 Other: Don't encrypt vault version 2022-11-21 09:58:56 +00:00
9868fae735 GODT-2105: Ensure ClientVersion is set in bug report request 2022-11-21 09:57:09 +00:00
c22037462e Other: Only update event ID in vault once all gluon updates were applied 2022-11-21 09:12:52 +00:00
1f0312573a GODT-1846: remove restart cues, implement restart-less behaviour.
Other: fixed case issue in SSL member function names.
Other: removed 'restart' mention in SMTP and IMAP SSL settings.
GODT-1846: modified gRPC server to introduce ConnectionMode settings.
GODT-1846: implemented connection mode handling in bridge-gui.
GODT-1846: implemented error reporting in bridge-gui for connection mode.
Other: gathered all IMAP/SMTP server settings.
GODT-1846: wired IMAP/SMTP port change errors.
Other: Renamed some error events and signals.
Other: Fixed crash in IMAP restart when not started.
Other: dismiss port error notifications before changing ports.
Other: misc. fixes.
2022-11-21 09:08:52 +00:00
46c0463e43 GODT-2107: Update user list after session revoke 2022-11-21 09:05:59 +00:00
e05b99a0f1 Other(test): Remove unneeded reporter expectations
Gluon used to have a bug where it would unnecessarily call the
reporter's ReportMessageWithContext method whenever an IMAP client would
drop unexpectedly. After fixing the bug, we can remove these gomock
EXPECT.AnyTimes() calls.
2022-11-21 09:05:11 +00:00
48dfdabaf4 GODT-1975: Migrate keychain secrets 2022-11-21 09:00:51 +00:00
7ed8d76d84 GODT-1976: Migrate app settings from prefs.json 2022-11-21 09:00:51 +00:00
9e6cbcb35e GODT-2040: Bump UID validity when clearing sync status 2022-11-20 21:48:22 +01:00
8c2096e813 Other: Bump liteapi to fix update merging algorithm 2022-11-20 12:10:48 +01:00
2972e1273f GODT-2045: Timeouts should be considered network issues 2022-11-20 10:07:33 +00:00
0ce0e4765b GODT-2122: Handle check for updates failure 2022-11-20 10:04:16 +00:00
1517dd81e6 Other: set proc.info logs verbosity lower in Launcher 2022-11-20 09:44:31 +01:00
b517e3cd5b Other(test): Set log level in feature tests 2022-11-19 16:16:54 +01:00
a240c4531a Other: Bump max update and batch size to 256 MiB / 256 respectively 2022-11-19 16:06:25 +01:00
7d84ab37f6 GODT-2100: Load users in parallel at startup 2022-11-19 12:13:33 +01:00
6bdcdf7fd2 GODT-2033: Only set user agent from IMAP ID if not empty 2022-11-18 19:55:23 +01:00
5e8e92b765 Other: fix build on other OS than linux... Ooops 2022-11-18 19:27:58 +01:00
2006984b47 Other: Bridge Tacoma v2.5.10 2022-11-18 19:18:55 +01:00
aaa8a35ea8 Other: set gui logs with other logs in user data directory. 2022-11-18 18:45:45 +01:00
204e320df4 GODT-2108: implement C++ Focus gRPC service client in bridge-gui. 2022-11-18 17:13:39 +00:00
24a0ed41b9 Other(test): Disable broken test
Bumping liteapi version made the test server stricter (more conformant
with the real server). As a result, one of the tests broke; fix should
be in GODT-1989, which is not ready yet.
2022-11-18 17:43:27 +01:00
3a08c1cdb6 Other: Update docstrings for locations 2022-11-18 17:32:15 +01:00
2ff5731b39 GODT-2059: Attempt to fix log crash
Don't store the logrus entry as it might be possible that the standard
logrus logger can get changed in between resulting in stale pointers
inside the `Logrus.Entry` instance.
2022-11-18 17:08:29 +01:00
eb2423b0ed Other: Move sending logic to smtp.go 2022-11-18 17:05:20 +01:00
e60bbaa60f Other: Add more user-level logs 2022-11-18 13:30:45 +00:00
65cc1d5ccf GODT-2110: Force attachment disposition if content ID is missing
We now set disposition during attachment upload. However, this presents
a problem: some clients use inline disposition but don't provide a
content ID. Our API doesn't support this. To mitigate the issue, we just
fall back to attachment disposition in this case.
2022-11-18 12:52:43 +01:00
f17b630b12 Other: Fix wrongly installed version
When manually installing an update, we want to manually install the
latest compatible version (`target`), not the absolute latest version
(`latest`). This is obviously a bug in the code because the mutex lock
being locked was `targetLock` but we were wrongly trying to install
`latest`.
2022-11-18 10:08:26 +00:00
50da1e4704 GODT-2081: if keychain cannot be loaded do not wipe Vault and use a temp one. 2022-11-18 08:25:52 +01:00
515a8689e9 Other: Bridge Tacoma v 2.5.9 2022-11-18 08:01:22 +01:00
04b30fd694 GODT-2103: Trigger the version changed event. 2022-11-17 18:01:22 +01:00
e5095b2154 Other: Add API debug option in QA builds 2022-11-17 13:20:31 +00:00
1e48ab4b9c GODT-2047: Clear last event ID when clearing sync status 2022-11-17 13:18:47 +00:00
fe5e8ce7f7 Other: Tidy up app.go a bit 2022-11-17 13:32:13 +01:00
319d51cb80 Other: Bump gluon version to prevent crash on log failure 2022-11-17 12:52:23 +01:00
14cad02b5a GODT-2109: removed log message "Parent process XXX is still alive". 2022-11-17 11:18:47 +01:00
bc30a9db68 Other: Bridge Tacoma v2.5.8. 2022-11-17 08:09:25 +01:00
c7cfcb29f6 GODT-2091: animated "Connecting..." label. + unstaged changes from GODT-2003. 2022-11-16 19:46:56 +01:00
e087a7972e GODT-2003: introduces 3 phases user state (SignedOut/Locked/Connected)
WIP: introduced UserState enum in GUI and implemented logic.
2022-11-16 16:24:55 +01:00
49b3c18903 GODT-2039: bridge monitors bridge-gui via its PID (port from v2.4) 2022-11-16 15:21:33 +01:00
4f3748a4f0 GODT-2056: kill old bridge from v2 lock file. 2022-11-16 13:48:31 +01:00
27cbcc6f5e GODT-2086: Changing the wording for signing in. 2022-11-16 13:48:31 +01:00
ed2d70dd15 Other: Bridge Tacoma v2.5.7 2022-11-16 13:48:31 +01:00
ae87d7b236 GODT-1913: pass reporter to gluon, limit restarts, add crash handlers. 2022-11-16 13:48:31 +01:00
31fb878bbd GODT-2070: Implement SASL login for SMTP
go-smtp now comes with out of the box support for SASL PLAIN but it
still requires manual implementation of SASL LOGIN (deprecated).
2022-11-16 13:48:31 +01:00
59278913ca GODT-2037: Handle and log API refresh event 2022-11-16 13:48:31 +01:00
2023df3ef8 Other: Log mailbox message counts at startup 2022-11-16 13:48:31 +01:00
48cf89b1a6 Other: Use cached labels for label sync rather than refetch 2022-11-16 13:48:31 +01:00
223b14e556 Other: Configure attachment pool size in vault 2022-11-16 13:48:31 +01:00
112d79c2be Other: Bump gluon version to include rebuilt parser libs 2022-11-16 13:48:31 +01:00
2aec508e43 Other: Bridge Tacoma v2.5.6 2022-11-16 13:48:31 +01:00
d0b13a8684 Other: build vault editor. 2022-11-16 13:48:31 +01:00
28d78453b7 GODT-2029: Handle deadlock when reordering user addresses 2022-11-16 13:48:31 +01:00
04f2dd1a0b GODT-2021: Remove gluon data when deleting user 2022-11-16 13:48:31 +01:00
8b0024d53e Other: Update Gluon to latest version 2022-11-16 13:48:30 +01:00
098685ec8b GODT-2030: Rework deletion check on expunge
Some messages were not being deleted properly because they were also
present in the All-Sent folder.

The code has now been changed to filter out AllMail, AllDraft and
AllSend. If there are no remaining labels, the message will be deleted
permanently.
2022-11-16 13:48:30 +01:00
6c9293ec14 Other: Use current time if date is missing 2022-11-16 13:48:30 +01:00
8cbbfb0e34 GODT-1977: missing import. 2022-11-16 13:48:30 +01:00
bbbc18b959 GODT-1977: revert the pre-release changes. 2022-11-16 13:48:30 +01:00
5aa495b240 GODT-1977: add pre-release flag to app version on qa builds 2022-11-16 13:48:30 +01:00
c08d0eff7a GODT-1977: fix launcher for v2 to v3 updates. 2022-11-16 13:48:30 +01:00
34213d1607 Other(CI): Use harbor image 2022-11-16 13:48:30 +01:00
82aa0b270c Other: Bridge Tacoma v2.5.5 2022-11-16 13:48:30 +01:00
847d6de6bf Other: Bump liteapi version 2022-11-16 13:48:30 +01:00
739fe826b3 GODT-2048: Add missing special use attributes 2022-11-16 13:48:30 +01:00
6bf67917fb GODT-2034: Basic vault migration ability (proof of concept) 2022-11-16 13:48:30 +01:00
4c4c592f31 GODT-2008: Add unit test asserting that primary address is listed first 2022-11-16 13:48:30 +01:00
8a08d146bc Other: Bridge Tacoma 2.5.4 2022-11-16 13:48:30 +01:00
da41398340 Other: Bump liteapi to mitigate race condition 2022-11-16 13:48:30 +01:00
d74873be31 GODT-1978: Don't install the same update twice 2022-11-16 13:48:30 +01:00
b9ffa96e8b GODT-1978: Check latest version on force update if unknown 2022-11-16 13:48:30 +01:00
8a666dc8cc Other: Add missing t.bridge = nil line 2022-11-16 13:48:30 +01:00
78fc5ec458 Other: Simple gRPC client/server under test 2022-11-16 13:48:30 +01:00
d093488522 GODT-1978: Auto-updates 2022-11-16 13:48:30 +01:00
1e29a5210f GODT-1954: Draft message support
Add special case handling for draft messages so that if a Draft is
updated via an event it is correctly updated on the IMAP client via a
the new `imap.MessageUpdated event`.

This patch also updates Gluon to the latest version.
2022-11-16 13:48:30 +01:00
c548ba85fe Other: Add more extensive logging 2022-11-16 13:48:30 +01:00
8bb60afabd GODT-2014: bridge quit if gRPC client ends stream (v3) 2022-11-16 13:48:30 +01:00
8b5cb7729c GODT-2013: CLI flag for frontend is required (v3) 2022-11-16 13:48:30 +01:00
f8d7b98d05 Other: Bridge Tacoma 2.5.3 2022-11-16 13:48:30 +01:00
bc7912e8fb GODT-2022: Fix change between address modes 2022-11-16 13:48:30 +01:00
0812491f09 Other: Bridge Tacoma v2.5.2 2022-11-16 13:48:30 +01:00
af542a2fc1 Other: Ensure logout works when offline 2022-11-16 13:48:30 +01:00
039d1b7f99 GODT-2023: Revert to v2 bridge password encoming format
v2 used base64.RawURLEncoding rather than hex to encode the bridge
password. We should use that in v3 as well.
2022-11-16 13:48:30 +01:00
4ded8784fc Other: Allow non-received messages to be imported to INBOX 2022-11-16 13:48:30 +01:00
943d95a725 Other: Add UserLoading/UserLoadFail events 2022-11-16 13:48:30 +01:00
75b788b793 GODT-1993: Use more efficient filtering for message deletion 2022-11-16 13:48:30 +01:00
048a83c8c9 Other: Bridge Tacoma v2.5.1 2022-11-16 13:48:30 +01:00
e92badef0e GODT-2004: Ensure log files don't have color formatting
This patch ensures that log files written to disk do not have any color
formatting present.

Sadly due to limitations of the logrus library, we have to force
coloring enabled on logs to stdout.
2022-11-16 13:48:30 +01:00
924a423488 Other: Add some more debug logs 2022-11-16 13:48:30 +01:00
1ad821b2b7 GODT-2011: Use new app version format 2022-11-16 13:48:30 +01:00
62d62474fb Other: Poll events after each IMAP operation
This matches bridge's behaviour (except for it being a non-blocking
wait).

It also cleans up some unused connector methods.
2022-11-16 13:48:30 +01:00
1dbc9a1366 GODT-2010: Add better logging for app focus feature 2022-11-16 13:48:30 +01:00
99745ac067 Other: Switch to faster message IDs route
The new API route lets us query exactly which message IDs a user has,
allowing us to begin syncing much faster than before.
2022-11-16 13:48:30 +01:00
dbfb7572a8 GODT-2008: Ensure user's addresses are returned in sorted order 2022-11-16 13:48:30 +01:00
a213b48f93 GODT-2002: Poll after SMTP send
After sending, a client might append to the sent folder over IMAP.
In this case, we perform deduplication and return the message ID of the
sent message. However, if we haven't already processed this message in
gluon, it doesn't work as expected.

This change polls the event stream immediately after send. Note that it
doesn't wait for these events to be processed; that should be done in a
follow-up commit.
2022-11-16 13:48:30 +01:00
b0f939bfaf Other: Do not compile go bridge as win32 app
Compile as console application so that we can inspect trace outputs and
IMAP/SMTP Commands.

Note: This needs to be reverted before release.
2022-11-16 13:48:30 +01:00
075e1ef236 Other: Fix wrong encoding used for public key during sending
The newer liteapi contact code saves keys as *crypto.Key, however the
legacy code expects them to be strings. There was a bug in connecting
the new code to the legacy code: it was assumed these strings were meant
to be a base64 encoded string but they were actually just raw string
bytes.
2022-11-16 13:48:30 +01:00
88ad98ed37 Other: Bridge Tacoma v2.5.0 2022-11-16 13:48:30 +01:00
0a972285a6 Other(docs): Add build command to vault-editor readme 2022-11-16 13:48:30 +01:00
358a2e5266 Other: Disable TLS pin checks on QA builds 2022-11-16 13:48:30 +01:00
5bb2eeafb7 Other: Don't mark certs twice as installed 2022-11-16 13:48:30 +01:00
94f84625d4 Other: Use Cache rather than Config on non-linux OS for user data 2022-11-16 13:48:30 +01:00
34e4625b64 Other: Don't try proxy if dial was cancelled 2022-11-16 13:48:30 +01:00
a797c01943 Other: Fix create draft action
If an InReplyTo entry in the header is present, the create draft action
should be reply, otherwise we use forward.
2022-11-16 13:48:30 +01:00
b72de5e3a4 Other: Fix goroutine leak in OnStatusDown
We should spawn the goroutine as a bridge async task rather than as a
normal goroutine
2022-11-16 13:48:30 +01:00
bf4afae5d9 Other: Put back missing force update GUI event 2022-11-16 13:48:30 +01:00
29dcd5450f Other: Add FEATURE_TEST_LOG_IMAP env variable
When set will log IMAP commands during feature tests.

This patch also updates Gluon to the latest version.
2022-11-16 13:48:30 +01:00
f1160a11af Other: wired 'use SSL for IMAP' toggle in bridge-gui-tester. 2022-11-16 13:48:30 +01:00
d9762010fa GODT-1982: updated gRPC and GUI for disk cache.
Other: modified bridge-gui-tester for new cache related gRPC interface.
Other: bridge-gui-tester has buttons for cache related errors.
2022-11-16 13:48:30 +01:00
93d9ae32fc Other: Only listen on localhost and add send test
We should only listen on constants.Host when serving IMAP and SMTP.
This change fixes that. It also adds a test that we can send over SMTP
and receive over IMAP.
2022-11-16 13:48:30 +01:00
cb42358e2f Other: Bump gluon version to not set internal ID header twice 2022-11-16 13:48:30 +01:00
8f420d728c GODT-1984: Handle permanent message deletion
Only delete messages when unlabeled from trash/spam if they only exists
in All Mail and (spam or trash).

This patch also ports delete_from_trash.feature and use status rather
than fetch to count messages in a mailboxes.
2022-11-16 13:48:30 +01:00
df818bc2b8 Other: Update UID validity in vault when necessary 2022-11-16 13:48:30 +01:00
2fbecc4675 GODT-1916: Use XDG_DATA_HOME to store persistent data on linux
This change ensures persistent data is stored in XDG_DATA_HOME
instead of XDG_CACHE_HOME on linux.

It adds the UserData() method on locations.Provider to return a path
suitable for storing persistent data. On linux, this returns
$XDG_DATA_HOME/protonmail (likely ~/.local/share/protonmail), and on
non-linux this returns os.UserConfigDir() because that is assumed
to be a more persistent location than os.UserCacheDir().

locations.Locations has been modified to use this new data directory;
gluon and logs are now stored here.
2022-11-16 13:48:30 +01:00
a553ced979 Other: go mod tidy/goimports/gofumpt after rebase on devel 2022-11-16 13:48:30 +01:00
82987a1835 Other: added missing build tags for restarter. 2022-11-16 13:48:30 +01:00
3c4e8730ac Other: Read sync workers setting from vault 2022-11-16 13:48:30 +01:00
d738fdff57 GODT-1974: Store everything in v3 path 2022-11-16 13:48:30 +01:00
d4c1016e55 Other: Consider vault corrupt if it cannot be unmarshaled 2022-11-16 13:48:30 +01:00
b1ce2dd73f Other: QA host URL when built with build_qa tag 2022-11-16 13:48:30 +01:00
d066e32719 GODT-1986: Handle case where an address has no decryption entities
It's possible (but very rare, I don't think proton still allows it)
for an address to have no keys. If we try to load the address keyring
for such an address, this change logs a warning that no decryption
entities were found in the unlocked keyring.

It bumps liteapi to a version that does not return an error when no
keys could be unlocked.
2022-11-16 13:48:30 +01:00
7f6094750e Other: Use logrus for liteapi logs so we can properly capture them 2022-11-16 13:48:30 +01:00
e4c08be28e Other: Don't remove/add gluon user when in combined mode
This used to be necessary because the connectors didn't have access
to the user's emails, but now they do, so we no longer need to do this.
2022-11-16 13:48:30 +01:00
7e03de0a21 Other: Lint fixes 2022-11-16 13:48:30 +01:00
d4da325e57 Other(refactor): Sort safe.Mutex types before locking to prevent deadlocks
This change implements safe.Mutex and safe.RWMutex, which wrap the
sync.Mutex and sync.RWMutex types and are assigned a globally unique
integer ID. The safe.Lock and safe.RLock methods sort the mutexes
by this integer ID before locking to ensure that locks for a given
set of mutexes are always performed in the same order, avoiding
deadlocks.
2022-11-16 13:48:30 +01:00
5a4f733518 Other(refactor): Remove unused safe types 2022-11-16 13:48:30 +01:00
fd80848fcd Other(refactor): Use normal value + mutex for user.updateCh 2022-11-16 13:48:30 +01:00
cab5ee6752 Other(refactor): Use normal value + mutex for user.apiLabels 2022-11-16 13:48:30 +01:00
0bc99dbd4f Other(refactor): Use normal value + mutex for user.apiAddrs 2022-11-16 13:48:30 +01:00
83339da26c Other(refactor): Use normal value + mutex for user.apiUser 2022-11-16 13:48:30 +01:00
8749d5dc7d Other(refactor): Remove always-nil return value of (*Bridge).Close 2022-11-16 13:48:30 +01:00
2bda47fcad Other(refactor): Less unwieldy user type in Bridge
Instead of the annoying safe.Map type, we just use a normal go map
and mutex pair, and use safe.Lock/safe.RLock as a helper function
2022-11-16 13:48:30 +01:00
85c0d6f837 Other: Hold user labels in memory
Labels can be held locally and updated in memory. This greatly improves
the responsiveness of IMAP mailbox operations as we don't need to fetch
all a user's labels to find the parent whenever a mailbox is moved.
2022-11-16 13:48:30 +01:00
04d9fa8f9e Other: Fix apple mail config 2022-11-16 13:48:30 +01:00
7b7a2068ea Other: Clear keychain entries on factory reset 2022-11-16 13:48:30 +01:00
4a31017332 Other: Add vault-editor readme 2022-11-16 13:48:30 +01:00
784896434d Other: Add vault editor CI tool 2022-11-16 13:48:30 +01:00
d376b88cf0 Other: Use msgpack instead of json in vault
msgpack is much faster at serializing and deserializing than json. Using
it in the vault gives a performance boost.
2022-11-16 13:48:30 +01:00
c7a5b8559c Other: Updating address flags should be handled 2022-11-16 13:48:30 +01:00
fd0c262645 Other: Implement subfolder support 2022-11-16 13:48:30 +01:00
4f7cb43c8f Other(CI): Allow race checks to fail
Although most race conditions have been eliminated, there are some known
ones, particularly in the go-imap client used by the integration tests,
which is out of our control.

Over time we will hopefully eliminate these but for now, let's not rely
on the race checks consistently passing.
2022-11-16 13:48:30 +01:00
2f40b030ec Other: Bump liteapi to v0.36.1 2022-11-16 13:48:30 +01:00
d2b1b9d34c Other: Rename BDD test action for hiding all mail 2022-11-16 13:48:30 +01:00
b594b5f90a Other: IMAP create duplicate mailbox tests 2022-11-16 13:48:30 +01:00
94e219137e Other: IMAP create tests 2022-11-16 13:48:30 +01:00
a26db09e54 Other: Add missing license 2022-11-16 13:48:30 +01:00
351c019310 Other(CI): Increase integration tests timeout 2022-11-16 13:48:30 +01:00
14fbdb5e04 Other: Fix race condition when changing address mode
When changing address mode, we would close all a user's update channels
and create them from scratch. This involved setting user.updateCh to a
new value. However, it was possible for other goroutines to read from
user.updateCh during this time. I replaced it with a call to
user.updateCh.Clear(), which is threadsafe.
2022-11-16 13:48:30 +01:00
dabc9717d1 Other(test): Increase timeouts because race check is slow 2022-11-16 13:48:30 +01:00
5adbf74cbe Other(test): Ensure calls are protected by mutex 2022-11-16 13:48:30 +01:00
3e54885ea0 Other(CI): Enable race checks in the CI 2022-11-16 13:48:30 +01:00
247e676b41 Other: Fix race conditions in integration tests 2022-11-16 13:48:30 +01:00
cb04dabea8 Other: Fix goroutine leaks in integration tests
We were closing the event QueuedChannel objects in the wrong place;
they should have been closed on test teardown, not on stopBridge
(which was just a test action and wasn't always called).

In order to make the events more scalable, i replace all the
QueuedChannel objects with a single event collector, which would
create QueuedChannels on demand when it receives an event of a new type.
2022-11-16 13:48:30 +01:00
7df2d70dbf Other: Fix race condition in bridge mock updater
We need to protect the latest version info by a lock.
2022-11-16 13:48:30 +01:00
350544e801 Other: Fix race conditions in internal/dialer
Some race conditions came from the tests themselves. But we had a race
condition reading the proxyAddress; this change protects it with a
mutex.
2022-11-16 13:48:30 +01:00
d6260d960c Other: Add race-condition and leak checks to the makefile 2022-11-16 13:48:30 +01:00
d0fb3509cc Other: Make user-agent implementation threadsafe 2022-11-16 13:48:30 +01:00
798cd5caf3 Other: Don't dump stack trace to error when not panicking 2022-11-16 13:48:30 +01:00
35fa43f47c Other: Properly handle SMTP to list in send recorder
Checking the BCC header is unreliable; it is usually omitted from messages.
Instead, we can use the SMTP "to" list for deduplication.
2022-11-16 13:48:30 +01:00
4df8ce1b58 Other: Linter to ignore test parameters 2022-11-16 13:48:30 +01:00
83c7396f2d Other: Separate getMessageHash from sendRecorder 2022-11-16 13:48:30 +01:00
709922c383 Other(lint): gofmt 2022-11-16 13:48:30 +01:00
16978b8949 Other(test): Add send recorder test case 2022-11-16 13:48:30 +01:00
7745b68228 GODT-1777: Also include Reply-To and In-Reply-To in message hash 2022-11-16 13:48:30 +01:00
036a416a25 GODT-1777: Message de-duplication in IMAP (+ cleanup) 2022-11-16 13:48:30 +01:00
c9808d07df GODT-1777: Message de-duplication in SMTP 2022-11-16 13:48:30 +01:00
8e34b51c77 GODT-1940: Fix message encryption
Update liteapi to v0.36.0 to include message encryption fix and fix
compile errors related to update.
2022-11-16 13:48:30 +01:00
afc5307a23 Other: Don't close IMAP/SMTP listeners if they could not be created 2022-11-16 13:48:30 +01:00
1919610793 Other: Bump gluon version 2022-11-16 13:48:30 +01:00
6fbf6d90dc Other: Fix IMAP/SMTP/Login leaks/race conditions
Depending on the timing of bridge closure, it was possible for the 
IMAP/SMTP servers to not have started serving yet. By grouping this in
a cancelable goroutine group (*xsync.Group), we mitigate this issue.

Further, depending on internet disconnection timing during user login,
it was possible for a user to be improperly logged in. This change 
fixes this and adds test coverage for it.

Lastly, depending on timing, certain background tasks (updates check,
connectivity ping) could be improperly started or closed. This change
groups them in the *xsync.Group as well to be closed properly.
2022-11-16 13:48:30 +01:00
828385b049 Other: Fix user sync leaks/race conditions
This fixes various race conditions and leaks related to the user's sync
and API event stream. It was possible for a sync/stream to begin after a
user was already closed; this change prevents that by managing the
goroutines related to sync/stream within cancellable groups.
2022-11-16 13:48:30 +01:00
6bbaf03f1f Other: Fix goroutine leaks in sync tests
Add missing Close calls.

Properly handle nil channel for `user.startSync`.

This patch also updated liteapi and Gluon to latest master and dev
version respectively.
2022-11-16 13:48:30 +01:00
6fdc8bd379 Other: Add missing test expectation 2022-11-16 13:48:30 +01:00
0f125196a6 Other: Bump go-smtp version to fix race condition
There was a race condition internal to the go-smtp library.
In order to fix it, a version bump was necessary.
However, this significantly changed the library interface.
2022-11-16 13:48:30 +01:00
974735d415 Other: Bump liteapi version to fix goroutine leaks 2022-11-16 13:48:30 +01:00
472b96795f Other: Bump liteapi version 2022-11-16 13:48:30 +01:00
81f4ef609b Other: Mitigate double-unlock of user keyring
We need to unlock the user keyring anyway to unlock the address keyring,
so we should just return it instead of re-unlocking the user keyring
when sending a message.
2022-11-16 13:48:30 +01:00
80d3f7d179 Other: Set \Draft flag on messages in drafts mailbox 2022-11-16 13:48:30 +01:00
c4343e0124 Other: Bump liteapi and clean up tests a bit 2022-11-16 13:48:30 +01:00
04b6571cb8 Other: Handle Seen/Flagged IMAP flags when APPENDing a message
When an IMAP client appends a message to a mailbox, it can specify
which flags it wants the appended message to have. We need to handle
these in a proton-specific way; not-seen messages need to be imported
with the Unread bool set to true, and flagged messages need to
additionally be imported with the Starred label.
2022-11-16 13:48:30 +01:00
a7a7d9a3d4 GODT-1742: Implement hide All Mail 2022-11-16 12:26:09 +01:00
395e7b54f6 Other(test): Don't check user agent immediately 2022-11-16 12:26:09 +01:00
c20143c212 Other: Increase integration test timeouts 2022-11-16 12:26:09 +01:00
1729c085c7 Other: Fix user logout hangs due to sync 2022-11-16 12:26:09 +01:00
bf29090ffa Other: Fix log message, rename test fixture 2022-11-16 12:26:09 +01:00
ca132881f9 Other: Update Gluon to v0.13.0 2022-11-16 12:26:09 +01:00
d47b5b99c5 GODT-1813: Cleanup old go-imap cache files 2022-11-16 12:26:09 +01:00
e0ff30e9a8 Other: Update gluon to v0.12.0 2022-11-16 12:26:09 +01:00
7c62312220 Other: Fix all linter errors 2022-11-16 12:26:09 +01:00
b36972ce71 GODT-1650: Implement Connector.CreateMessage 2022-11-16 12:26:09 +01:00
023e7b2d32 Other: Bump liteapi version to v0.34.0 2022-11-16 12:26:09 +01:00
e10cd2a3ed GODT-1901: Allow to set IMAP SSL from UI 2022-11-16 12:26:09 +01:00
209c315a76 Other: Remove double case squash 2022-11-16 12:26:09 +01:00
7fe2c094a9 Other: Match any case in IMAP/SMTP auth, with test 2022-11-16 12:26:09 +01:00
23d3e54ddb Other: Move log init to proper place 2022-11-16 12:26:09 +01:00
6b2b98a262 Other: Fix launcher argument type 2022-11-16 12:26:09 +01:00
fba8568474 Other: Log message at startup 2022-11-16 12:26:09 +01:00
2a97939807 Other: Clean locations on teardown 2022-11-16 12:26:09 +01:00
a74b025de3 Other: Factory reset 2022-11-16 12:26:09 +01:00
cec44be7c3 Other: SetMainExecutable, ForceLauncher 2022-11-16 12:26:09 +01:00
a4852c1b36 Other: Get test events before starting bridge to ensure all are captured 2022-11-16 12:26:09 +01:00
ef2dea89b4 Other: Safer vault 2022-11-16 12:26:09 +01:00
593d86f3a7 Other: Single instance 2022-11-16 12:26:09 +01:00
fd63611b41 Other: Safer user types 2022-11-16 12:26:09 +01:00
4dc32dc7f2 Other: Make configure apple mail work in split and combined mode 2022-11-16 12:26:09 +01:00
1e845adc17 Other: Fix wrong Errorf type 2022-11-16 12:26:09 +01:00
fb6435e30d Other: Add libsecret to container 2022-11-16 12:26:09 +01:00
6ee71d238b Other: Fix force-update test after version bump 2022-11-16 12:26:09 +01:00
d330000c8d Other: Put back old 2FA/two-pass flow in GUI 2022-11-16 12:26:09 +01:00
1517a7b665 Other: Connect some more events 2022-11-16 12:26:09 +01:00
da33a6c48c Other: Add launcher flag to ensure bridge starts 2022-11-16 12:26:09 +01:00
89e07921f1 Other: Bump version 2022-11-16 12:26:09 +01:00
2450511555 Other: Put back split login process in backend 2022-11-16 12:26:09 +01:00
da1ee99c53 Other: Fix send with plus address 2022-11-16 12:26:09 +01:00
1c922ca083 Other: Fix flaky cookies test 2022-11-16 12:26:09 +01:00
14a578f319 Other: Linter fixes after bumping linter version 2022-11-16 12:26:09 +01:00
4a5c411665 Other: Try get the CI working again (WIP) 2022-11-16 12:26:09 +01:00
0de30afba1 Other: Better cookies test 2022-11-16 12:26:09 +01:00
245f2afeac GODT-1816: Fix variable case conflict 2022-11-16 12:26:09 +01:00
0f81286ff5 GODT-1816: Allow debug logs to written to disk 2022-11-16 12:26:09 +01:00
f01c70e506 GODT-1816: Connect Gluon Logs to bridge Logs
Ensure the IMAP commands and SMTP commands are logged to trace channels
with an entry so they are recognizable as before.
2022-11-16 12:26:09 +01:00
03e14154a6 Other: Bump gluon version 2022-11-16 12:26:09 +01:00
509a767e50 GODT-1657: More stable sync, with some tests 2022-11-16 12:26:09 +01:00
e7526f2e78 GODT-1650: Log IMAP errors 2022-11-16 12:26:09 +01:00
9d69a2e565 GODT-1657: Stable sync (still needs more tests) 2022-11-16 12:26:09 +01:00
705875cff2 Other: Use proper gRPC insecure credentials 2022-11-16 12:26:09 +01:00
39b366ee69 GODT-1650: Password as bytes in API login 2022-11-16 12:26:09 +01:00
edd326efd9 GODT-1650: Mixed case and failure sending tests 2022-11-16 12:26:09 +01:00
4f634689c2 GODT-1650: SMTP embedded message tests 2022-11-16 12:26:09 +01:00
db429bd838 GODT-1650: SMTP BCC tests 2022-11-16 12:26:09 +01:00
cce372fc50 GODT-1650: Test of end-to-end send with attachments (internal) 2022-11-16 12:26:09 +01:00
df7479f506 GODT-1650: sending with multiple addresses 2022-11-16 12:26:09 +01:00
0badd69409 GODT-1650: sending tests with attachments 2022-11-16 12:26:09 +01:00
c953b8030a GODT-1650: text/html sending tests 2022-11-16 12:26:09 +01:00
ba9368426c GODT-1650: Send extras 2022-11-16 12:26:09 +01:00
2cb739027b GODT-1650: Use trace logger for gluon 2022-11-16 12:26:09 +01:00
120ac6c480 GODT-1609: Fix tests 2022-11-16 12:26:09 +01:00
6ac68984f2 GODT-1609: Fix bridge password encoding 2022-11-16 12:26:09 +01:00
51633e000b GODT-1609: apply change from MR 2022-11-16 12:26:09 +01:00
b536b8707e GODT-1609: use byte array for password 2022-11-16 12:26:09 +01:00
3b5f931f06 GODT-1609: remove unused gRPC event (c++ side) 2022-11-16 12:26:09 +01:00
bf15eebd2d GODT-1650: Remove unused gRPC event (go side) 2022-11-16 12:26:09 +01:00
4fc22e25ba GODT-1650: Bump gluon to latest version 2022-11-16 12:26:09 +01:00
0f9a9a377b GODT-1650: Bump liteapi to v0.31.1 2022-11-16 12:26:09 +01:00
d79e6f2704 GODT-1815: Focus service docs 2022-11-16 12:26:09 +01:00
e9672e6bba GODT-1815: Combined/Split mode 2022-11-16 12:26:08 +01:00
9670e29d9f GODT-1815: Start with missing gluon files 2022-11-16 12:26:08 +01:00
612fb7ad7b GODT-1815: Start without internet, load users later 2022-11-16 12:26:08 +01:00
1da1188351 GODT-1815: Upgrade to liteapi v0.31.0 2022-11-16 12:26:08 +01:00
39433fe707 GODT-1779: Remove go-imap 2022-11-16 12:26:08 +01:00
3b0bc1ca15 Revert "GODT-1932: frontend is instantiated before bridge."
This reverts commit db2379e2fd.
2022-11-16 12:26:08 +01:00
cc9ad17ea5 Revert "GODT-2014: bridge quit if gRPC client ends stream."
This reverts commit 8ca849b7a8.
2022-11-16 12:26:08 +01:00
f965d01922 Revert "GODT-2013: CLI flag for frontend is required."
This reverts commit 4bb29b1b5c.
2022-11-16 12:26:08 +01:00
7cb9546d21 Revert "GODT-2039: bridge monitors bridge-gui via its PID."
This reverts commit 3b9a3aaad2.
2022-11-16 12:26:08 +01:00
debe87f2f5 Other: Bridge Osney v2.4.8 2022-11-14 09:28:07 +01:00
cca2807256 GODT-2071: fix --no-window flag that was broken on Windows. 2022-11-11 21:34:56 +01:00
7b73f76e78 Other: Bridge Osney v2.4.7 2022-11-11 14:06:56 +01:00
b1eefd6c85 GODT-2078: Launcher inception. 2022-11-11 12:26:06 +01:00
bbcb7ad980 GODT-2039: fix --parent-pid flag is removed from command-line when restarting the application. 2022-11-11 11:10:27 +01:00
984c43cd75 Other: Bridge Osney v2.4.6 2022-11-10 15:41:33 +01:00
ec4c0fdd09 GODT-2019: when signing out and a single user is connected, we do not go back to the welcome screen. 2022-11-10 15:31:56 +01:00
51d4a9c7ee GODT-2071: bridge-gui report error if an orphan bridge is detected. 2022-11-10 15:31:55 +01:00
19930f63e2 GODT-2046: bridge-gui log is included in optional archive sent with bug reports. 2022-11-10 15:31:55 +01:00
3b9a3aaad2 GODT-2039: bridge monitors bridge-gui via its PID. 2022-11-10 15:31:55 +01:00
f5148074fd GODT-2038: interrupt gRPC initialisation of bridge process terminates. 2022-11-10 15:31:54 +01:00
a949a113cf Other: added timestamp to bridge-gui logs.
Changed format to be closer to logrus output.
2022-11-10 15:31:54 +01:00
227e9df419 GODT-2035: bridge-gui log includes Qt version info. 2022-11-10 15:31:53 +01:00
2a6d462be1 GODT-2031: updated bridge description. 2022-11-10 15:31:53 +01:00
bb03fa26cd Other: fix make run-qt target for Darwin. 2022-11-09 17:07:42 +01:00
1149 changed files with 83621 additions and 153708 deletions

5
.gitignore vendored
View File

@ -6,6 +6,7 @@
.*.sw?
*~
.idea
.vscode
# Test files
godog.test
@ -40,3 +41,7 @@ cmake-build-*/
# Doxygen doc files
_doc/
# gRPC auto-generated C++ source files
*.pb.cc
*.pb.h

View File

@ -16,37 +16,23 @@
# along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
---
image: gitlab.protontech.ch:4567/go/bridge-internal:go18
image: gitlab.protontech.ch:4567/go/bridge-internal:test-go1.20
default:
tags:
- shared-small
variables:
GOPRIVATE: gitlab.protontech.ch
GOMAXPROCS: $(( ${CI_TAG_CPU} / 2 ))
before_script:
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null
- mkdir -p .cache/bin
- export PATH=$(pwd)/.cache/bin:$PATH
- export GOPATH="$CI_PROJECT_DIR/.cache"
- make install-dev-dependencies
- git checkout .
cache:
key: go18-mod
paths:
- .cache
policy: pull
- apt update && apt-get -y install libsecret-1-dev
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
stages:
- cache
- test
- build
- check
- mirror
.rules-branch-and-MR-always:
rules:
- if: $CI_COMMIT_BRANCH || $CI_PIPELINE_SOURCE == "merge_request_event"
when: always
allow_failure: false
- when: never
.rules-branch-and-MR-manual:
rules:
@ -55,9 +41,9 @@ stages:
allow_failure: true
- when: never
.rules-branch-manual-MR-always:
.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: false
- if: $CI_COMMIT_BRANCH
@ -65,112 +51,61 @@ stages:
allow_failure: true
- when: never
# Stage: CACHE
# This will ensure latest dependency versions and updates the cache for
# all other following jobs which only pull the cache.
cache-push:
stage: cache
extends:
- .rules-branch-and-MR-always
script:
- echo ""
cache:
key: go18-mod
paths:
- .cache
# Stage: TEST
lint:
stage: test
extends:
- .rules-branch-and-MR-always
before_script:
- mkdir -p .cache/bin
- export PATH=$(pwd)/.cache/bin:$PATH
- export GOPATH="$CI_PROJECT_DIR/.cache"
script:
- env GOMAXPROCS=$(( ${CI_TAG_CPU} / 2 )) make lint
tags:
- medium
test-linux:
stage: test
extends:
- .rules-branch-manual-MR-always
script:
- apt-get -y install pass gnupg rng-tools
# First have enough of entropy (cat /proc/sys/kernel/random/entropy_avail).
- rngd -r /dev/urandom
# Generate GPG key without password for the password manager.
- gpg --batch --yes --passphrase '' --quick-generate-key 'tester@example.com'
# Use the last created GPG ID for the password manager.
- pass init `gpg --list-keys | grep "^ " | tail -1 | tr -d '[:space:]'`
# Then finally run the tests
- make test
tags:
- medium
test-windows:
extends:
- .build-windows-base
- .rules-branch-and-MR-always
stage: test
needs: []
script:
- make test
test-integration:
stage: test
extends:
- .rules-branch-manual-MR-always
script:
- VERBOSITY=debug make -C test test
tags:
- large
dependency-updates:
stage: test
script:
- make updates
# Stage: BUILD
.build-base:
stage: build
needs: ["lint"]
.rules-branch-manual-scheduled-and-test-branch-always:
rules:
# GODT-1833: use `=~ /qa/` after mac and windows runners are fixed
- if: $CI_JOB_NAME =~ /build-linux-qa/ && $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_PIPELINE_SOURCE == "schedule"
when: always
allow_failure: false
- if: $CI_COMMIT_BRANCH || $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME=~ /^test/
when: always
allow_failure: false
- if: $CI_COMMIT_BRANCH
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
script:
- make build
- git diff && git diff-index --quiet HEAD
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
paths:
- bridge_*.tgz
tags:
- large
build-linux:
extends: .build-base
image: gitlab.protontech.ch:4567/go/bridge-internal:qt6
# ENV
.env-windows:
before_script:
- export BRIDGE_SYNC_FORCE_MINIMUM_SPEC=1
- export GOROOT=/c/Go1.20/
- export PATH=$GOROOT/bin:$PATH
- export GOARCH=amd64
- export GOPATH=~/go1.20
- export GO111MODULE=on
- export PATH="${GOPATH}/bin:${PATH}"
- export MSYSTEM=
- export QT6DIR=/c/grrrQt/6.4.3/msvc2019_64
- export PATH=$PATH:${QT6DIR}/bin
- export PATH="/c/Program Files/Microsoft Visual Studio/2022/Community/Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/bin:$PATH"
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
- git config --global safe.directory '*'
- git status --porcelain
cache: {}
tags:
- windows-bridge
.env-darwin:
before_script:
- export BRIDGE_SYNC_FORCE_MINIMUM_SPEC=1
- export PATH=/usr/local/bin:$PATH
- export PATH=/usr/local/opt/git/bin:$PATH
- export PATH=/usr/local/opt/make/libexec/gnubin:$PATH
- export PATH=/usr/local/opt/gnu-sed/libexec/gnubin:$PATH
- export GOROOT=~/local/opt/go@1.20
- export PATH="${GOROOT}/bin:$PATH"
- export GOPATH=~/go1.20
- export PATH="${GOPATH}/bin:$PATH"
- export QT6DIR=/opt/Qt/6.4.3/macos
- export PATH="${QT6DIR}/bin:$PATH"
- uname -a
cache: {}
tags:
- macos-m1-bridge
.env-linux-build:
image: gitlab.protontech.ch:4567/go/bridge-internal:build-go1.20-qt6.4.3
variables:
VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.cache
cache:
@ -178,133 +113,191 @@ build-linux:
paths:
- .cache
when: 'always'
before_script:
- mkdir -p .cache/bin
- export BRIDGE_SYNC_FORCE_MINIMUM_SPEC=1
- export PATH=$(pwd)/.cache/bin:$PATH
- export GOPATH="$CI_PROJECT_DIR/.cache"
- export PATH=$PATH:$QT6DIR/bin
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
tags:
- shared-large
# Stage: TEST
lint:
stage: test
extends:
- .rules-branch-manual-MR-and-devel-always
script:
- make lint
tags:
- shared-medium
bug-report-preview:
stage: test
extends:
- .rules-branch-and-MR-manual
script:
- make lint-bug-report-preview
tags:
- shared-medium
.script-test:
stage: test
extends:
- .rules-branch-manual-MR-and-devel-always
script:
- make test
artifacts:
name: "bridge-linux-$CI_COMMIT_SHORT_SHA"
paths:
- coverage/**
test-linux:
extends:
- .script-test
tags:
- shared-large
fuzz-linux:
stage: test
extends:
- .rules-branch-manual-MR-and-devel-always
script:
- make fuzz
tags:
- shared-large
test-linux-race:
extends:
- test-linux
- .rules-branch-and-MR-manual
script:
- make test-race
test-integration:
extends:
- test-linux
script:
- make test-integration
test-integration-race:
extends:
- test-integration
- .rules-branch-and-MR-manual
script:
- make test-integration-race
test-integration-nightly:
extends:
- test-integration
- .rules-branch-manual-scheduled-and-test-branch-always
needs:
- test-integration
script:
- make test-integration-nightly
test-windows:
extends:
- .env-windows
- .script-test
test-darwin:
extends:
- .env-darwin
- .script-test
test-coverage:
stage: test
extends:
- .rules-branch-manual-scheduled-and-test-branch-always
script:
- ./utils/coverage.sh
coverage: '/total:.*\(statements\).*\d+\.\d+%/'
needs:
- test-linux
- test-windows
- test-darwin
- test-integration
- test-integration-nightly
tags:
- shared-small
artifacts:
paths:
- coverage*
- coverage/**
when: 'always'
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
# Stage: BUILD
.script-build:
stage: build
needs: ["lint"]
extends:
- .rules-branch-and-MR-manual
script:
- make build
- git diff && git diff-index --quiet HEAD
- make vault-editor
artifacts:
expire_in: 1 day
when: always
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
paths:
- bridge_*.tgz
- vault-editor
build-linux:
extends:
- .script-build
- .env-linux-build
build-linux-qa:
extends: build-linux
extends:
- build-linux
- .rules-branch-manual-MR-and-devel-always
variables:
BUILD_TAGS: "build_qa"
artifacts:
name: "bridge-linux-qa-$CI_COMMIT_SHORT_SHA"
.build-darwin-base:
extends: .build-base
before_script:
- export PATH=/usr/local/bin:$PATH
- export PATH=/usr/local/opt/git/bin:$PATH
- export PATH=/usr/local/opt/make/libexec/gnubin:$PATH
- export PATH=/usr/local/opt/go@1.13/bin:$PATH
- export PATH=/usr/local/opt/gnu-sed/libexec/gnubin:$PATH
- export GOPATH=~/go
- export PATH=$GOPATH/bin:$PATH
- export CGO_CPPFLAGS='-Wno-error -Wno-nullability-completeness -Wno-expansion-to-defined -Wno-builtin-requires-header'
script:
- go version
- make build
- git diff && git diff-index --quiet HEAD
cache: {}
tags:
- macOS
build-darwin:
extends: .build-darwin-base
artifacts:
name: "bridge-darwin-$CI_COMMIT_SHORT_SHA"
extends:
- .script-build
- .env-darwin
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
before_script:
- export GOROOT=/c/Go1.18/
- export PATH=$GOROOT/bin:$PATH
- export GOARCH=amd64
- export GOPATH=~/go18
- export GO111MODULE=on
- export PATH="${GOPATH}/bin:${PATH}"
- export MSYSTEM=
- export QT6DIR=/c/grrrQt/6.3.1/msvc2019_64
- export PATH=$PATH:${QT6DIR}/bin
- export PATH="/c/Program Files/Microsoft Visual Studio/2022/Community/Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/bin:$PATH"
script:
- make build
- git diff && git diff-index --quiet HEAD
tags:
- windows-bridge
build-windows:
extends: .build-windows-base
artifacts:
name: "bridge-windows-$CI_COMMIT_SHORT_SHA"
extends:
- .script-build
- .env-windows
build-windows-qa:
extends: .build-windows-base
extends:
- build-windows
variables:
BUILD_TAGS: "build_qa"
artifacts:
name: "bridge-windows-qa-$CI_COMMIT_SHORT_SHA"
# Stage: CHECK
check-gobinsec:
stage: check
needs: ["build-linux-qa"]
trigger-qa-installer:
stage: build
needs: ["lint"]
extends:
- .rules-branch-manual-MR-always
cache:
key: gobinsec-cache-v3
paths:
- ./gobinsec-cache-valid.yml
policy: pull-push
before_script:
- mkdir build
- tar -xzf bridge_linux_*.tgz -C build
- "[ ! -f ./gobinsec-cache-valid.yml ] && wget bridgeteam.protontech.ch/bridgeteam/gobinsec-cache-valid.yml"
- mv ./gobinsec-cache-valid.yml ./utils/gobinsec_update/gobinsec-cache-valid.yml
script:
- ./utils/gobinsec_update.sh
- cp ./utils/gobinsec_update/gobinsec-cache-valid.yml ./gobinsec-cache.yml
- cat ./gobinsec-cache.yml
- gobinsec -wait -cache -config utils/gobinsec_conf.yml build/bridge
- cp ./gobinsec-cache.yml ./gobinsec-cache-valid.yml # Only update cache file if gobinsec succeeds
- .rules-branch-and-MR-manual
variables:
APP: bridge
WORKFLOW: build-all
SRC_TAG: $CI_COMMIT_BRANCH
SRC_HASH: $CI_COMMIT_SHA
trigger:
project: "jcuth/bridge-release"
branch: master
# Stage: MIRROR
mirror-repo:
stage: mirror
only:
refs:
- master
script:
- |
cat <<EOF > ~/.ssh/config
Host github.com
Hostname ssh.github.com
User git
Port 443
ProxyCommand connect-proxy -H $http_proxy %h %p
EOF
- ssh-keyscan -t rsa ${CI_SERVER_HOST} > ~/.ssh/known_hosts
- |
cat <<EOF >> ~/.ssh/known_hosts
# ssh.github.com:443 SSH-2.0-babeld-2e9d163d
[ssh.github.com]:443 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==
EOF
- echo "$mirror_key" | tr -d '\r' | ssh-add - > /dev/null
- ssh-add -l
- git clone "$CI_REPOSITORY_URL" --branch master _REPO_CLONE;
- cd _REPO_CLONE
- git remote add public $mirror_url
- git push public master
# Pushing the latest tag from master history
- git push public "$(git describe --tags --abbrev=0 || echo master)"
# TODO: PUT BACK ALL THE JOBS! JUST DID THIS FOR NOW TO GET CI WORKING AGAIN...

View File

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

View File

@ -3,20 +3,23 @@
## Prerequisites
* 64-bit OS:
- the go-rfc5322 module cannot currently be compiled for 32-bit OSes
* Go 1.18
* Go 1.20
* Bash with basic build utils: make, gcc, sed, find, grep, ...
- For Windows it is recommended to use MinGW 64bit shell from [MSYS2](https://www.msys2.org/)
* GCC (linux), msvc (windows) or Xcode (macOS)
* Windres (windows)
* libglvnd and libsecret development files (linux)
- 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)
* pkg-config (Linux)
* cmake, ninja-build and Qt 6.4.3 are required to build the graphical user interface. On Linux,
the Mesa OpenGL development files are also needed.
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
In order to build Bridge app with Qt interface we are using
[Qt 6.3](https://doc.qt.io/qt-6/gettingstarted.html).
[Qt 6.4.3](https://doc.qt.io/qt-6/gettingstarted.html).
Please note that qmake path must be in your `PATH` to ensure Qt to be found.
Also, before you start build **on Windows**, please unset the `MSYSTEM` variable
@ -44,9 +47,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

@ -23,12 +23,9 @@ Proton Mail Bridge includes the following 3rd party software:
<!-- START AUTOGEN -->
* [notificator](https://github.com/0xAX/notificator) available under [license](https://github.com/0xAX/notificator/blob/master/LICENSE)
* [semver](https://github.com/Masterminds/semver/v3) available under [license](https://github.com/Masterminds/semver/v3/blob/master/LICENSE)
* [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-crypto](https://github.com/ProtonMail/go-crypto) available under [license](https://github.com/ProtonMail/go-crypto/blob/master/LICENSE)
* [go-imap-id](https://github.com/ProtonMail/go-imap-id) available under [license](https://github.com/ProtonMail/go-imap-id/blob/master/LICENSE)
* [go-rfc5322](https://github.com/ProtonMail/go-rfc5322) available under [license](https://github.com/ProtonMail/go-rfc5322/blob/master/LICENSE)
* [go-srp](https://github.com/ProtonMail/go-srp) available under [license](https://github.com/ProtonMail/go-srp/blob/master/LICENSE)
* [go-vcard](https://github.com/ProtonMail/go-vcard) available under [license](https://github.com/ProtonMail/go-vcard/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)
* [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)
@ -39,14 +36,11 @@ Proton Mail Bridge includes the following 3rd party software:
* [docker-credential-helpers](https://github.com/docker/docker-credential-helpers) available under [license](https://github.com/docker/docker-credential-helpers/blob/master/LICENSE)
* [go-sysinfo](https://github.com/elastic/go-sysinfo) available under [license](https://github.com/elastic/go-sysinfo/blob/master/LICENSE)
* [go-imap](https://github.com/emersion/go-imap) available under [license](https://github.com/emersion/go-imap/blob/master/LICENSE)
* [go-imap-appendlimit](https://github.com/emersion/go-imap-appendlimit) available under [license](https://github.com/emersion/go-imap-appendlimit/blob/master/LICENSE)
* [go-imap-move](https://github.com/emersion/go-imap-move) available under [license](https://github.com/emersion/go-imap-move/blob/master/LICENSE)
* [go-imap-quota](https://github.com/emersion/go-imap-quota) available under [license](https://github.com/emersion/go-imap-quota/blob/master/LICENSE)
* [go-imap-unselect](https://github.com/emersion/go-imap-unselect) available under [license](https://github.com/emersion/go-imap-unselect/blob/master/LICENSE)
* [go-imap-id](https://github.com/emersion/go-imap-id) available under [license](https://github.com/emersion/go-imap-id/blob/master/LICENSE)
* [go-message](https://github.com/emersion/go-message) available under [license](https://github.com/emersion/go-message/blob/master/LICENSE)
* [go-sasl](https://github.com/emersion/go-sasl) available under [license](https://github.com/emersion/go-sasl/blob/master/LICENSE)
* [go-smtp](https://github.com/emersion/go-smtp) available under [license](https://github.com/emersion/go-smtp/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)
* [color](https://github.com/fatih/color) available under [license](https://github.com/fatih/color/blob/master/LICENSE)
* [sentry-go](https://github.com/getsentry/sentry-go) available under [license](https://github.com/getsentry/sentry-go/blob/master/LICENSE)
* [resty](https://github.com/go-resty/resty/v2) available under [license](https://github.com/go-resty/resty/v2/blob/master/LICENSE)
@ -57,16 +51,15 @@ Proton Mail Bridge includes the following 3rd party software:
* [go-multierror](https://github.com/hashicorp/go-multierror) available under [license](https://github.com/hashicorp/go-multierror/blob/master/LICENSE)
* [html2text](https://github.com/jaytaylor/html2text) available under [license](https://github.com/jaytaylor/html2text/blob/master/LICENSE)
* [go-keychain](https://github.com/keybase/go-keychain) available under [license](https://github.com/keybase/go-keychain/blob/master/LICENSE)
* [aurora](https://github.com/logrusorgru/aurora) available under [license](https://github.com/logrusorgru/aurora/blob/master/LICENSE)
* [dns](https://github.com/miekg/dns) available under [license](https://github.com/miekg/dns/blob/master/LICENSE)
* [jsondiff](https://github.com/nsf/jsondiff) available under [license](https://github.com/nsf/jsondiff/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)
* [du](https://github.com/ricochet2200/go-disk-usage/du) available under [license](https://github.com/ricochet2200/go-disk-usage/du/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)
* [testify](https://github.com/stretchr/testify) available under [license](https://github.com/stretchr/testify/blob/master/LICENSE)
* [cli](https://github.com/urfave/cli/v2) available under [license](https://github.com/urfave/cli/v2/blob/master/LICENSE)
* [msgpack](https://github.com/vmihailenco/msgpack/v5) available under [license](https://github.com/vmihailenco/msgpack/v5/blob/master/LICENSE)
* [bbolt](https://go.etcd.io/bbolt) available under [license](https://github.com/etcd-io/bbolt/blob/master/LICENSE)
* [goleak](https://go.uber.org/goleak) available under [license](https://pkg.go.dev/go.uber.org/goleak?tab=licenses)
* [exp](https://golang.org/x/exp) available under [license](https://cs.opensource.google/go/x/exp/+/master:LICENSE)
* [net](https://golang.org/x/net) available under [license](https://cs.opensource.google/go/x/net/+/master:LICENSE)
* [sys](https://golang.org/x/sys) available under [license](https://cs.opensource.google/go/x/sys/+/master:LICENSE)
@ -75,10 +68,13 @@ Proton Mail Bridge includes the following 3rd party software:
* [protobuf](https://google.golang.org/protobuf) available under [license](https://github.com/protocolbuffers/protobuf/blob/main/LICENSE)
* [plist](https://howett.net/plist) available under [license](https://github.com/DHowett/go-plist/blob/main/LICENSE)
* [bcrypt](https://github.com/ProtonMail/bcrypt) available under [license](https://github.com/ProtonMail/bcrypt/blob/master/LICENSE)
* [go-crypto](https://github.com/ProtonMail/go-crypto) available under [license](https://github.com/ProtonMail/go-crypto/blob/master/LICENSE)
* [go-mime](https://github.com/ProtonMail/go-mime) available under [license](https://github.com/ProtonMail/go-mime/blob/master/LICENSE)
* [go-srp](https://github.com/ProtonMail/go-srp) available under [license](https://github.com/ProtonMail/go-srp/blob/master/LICENSE)
* [readline](https://github.com/abiosoft/readline) available under [license](https://github.com/abiosoft/readline/blob/master/LICENSE)
* [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)
* [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)
@ -87,34 +83,57 @@ Proton Mail Bridge includes the following 3rd party software:
* [wincred](https://github.com/danieljoos/wincred) available under [license](https://github.com/danieljoos/wincred/blob/master/LICENSE)
* [go-spew](https://github.com/davecgh/go-spew) available under [license](https://github.com/davecgh/go-spew/blob/master/LICENSE)
* [go-windows](https://github.com/elastic/go-windows) available under [license](https://github.com/elastic/go-windows/blob/master/LICENSE)
* [go-vcard](https://github.com/emersion/go-vcard) available under [license](https://github.com/emersion/go-vcard/blob/master/LICENSE)
* [go-textwrapper](https://github.com/emersion/go-textwrapper) available under [license](https://github.com/emersion/go-textwrapper/blob/master/LICENSE)
* [fgprof](https://github.com/felixge/fgprof) available under [license](https://github.com/felixge/fgprof/blob/master/LICENSE)
* [go-shlex](https://github.com/flynn-archive/go-shlex) available under [license](https://github.com/flynn-archive/go-shlex/blob/master/LICENSE)
* [mimetype](https://github.com/gabriel-vasile/mimetype) available under [license](https://github.com/gabriel-vasile/mimetype/blob/master/LICENSE)
* [sse](https://github.com/gin-contrib/sse) available under [license](https://github.com/gin-contrib/sse/blob/master/LICENSE)
* [gin](https://github.com/gin-gonic/gin) available under [license](https://github.com/gin-gonic/gin/blob/master/LICENSE)
* [locales](https://github.com/go-playground/locales) available under [license](https://github.com/go-playground/locales/blob/master/LICENSE)
* [universal-translator](https://github.com/go-playground/universal-translator) available under [license](https://github.com/go-playground/universal-translator/blob/master/LICENSE)
* [validator](https://github.com/go-playground/validator/v10) available under [license](https://github.com/go-playground/validator/v10/blob/master/LICENSE)
* [go-json](https://github.com/goccy/go-json) available under [license](https://github.com/goccy/go-json/blob/master/LICENSE)
* [uuid](https://github.com/gofrs/uuid) available under [license](https://github.com/gofrs/uuid/blob/master/LICENSE)
* [protobuf](https://github.com/golang/protobuf) available under [license](https://github.com/golang/protobuf/blob/master/LICENSE)
* [pprof](https://github.com/google/pprof) available under [license](https://github.com/google/pprof/blob/master/LICENSE)
* [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)
* [golang-lru](https://github.com/hashicorp/golang-lru) available under [license](https://github.com/hashicorp/golang-lru/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)
* [go-runewidth](https://github.com/mattn/go-runewidth) available under [license](https://github.com/mattn/go-runewidth/blob/master/LICENSE)
* [go-sqlite3](https://github.com/mattn/go-sqlite3) available under [license](https://github.com/mattn/go-sqlite3/blob/master/LICENSE)
* [concurrent](https://github.com/modern-go/concurrent) available under [license](https://github.com/modern-go/concurrent/blob/master/LICENSE)
* [reflect2](https://github.com/modern-go/reflect2) available under [license](https://github.com/modern-go/reflect2/blob/master/LICENSE)
* [tablewriter](https://github.com/olekukonko/tablewriter) available under [license](https://github.com/olekukonko/tablewriter/blob/master/LICENSE)
* [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-ordered-json](https://gitlab.com/c0b/go-ordered-json)
* [arch](https://golang.org/x/arch) available under [license](https://cs.opensource.google/go/x/arch/+/master:LICENSE)
* [crypto](https://golang.org/x/crypto) available under [license](https://cs.opensource.google/go/x/crypto/+/master:LICENSE)
* [mod](https://golang.org/x/mod) available under [license](https://cs.opensource.google/go/x/mod/+/master:LICENSE)
* [sync](https://golang.org/x/sync) available under [license](https://cs.opensource.google/go/x/sync/+/master:LICENSE)
* [tools](https://golang.org/x/tools) available under [license](https://cs.opensource.google/go/x/tools/+/master:LICENSE)
* [genproto](https://google.golang.org/genproto)
gopkg.in/yaml.v3
* [genproto](https://google.golang.org/genproto) available under [license](https://pkg.go.dev/google.golang.org/genproto?tab=licenses)
* [yaml](https://gopkg.in/yaml.v3) available under [license](https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE)
* [docker-credential-helpers](https://github.com/ProtonMail/docker-credential-helpers) available under [license](https://github.com/ProtonMail/docker-credential-helpers/blob/master/LICENSE)
* [go-imap](https://github.com/ProtonMail/go-imap) available under [license](https://github.com/ProtonMail/go-imap/blob/master/LICENSE)
* [go-message](https://github.com/ProtonMail/go-message) available under [license](https://github.com/ProtonMail/go-message/blob/master/LICENSE)
* [go-smtp](https://github.com/ProtonMail/go-smtp) available under [license](https://github.com/ProtonMail/go-smtp/blob/master/LICENSE)
* [resty](https://github.com/LBeernaertProton/resty/v2) available under [license](https://github.com/LBeernaertProton/resty/v2/blob/master/LICENSE)
* [go-keychain](https://github.com/cuthix/go-keychain) available under [license](https://github.com/cuthix/go-keychain/blob/master/LICENSE)
<!-- END AUTOGEN -->

File diff suppressed because it is too large Load Diff

167
Makefile
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?=2.4.5+git
BRIDGE_APP_VERSION?=3.7.1+git
APP_VERSION:=${BRIDGE_APP_VERSION}
APP_FULL_NAME:=Proton Mail Bridge
APP_VENDOR:=Proton AG
@ -19,27 +19,33 @@ SRC_ICO:=bridge.ico
SRC_ICNS:=Bridge.icns
SRC_SVG:=bridge.svg
EXE_NAME:=proton-bridge
REVISION:=$(shell git rev-parse --short=10 HEAD)
REVISION:=$(shell ./utils/get_revision.sh)
TAG:=$(shell ./utils/get_revision.sh tag)
BUILD_TIME:=$(shell date +%FT%T%z)
MACOS_MIN_VERSION=11.0
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/v2/internal/constants., Version=${APP_VERSION} Revision=${REVISION} BuildTime=${BUILD_TIME})
GO_LDFLAGS+=-X "github.com/ProtonMail/proton-bridge/v2/internal/constants.FullAppName=${APP_FULL_NAME}"
GO_LDFLAGS:=$(addprefix -X github.com/ProtonMail/proton-bridge/v3/internal/constants., Version=${APP_VERSION} Revision=${REVISION} Tag=${TAG} BuildTime=${BUILD_TIME})
GO_LDFLAGS+=-X "github.com/ProtonMail/proton-bridge/v3/internal/constants.FullAppName=${APP_FULL_NAME}"
ifneq "${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
GO_LDFLAGS_LAUNCHER+=-H=windowsgui
#GO_LDFLAGS+=-H=windowsgui # Disabled so we can inspect trace logs from the bridge for debugging.
GO_LDFLAGS_LAUNCHER+=-H=windowsgui # Having this flag prevent a temporary cmd.exe window from popping when starting the application on Windows 11.
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})
@ -88,16 +94,16 @@ go-build=go build $(1) -o $(2) $(3)
go-build-finalize=${go-build}
ifeq "${GOOS}-$(shell uname -m)" "darwin-arm64"
go-build-finalize= \
MACOSX_DEPLOYMENT_TARGET=${MACOS_MIN_VERSION} CGO_ENABLED=1 CGO_CFLAGS="-mmacosx-version-min=${MACOS_MIN_VERSION}" GOARCH=arm64 $(call go-build,$(1),$(2)_arm,$(3)) && \
MACOSX_DEPLOYMENT_TARGET=${MACOS_MIN_VERSION} CGO_ENABLED=1 CGO_CFLAGS="-mmacosx-version-min=${MACOS_MIN_VERSION}" GOARCH=amd64 $(call go-build,$(1),$(2)_amd,$(3)) && \
MACOSX_DEPLOYMENT_TARGET=${MACOS_MIN_VERSION_ARM64} CGO_ENABLED=1 CGO_CFLAGS="-mmacosx-version-min=${MACOS_MIN_VERSION_ARM64}" GOARCH=arm64 $(call go-build,$(1),$(2)_arm,$(3)) && \
MACOSX_DEPLOYMENT_TARGET=${MACOS_MIN_VERSION_AMD64} CGO_ENABLED=1 CGO_CFLAGS="-mmacosx-version-min=${MACOS_MIN_VERSION_AMD64}" GOARCH=amd64 $(call go-build,$(1),$(2)_amd,$(3)) && \
lipo -create -output $(2) $(2)_arm $(2)_amd && rm -f $(2)_arm $(2)_amd
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}
@ -110,6 +116,9 @@ build-launcher: ${RESOURCE_FILE}
versioner:
go build ${BUILD_FLAGS} -o versioner utils/versioner/main.go
vault-editor:
$(call go-build-finalize,"-tags=debug","vault-editor","./utils/vault-editor/main.go")
hasher:
go build -o hasher utils/hasher/main.go
@ -150,8 +159,11 @@ ${EXE_TARGET}: check-build-essentials ${EXE_NAME}
BRIDGE_VENDOR="${APP_VENDOR}" \
BRIDGE_APP_VERSION=${APP_VERSION} \
BRIDGE_REVISION=${REVISION} \
BRIDGE_TAG=${TAG} \
BRIDGE_DSN_SENTRY=${DSN_SENTRY} \
BRIDGE_BUILD_TIME=${BUILD_TIME} \
BRIDGE_GUI_BUILD_CONFIG=Release \
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}"
@ -173,7 +185,7 @@ ${RESOURCE_FILE}: ./dist/info.rc ./dist/${SRC_ICO} .FORCE
## Dev dependencies
.PHONY: install-devel-tools install-linter install-go-mod-outdated install-git-hooks
LINTVER:="v1.50.0"
LINTVER:="v1.52.2"
LINTSRC:="https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh"
install-dev-dependencies: install-devel-tools install-linter install-go-mod-outdated
@ -217,10 +229,50 @@ add-license:
change-copyright-year:
./utils/missing_license.sh change-year
GOCOVERAGE=-covermode=count -coverpkg=github.com/ProtonMail/proton-bridge/v3/internal/...,github.com/ProtonMail/proton-bridge/v3/pkg/...,
GOCOVERDIR=-args -test.gocoverdir=$$PWD/coverage
test: gofiles
go test -coverprofile=/tmp/coverage.out -run=${TESTRUN} \
./internal/...\
./pkg/...
mkdir -p coverage/unit-${GOOS}
go test \
-v -timeout=20m -p=1 -count=1 \
${GOCOVERAGE} \
-run=${TESTRUN} ./internal/... ./pkg/... \
${GOCOVERDIR}/unit-${GOOS}
test-race: gofiles
go test -v -timeout=40m -p=1 -count=1 -race -failfast -run=${TESTRUN} ./internal/... ./pkg/...
test-integration: gofiles
mkdir -p coverage/integration
go test \
-v -timeout=60m -p=1 -count=1 -tags=test_integration \
${GOCOVERAGE} \
github.com/ProtonMail/proton-bridge/v3/tests \
${GOCOVERDIR}/integration
test-integration-debug: gofiles
dlv test github.com/ProtonMail/proton-bridge/v3/tests -- -test.v -test.timeout=10m -test.parallel=1 -test.count=1
test-integration-race: gofiles
go test -v -timeout=60m -p=1 -count=1 -race -failfast github.com/ProtonMail/proton-bridge/v3/tests
test-integration-nightly: gofiles
mkdir -p coverage/integration
go test \
-v -timeout=90m -p=1 -count=1 -tags=test_integration \
${GOCOVERAGE} \
github.com/ProtonMail/proton-bridge/v3/tests \
${GOCOVERDIR}/integration \
nightly
fuzz: gofiles
go test -fuzz=FuzzUnmarshal -parallel=4 -fuzztime=60s $(PWD)/internal/legacy/credentials
go test -fuzz=FuzzNewParser -parallel=4 -fuzztime=60s $(PWD)/pkg/message/parser
go test -fuzz=FuzzReadHeaderBody -parallel=4 -fuzztime=60s $(PWD)/pkg/message
go test -fuzz=FuzzDecodeHeader -parallel=4 -fuzztime=60s $(PWD)/pkg/mime
go test -fuzz=FuzzDecodeCharset -parallel=4 -fuzztime=60s $(PWD)/pkg/mime
bench:
go test -run '^$$' -bench=. -memprofile bench_mem.pprof -cpuprofile bench_cpu.pprof ./internal/store
@ -230,18 +282,31 @@ bench:
coverage: test
go tool cover -html=/tmp/coverage.out -o=coverage.html
integration-test-bridge:
${MAKE} -C test test-bridge
mocks:
mockgen --package mocks github.com/ProtonMail/proton-bridge/v2/internal/users Locator,PanicHandler,CredentialsStorer,StoreMaker > internal/users/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/v2/pkg/listener Listener > internal/users/mocks/listener_mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/v2/internal/store PanicHandler,BridgeUser,ChangeNotifier,Storer > internal/store/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/v2/pkg/listener Listener > internal/store/mocks/utils_mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/v2/pkg/pmapi Client,Manager > pkg/pmapi/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/v2/pkg/message Fetcher > pkg/message/mocks/mocks.go
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/gluon/async PanicHandler > internal/bridge/mocks/async_mocks.go
mockgen --package mocks github.com/ProtonMail/gluon/reporter Reporter > internal/bridge/mocks/gluon_mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/updater Downloader,Installer > internal/updater/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/telemetry HeartbeatManager > internal/telemetry/mocks/mocks.go
cp internal/telemetry/mocks/mocks.go internal/bridge/mocks/telemetry_mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/services/userevents \
EventSource,EventIDStore > internal/services/userevents/mocks/mocks.go
mockgen --package userevents github.com/ProtonMail/proton-bridge/v3/internal/services/userevents \
EventSubscriber,MessageEventHandler,LabelEventHandler,AddressEventHandler,RefreshEventHandler,UserEventHandler,UserUsedSpaceEventHandler > tmp
mv tmp internal/services/userevents/mocks_test.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/events EventPublisher \
> internal/events/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/services/useridentity IdentityProvider,Telemetry \
> internal/services/useridentity/mocks/mocks.go
mockgen --self_package "github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice" -package syncservice github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice \
ApplyStageInput,BuildStageInput,BuildStageOutput,DownloadStageInput,DownloadStageOutput,MetadataStageInput,MetadataStageOutput,\
StateProvider,Regulator,UpdateApplier,MessageBuilder,APIClient,Reporter,DownloadRateModifier \
> tmp
mv tmp internal/services/syncservice/mocks_test.go
mockgen --package mocks github.com/ProtonMail/gluon/connector IMAPStateWrite > internal/services/imapservice/mocks/mocks.go
lint: gofiles lint-golang lint-license lint-dependencies lint-changelog
lint: gofiles lint-golang lint-license lint-dependencies lint-changelog lint-bug-report
lint-license:
./utils/missing_license.sh check
@ -257,6 +322,12 @@ lint-golang:
$(info linting with GOMAXPROCS=${GOMAXPROCS})
golangci-lint run ./...
lint-bug-report:
python3 utils/validate_bug_report_file.py --file "internal/frontend/bridge-gui/bridge-gui/qml/Resources/bug_report_flow.json"
lint-bug-report-preview:
python3 utils/validate_bug_report_file.py --file "internal/frontend/bridge-gui/bridge-gui/qml/Resources/bug_report_flow.json" --preview
gobinsec: gobinsec-cache.yml build
gobinsec -wait -cache -config utils/gobinsec_conf.yml ${EXE_TARGET} ${DEPLOY_DIR}/${TARGET_OS}/${LAUNCHER_EXE}
@ -271,7 +342,7 @@ updates: install-go-mod-outdated
doc:
godoc -http=:6060
release-notes: release-notes/bridge_stable.html release-notes/bridge_early.html
release-notes: release-notes/bridge_stable.html release-notes/bridge_early.html utils/release_notes.sh
release-notes/%.html: release-notes/%.md
./utils/release_notes.sh $^
@ -284,7 +355,7 @@ gofiles: ./internal/bridge/credits.go
cd ./utils/ && ./credits.sh bridge
## Run and debug
.PHONY: run run-qt run-qt-cli run-nogui run-nogui-cli 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
@ -295,18 +366,46 @@ run: run-qt
run-cli: run-nogui
run-noninteractive: build-nogui clean-vendor gofiles
PROTONMAIL_ENV=dev ./${LAUNCHER_EXE} ${RUN_FLAGS} -n
run-qt: build-gui
ifeq "${TARGET_OS}" "darwin"
PROTONMAIL_ENV=dev ${DARWINAPP_CONTENTS}/MacOS/${LAUNCHER_EXE} ${RUN_FLAGS}
else
PROTONMAIL_ENV=dev ./${DEPLOY_DIR}/${TARGET_OS}/${LAUNCHER_EXE} ${RUN_FLAGS}
endif
run-nogui: build-nogui clean-vendor gofiles
PROTONMAIL_ENV=dev ./${LAUNCHER_EXE} ${RUN_FLAGS} -c
run-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:
@ -329,6 +428,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

@ -1,4 +1,4 @@
// Copyright (c) 2022 Proton AG
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
@ -17,6 +17,14 @@
package main
import (
"os"
"strings"
"github.com/ProtonMail/proton-bridge/v3/internal/app"
"github.com/bradenaw/juniper/xslices"
)
/*
___....___
^^ __..-:'':__:..:__:'':-..__
@ -34,41 +42,6 @@ package main
~~^_~^~/ \~^-~^~ _~^-~_^~-^~_^~~-^~_~^~-~_~-^~_^/ \~^ ~~_ ^
*/
import (
"os"
"github.com/ProtonMail/proton-bridge/v2/internal/app/base"
"github.com/ProtonMail/proton-bridge/v2/internal/app/bridge"
"github.com/ProtonMail/proton-bridge/v2/internal/constants"
"github.com/sirupsen/logrus"
)
const (
appUsage = "Proton Mail IMAP and SMTP Bridge"
configName = "bridge"
updateURLName = "bridge"
keychainName = "bridge"
cacheVersion = "c11"
)
func main() {
base, err := base.New(
constants.FullAppName,
appUsage,
configName,
updateURLName,
keychainName,
cacheVersion,
)
if err != nil {
logrus.WithError(err).Fatal("Failed to create app base")
}
// Other instance already running.
if base == nil {
return
}
if err := bridge.New(base).Run(os.Args); err != nil {
logrus.WithError(err).Fatal("Bridge exited with error")
}
_ = app.New().Run(xslices.Filter(os.Args, func(arg string) bool { return !strings.Contains(arg, "-psn_") }))
}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2022 Proton AG
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
@ -18,22 +18,23 @@
package main
import (
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"time"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/v2/internal/config/useragent"
"github.com/ProtonMail/proton-bridge/v2/internal/constants"
"github.com/ProtonMail/proton-bridge/v2/internal/crash"
"github.com/ProtonMail/proton-bridge/v2/internal/locations"
"github.com/ProtonMail/proton-bridge/v2/internal/logging"
"github.com/ProtonMail/proton-bridge/v2/internal/sentry"
"github.com/ProtonMail/proton-bridge/v2/internal/updater"
"github.com/ProtonMail/proton-bridge/v2/internal/versioner"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/crash"
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
"github.com/ProtonMail/proton-bridge/v3/internal/sentry"
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
"github.com/ProtonMail/proton-bridge/v3/internal/versioner"
"github.com/bradenaw/juniper/xslices"
"github.com/elastic/go-sysinfo"
"github.com/elastic/go-sysinfo/types"
@ -44,86 +45,118 @@ import (
const (
appName = "Proton Mail Launcher"
configName = "bridge"
exeName = "bridge"
guiName = "bridge-gui"
launcherName = "launcher"
FlagCLI = "--cli"
FlagCLIShort = "-c"
FlagCLI = "cli"
FlagCLIShort = "c"
FlagNonInteractive = "noninteractive"
FlagNonInteractiveShort = "n"
FlagLauncher = "--launcher"
FlagWait = "--wait"
FlagSessionID = "--session-id"
)
func main() { //nolint:funlen
reporter := sentry.NewReporter(appName, constants.Version, useragent.New())
logrus.SetLevel(logrus.DebugLevel)
l := logrus.WithField("launcher_version", constants.Version)
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, configName))
locationsProvider, err := locations.NewDefaultProvider(filepath.Join(constants.VendorName, constants.ConfigName))
if err != nil {
logrus.WithError(err).Fatal("Failed to get locations provider")
l.WithError(err).Fatal("Failed to get locations provider")
}
locations := locations.New(locationsProvider, configName)
locations := locations.New(locationsProvider, constants.ConfigName)
logsPath, err := locations.ProvideLogsPath()
if err != nil {
logrus.WithError(err).Fatal("Failed to get logs path")
}
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath))
if err := logging.Init(logsPath); err != nil {
logrus.WithError(err).Fatal("Failed to setup logging")
l.WithError(err).Fatal("Failed to get logs path")
}
logging.SetLevel(os.Getenv("VERBOSITY"))
sessionID := logging.NewSessionID()
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath, sessionID, launcherName))
var closer io.Closer
if closer, err = logging.Init(
logsPath,
sessionID,
logging.LauncherShortAppName,
logging.DefaultMaxLogFileSize,
logging.NoPruning,
os.Getenv("VERBOSITY"),
); err != nil {
l.WithError(err).Fatal("Failed to setup logging")
}
defer func() {
_ = logging.Close(closer)
}()
updatesPath, err := locations.ProvideUpdatesPath()
if err != nil {
logrus.WithError(err).Fatal("Failed to get updates path")
l.WithError(err).Fatal("Failed to get updates path")
}
key, err := crypto.NewKeyFromArmored(updater.DefaultPublicKey)
if err != nil {
logrus.WithError(err).Fatal("Failed to create new verification key")
l.WithError(err).Fatal("Failed to create new verification key")
}
kr, err := crypto.NewKeyRing(key)
if err != nil {
logrus.WithError(err).Fatal("Failed to create new verification keyring")
l.WithError(err).Fatal("Failed to create new verification keyring")
}
versioner := versioner.New(updatesPath)
exeToLaunch := guiName
args := os.Args[1:]
if inCLIMode(args) {
exeToLaunch = exeName
}
exe, err := getPathToUpdatedExecutable(exeToLaunch, versioner, kr, reporter)
if err != nil {
if exe, err = getFallbackExecutable(exeToLaunch, versioner); err != nil {
logrus.WithError(err).Fatal("Failed to find any launchable executable")
}
}
launcher, err := os.Executable()
if err != nil {
logrus.WithError(err).Fatal("Failed to determine path to launcher")
}
args, wait, mainExe := findAndStripWait(args)
if wait {
waitForProcessToFinish(mainExe)
l = l.WithField("launcher_path", launcher)
args := os.Args[1:]
exe, err := getPathToUpdatedExecutable(filepath.Base(launcher), versioner, kr)
if err != nil {
exeToLaunch := guiName
if inCLIMode(args) {
exeToLaunch = exeName
}
cmd := execabs.Command(exe, appendLauncherPath(launcher, args)...) //nolint:gosec
l = l.WithField("exe_to_launch", exeToLaunch)
l.WithError(err).Info("No more updates found, looking up bridge executable")
path, err := versioner.GetExecutableInDirectory(exeToLaunch, filepath.Dir(launcher))
if err != nil {
l.WithError(err).Fatal("No executable in launcher directory")
}
exe = path
}
l = l.WithField("exe_path", exe)
args, wait, mainExes := findAndStripWait(args)
if wait {
for _, mainExe := range mainExes {
waitForProcessToFinish(mainExe)
}
}
cmd := execabs.Command(exe, appendLauncherPath(launcher, append(args, FlagSessionID, string(sessionID)))...) //nolint:gosec
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = os.Environ()
// On windows, if you use Run(), a terminal stays open; we don't want that.
if //goland:noinspection GoBoolExpressions
@ -134,7 +167,7 @@ func main() { //nolint:funlen
}
if err != nil {
logrus.WithError(err).Fatal("Failed to launch")
l.WithError(err).Fatal("Failed to launch")
}
}
@ -148,16 +181,21 @@ func appendLauncherPath(path string, args []string) []string {
return args
}
// inCLIMode detect if CLI mode is asked.
func inCLIMode(args []string) bool {
return sliceContains(args, FlagCLI) || sliceContains(args, FlagCLIShort)
}
// sliceContains checks if a value is present in a list.
func sliceContains[T comparable](list []T, s T) bool {
return xslices.Any(list, func(arg T) bool { return arg == s })
}
// inCLIMode detect if CLI mode is asked.
func inCLIMode(args []string) bool {
return hasFlag(args, FlagCLI) || hasFlag(args, FlagCLIShort) || hasFlag(args, FlagNonInteractive) || hasFlag(args, FlagNonInteractiveShort)
}
// hasFlag checks if a flag is present in a list.
func hasFlag(args []string, flag string) bool {
return xslices.Any(args, func(arg string) bool { return (arg == "-"+flag) || (arg == "--"+flag) })
}
// findAndStrip check if a value is present in s list and remove all occurrences of the value from this list.
func findAndStrip[T comparable](slice []T, v T) (strippedList []T, found bool) {
strippedList = xslices.Filter(slice, func(value T) bool {
@ -167,12 +205,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
@ -181,23 +218,24 @@ 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(
name string,
versioner *versioner.Versioner,
ver *versioner.Versioner,
kr *crypto.KeyRing,
reporter *sentry.Reporter,
) (string, error) {
versions, err := versioner.ListVersions()
versions, err := ver.ListVersions()
if err != nil {
return "", errors.Wrap(err, "failed to list available versions")
}
@ -208,15 +246,15 @@ func getPathToUpdatedExecutable(
}
for _, version := range versions {
vlog := logrus.WithField("version", version)
vlog := logrus.WithFields(logrus.Fields{
"version": constants.Version,
"check_version": version,
"name": name,
})
if err := version.VerifyFiles(kr); err != nil {
vlog.WithError(err).Error("Files failed verification and will be removed")
if err := reporter.ReportMessage(fmt.Sprintf("version %v failed verification: %v", version, err)); err != nil {
vlog.WithError(err).Error("Failed to report corrupt update files")
}
if err := version.Remove(); err != nil {
vlog.WithError(err).Error("Failed to remove files")
}
@ -241,17 +279,6 @@ func getPathToUpdatedExecutable(
return "", errors.New("no available newer versions")
}
func getFallbackExecutable(name string, versioner *versioner.Versioner) (string, error) {
logrus.Info("Searching for fallback executable")
launcher, err := os.Executable()
if err != nil {
return "", errors.Wrap(err, "failed to determine path to launcher")
}
return versioner.GetExecutableInDirectory(name, filepath.Dir(launcher))
}
// waitForProcessToFinish waits until the process with the given path is finished.
func waitForProcessToFinish(exePath string) {
for {
@ -270,7 +297,8 @@ func waitForProcessToFinish(exePath string) {
if xslices.Any(processes, func(process types.Process) bool {
info, err := process.Info()
if err != nil {
logrus.WithError(err).Error("Could not retrieve process info")
logrus.WithError(err).Trace("Could not retrieve process info")
return false
}
return sameFile(exeInfo, info.Exe)
@ -288,6 +316,7 @@ func sameFile(info os.FileInfo, path string) bool {
pathInfo, err := os.Stat(path)
if err != nil {
logrus.WithError(err).WithField("file", path).Error("Could not retrieve file info")
return false
}
return os.SameFile(pathInfo, info)

View File

@ -1,4 +1,4 @@
// Copyright (c) 2022 Proton AG
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
@ -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"}))
}

22
dist/bridgeMacOS.svg vendored Normal file
View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 260 260" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g>
<g transform="matrix(1.27944,0,0,1.35453,-34.9539,-16.0513)">
<path d="M40,62.391C40,38.979 58.979,20 82.391,20L177.609,20C201.021,20 220,38.979 220,62.391L220,157.609C220,181.021 201.021,200 177.609,200L82.391,200C58.979,200 40,181.021 40,157.609L40,62.391Z" style="fill:white;fill-rule:nonzero;"/>
</g>
<g transform="matrix(1.41874,0,0,1.41874,-55.214,-18.9171)">
<path d="M129.748,48.657C101.923,48.657 79.369,71.21 79.369,99.035L79.369,149.747C79.369,155.139 83.74,159.509 89.131,159.509L171.407,159.509C176.22,159.509 180.126,155.604 180.126,150.79L180.126,99.035C180.126,71.214 157.572,48.657 129.748,48.657ZM158.746,98.755L136.726,117.305C132.752,120.655 126.939,120.655 122.965,117.305L100.945,98.755C100.945,83.014 113.708,70.251 129.45,70.251L130.242,70.251C145.983,70.251 158.746,83.014 158.746,98.755Z" style="fill:rgb(109,74,255);fill-rule:nonzero;"/>
</g>
<g transform="matrix(1.41874,0,0,1.41874,-55.214,-18.9171)">
<path d="M129.748,48.657C101.923,48.657 79.369,71.21 79.369,99.035L79.369,149.748C79.369,155.139 83.74,159.509 89.131,159.509L171.407,159.509C176.22,159.509 180.126,155.604 180.126,150.79L180.126,99.035C180.126,71.214 157.572,48.657 129.748,48.657ZM158.746,98.755L136.726,117.305C132.752,120.655 126.939,120.655 122.965,117.305L100.945,98.755C100.945,83.014 113.708,70.251 129.45,70.251L130.242,70.251C145.983,70.251 158.746,83.014 158.746,98.755Z" style="fill:url(#_Linear1);fill-rule:nonzero;"/>
</g>
<g transform="matrix(1.41874,0,0,1.41874,-55.214,-18.9171)">
<path d="M136.764,117.529C134.468,119.388 128.499,121.99 122.989,117.529C117.479,113.069 106.044,103.208 101.015,98.835L101.041,98.835L100.946,98.756C100.946,83.014 113.709,70.251 129.45,70.251L130.242,70.251C145.984,70.251 158.746,83.014 158.746,98.756L158.652,98.835L158.737,98.835L158.737,159.51L171.407,159.51C176.221,159.51 180.126,155.604 180.126,150.79L180.126,99.035C180.126,71.214 157.573,48.657 129.748,48.657C101.923,48.657 79.37,71.211 79.37,99.035L79.37,102.219L110.526,129.008C112.822,131.195 118.857,134.256 124.629,129.008C130.401,123.761 135.124,119.169 136.764,117.529Z" style="fill:url(#_Radial2);fill-rule:nonzero;"/>
</g>
</g>
<defs>
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(15.0864,-42.636,42.636,15.0864,82.9769,172.3)"><stop offset="0" style="stop-color:rgb(40,176,232);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(197,183,255);stop-opacity:0"/></linearGradient>
<radialGradient id="_Radial2" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-94.8157,-85.2706,69.7189,-77.5232,174.186,168.693)"><stop offset="0" style="stop-color:rgb(226,219,255);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(109,74,255);stop-opacity:1"/></radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

2
dist/info.rc vendored
View File

@ -3,7 +3,7 @@
IDI_ICON1 ICON DISCARDABLE STRINGIZE(ICO_FILE)
#define FILE_COMMENTS "The Bridge is an application that runs on your computer in the background and seamlessly encrypts and decrypts your mail as it enters and leaves your computer."
#define FILE_COMMENTS "Proton Mail Bridge is a desktop application that runs in the background, encrypting and decrypting messages as they enter and leave your computer."
#define FILE_DESCRIPTION "Proton Mail Bridge"
#define INTERNAL_NAME STRINGIZE(EXE_NAME)
#define PRODUCT_NAME "Proton Mail Bridge for Windows"

View File

@ -3,7 +3,7 @@ Type=Application
Version=1.1
Name=Proton Mail Bridge
GenericName=Proton Mail Bridge for Linux
Comment=The Bridge is an application that runs on your computer in the background and seamlessly encrypts and decrypts your mail as it enters and leaves your computer.
Comment=Proton Mail Bridge is a desktop application that runs in the background, encrypting and decrypting messages as they enter and leave your computer.
Icon=protonmail-bridge
Exec=protonmail-bridge
Terminal=false

View File

@ -2,13 +2,13 @@
## First login and sync
When user logs in to the bridge for the first time, immediatelly starts the first sync.
When user logs in to the bridge for the first time, immediately starts the first sync.
First sync downloads all headers of all e-mails and creates database to have proper UIDs
and indexes for IMAP. See [database](database.md) for more information.
By default, whenever it's possible, sync downloads only all e-mails maiblox which already
have list of labels so we can construct all mailboxes (inbox, sent, trash, custom folders
and lables) without need to download each e-mail headers many times.
and labels) without need to download each e-mail headers many times.
Note that we need to download also bodies to calculate size of the e-mail and set proper
content type (clients uses content type for guess if e-mail contains attachment)--but only
@ -22,7 +22,7 @@ client right after adding account.
When account is added to client, client start the sync. This sync will ask Bridge app
for all headers (done quickly) and then starts to download all bodies and attachment.
Unfortunatelly for some e-mail more than once if the same e-mail is in more mailboxes
Unfortunately for some e-mail more than once if the same e-mail is in more mailboxes
(e.g. inbox and all mail)--there is no way to tell over IMAP it's the same message.
After successful login of client to IMAP, Bridge starts event loop. That periodicly ask
@ -37,7 +37,7 @@ sequenceDiagram
Note right of B: Set up PM account<br/>by user
loop First sync
B ->> S: Fetch body and attachements
B ->> S: Fetch body and attachments
Note right of B: Build local database<br/>(e-mail UIDs)
end
@ -58,8 +58,8 @@ sequenceDiagram
C ->> B: IMAP SELECT directory
C ->> B: IMAP SEARCH e-mails UIDs
C ->> B: IMAP FETCH of e-mail UID
B ->> S: Fetch body and attachements
Note right of B: Decrypt message<br/>and attachement
B ->> S: Fetch body and attachments
Note right of B: Decrypt message<br/>and attachment
B ->> C: IMAP response
end
```

View File

@ -1,12 +1,12 @@
# Update mechanism of Bridge
There are mulitple options how to change version of application:
There are multiple options how to change version of application:
* Automatic in-app update
* Manual in-app update
* Manual install
In-app update ends with restarting bridge into new version. Automatic in-app
update is downloading, verifying and installing the new version immediatelly
update is downloading, verifying and installing the new version immediately
without user confirmation. For manual in-app update user needs to confirm first.
Update is done from special update file published on website.
@ -25,7 +25,7 @@ The bridge is installed and executed differently for given OS:
* macOS app does not use launcher
* No launcher, only one executable
* In-App udpate replaces the bridge files in installation path directly
* In-App update replaces the bridge files in installation path directly
```mermaid

2
extern/vcpkg vendored

113
go.mod
View File

@ -1,36 +1,30 @@
module github.com/ProtonMail/proton-bridge/v2
module github.com/ProtonMail/proton-bridge/v3
go 1.18
go 1.20
require (
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
github.com/Masterminds/semver/v3 v3.1.1
github.com/Masterminds/semver/v3 v3.2.0
github.com/ProtonMail/gluon v0.17.1-0.20231114153341-2ecbdd2739f7
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
github.com/ProtonMail/go-crypto v0.0.0-20220824120805-4b6e5c587895
github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde
github.com/ProtonMail/go-rfc5322 v0.11.0
github.com/ProtonMail/go-srp v0.0.5
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5
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.20231116144214-8a47c8d92fbc
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton
github.com/PuerkitoBio/goquery v1.8.1
github.com/abiosoft/ishell v2.0.0+incompatible
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37
github.com/bradenaw/juniper v0.8.0
github.com/bradenaw/juniper v0.12.0
github.com/cucumber/godog v0.12.5
github.com/cucumber/messages-go/v16 v16.0.1
github.com/docker/docker-credential-helpers v0.6.4
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
github.com/emersion/go-imap-appendlimit v0.0.0-20210907172056-e3baed77bbe4
github.com/emersion/go-imap-move v0.0.0-20210907172020-fe4558f9c872
github.com/emersion/go-imap-quota v0.0.0-20210203125329-619074823f3c
github.com/emersion/go-imap-unselect v0.0.0-20210907172115-4c2c4843bf69
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
github.com/emersion/go-smtp v0.15.0
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594
github.com/emersion/go-smtp v0.15.1-0.20221021114529-49b17434419d
github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3
github.com/fatih/color v1.13.0
github.com/getsentry/sentry-go v0.13.0
github.com/getsentry/sentry-go v0.15.0
github.com/go-resty/resty/v2 v2.7.0
github.com/godbus/dbus v4.1.0+incompatible
github.com/golang/mock v1.6.0
@ -38,71 +32,96 @@ require (
github.com/google/uuid v1.3.0
github.com/hashicorp/go-multierror v1.1.1
github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba
github.com/keybase/go-keychain v0.0.0-20220610143837-c2ce06069005
github.com/logrusorgru/aurora v2.0.3+incompatible
github.com/keybase/go-keychain v0.0.0
github.com/miekg/dns v1.1.50
github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58
github.com/pkg/errors v0.9.1
github.com/ricochet2200/go-disk-usage/du v0.0.0-20210707232629-ac9918953285
github.com/sirupsen/logrus v1.9.0
github.com/stretchr/testify v1.8.0
github.com/urfave/cli/v2 v2.16.3
github.com/pkg/profile v1.7.0
github.com/sirupsen/logrus v1.9.2
github.com/stretchr/testify v1.8.3
github.com/urfave/cli/v2 v2.24.4
github.com/vmihailenco/msgpack/v5 v5.3.5
go.etcd.io/bbolt v1.3.6
golang.org/x/exp v0.0.0-20220921164117-439092de6870
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.49.0
google.golang.org/protobuf v1.28.1
go.uber.org/goleak v1.2.1
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
golang.org/x/net v0.17.0
golang.org/x/sys v0.13.0
golang.org/x/text v0.13.0
google.golang.org/grpc v1.56.3
google.golang.org/protobuf v1.30.0
howett.net/plist v1.0.0
)
require (
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // indirect
github.com/ProtonMail/go-mime v0.0.0-20220429130430-2192574d760f // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230717121622-edf196117233 // indirect
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect
github.com/ProtonMail/go-srp v0.0.7 // indirect
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect
github.com/andybalholm/cascadia v1.3.1 // indirect
github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 // indirect
github.com/andybalholm/cascadia v1.3.2 // indirect
github.com/bytedance/sonic v1.9.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/chzyer/test v1.0.0 // indirect
github.com/cloudflare/circl v1.2.0 // indirect
github.com/cloudflare/circl v1.3.3 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/cronokirby/saferith v0.33.0 // indirect
github.com/cucumber/gherkin-go/v19 v19.0.3 // indirect
github.com/danieljoos/wincred v1.1.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/elastic/go-windows v1.0.1 // indirect
github.com/emersion/go-vcard v0.0.0-20220507122617-d4056df0ec4a // indirect
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect
github.com/felixge/fgprof v0.9.3 // indirect
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.9.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.14.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/gofrs/uuid v4.3.0+incompatible // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/go-memdb v1.3.3 // indirect
github.com/hashicorp/golang-lru v0.5.4 // 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.4 // 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.19 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/mattn/go-sqlite3 v1.14.17 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/pierrec/lz4/v4 v4.1.17 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
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/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
golang.org/x/crypto v0.1.0 // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/tools v0.1.12 // indirect
google.golang.org/genproto v0.0.0-20220921223823-23cae91e6737 // indirect
gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/mod v0.8.0 // indirect
golang.org/x/sync v0.2.0 // indirect
golang.org/x/tools v0.6.0 // indirect
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace (
github.com/docker/docker-credential-helpers => github.com/ProtonMail/docker-credential-helpers v1.1.0
github.com/emersion/go-imap => github.com/ProtonMail/go-imap v0.0.0-20201228133358-4db68cea0cac
github.com/emersion/go-message => github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753
github.com/keybase/go-keychain => github.com/cuthix/go-keychain v0.0.0-20220405075754-31e7cee908fe
github.com/emersion/go-message => github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7
github.com/emersion/go-smtp => github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865
github.com/go-resty/resty/v2 => github.com/LBeernaertProton/resty/v2 v2.0.0-20231030122409-92db8bee3605
github.com/keybase/go-keychain => github.com/cuthix/go-keychain v0.0.0-20230517073537-fc1740a83768
)

282
go.sum
View File

@ -15,39 +15,37 @@ github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557 h1:l6surSnJ3RP4qA
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/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/LBeernaertProton/resty/v2 v2.0.0-20231030122409-92db8bee3605 h1:54Fh3JS6s2Tjy6ZIRLtt1amZOqfYDcjErdye45z8fkQ=
github.com/LBeernaertProton/resty/v2 v2.0.0-20231030122409-92db8bee3605/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A=
github.com/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.17.1-0.20231114153341-2ecbdd2739f7 h1:w+VoSAq9FQvKMm3DlH1MIEZ1KGe7LJ+81EJFVwSV4VU=
github.com/ProtonMail/gluon v0.17.1-0.20231114153341-2ecbdd2739f7/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo=
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
github.com/ProtonMail/go-crypto v0.0.0-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-imap v0.0.0-20201228133358-4db68cea0cac h1:2xU3QncAiS/W3UlWZTkbNKW5WkLzk6Egl1T0xX+sbjs=
github.com/ProtonMail/go-imap v0.0.0-20201228133358-4db68cea0cac/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU=
github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde h1:5koQozTDELymYOyFbQ/VSubexAEXzDR8qGM5mO8GRdw=
github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde/go.mod h1:795VPXcRUIQ9JyMNHP4el582VokQfippgjkQP3Gk0r0=
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-rfc5322 v0.11.0 h1:o5Obrm4DpmQEffvgsVqG6S4BKwC1Wat+hYwjIp2YcCY=
github.com/ProtonMail/go-rfc5322 v0.11.0/go.mod h1:6oOKr0jXvpoE6pwTx/HukigQpX2J9WUf6h0auplrFTw=
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/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5 h1:Uga1DHFN4GUxuDQr0F71tpi8I9HqPIlZodZAI1lR6VQ=
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5/go.mod h1:oeP9CMN+ajWp5jKp1kue5daJNwMMxLF+ujPaUIoJWlA=
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/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
github.com/ProtonMail/go-crypto v0.0.0-20230717121622-edf196117233 h1:bdoKdh0f66/lrgVfYlxw0aqISY/KOqXmFJyGt7rGmnc=
github.com/ProtonMail/go-crypto v0.0.0-20230717121622-edf196117233/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7 h1:+j+Kd/DyZ/qGfMT9htAT7HxqIEbZHsatsx+m8AoV6fc=
github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
github.com/ProtonMail/go-proton-api v0.4.1-0.20231116144214-8a47c8d92fbc h1:GBRKoFAldApEMkMrsFN1ZxG0eG797w6LTv/dFMDcsqQ=
github.com/ProtonMail/go-proton-api v0.4.1-0.20231116144214-8a47c8d92fbc/go.mod h1:WEXJqj5DSc2YI77SgXdpMY0nk33Qy92Vu2r4tOEazA8=
github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865 h1:EP1gnxLL5Z7xBSymE9nSTM27nRYINuvssAtDmG0suD8=
github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton h1:8tqHYM6IGsdEc6Vxf1TWiwpHNj8yIEQNACPhxsDagrk=
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton/go.mod h1:omVkSsfPAhmptzPF/piMXb16wKIWUvVhZbVW7sJKh0A=
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw=
github.com/abiosoft/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzgHf/aS6+zVXRj14cVk9qg=
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8=
@ -56,11 +54,9 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37 h1:28uU3TtuvQ6KRndxg9TrC868jBWmSKgh0GTXkACCXmA=
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37/go.mod h1:6AXRstqK+32jeFmw89QGL2748+dj34Av4xc/I9oo9BY=
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
github.com/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/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
@ -68,19 +64,28 @@ 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.12.0 h1:Q/7icpPQD1nH/La5DobQfNEtwyrBSiSu47jOQx7lJEM=
github.com/bradenaw/juniper v0.12.0/go.mod h1:Z2B7aJlQ7xbfWsnMLROj5t/5FQ94/MkIdKC30J4WvzI=
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/bwesterb/go-ristretto v1.2.1/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
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.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
@ -98,8 +103,8 @@ github.com/cucumber/godog v0.12.5/go.mod h1:u6SD7IXC49dLpPN35kal0oYEjsXZWee4pW6T
github.com/cucumber/messages-go/v16 v16.0.0/go.mod h1:EJcyR5Mm5ZuDsKJnT2N9KRnBK30BGjtYotDKpwQ0v6g=
github.com/cucumber/messages-go/v16 v16.0.1 h1:fvkpwsLgnIm0qugftrw2YwNlio+ABe2Iu94Ap8GMYIY=
github.com/cucumber/messages-go/v16 v16.0.1/go.mod h1:EJcyR5Mm5ZuDsKJnT2N9KRnBK30BGjtYotDKpwQ0v6g=
github.com/cuthix/go-keychain v0.0.0-20220405075754-31e7cee908fe h1:KRj3wdvA9yE92prNmOjS7x5DOqoyjxqdE30qnrmTasc=
github.com/cuthix/go-keychain v0.0.0-20220405075754-31e7cee908fe/go.mod h1:ZoZU1fnBy3mOLWr3Pg+Y2+nTKtu6ypDte2kZg9HvSwY=
github.com/cuthix/go-keychain v0.0.0-20230517073537-fc1740a83768 h1:Jrcoxtrk4qpuzKIYPlEkjIK0M+bABs0oW2QzrOuwlzk=
github.com/cuthix/go-keychain v0.0.0-20230517073537-fc1740a83768/go.mod h1:ZoZU1fnBy3mOLWr3Pg+Y2+nTKtu6ypDte2kZg9HvSwY=
github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg=
github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0=
github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0=
@ -112,41 +117,49 @@ 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-appendlimit v0.0.0-20210907172056-e3baed77bbe4 h1:U6LL6F1dYqXpVTwEbXhcfU8hgpNvmjB9xeOAiHN695o=
github.com/emersion/go-imap-appendlimit v0.0.0-20210907172056-e3baed77bbe4/go.mod h1:ikgISoP7pRAolqsVP64yMteJa2FIpS6ju88eBT6K1yQ=
github.com/emersion/go-imap-move v0.0.0-20210907172020-fe4558f9c872 h1:HGBfonz0q/zq7y3ew+4oy4emHSvk6bkmV0mdDG3E77M=
github.com/emersion/go-imap-move v0.0.0-20210907172020-fe4558f9c872/go.mod h1:QuMaZcKFDVI0yCrnAbPLfbwllz1wtOrZH8/vZ5yzp4w=
github.com/emersion/go-imap-quota v0.0.0-20210203125329-619074823f3c h1:khcEdu1yFiZjBgi7gGnQiLhpSgghJ0YTnKD0l4EUqqc=
github.com/emersion/go-imap-quota v0.0.0-20210203125329-619074823f3c/go.mod h1:iApyhIQBiU4XFyr+3kdJyyGqle82TbQyuP2o+OZHrV0=
github.com/emersion/go-imap-unselect v0.0.0-20210907172115-4c2c4843bf69 h1:ltTnRlPdSMMb0a/pg7S31T3g+syYeSS5UVJtiR7ez1Y=
github.com/emersion/go-imap-unselect v0.0.0-20210907172115-4c2c4843bf69/go.mod h1:+gnnZx3Mg3MnCzZrv0eZdp5puxXQUgGT/6N6L7ShKfM=
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
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=
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead h1:fI1Jck0vUrXT8bnphprS1EoVRe2Q5CKCX8iDlpqjQ/Y=
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-smtp v0.15.0 h1:3+hMGMGrqP/lqd7qoxZc1hTU8LY8gHV9RFGWlqSDmP8=
github.com/emersion/go-smtp v0.15.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/emersion/go-vcard v0.0.0-20220507122617-d4056df0ec4a h1:cltZpe6s0SJtqK5c/5y2VrIYi8BAtDM6qjmiGYqfTik=
github.com/emersion/go-vcard v0.0.0-20220507122617-d4056df0ec4a/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3 h1:hQ1wTMaKcGfobYRT88RM8NFNyX+IQHvagkm/tqViU98=
github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
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=
github.com/getsentry/sentry-go v0.13.0 h1:20dgTiUSfxRB/EhMPtxcL9ZEbM1ZdR+W/7f7NWD+xWo=
github.com/getsentry/sentry-go v0.13.0/go.mod h1:EOsfu5ZdvKPfeHYV6pTVQnsjfp30+XA7//UooKNumH0=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/getsentry/sentry-go v0.15.0 h1:CP9bmA7pralrVUedYZsmIHWpq/pBtXTSew7xvVpfLaA=
github.com/getsentry/sentry-go v0.15.0/go.mod h1:RZPJKSw+adu8PBNygiri/A98FqVr2HtRckJk9XVxJ9I=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-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-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.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4=
github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
@ -165,8 +178,8 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@ -174,9 +187,12 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
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=
@ -222,6 +238,7 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO
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=
@ -230,22 +247,27 @@ github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 h1:rp+c0RAYOWj8
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
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 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
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 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
@ -254,11 +276,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.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
@ -271,21 +296,31 @@ github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS4
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249 h1:NHrXEjTNQY7P0Zfx1aMrNhpgxHmow66XQtm0aQLY0AE=
github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249/go.mod h1:mpRZBD8SJ55OIICQ3iWH0Yz3cjzA61JdqMLoWXeB2+8=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/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.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc=
github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
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.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=
@ -300,13 +335,12 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z
github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/ricochet2200/go-disk-usage/du v0.0.0-20210707232629-ac9918953285 h1:d54EL9l+XteliUfUCGsEwwuk65dmmxX85VXF+9T6+50=
github.com/ricochet2200/go-disk-usage/du v0.0.0-20210707232629-ac9918953285/go.mod h1:fxIDly1xtudczrZeOOlfaUvd2OPb2qZAPuWdU2BsBTk=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
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.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@ -314,9 +348,8 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
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/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y=
github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
@ -335,21 +368,28 @@ 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/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/urfave/cli/v2 v2.16.3 h1:gHoFIwpPjoyIMbJp/VFd+/vuD0dAgFK4B6DpEMFJfQk=
github.com/urfave/cli/v2 v2.16.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.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/urfave/cli/v2 v2.24.4 h1:0gyJJEBYtCV87zI/x2nZCPyDxD51K6xM8SkwjHFCNEU=
github.com/urfave/cli/v2 v2.24.4/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
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=
@ -358,33 +398,38 @@ 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/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a h1:DxppxFKRqJ8WD6oJ3+ZXKDY0iMONQDl5UTg2aTyHh8k=
gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a/go.mod h1:NREvu3a57BaK0R1+ztrEzHWiZAihohNLQ6trPxlIqZI=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
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.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.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.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.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
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-20220921164117-439092de6870 h1:j8b6j9gzSigH28O5SjSpQSSh9lFd6f5D/q0aHjNTulc=
golang.org/x/exp v0.0.0-20220921164117-439092de6870/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -396,14 +441,12 @@ golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHl
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mobile v0.0.0-20200801112145-973feb4309de/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4=
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=
@ -422,10 +465,15 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-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.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -435,7 +483,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-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -445,7 +496,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=
@ -453,7 +503,6 @@ golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -462,22 +511,44 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-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.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.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.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -496,11 +567,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.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -523,17 +594,17 @@ 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-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.49.0 h1:WTLtQzmQori5FUH25Pq4WT22oCsv8USpQ+F6rqtsmxw=
google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc=
google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@ -559,3 +630,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

@ -1,85 +0,0 @@
// Copyright (c) 2022 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 api provides HTTP API of the Bridge.
//
// API endpoints:
// - /focus, see focusHandler
package api
import (
"fmt"
"net/http"
"time"
"github.com/ProtonMail/proton-bridge/v2/internal/bridge"
"github.com/ProtonMail/proton-bridge/v2/internal/config/settings"
"github.com/ProtonMail/proton-bridge/v2/internal/events"
"github.com/ProtonMail/proton-bridge/v2/pkg/listener"
"github.com/ProtonMail/proton-bridge/v2/pkg/ports"
"github.com/sirupsen/logrus"
)
var log = logrus.WithField("pkg", "api") //nolint:gochecknoglobals
type apiServer struct {
host string
settings *settings.Settings
eventListener listener.Listener
}
// NewAPIServer returns prepared API server struct.
func NewAPIServer(settings *settings.Settings, eventListener listener.Listener) *apiServer { //nolint:revive
return &apiServer{
host: bridge.Host,
settings: settings,
eventListener: eventListener,
}
}
// Starts the server.
func (api *apiServer) ListenAndServe() {
mux := http.NewServeMux()
mux.HandleFunc("/focus", wrapper(api, focusHandler))
addr := api.getAddress()
server := &http.Server{
Addr: addr,
Handler: mux,
ReadHeaderTimeout: 5 * time.Second, // fix gosec G112 (vulnerability to [Slowloris](https://www.cloudflare.com/en-gb/learning/ddos/ddos-attack-tools/slowloris/) attack).
}
log.Info("API listening at ", addr)
if err := server.ListenAndServe(); err != nil {
api.eventListener.Emit(events.ErrorEvent, "API failed: "+err.Error())
log.Error("API failed: ", err)
}
defer server.Close() //nolint:errcheck
}
func (api *apiServer) getAddress() string {
port := api.settings.GetInt(settings.APIPortKey)
newPort := ports.FindFreePortFrom(port)
if newPort != port {
api.settings.SetInt(settings.APIPortKey, newPort)
}
return getAPIAddress(api.host, newPort)
}
func getAPIAddress(host string, port int) string {
return fmt.Sprintf("%s:%d", host, port)
}

View File

@ -1,51 +0,0 @@
// Copyright (c) 2022 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 api
import (
"net/http"
"github.com/ProtonMail/proton-bridge/v2/pkg/listener"
)
// httpHandler with Go's Response and Request.
type httpHandler func(http.ResponseWriter, *http.Request)
// handler with our context.
type handler func(handlerContext) error
type handlerContext struct {
req *http.Request
resp http.ResponseWriter
eventListener listener.Listener
}
func wrapper(api *apiServer, callback handler) httpHandler {
return func(w http.ResponseWriter, req *http.Request) {
ctx := handlerContext{
req: req,
resp: w,
eventListener: api.eventListener,
}
err := callback(ctx)
if err != nil {
log.Error("API callback of ", req.URL, " failed: ", err)
http.Error(w, err.Error(), 500)
}
}
}

View File

@ -1,51 +0,0 @@
// Copyright (c) 2022 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 api
import (
"fmt"
"net/http"
"github.com/ProtonMail/proton-bridge/v2/internal/bridge"
"github.com/ProtonMail/proton-bridge/v2/internal/events"
)
// focusHandler should be called from other instances (attempt to start bridge
// for the second time) to get focus in the currently running instance.
func focusHandler(ctx handlerContext) error {
log.Info("Focus from other instance")
ctx.eventListener.Emit(events.SecondInstanceEvent, "")
fmt.Fprintf(ctx.resp, "OK")
return nil
}
// CheckOtherInstanceAndFocus is helper for new instances to check if there is
// already a running instance and get it's focus.
func CheckOtherInstanceAndFocus(port int) error {
addr := getAPIAddress(bridge.Host, port)
resp, err := (&http.Client{}).Get("http://" + addr + "/focus")
if err != nil {
return err
}
defer resp.Body.Close() //nolint:errcheck
if resp.StatusCode != 200 {
log.Error("Focus error: ", resp.StatusCode)
}
return nil
}

507
internal/app/app.go Normal file
View File

@ -0,0 +1,507 @@
// 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 app
import (
"fmt"
"io"
"net/http"
"net/http/cookiejar"
"net/url"
"os"
"path/filepath"
"runtime"
"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"
"github.com/ProtonMail/proton-bridge/v3/internal/crash"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/focus"
"github.com/ProtonMail/proton-bridge/v3/internal/frontend/theme"
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
"github.com/ProtonMail/proton-bridge/v3/internal/sentry"
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
"github.com/ProtonMail/proton-bridge/v3/pkg/restarter"
"github.com/pkg/profile"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
// Visible flags.
const (
flagCPUProfile = "cpu-prof"
flagCPUProfileShort = "p"
flagTraceProfile = "trace-prof"
flagTraceProfileShort = "t"
flagMemProfile = "mem-prof"
flagMemProfileShort = "m"
flagLogLevel = "log-level"
flagLogLevelShort = "l"
flagGRPC = "grpc"
flagGRPCShort = "g"
flagCLI = "cli"
flagCLIShort = "c"
flagNonInteractive = "noninteractive"
flagNonInteractiveShort = "n"
flagLogIMAP = "log-imap"
flagLogSMTP = "log-smtp"
)
// Hidden flags.
const (
flagLauncher = "launcher"
flagNoWindow = "no-window"
flagParentPID = "parent-pid"
flagSoftwareRenderer = "software-renderer"
flagSessionID = "session-id"
)
const (
appUsage = "Proton Mail IMAP and SMTP Bridge"
appShortName = "bridge"
)
func New() *cli.App {
app := cli.NewApp()
app.Name = constants.FullAppName
app.Usage = appUsage
app.Flags = []cli.Flag{
&cli.BoolFlag{
Name: flagCPUProfile,
Aliases: []string{flagCPUProfileShort},
Usage: "Generate CPU profile",
},
&cli.BoolFlag{
Name: flagTraceProfile,
Aliases: []string{flagTraceProfileShort},
Usage: "Generate Trace profile",
},
&cli.BoolFlag{
Name: flagMemProfile,
Aliases: []string{flagMemProfileShort},
Usage: "Generate memory profile",
},
&cli.StringFlag{
Name: flagLogLevel,
Aliases: []string{flagLogLevelShort},
Usage: "Set the log level (one of panic, fatal, error, warn, info, debug)",
},
&cli.BoolFlag{
Name: flagGRPC,
Aliases: []string{flagGRPCShort},
Usage: "Start the gRPC service",
},
&cli.BoolFlag{
Name: flagCLI,
Aliases: []string{flagCLIShort},
Usage: "Start the command line interface",
},
&cli.BoolFlag{
Name: flagNonInteractive,
Aliases: []string{flagNonInteractiveShort},
Usage: "Start the app in non-interactive mode",
},
&cli.StringFlag{
Name: flagLogIMAP,
Usage: "Enable logging of IMAP communications (all|client|server) (may contain decrypted data!)",
},
&cli.BoolFlag{
Name: flagLogSMTP,
Usage: "Enable logging of SMTP communications (may contain decrypted data!)",
},
// Hidden flags
&cli.BoolFlag{
Name: flagNoWindow,
Usage: "Don't show window after start",
Hidden: true,
},
&cli.StringFlag{
Name: flagLauncher,
Usage: "The launcher used to start the app",
Hidden: true,
},
&cli.IntFlag{
Name: flagParentPID,
Usage: "Process ID of the parent",
Hidden: true,
Value: -1,
},
&cli.BoolFlag{
Name: flagSoftwareRenderer, // This flag is ignored by bridge, but should be passed to launcher in case of restart, so it need to be accepted by the CLI parser.
Usage: "GUI is using software renderer",
Hidden: true,
Value: false,
},
&cli.StringFlag{
Name: flagSessionID,
Hidden: true,
},
}
app.Action = run
return app
}
func run(c *cli.Context) error {
// Get the current bridge version.
version, err := semver.NewVersion(constants.Version)
if err != nil {
return fmt.Errorf("could not create version: %w", err)
}
// Create a user agent that will be used for all requests.
identifier := useragent.New()
// Create a new Sentry client that will be used to report crashes etc.
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
// the current exe, and fall back to os.Args[0] if that fails.
var exe string
if launcher := c.String(flagLauncher); launcher != "" {
exe = launcher
} else if executable, err := os.Executable(); err == nil {
exe = executable
} else {
exe = os.Args[0]
}
var logCloser io.Closer
defer func() {
_ = logging.Close(logCloser)
}()
// Restart the app if requested.
err = withRestarter(exe, func(restarter *restarter.Restarter) error {
// Handle crashes with various actions.
return withCrashHandler(restarter, reporter, func(crashHandler *crash.Handler, quitCh <-chan struct{}) error {
migrationErr := migrateOldVersions()
// 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.
if err := migrateKeychainHelper(locations); err != nil {
logrus.WithError(err).Error("Failed to migrate keychain helper")
}
// Initialize logging.
return withLogging(c, crashHandler, locations, func(closer io.Closer) error {
logCloser = closer
// If there was an error during migration, log it now.
if migrationErr != nil {
logrus.WithError(migrationErr).Error("Failed to migrate old app data")
}
// Ensure we are the only instance running.
settings, err := locations.ProvideSettingsPath()
if err != nil {
logrus.WithError(err).Error("Failed to get settings path")
}
return withSingleInstance(settings, locations.GetLockFile(), version, func() error {
// Look for available keychains
return WithKeychainList(func(keychains *keychain.List) error {
// Unlock the encrypted vault.
return WithVault(locations, keychains, crashHandler, func(v *vault.Vault, insecure, corrupt bool) error {
if !v.Migrated() {
// Migrate old settings into the vault.
if err := migrateOldSettings(v); err != nil {
logrus.WithError(err).Error("Failed to migrate old settings")
}
// Migrate old accounts into the vault.
if err := migrateOldAccounts(locations, keychains, v); err != nil {
logrus.WithError(err).Error("Failed to migrate old accounts")
}
// The vault has been migrated.
if err := v.SetMigrated(); err != nil {
logrus.WithError(err).Error("Failed to mark vault as migrated")
}
}
logrus.WithFields(logrus.Fields{
"lastVersion": v.GetLastVersion().String(),
"showAllMail": v.GetShowAllMail(),
"updateCh": v.GetUpdateChannel(),
"autoUpdate": v.GetAutoUpdate(),
"rollout": v.GetUpdateRollout(),
"DoH": v.GetProxyAllowed(),
}).Info("Vault loaded")
// Load the cookies from the vault.
return withCookieJar(v, func(cookieJar http.CookieJar) error {
// Create a new bridge instance.
return withBridge(c, exe, locations, version, identifier, crashHandler, reporter, v, cookieJar, keychains, func(b *bridge.Bridge, eventCh <-chan events.Event) error {
if insecure {
logrus.Warn("The vault key could not be retrieved; the vault will not be encrypted")
b.PushError(bridge.ErrVaultInsecure)
}
if corrupt {
logrus.Warn("The vault is corrupt and has been wiped")
b.PushError(bridge.ErrVaultCorrupt)
}
// Remove old updates files
b.RemoveOldUpdates()
// Run the frontend.
return runFrontend(c, crashHandler, restarter, locations, b, eventCh, quitCh, c.Int(flagParentPID))
})
})
})
})
})
})
})
})
})
})
// if an error occurs, it must be logged now because we're about to close the log file.
if err != nil {
logrus.Fatal(err)
}
return err
}
// If there's another instance already running, try to raise it and exit.
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(settingPath, lockFile, version)
if err != nil {
logrus.Info("Another instance is already running; raising it")
if ok := focus.TryRaise(settingPath); !ok {
return fmt.Errorf("another instance is already running but it could not be raised")
}
logrus.Info("The other instance has been raised")
return nil
}
defer func() {
if err := lock.Close(); err != nil {
logrus.WithError(err).Error("Failed to close lock file")
}
}()
return fn()
}
// Initialize our logging system.
func withLogging(c *cli.Context, crashHandler *crash.Handler, locations *locations.Locations, fn func(closer io.Closer) error) error {
logrus.Debug("Initializing logging")
defer logrus.Debug("Logging stopped")
// Get a place to keep our logs.
logsPath, err := locations.ProvideLogsPath()
if err != nil {
return fmt.Errorf("could not provide logs path: %w", err)
}
logrus.WithField("path", logsPath).Debug("Received logs path")
// Initialize logging.
sessionID := logging.NewSessionIDFromString(c.String(flagSessionID))
var closer io.Closer
if closer, err = logging.Init(
logsPath,
sessionID,
logging.BridgeShortAppName,
logging.DefaultMaxLogFileSize,
logging.DefaultPruningSize,
c.String(flagLogLevel),
); err != nil {
return fmt.Errorf("could not initialize logging: %w", err)
}
// Ensure we dump a stack trace if we crash.
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath, sessionID, appShortName))
logrus.
WithField("appName", constants.FullAppName).
WithField("version", constants.Version).
WithField("revision", constants.Revision).
WithField("tag", constants.Tag).
WithField("build", constants.BuildTime).
WithField("runtime", runtime.GOOS).
WithField("args", os.Args).
WithField("SentryID", sentry.GetProtectedHostname()).
Info("Run app")
return fn(closer)
}
// WithLocations provides access to locations where we store our files.
func WithLocations(fn func(*locations.Locations) error) error {
logrus.Debug("Creating locations")
defer logrus.Debug("Locations stopped")
// Create a locations provider to determine where to store our files.
provider, err := locations.NewDefaultProvider(filepath.Join(constants.VendorName, constants.ConfigName))
if err != nil {
return fmt.Errorf("could not create locations provider: %w", err)
}
// Create a new locations object that will be used to provide paths to store files.
return fn(locations.New(provider, constants.ConfigName))
}
// Start profiling if requested.
func withProfiler(c *cli.Context, fn func() error) error {
defer logrus.Debug("Profiler stopped")
if c.Bool(flagCPUProfile) {
logrus.Debug("Running with CPU profiling")
defer profile.Start(profile.CPUProfile, profile.ProfilePath(".")).Stop()
}
if c.Bool(flagTraceProfile) {
logrus.Debug("Running with Trace profiling")
defer profile.Start(profile.TraceProfile, profile.ProfilePath(".")).Stop()
}
if c.Bool(flagMemProfile) {
logrus.Debug("Running with memory profiling")
defer profile.Start(profile.MemProfile, profile.MemProfileAllocs, profile.ProfilePath(".")).Stop()
}
return fn()
}
// Restart the app if necessary.
func withRestarter(exe string, fn func(*restarter.Restarter) error) error {
logrus.Debug("Creating restarter")
defer logrus.Debug("Restarter stopped")
restarter := restarter.New(exe)
defer restarter.Restart()
return fn(restarter)
}
// Handle crashes if they occur.
func withCrashHandler(restarter *restarter.Restarter, reporter *sentry.Reporter, fn func(*crash.Handler, <-chan struct{}) error) error {
logrus.Debug("Creating crash handler")
defer logrus.Debug("Crash handler stopped")
crashHandler := crash.NewHandler(crash.ShowErrorNotification(constants.FullAppName))
defer async.HandlePanic(crashHandler)
// On crash, send crash report to Sentry.
crashHandler.AddRecoveryAction(reporter.ReportException)
// On crash, notify the user and restart the app.
crashHandler.AddRecoveryAction(crash.ShowErrorNotification(constants.FullAppName))
// On crash, restart the app.
crashHandler.AddRecoveryAction(func(any) error { restarter.Set(true, true); return nil })
// quitCh is closed when the app is quitting.
quitCh := make(chan struct{})
// On crash, quit the app.
crashHandler.AddRecoveryAction(func(any) error { close(quitCh); return nil })
return fn(crashHandler, quitCh)
}
// Use a custom cookie jar to persist values across runs.
func withCookieJar(vault *vault.Vault, fn func(http.CookieJar) error) error {
logrus.Debug("Creating cookie jar")
defer logrus.Debug("Cookie jar stopped")
// Create the underlying cookie jar.
jar, err := cookiejar.New(nil)
if err != nil {
return fmt.Errorf("could not create cookie jar: %w", err)
}
// Create the cookie jar which persists to the vault.
persister, err := cookies.NewCookieJar(jar, vault)
if err != nil {
return fmt.Errorf("could not create cookie jar: %w", err)
}
if err := setDeviceCookies(persister); err != nil {
return fmt.Errorf("could not set device cookies: %w", err)
}
// Persist the cookies to the vault when we close.
defer func() {
logrus.Debug("Persisting cookies")
if err := persister.PersistCookies(); err != nil {
logrus.WithError(err).Error("Failed to persist cookies")
}
}()
return fn(persister)
}
// WithKeychainList init the list of usable keychains.
func WithKeychainList(fn func(*keychain.List) error) error {
logrus.Debug("Creating keychain list")
defer logrus.Debug("Keychain list stop")
return fn(keychain.NewList())
}
func setDeviceCookies(jar *cookies.Jar) error {
url, err := url.Parse(constants.APIHost)
if err != nil {
return err
}
for name, value := range map[string]string{
"hhn": sentry.GetProtectedHostname(),
"tz": sentry.GetTimeZone(),
"lng": sentry.GetSystemLang(),
"clr": string(theme.DefaultTheme()),
} {
jar.SetCookies(url, []*http.Cookie{{Name: name, Value: value, Secure: true}})
}
return nil
}

View File

@ -1,35 +0,0 @@
// Copyright (c) 2022 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 base
import "strings"
// StripProcessSerialNumber removes additional flag from macOS.
// More info:
// http://mirror.informatimago.com/next/developer.apple.com/documentation/Carbon/Reference/Process_Manager/prmref_main/data_type_5.html#//apple_ref/doc/uid/TP30000208/C001951
func StripProcessSerialNumber(args []string) []string {
res := args[:0]
for _, arg := range args {
if !strings.Contains(arg, "-psn_") {
res = append(res, arg)
}
}
return res
}

View File

@ -1,430 +0,0 @@
// Copyright (c) 2022 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 base implements a common application base currently shared by bridge and IE.
// The base includes the following:
// - access to standard filesystem locations like config, cache, logging dirs
// - an extensible crash handler
// - versioned cache directory
// - persistent settings
// - event listener
// - credentials store
// - pmapi Manager
//
// In addition, the base initialises logging and reacts to command line arguments
// which control the log verbosity and enable cpu/memory profiling.
package base
import (
"math/rand"
"os"
"path/filepath"
"runtime"
"runtime/pprof"
"time"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/go-autostart"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/v2/internal/api"
"github.com/ProtonMail/proton-bridge/v2/internal/config/cache"
"github.com/ProtonMail/proton-bridge/v2/internal/config/settings"
"github.com/ProtonMail/proton-bridge/v2/internal/config/tls"
"github.com/ProtonMail/proton-bridge/v2/internal/config/useragent"
"github.com/ProtonMail/proton-bridge/v2/internal/constants"
"github.com/ProtonMail/proton-bridge/v2/internal/cookies"
"github.com/ProtonMail/proton-bridge/v2/internal/crash"
"github.com/ProtonMail/proton-bridge/v2/internal/events"
"github.com/ProtonMail/proton-bridge/v2/internal/locations"
"github.com/ProtonMail/proton-bridge/v2/internal/logging"
"github.com/ProtonMail/proton-bridge/v2/internal/sentry"
"github.com/ProtonMail/proton-bridge/v2/internal/updater"
"github.com/ProtonMail/proton-bridge/v2/internal/users/credentials"
"github.com/ProtonMail/proton-bridge/v2/internal/versioner"
"github.com/ProtonMail/proton-bridge/v2/pkg/keychain"
"github.com/ProtonMail/proton-bridge/v2/pkg/listener"
"github.com/ProtonMail/proton-bridge/v2/pkg/pmapi"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
const (
flagCPUProfile = "cpu-prof"
flagCPUProfileShort = "p"
flagMemProfile = "mem-prof"
flagMemProfileShort = "m"
flagLogLevel = "log-level"
flagLogLevelShort = "l"
FlagGRPC = "grpc" // FlagGRPC starts the gRPC frontend
FlagGRPCShort = "g"
FlagCLI = "cli" // FlagCLI indicate to start with command line interface.
flagCLIShort = "c"
flagRestart = "restart"
FlagLauncher = "launcher"
FlagNoWindow = "no-window"
)
type Base struct {
SentryReporter *sentry.Reporter
CrashHandler *crash.Handler
Locations *locations.Locations
Settings *settings.Settings
Lock *os.File
Cache *cache.Cache
Listener listener.Listener
Creds *credentials.Store
CM pmapi.Manager
CookieJar *cookies.Jar
UserAgent *useragent.UserAgent
Updater *updater.Updater
Versioner *versioner.Versioner
TLS *tls.TLS
Autostart *autostart.App
Name string // the app's name
usage string // the app's usage description
command string // the command used to launch the app (either the exe path or the launcher path)
restart bool // whether the app is currently set to restart
launcher string // launcher to be used if not set in args
mainExecutable string // mainExecutable the main executable process.
teardown []func() error // actions to perform when app is exiting
}
func New( //nolint:funlen
appName,
appUsage,
configName,
updateURLName,
keychainName,
cacheVersion string,
) (*Base, error) {
userAgent := useragent.New()
sentryReporter := sentry.NewReporter(appName, constants.Version, userAgent)
crashHandler := crash.NewHandler(
sentryReporter.ReportException,
crash.ShowErrorNotification(appName),
)
defer crashHandler.HandlePanic()
rand.Seed(time.Now().UnixNano())
os.Args = StripProcessSerialNumber(os.Args)
locationsProvider, err := locations.NewDefaultProvider(filepath.Join(constants.VendorName, configName))
if err != nil {
return nil, err
}
locations := locations.New(locationsProvider, configName)
logsPath, err := locations.ProvideLogsPath()
if err != nil {
return nil, err
}
if err := logging.Init(logsPath); err != nil {
return nil, err
}
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath))
if err := migrateFiles(configName); err != nil {
logrus.WithError(err).Warn("Old config files could not be migrated")
}
if err := locations.Clean(); err != nil {
return nil, err
}
settingsPath, err := locations.ProvideSettingsPath()
if err != nil {
return nil, err
}
settingsObj := settings.New(settingsPath)
lock, err := checkSingleInstance(locations.GetLockFile(), settingsObj)
if err != nil {
logrus.WithError(err).Warnf("%v is already running", appName)
return nil, api.CheckOtherInstanceAndFocus(settingsObj.GetInt(settings.APIPortKey))
}
if err := migrateRebranding(settingsObj, keychainName); err != nil {
logrus.WithError(err).Warn("Rebranding migration failed")
}
cachePath, err := locations.ProvideCachePath()
if err != nil {
return nil, err
}
cache, err := cache.New(cachePath, cacheVersion)
if err != nil {
return nil, err
}
if err := cache.RemoveOldVersions(); err != nil {
return nil, err
}
listener := listener.New()
events.SetupEvents(listener)
// If we can't load the keychain for whatever reason,
// we signal to frontend and supply a dummy keychain that always returns errors.
kc, err := keychain.NewKeychain(settingsObj, keychainName)
if err != nil {
listener.Emit(events.CredentialsErrorEvent, err.Error())
kc = keychain.NewMissingKeychain()
}
cfg := pmapi.NewConfig(configName, constants.Version)
cfg.GetUserAgent = userAgent.String
cfg.UpgradeApplicationHandler = func() { listener.Emit(events.UpgradeApplicationEvent, "") }
cfg.TLSIssueHandler = func() { listener.Emit(events.TLSCertIssue, "") }
cm := pmapi.New(cfg)
sentryReporter.SetClientFromManager(cm)
cm.AddConnectionObserver(pmapi.NewConnectionObserver(
func() { listener.Emit(events.InternetConnChangedEvent, events.InternetOff) },
func() { listener.Emit(events.InternetConnChangedEvent, events.InternetOn) },
))
jar, err := cookies.NewCookieJar(settingsObj)
if err != nil {
return nil, err
}
cm.SetCookieJar(jar)
key, err := crypto.NewKeyFromArmored(updater.DefaultPublicKey)
if err != nil {
return nil, err
}
kr, err := crypto.NewKeyRing(key)
if err != nil {
return nil, err
}
updatesDir, err := locations.ProvideUpdatesPath()
if err != nil {
return nil, err
}
versioner := versioner.New(updatesDir)
installer := updater.NewInstaller(versioner)
updater := updater.New(
cm,
installer,
settingsObj,
kr,
semver.MustParse(constants.Version),
updateURLName,
runtime.GOOS,
)
exe, err := os.Executable()
if err != nil {
return nil, err
}
autostart := &autostart.App{
Name: startupNameForRebranding(appName),
DisplayName: appName,
Exec: []string{exe, "--" + FlagNoWindow},
}
return &Base{
SentryReporter: sentryReporter,
CrashHandler: crashHandler,
Locations: locations,
Settings: settingsObj,
Lock: lock,
Cache: cache,
Listener: listener,
Creds: credentials.NewStore(kc),
CM: cm,
CookieJar: jar,
UserAgent: userAgent,
Updater: updater,
Versioner: versioner,
TLS: tls.New(settingsPath),
Autostart: autostart,
Name: appName,
usage: appUsage,
// By default, the command is the app's executable.
// This can be changed at runtime by using the "--launcher" flag.
command: exe,
// By default, the command is the app's executable.
// This can be changed at runtime by summoning the SetMainExecutable gRPC call.
mainExecutable: exe,
}, nil
}
func (b *Base) NewApp(mainLoop func(*Base, *cli.Context) error) *cli.App {
app := cli.NewApp()
app.Name = b.Name
app.Usage = b.usage
app.Version = constants.Version
app.Action = b.wrapMainLoop(mainLoop)
app.Flags = []cli.Flag{
&cli.BoolFlag{
Name: flagCPUProfile,
Aliases: []string{flagCPUProfileShort},
Usage: "Generate CPU profile",
},
&cli.BoolFlag{
Name: flagMemProfile,
Aliases: []string{flagMemProfileShort},
Usage: "Generate memory profile",
},
&cli.StringFlag{
Name: flagLogLevel,
Aliases: []string{flagLogLevelShort},
Usage: "Set the log level (one of panic, fatal, error, warn, info, debug)",
},
&cli.BoolFlag{
Name: FlagGRPC,
Aliases: []string{FlagGRPCShort},
Usage: "Start the gRPC service",
},
&cli.BoolFlag{
Name: FlagCLI,
Aliases: []string{flagCLIShort},
Usage: "Use command line interface",
},
&cli.BoolFlag{
Name: FlagNoWindow,
Usage: "Don't show window after start",
},
&cli.StringFlag{
Name: flagRestart,
Usage: "The number of times the application has already restarted",
Hidden: true,
},
&cli.StringFlag{
Name: FlagLauncher,
Usage: "The launcher to use to restart the application",
Hidden: true,
},
}
return app
}
// SetToRestart sets the app to restart the next time it is closed.
func (b *Base) SetToRestart() {
b.restart = true
}
func (b *Base) ForceLauncher(launcher string) {
b.launcher = launcher
b.setupLauncher(launcher)
}
func (b *Base) SetMainExecutable(exe string) {
logrus.Info("Main Executable set to ", exe)
b.mainExecutable = exe
}
// AddTeardownAction adds an action to perform during app teardown.
func (b *Base) AddTeardownAction(fn func() error) {
b.teardown = append(b.teardown, fn)
}
func (b *Base) wrapMainLoop(appMainLoop func(*Base, *cli.Context) error) cli.ActionFunc { //nolint:funlen
return func(c *cli.Context) error {
defer b.CrashHandler.HandlePanic()
defer func() { _ = b.Lock.Close() }()
// If launcher was used to start the app, use that for restart
// and autostart.
if launcher := c.String(FlagLauncher); launcher != "" {
b.setupLauncher(launcher)
}
if c.Bool(flagCPUProfile) {
startCPUProfile()
defer pprof.StopCPUProfile()
}
if c.Bool(flagMemProfile) {
defer makeMemoryProfile()
}
logging.SetLevel(c.String(flagLogLevel))
b.CM.SetLogging(logrus.WithField("pkg", "pmapi"), logrus.GetLevel() == logrus.TraceLevel)
logrus.
WithField("appName", b.Name).
WithField("version", constants.Version).
WithField("revision", constants.Revision).
WithField("build", constants.BuildTime).
WithField("runtime", runtime.GOOS).
WithField("args", os.Args).
Info("Run app")
b.CrashHandler.AddRecoveryAction(func(interface{}) error {
sentry.Flush(2 * time.Second)
if c.Int(flagRestart) > maxAllowedRestarts {
logrus.
WithField("restart", c.Int("restart")).
Warn("Not restarting, already restarted too many times")
os.Exit(1)
return nil
}
return b.restartApp(true)
})
if err := appMainLoop(b, c); err != nil {
return err
}
if err := b.doTeardown(); err != nil {
return err
}
if b.restart {
return b.restartApp(false)
}
return nil
}
}
func (b *Base) doTeardown() error {
for _, action := range b.teardown {
if err := action(); err != nil {
return err
}
}
return nil
}
func (b *Base) setupLauncher(launcher string) {
b.command = launcher
// Bridge supports no-window option which we should use
// for autostart.
b.Autostart.Exec = []string{launcher, "--" + FlagNoWindow}
}

View File

@ -1,131 +0,0 @@
// Copyright (c) 2022 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 base
import (
"os"
"path/filepath"
"github.com/ProtonMail/proton-bridge/v2/internal/constants"
"github.com/ProtonMail/proton-bridge/v2/internal/locations"
"github.com/sirupsen/logrus"
)
// migrateFiles migrates files from their old (pre-refactor) locations to their new locations.
// We can remove this eventually.
//
// | entity | old location | new location |
// |-----------|-------------------------------------------|----------------------------------------|
// | prefs | ~/.cache/protonmail/<app>/c11/prefs.json | ~/.config/protonmail/<app>/prefs.json |
// | c11 1.5.x | ~/.cache/protonmail/<app>/c11 | ~/.cache/protonmail/<app>/cache/c11 |
// | c11 1.6.x | ~/.cache/protonmail/<app>/cache/c11 | ~/.config/protonmail/<app>/cache/c11 |
// | updates | ~/.cache/protonmail/<app>/updates | ~/.config/protonmail/<app>/updates |.
func migrateFiles(configName string) error {
locationsProvider, err := locations.NewDefaultProvider(filepath.Join(constants.VendorName, configName))
if err != nil {
return err
}
locations := locations.New(locationsProvider, configName)
userCacheDir := locationsProvider.UserCache()
if err := migratePrefsFrom15x(locations, userCacheDir); err != nil {
return err
}
if err := migrateCacheFromBoth15xAnd16x(locations, userCacheDir); err != nil {
return err
}
if err := migrateUpdatesFrom16x(configName, locations); err != nil { //nolint:revive It is more clear to structure this way
return err
}
return nil
}
func migratePrefsFrom15x(locations *locations.Locations, userCacheDir string) error {
newSettingsDir, err := locations.ProvideSettingsPath()
if err != nil {
return err
}
return moveIfExists(
filepath.Join(userCacheDir, "c11", "prefs.json"),
filepath.Join(newSettingsDir, "prefs.json"),
)
}
func migrateCacheFromBoth15xAnd16x(locations *locations.Locations, userCacheDir string) error {
olderCacheDir := userCacheDir
newerCacheDir := locations.GetOldCachePath()
latestCacheDir, err := locations.ProvideCachePath()
if err != nil {
return err
}
// Migration for versions before 1.6.x.
if err := moveIfExists(
filepath.Join(olderCacheDir, "c11"),
filepath.Join(latestCacheDir, "c11"),
); err != nil {
return err
}
// Migration for versions 1.6.x.
return moveIfExists(
filepath.Join(newerCacheDir, "c11"),
filepath.Join(latestCacheDir, "c11"),
)
}
func migrateUpdatesFrom16x(configName string, locations *locations.Locations) error {
// In order to properly update Bridge 1.6.X and higher we need to
// change the launcher first. Since this is not part of automatic
// updates the migration must wait until manual update. Until that
// we need to keep old path.
if configName == "bridge" {
return nil
}
oldUpdatesPath := locations.GetOldUpdatesPath()
// Do not use ProvideUpdatesPath, that creates dir right away.
newUpdatesPath := locations.GetUpdatesPath()
return moveIfExists(oldUpdatesPath, newUpdatesPath)
}
func moveIfExists(source, destination string) error {
l := logrus.WithField("source", source).WithField("destination", destination)
if _, err := os.Stat(source); os.IsNotExist(err) {
l.Info("No need to migrate file, source doesn't exist")
return nil
}
if _, err := os.Stat(destination); !os.IsNotExist(err) {
// Once migrated, files should not stay in source anymore. Therefore
// if some files are still in source location but target already exist,
// it's suspicious. Could happen by installing new version, then the
// old one because of some reason, and then the new one again.
// Good to see as warning because it could be a reason why Bridge is
// behaving weirdly, like wrong configuration, or db re-sync and so on.
l.Warn("No need to migrate file, target already exists")
return nil
}
l.Info("Migrating files")
return os.Rename(source, destination)
}

View File

@ -1,197 +0,0 @@
// Copyright (c) 2022 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 base
import (
"errors"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/ProtonMail/proton-bridge/v2/internal/config/settings"
"github.com/ProtonMail/proton-bridge/v2/pkg/keychain"
"github.com/hashicorp/go-multierror"
"github.com/sirupsen/logrus"
)
const darwin = "darwin"
func migrateRebranding(settingsObj *settings.Settings, keychainName string) (result error) {
if err := migrateStartupBeforeRebranding(); err != nil {
result = multierror.Append(result, err)
}
lastUsedVersion := settingsObj.Get(settings.LastVersionKey)
// Skipping migration: it is first bridge start or cache was cleared.
if lastUsedVersion == "" {
settingsObj.SetBool(settings.RebrandingMigrationKey, true)
return
}
// Skipping rest of migration: already done
if settingsObj.GetBool(settings.RebrandingMigrationKey) {
return
}
switch runtime.GOOS {
case "windows", "linux":
// GODT-1260 we would need admin rights to changes desktop files
// and start menu items.
settingsObj.SetBool(settings.RebrandingMigrationKey, true)
case darwin:
if shouldContinue, err := isMacBeforeRebranding(); !shouldContinue || err != nil {
if err != nil {
result = multierror.Append(result, err)
}
break
}
if err := migrateMacKeychainBeforeRebranding(settingsObj, keychainName); err != nil {
result = multierror.Append(result, err)
}
settingsObj.SetBool(settings.RebrandingMigrationKey, true)
}
return result
}
// migrateMacKeychainBeforeRebranding deals with write access restriction to
// mac keychain passwords which are caused by application renaming. The old
// passwords are copied under new name in order to have write access afer
// renaming.
func migrateMacKeychainBeforeRebranding(settingsObj *settings.Settings, keychainName string) error {
l := logrus.WithField("pkg", "app/base/migration")
l.Warn("Migrating mac keychain")
helperConstructor, ok := keychain.Helpers["macos-keychain"]
if !ok {
return errors.New("cannot find macos-keychain helper")
}
oldKC, err := helperConstructor("ProtonMailBridgeService")
if err != nil {
l.WithError(err).Error("Keychain constructor failed")
return err
}
idByURL, err := oldKC.List()
if err != nil {
l.WithError(err).Error("List old keychain failed")
return err
}
newKC, err := keychain.NewKeychain(settingsObj, keychainName)
if err != nil {
return err
}
for url, id := range idByURL {
li := l.WithField("id", id).WithField("url", url)
userID, secret, err := oldKC.Get(url)
if err != nil {
li.WithField("userID", userID).
WithField("err", err).
Error("Faild to get old item")
continue
}
if _, _, err := newKC.Get(userID); err == nil {
li.Warn("Skipping migration, item already exists.")
continue
}
if err := newKC.Put(userID, secret); err != nil {
li.WithError(err).Error("Failed to migrate user")
}
li.Info("Item migrated")
}
return nil
}
// migrateStartupBeforeRebranding removes old startup links. The creation of new links is
// handled by bridge initialisation.
func migrateStartupBeforeRebranding() error {
path, err := os.UserHomeDir()
if err != nil {
return err
}
switch runtime.GOOS {
case "windows":
path = filepath.Join(path, `AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\ProtonMail Bridge.lnk`)
case "linux":
path = filepath.Join(path, `.config/autostart/ProtonMail Bridge.desktop`)
case darwin:
path = filepath.Join(path, `Library/LaunchAgents/ProtonMail Bridge.plist`)
default:
return errors.New("unknown GOOS")
}
if _, err := os.Stat(path); os.IsNotExist(err) {
return nil
}
logrus.WithField("pkg", "app/base/migration").Warn("Migrating autostartup links")
return os.Remove(path)
}
// startupNameForRebranding returns the name for autostart launcher based on
// type of rebranded instance i.e. update or manual.
//
// This only affects darwin when udpate re-writes the old startup and then
// manual installed it would not run proper exe. Therefore we return "old" name
// for updates and "new" name for manual which would be properly migrated.
//
// For orther (linux and windows) the link is always pointing to launcher which
// path didn't changed.
func startupNameForRebranding(origin string) string {
if runtime.GOOS == darwin {
if path, err := os.Executable(); err == nil && strings.Contains(path, "ProtonMail Bridge") {
return "ProtonMail Bridge"
}
}
// No need to solve for other OS. See comment above.
return origin
}
// isBeforeRebranding decide if last used version was older than 2.2.0. If
// cannot decide it returns false with error.
func isMacBeforeRebranding() (bool, error) {
// previous version | update | do mac migration |
// | first | false |
// cleared-cache | manual | false |
// cleared-cache | in-app | false |
// old | in-app | false |
// old in-app | in-app | false |
// old | manual | true |
// old in-app | manual | true |
// manual | in-app | false |
// Skip if it was in-app update and not manual
if path, err := os.Executable(); err != nil || strings.Contains(path, "ProtonMail Bridge") {
return false, err
}
return true, nil
}

View File

@ -1,56 +0,0 @@
// Copyright (c) 2022 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 base
import (
"os"
"path/filepath"
"runtime"
"runtime/pprof"
"github.com/sirupsen/logrus"
)
// startCPUProfile starts CPU pprof.
func startCPUProfile() {
f, err := os.Create("./cpu.pprof")
if err != nil {
logrus.Fatal("Could not create CPU profile: ", err)
}
if err := pprof.StartCPUProfile(f); err != nil {
logrus.Fatal("Could not start CPU profile: ", err)
}
}
// makeMemoryProfile generates memory pprof.
func makeMemoryProfile() {
name := "./mem.pprof"
f, err := os.Create(name)
if err != nil {
logrus.Fatal("Could not create memory profile: ", err)
}
if abs, err := filepath.Abs(name); err == nil {
name = abs
}
logrus.Info("Writing memory profile to ", name)
runtime.GC() // get up-to-date statistics
if err := pprof.WriteHeapProfile(f); err != nil {
logrus.Fatal("Could not write memory profile: ", err)
}
_ = f.Close()
}

View File

@ -1,112 +0,0 @@
// Copyright (c) 2022 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 base
import (
"os"
"strconv"
"github.com/sirupsen/logrus"
"golang.org/x/sys/execabs"
)
// maxAllowedRestarts controls after how many crashes the app will give up restarting.
const maxAllowedRestarts = 10
func (b *Base) restartApp(crash bool) error {
var args []string
if crash {
args = incrementRestartFlag(os.Args)[1:]
defer func() { os.Exit(1) }()
} else {
args = os.Args[1:]
}
if b.launcher != "" {
args = forceLauncherFlag(args, b.launcher)
}
args = append(args, "--wait", b.mainExecutable)
logrus.
WithField("command", b.command).
WithField("args", args).
Warn("Restarting")
return execabs.Command(b.command, args...).Start() //nolint:gosec
}
// incrementRestartFlag increments the value of the restart flag.
// If no such flag is present, it is added with initial value 1.
func incrementRestartFlag(args []string) []string {
res := append([]string{}, args...)
hasFlag := false
for k, v := range res {
if v != "--restart" {
continue
}
hasFlag = true
if k+1 >= len(res) {
continue
}
n, err := strconv.Atoi(res[k+1])
if err != nil {
res[k+1] = "1"
} else {
res[k+1] = strconv.Itoa(n + 1)
}
}
if !hasFlag {
res = append(res, "--restart", "1")
}
return res
}
// forceLauncherFlag replace or add the launcher args with the one set in the app.
func forceLauncherFlag(args []string, launcher string) []string {
res := append([]string{}, args...)
hasFlag := false
for k, v := range res {
if v != "--launcher" {
continue
}
if k+1 >= len(res) {
continue
}
hasFlag = true
res[k+1] = launcher
}
if !hasFlag {
res = append(res, "--launcher", launcher)
}
return res
}

View File

@ -1,63 +0,0 @@
// Copyright (c) 2022 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 base
import (
"strings"
"testing"
"github.com/Masterminds/semver/v3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestIncrementRestartFlag(t *testing.T) {
tests := []struct {
in []string
out []string
}{
{[]string{"./bridge", "--restart", "1"}, []string{"./bridge", "--restart", "2"}},
{[]string{"./bridge", "--restart", "2"}, []string{"./bridge", "--restart", "3"}},
{[]string{"./bridge", "--other", "--restart", "2"}, []string{"./bridge", "--other", "--restart", "3"}},
{[]string{"./bridge", "--restart", "2", "--other"}, []string{"./bridge", "--restart", "3", "--other"}},
{[]string{"./bridge", "--restart", "2", "--other", "2"}, []string{"./bridge", "--restart", "3", "--other", "2"}},
{[]string{"./bridge"}, []string{"./bridge", "--restart", "1"}},
{[]string{"./bridge", "--something"}, []string{"./bridge", "--something", "--restart", "1"}},
{[]string{"./bridge", "--something", "--else"}, []string{"./bridge", "--something", "--else", "--restart", "1"}},
{[]string{"./bridge", "--restart", "bad"}, []string{"./bridge", "--restart", "1"}},
{[]string{"./bridge", "--restart", "bad", "--other"}, []string{"./bridge", "--restart", "1", "--other"}},
}
for _, tt := range tests {
t.Run(strings.Join(tt.in, " "), func(t *testing.T) {
assert.Equal(t, tt.out, incrementRestartFlag(tt.in))
})
}
}
func TestVersionLessThan(t *testing.T) {
r := require.New(t)
old := semver.MustParse("1.1.0")
current := semver.MustParse("1.1.1")
newer := semver.MustParse("1.1.2")
r.True(old.LessThan(current))
r.False(current.LessThan(current))
r.False(newer.LessThan(current))
}

View File

@ -1,101 +0,0 @@
// Copyright (c) 2022 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
//go:build !windows
// +build !windows
package base
import (
"errors"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/proton-bridge/v2/internal/config/settings"
"github.com/ProtonMail/proton-bridge/v2/internal/constants"
"github.com/allan-simon/go-singleinstance"
"golang.org/x/sys/unix"
)
// checkSingleInstance returns error if a bridge instance is already running
// This instance should be stop and window of running window should be brought
// to focus.
//
// 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, settingsObj *settings.Settings) (*os.File, error) {
if lock, err := singleinstance.CreateLockFile(lockFilePath); err == nil {
// Bridge is not runnig, continue normally
return lock, nil
}
if err := runningVersionIsOlder(settingsObj); err != nil {
return nil, err
}
pid, err := getPID(lockFilePath)
if err != nil {
return nil, err
}
if err := unix.Kill(pid, unix.SIGTERM); err != nil {
return nil, err
}
// Need to wait some time to release file lock
time.Sleep(time.Second)
return singleinstance.CreateLockFile(lockFilePath)
}
func getPID(lockFilePath string) (int, error) {
file, err := os.Open(filepath.Clean(lockFilePath))
if err != nil {
return 0, err
}
defer func() { _ = file.Close() }()
rawPID := make([]byte, 10) // PID is probably up to 7 digits long, 10 should be enough
n, err := file.Read(rawPID)
if err != nil {
return 0, err
}
return strconv.Atoi(strings.TrimSpace(string(rawPID[:n])))
}
func runningVersionIsOlder(settingsObj *settings.Settings) error {
currentVer, err := semver.StrictNewVersion(constants.Version)
if err != nil {
return err
}
runningVer, err := semver.StrictNewVersion(settingsObj.Get(settings.LastVersionKey))
if err != nil {
return err
}
if !runningVer.LessThan(currentVer) {
return errors.New("running version is not older")
}
return nil
}

167
internal/app/bridge.go Normal file
View File

@ -0,0 +1,167 @@
// 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 app
import (
"fmt"
"net/http"
"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"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/crash"
"github.com/ProtonMail/proton-bridge/v3/internal/dialer"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
"github.com/ProtonMail/proton-bridge/v3/internal/sentry"
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/internal/versioner"
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
// deleteOldGoIMAPFiles Set with `-ldflags -X app.deleteOldGoIMAPFiles=true` to enable cleanup of old imap cache data.
var deleteOldGoIMAPFiles bool //nolint:gochecknoglobals
// withBridge creates and tears down the bridge.
func withBridge(
c *cli.Context,
exe string,
locations *locations.Locations,
version *semver.Version,
identifier *useragent.UserAgent,
crashHandler *crash.Handler,
reporter *sentry.Reporter,
vault *vault.Vault,
cookieJar http.CookieJar,
keychains *keychain.List,
fn func(*bridge.Bridge, <-chan events.Event) error,
) error {
logrus.Debug("Creating bridge")
defer logrus.Debug("Bridge stopped")
// Delete old go-imap cache files
if deleteOldGoIMAPFiles {
logrus.Debug("Deleting old go-imap cache files")
if err := locations.CleanGoIMAPCache(); err != nil {
logrus.WithError(err).Error("Failed to remove old go-imap cache")
}
}
// Create the underlying dialer used by the bridge.
// It only connects to trusted servers and reports any untrusted servers it finds.
pinningDialer := dialer.NewPinningTLSDialer(
dialer.NewBasicTLSDialer(constants.APIHost),
dialer.NewTLSReporter(constants.APIHost, constants.AppVersion(version.Original()), identifier, dialer.TrustedAPIPins),
dialer.NewTLSPinChecker(dialer.TrustedAPIPins),
)
// Create a proxy dialer which switches to a proxy if the request fails.
proxyDialer := dialer.NewProxyTLSDialer(pinningDialer, constants.APIHost, crashHandler)
// Create the autostarter.
autostarter := newAutostarter(exe)
// Create the update installer.
updater, err := newUpdater(locations)
if err != nil {
return fmt.Errorf("could not create updater: %w", err)
}
// Create a new bridge.
bridge, eventCh, err := bridge.New(
// The app stuff.
locations,
vault,
autostarter,
updater,
version,
keychains,
// The API stuff.
constants.APIHost,
cookieJar,
identifier,
pinningDialer,
dialer.CreateTransportWithDialer(proxyDialer),
proxyDialer,
// Crash and report stuff
crashHandler,
reporter,
imap.DefaultEpochUIDValidityGenerator(),
nil,
// The logging stuff.
c.String(flagLogIMAP) == "client" || c.String(flagLogIMAP) == "all",
c.String(flagLogIMAP) == "server" || c.String(flagLogIMAP) == "all",
c.Bool(flagLogSMTP),
)
if err != nil {
return fmt.Errorf("could not create bridge: %w", err)
}
// Ensure we close bridge when we exit.
defer bridge.Close(c.Context)
return fn(bridge, eventCh)
}
func newAutostarter(exe string) *autostart.App {
logrus.Debug("Creating autostarter")
return &autostart.App{
Name: constants.FullAppName,
DisplayName: constants.FullAppName,
Exec: []string{exe, "--" + flagNoWindow},
}
}
func newUpdater(locations *locations.Locations) (*updater.Updater, error) {
updatesDir, err := locations.ProvideUpdatesPath()
if err != nil {
return nil, fmt.Errorf("could not provide updates path: %w", err)
}
logrus.WithField("updates", updatesDir).Debug("Creating updater")
key, err := crypto.NewKeyFromArmored(updater.DefaultPublicKey)
if err != nil {
return nil, fmt.Errorf("could not create key from armored: %w", err)
}
verifier, err := crypto.NewKeyRing(key)
if err != nil {
return nil, fmt.Errorf("could not create key ring: %w", err)
}
return updater.NewUpdater(
versioner.New(updatesDir),
verifier,
constants.UpdateName,
runtime.GOOS,
), nil
}

View File

@ -1,281 +0,0 @@
// Copyright (c) 2022 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 implements the bridge CLI application.
package bridge
import (
"time"
"github.com/ProtonMail/proton-bridge/v2/internal/api"
"github.com/ProtonMail/proton-bridge/v2/internal/app/base"
pkgBridge "github.com/ProtonMail/proton-bridge/v2/internal/bridge"
"github.com/ProtonMail/proton-bridge/v2/internal/config/settings"
"github.com/ProtonMail/proton-bridge/v2/internal/constants"
"github.com/ProtonMail/proton-bridge/v2/internal/frontend"
"github.com/ProtonMail/proton-bridge/v2/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/v2/internal/imap"
"github.com/ProtonMail/proton-bridge/v2/internal/smtp"
"github.com/ProtonMail/proton-bridge/v2/internal/store"
"github.com/ProtonMail/proton-bridge/v2/internal/store/cache"
"github.com/ProtonMail/proton-bridge/v2/internal/updater"
"github.com/ProtonMail/proton-bridge/v2/pkg/message"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
const (
flagLogIMAP = "log-imap"
flagLogSMTP = "log-smtp"
flagNonInteractive = "noninteractive"
flagNonInteractiveShort = "n"
// Memory cache was estimated by empirical usage in the past, and it was set to 100MB.
// NOTE: This value must not be less than maximal size of one email (~30MB).
inMemoryCacheLimit = 100 * (1 << 20)
)
func New(base *base.Base) *cli.App {
app := base.NewApp(main)
app.Flags = append(app.Flags, []cli.Flag{
&cli.StringFlag{
Name: flagLogIMAP,
Usage: "Enable logging of IMAP communications (all|client|server) (may contain decrypted data!)",
},
&cli.BoolFlag{
Name: flagLogSMTP,
Usage: "Enable logging of SMTP communications (may contain decrypted data!)",
},
&cli.BoolFlag{
Name: flagNonInteractive,
Aliases: []string{flagNonInteractiveShort},
Usage: "Start Bridge entirely non-interactively",
},
}...)
return app
}
func main(b *base.Base, c *cli.Context) error { //nolint:funlen
frontendType := getFrontendTypeFromCLIParams(c)
if frontendType == frontend.Unknown {
_ = cli.ShowAppHelp(c)
return errors.New("no frontend was specified. Use --grpc, --cli or --noninteractive")
}
f := frontend.New(
frontendType,
!c.Bool(base.FlagNoWindow),
b.CrashHandler,
b.Listener,
b.Updater,
b,
b.Locations,
)
cache, cacheErr := loadMessageCache(b)
if cacheErr != nil {
logrus.WithError(cacheErr).Error("Could not load local cache.")
}
builder := message.NewBuilder(
b.Settings.GetInt(settings.FetchWorkers),
b.Settings.GetInt(settings.AttachmentWorkers),
)
bridge := pkgBridge.New(
b.Locations,
b.Cache,
b.Settings,
b.SentryReporter,
b.CrashHandler,
b.Listener,
b.TLS,
b.UserAgent,
cache,
builder,
b.CM,
b.Creds,
b.Updater,
b.Versioner,
b.Autostart,
)
imapBackend := imap.NewIMAPBackend(b.CrashHandler, b.Listener, b.Cache, b.Settings, bridge)
smtpBackend := smtp.NewSMTPBackend(b.CrashHandler, b.Listener, b.Settings, bridge)
tlsConfig, err := bridge.GetTLSConfig()
if err != nil {
return err
}
if cacheErr != nil {
bridge.AddError(pkgBridge.ErrLocalCacheUnavailable)
}
go func() {
defer b.CrashHandler.HandlePanic()
api.NewAPIServer(b.Settings, b.Listener).ListenAndServe()
}()
go func() {
defer b.CrashHandler.HandlePanic()
imapPort := b.Settings.GetInt(settings.IMAPPortKey)
imap.NewIMAPServer(
b.CrashHandler,
c.String(flagLogIMAP) == "client" || c.String(flagLogIMAP) == "all",
c.String(flagLogIMAP) == "server" || c.String(flagLogIMAP) == "all",
imapPort, tlsConfig, imapBackend, b.UserAgent, b.Listener).ListenAndServe()
}()
go func() {
defer b.CrashHandler.HandlePanic()
smtpPort := b.Settings.GetInt(settings.SMTPPortKey)
useSSL := b.Settings.GetBool(settings.SMTPSSLKey)
smtp.NewSMTPServer(
b.CrashHandler,
c.Bool(flagLogSMTP),
smtpPort, useSSL, tlsConfig, smtpBackend, b.Listener).ListenAndServe()
}()
// We want to remove old versions if the app exits successfully.
b.AddTeardownAction(b.Versioner.RemoveOldVersions)
// We want cookies to be saved to disk so they are loaded the next time.
b.AddTeardownAction(b.CookieJar.PersistCookies)
if frontendType == frontend.NonInteractive {
return <-(make(chan error))
}
// Watch for updates routine
go func() {
ticker := time.NewTicker(constants.UpdateCheckInterval)
for {
checkAndHandleUpdate(b.Updater, f, b.Settings.GetBool(settings.AutoUpdateKey))
<-ticker.C
}
}()
return f.Loop(bridge)
}
func getFrontendTypeFromCLIParams(c *cli.Context) frontend.Type {
switch {
case c.Bool(base.FlagGRPC):
return frontend.GRPC
case c.Bool(base.FlagCLI):
return frontend.CLI
case c.Bool(flagNonInteractive):
return frontend.NonInteractive
default:
return frontend.Unknown
}
}
func checkAndHandleUpdate(u types.Updater, f frontend.Frontend, autoUpdate bool) {
log := logrus.WithField("pkg", "app/bridge")
version, err := u.Check()
if err != nil {
log.WithError(err).Error("An error occurred while checking for updates")
return
}
f.WaitUntilFrontendIsReady()
// Update links in UI
f.SetVersion(version)
if !u.IsUpdateApplicable(version) {
log.Info("No need to update")
return
}
log.WithField("version", version.Version).Info("An update is available")
if !autoUpdate {
f.NotifyManualUpdate(version, u.CanInstall(version))
return
}
if !u.CanInstall(version) {
log.Info("A manual update is required")
f.NotifySilentUpdateError(updater.ErrManualUpdateRequired)
return
}
if err := u.InstallUpdate(version); err != nil {
if errors.Cause(err) == updater.ErrDownloadVerify {
log.WithError(err).Warning("Skipping update installation due to temporary error")
} else {
log.WithError(err).Error("The update couldn't be installed")
f.NotifySilentUpdateError(err)
}
return
}
f.NotifySilentUpdateInstalled()
}
// loadMessageCache loads local cache in case it is enabled in settings and available.
// In any other case it is returning in-memory cache. Could also return an error in case
// local cache is enabled but unavailable (in-memory cache will be returned nevertheless).
func loadMessageCache(b *base.Base) (cache.Cache, error) {
if !b.Settings.GetBool(settings.CacheEnabledKey) {
return cache.NewInMemoryCache(inMemoryCacheLimit), nil
}
var compressor cache.Compressor
// NOTE(GODT-1158): Changing compression is not an option currently
// available for user but, if user changes compression setting we have
// to nuke the cache.
if b.Settings.GetBool(settings.CacheCompressionKey) {
compressor = &cache.GZipCompressor{}
} else {
compressor = &cache.NoopCompressor{}
}
var path string
if customPath := b.Settings.Get(settings.CacheLocationKey); customPath != "" {
path = customPath
} else {
path = b.Cache.GetDefaultMessageCacheDir()
// Store path so it will always persist if default location
// will be changed in new version.
b.Settings.Set(settings.CacheLocationKey, path)
}
// To prevent memory peaks we set maximal write concurrency for store
// build jobs.
store.SetBuildAndCacheJobLimit(b.Settings.GetInt(settings.CacheConcurrencyWrite))
messageCache, err := cache.NewOnDiskCache(path, compressor, cache.Options{
MinFreeAbs: uint64(b.Settings.GetInt(settings.CacheMinFreeAbsKey)),
MinFreeRat: b.Settings.GetFloat64(settings.CacheMinFreeRatKey),
ConcurrentRead: b.Settings.GetInt(settings.CacheConcurrencyRead),
ConcurrentWrite: b.Settings.GetInt(settings.CacheConcurrencyWrite),
})
if err != nil {
return cache.NewInMemoryCache(inMemoryCacheLimit), err
}
return messageCache, nil
}

70
internal/app/frontend.go Normal file
View File

@ -0,0 +1,70 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package app
import (
"fmt"
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
"github.com/ProtonMail/proton-bridge/v3/internal/crash"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
bridgeCLI "github.com/ProtonMail/proton-bridge/v3/internal/frontend/cli"
"github.com/ProtonMail/proton-bridge/v3/internal/frontend/grpc"
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
"github.com/ProtonMail/proton-bridge/v3/pkg/restarter"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
func runFrontend(
c *cli.Context,
crashHandler *crash.Handler,
restarter *restarter.Restarter,
locations *locations.Locations,
bridge *bridge.Bridge,
eventCh <-chan events.Event,
quitCh <-chan struct{},
parentPID int,
) error {
logrus.Debug("Running frontend")
defer logrus.Debug("Frontend stopped")
switch {
case c.Bool(flagCLI):
return bridgeCLI.New(bridge, restarter, eventCh, crashHandler, quitCh).Loop()
case c.Bool(flagNonInteractive):
<-quitCh
return nil
case c.Bool(flagGRPC):
service, err := grpc.NewService(crashHandler, restarter, locations, bridge, eventCh, quitCh, !c.Bool(flagNoWindow), parentPID)
if err != nil {
return fmt.Errorf("could not create service: %w", err)
}
return service.Loop()
default:
if err := cli.ShowAppHelp(c); err != nil {
logrus.WithError(err).Error("Failed to show app help")
}
return fmt.Errorf("no frontend specified, use --cli, --grpc or --noninteractive")
}
}

371
internal/app/migration.go Normal file
View File

@ -0,0 +1,371 @@
// 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 app
import (
"encoding/json"
"fmt"
"io/fs"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/proton-bridge/v3/internal/legacy/credentials"
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/pkg/algo"
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
"github.com/allan-simon/go-singleinstance"
"github.com/hashicorp/go-multierror"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
// nolint:gosec
func migrateKeychainHelper(locations *locations.Locations) error {
logrus.Info("Migrating keychain helper")
settings, err := locations.ProvideSettingsPath()
if err != nil {
return fmt.Errorf("failed to get settings path: %w", err)
}
// If keychain helper file is already there do not migrate again.
if keychainName, _ := vault.GetHelper(settings); keychainName != "" {
return nil
}
configDir, err := os.UserConfigDir()
if err != nil {
return fmt.Errorf("failed to get user config dir: %w", err)
}
b, err := os.ReadFile(filepath.Join(configDir, "protonmail", "bridge", "prefs.json"))
if errors.Is(err, fs.ErrNotExist) {
return nil
} else if err != nil {
return fmt.Errorf("failed to read old prefs file: %w", err)
}
var prefs struct {
Helper string `json:"preferred_keychain"`
}
if err := json.Unmarshal(b, &prefs); err != nil {
return fmt.Errorf("failed to unmarshal old prefs file: %w", err)
}
return vault.SetHelper(settings, prefs.Helper)
}
// nolint:gosec
func migrateOldSettings(v *vault.Vault) error {
logrus.Info("Migrating settings")
configDir, err := os.UserConfigDir()
if err != nil {
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
} else if err != nil {
return fmt.Errorf("failed to read old prefs file: %w", err)
}
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, keychains *keychain.List, v *vault.Vault) error {
logrus.Info("Migrating accounts")
settings, err := locations.ProvideSettingsPath()
if err != nil {
return fmt.Errorf("failed to get settings path: %w", err)
}
helper, err := vault.GetHelper(settings)
if err != nil {
return fmt.Errorf("failed to get helper: %w", err)
}
keychain, err := keychain.NewKeychain(helper, "bridge", keychains.GetHelpers(), keychains.GetDefaultHelper())
if err != nil {
return fmt.Errorf("failed to create keychain: %w", err)
}
store := credentials.NewStore(keychain)
users, err := store.List()
if err != nil {
return fmt.Errorf("failed to create credentials store: %w", err)
}
var migrationErrors error
for _, userID := range users {
if err := migrateOldAccount(userID, store, v); err != nil {
migrationErrors = multierror.Append(migrationErrors, err)
}
}
return migrationErrors
}
func migrateOldAccount(userID string, store *credentials.Store, v *vault.Vault) error {
l := logrus.WithField("userID", userID)
l.Info("Migrating account")
creds, err := store.Get(userID)
if err != nil {
return fmt.Errorf("failed to get user %q: %w", userID, err)
}
authUID, authRef, err := creds.SplitAPIToken()
if err != nil {
return fmt.Errorf("failed to split api token for user %q: %w", userID, err)
}
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)
}
l = l.WithField("username", logging.Sensitive(user.Username()))
l.Info("Migrated account with random bridge password")
defer func() {
if err := user.Close(); err != nil {
logrus.WithField("userID", userID).WithError(err).Error("Failed to close vault user after migration")
}
}()
dec, err := algo.B64RawDecode([]byte(creds.BridgePassword))
if err != nil {
return fmt.Errorf("failed to decode bridge password for user %q: %w", userID, err)
}
if err := user.SetBridgePass(dec); err != nil {
return fmt.Errorf("failed to set bridge password for user %q: %w", userID, err)
}
l = l.WithField("password", logging.Sensitive(string(algo.B64RawEncode(dec))))
l.Info("Migrated existing bridge password")
if !creds.IsCombinedAddressMode {
if err := user.SetAddressMode(vault.SplitMode); err != nil {
return fmt.Errorf("failed to set split address mode to user %q: %w", userID, err)
}
}
return nil
}
func migratePrefsToVault(vault *vault.Vault, b []byte) error {
var prefs struct {
IMAPPort int `json:"user_port_imap,,string"`
SMTPPort int `json:"user_port_smtp,,string"`
SMTPSSL bool `json:"user_ssl_smtp,,string"`
AutoUpdate bool `json:"autoupdate,,string"`
UpdateChannel updater.Channel `json:"update_channel"`
UpdateRollout float64 `json:"rollout,,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"`
AttachmentWorkers int `json:"attachment_workers,,string"`
ShowAllMail bool `json:"is_all_mail_visible,,string"`
Cookies string `json:"cookies"`
}
if err := json.Unmarshal(b, &prefs); err != nil {
return fmt.Errorf("failed to unmarshal old prefs file: %w", err)
}
var errs error
if err := vault.SetIMAPPort(prefs.IMAPPort); err != nil {
errs = multierror.Append(errs, fmt.Errorf("failed to migrate IMAP port: %w", err))
}
if err := vault.SetSMTPPort(prefs.SMTPPort); err != nil {
errs = multierror.Append(errs, fmt.Errorf("failed to migrate SMTP port: %w", err))
}
if err := vault.SetSMTPSSL(prefs.SMTPSSL); err != nil {
errs = multierror.Append(errs, fmt.Errorf("failed to migrate SMTP SSL: %w", err))
}
if err := vault.SetAutoUpdate(prefs.AutoUpdate); err != nil {
errs = multierror.Append(errs, fmt.Errorf("failed to migrate auto update: %w", err))
}
if err := vault.SetUpdateChannel(prefs.UpdateChannel); err != nil {
errs = multierror.Append(errs, fmt.Errorf("failed to migrate update channel: %w", err))
}
if err := vault.SetUpdateRollout(prefs.UpdateRollout); err != nil {
errs = multierror.Append(errs, fmt.Errorf("failed to migrate rollout: %w", err))
}
if err := vault.SetFirstStart(prefs.FirstStart); err != nil {
errs = multierror.Append(errs, fmt.Errorf("failed to migrate first start: %w", err))
}
if err := vault.SetColorScheme(prefs.ColorScheme); err != nil {
errs = multierror.Append(errs, fmt.Errorf("failed to migrate color scheme: %w", err))
}
if err := vault.SetLastVersion(prefs.LastVersion); err != nil {
errs = multierror.Append(errs, fmt.Errorf("failed to migrate last version: %w", err))
}
if err := vault.SetAutostart(prefs.Autostart); err != nil {
errs = multierror.Append(errs, fmt.Errorf("failed to migrate autostart: %w", err))
}
if err := vault.SetProxyAllowed(prefs.AllowProxy); err != nil {
errs = multierror.Append(errs, fmt.Errorf("failed to migrate allow proxy: %w", err))
}
if err := vault.SetShowAllMail(prefs.ShowAllMail); err != nil {
errs = multierror.Append(errs, fmt.Errorf("failed to migrate show all mail: %w", err))
}
if err := vault.SetCookies([]byte(prefs.Cookies)); err != nil {
errs = multierror.Append(errs, fmt.Errorf("failed to migrate cookies: %w", err))
}
return errs
}
func migrateOldVersions() (allErrors error) {
cacheDir, cacheError := os.UserCacheDir()
if cacheError != nil {
allErrors = multierror.Append(allErrors, errors.Wrap(cacheError, "cannot get os cache"))
return // not need to continue for now (with more migrations might be still ok to continue)
}
if err := killV2AppAndRemoveV2LockFiles(filepath.Join(cacheDir, "protonmail", "bridge", "bridge.lock")); err != nil {
allErrors = multierror.Append(allErrors, errors.Wrap(err, "cannot migrate lockfiles"))
}
return
}
func killV2AppAndRemoveV2LockFiles(lockFilePathV2 string) error {
l := logrus.WithField("path", lockFilePathV2)
if _, err := os.Stat(lockFilePathV2); os.IsNotExist(err) {
l.Debug("no v2 lockfile")
return nil
}
lock, err := singleinstance.CreateLockFile(lockFilePathV2)
if err == nil {
l.Debug("no other v2 instance is running")
if errClose := lock.Close(); errClose != nil {
l.WithError(errClose).Error("Cannot close lock file")
}
return os.Remove(lockFilePathV2)
}
// The other instance is an older version, so we should kill it.
pid, err := getPID(lockFilePathV2)
if err != nil {
return errors.Wrap(err, "cannot get v2 pid")
}
if err := killPID(pid); err != nil {
return errors.Wrapf(err, "cannot kill v2 app (PID %d)", pid)
}
// Need to wait some time to release file lock
time.Sleep(time.Second)
return nil
}
func getPID(lockFilePath string) (int, error) {
file, err := os.Open(filepath.Clean(lockFilePath))
if err != nil {
return 0, err
}
defer func() { _ = file.Close() }()
rawPID := make([]byte, 10) // PID is probably up to 7 digits long, 10 should be enough
n, err := file.Read(rawPID)
if err != nil {
return 0, err
}
return strconv.Atoi(strings.TrimSpace(string(rawPID[:n])))
}
func killPID(pid int) error {
p, err := os.FindProcess(pid)
if err != nil {
return err
}
return p.Kill()
}

View File

@ -0,0 +1,227 @@
// 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 app
import (
"net/http/cookiejar"
"net/url"
"os"
"path/filepath"
"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"
"github.com/ProtonMail/proton-bridge/v3/internal/legacy/credentials"
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/pkg/algo"
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
"github.com/stretchr/testify/require"
)
func TestMigratePrefsToVaultWithKeys(t *testing.T) {
// Create a new vault.
vault, corrupt, err := vault.New(t.TempDir(), t.TempDir(), []byte("my secret key"), async.NoopPanicHandler{})
require.NoError(t, err)
require.False(t, corrupt)
// load the old prefs file.
configDir := filepath.Join("testdata", "with_keys")
// Migrate the old prefs file to the new vault.
require.NoError(t, migrateOldSettingsWithDir(configDir, vault))
// Check Json Settings
validateJSONPrefs(t, vault)
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))
}
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)
// load the old prefs file.
configDir := filepath.Join("testdata", "without_keys")
// Migrate the old prefs file to the new vault.
require.NoError(t, migrateOldSettingsWithDir(configDir, vault))
// 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) {
// Migration tested only for linux.
if runtime.GOOS != "linux" {
return
}
tmpDir := t.TempDir()
// Prepare for keychain migration test
{
require.NoError(t, os.Setenv("XDG_CONFIG_HOME", tmpDir))
oldCacheDir := filepath.Join(tmpDir, "protonmail", "bridge")
require.NoError(t, os.MkdirAll(oldCacheDir, 0o700))
oldPrefs, err := os.ReadFile(filepath.Join("testdata", "without_keys", "protonmail", "bridge", "prefs.json"))
require.NoError(t, err)
require.NoError(t, os.WriteFile(
filepath.Join(oldCacheDir, "prefs.json"),
oldPrefs, 0o600,
))
}
locations := locations.New(bridge.NewTestLocationsProvider(tmpDir), "config-name")
settingsFolder, err := locations.ProvideSettingsPath()
require.NoError(t, err)
// Check that there is nothing yet
keychainName, err := vault.GetHelper(settingsFolder)
require.NoError(t, err)
require.Equal(t, "", keychainName)
// Check migration
require.NoError(t, migrateKeychainHelper(locations))
keychainName, err = vault.GetHelper(settingsFolder)
require.NoError(t, err)
require.Equal(t, "secret-service", keychainName)
// Change the migrated value
require.NoError(t, vault.SetHelper(settingsFolder, "different"))
// Calling migration again will not overwrite existing prefs
require.NoError(t, migrateKeychainHelper(locations))
keychainName, err = vault.GetHelper(settingsFolder)
require.NoError(t, err)
require.Equal(t, "different", keychainName)
}
func TestUserMigration(t *testing.T) {
kcl := keychain.NewTestKeychainsList()
kc, err := keychain.NewKeychain("mock", "bridge", kcl.GetHelpers(), kcl.GetDefaultHelper())
require.NoError(t, err)
require.NoError(t, kc.Put("brokenID", "broken"))
require.NoError(t, kc.Put(
"emptyID",
(&credentials.Credentials{}).Marshal(),
))
wantUID := "uidtoken"
wantRefresh := "refreshtoken"
wantCredentials := credentials.Credentials{
UserID: "validID",
Name: "user@pm.me",
Emails: "user@pm.me;alias@pm.me",
APIToken: wantUID + ":" + wantRefresh,
MailboxPassword: []byte("secret"),
BridgePassword: "bElu2Q1Vusy28J3Wf56cIg",
Version: "v2.3.X",
Timestamp: 100,
IsCombinedAddressMode: true,
}
require.NoError(t, kc.Put(
wantCredentials.UserID,
wantCredentials.Marshal(),
))
tmpDir := t.TempDir()
locations := locations.New(bridge.NewTestLocationsProvider(tmpDir), "config-name")
settingsFolder, err := locations.ProvideSettingsPath()
require.NoError(t, err)
require.NoError(t, vault.SetHelper(settingsFolder, "mock"))
token, err := crypto.RandomToken(32)
require.NoError(t, err)
v, corrupt, err := vault.New(settingsFolder, settingsFolder, token, async.NoopPanicHandler{})
require.NoError(t, err)
require.False(t, corrupt)
require.NoError(t, migrateOldAccounts(locations, kcl, v))
require.Equal(t, []string{wantCredentials.UserID}, v.GetUserIDs())
require.NoError(t, v.GetUser(wantCredentials.UserID, func(u *vault.User) {
require.Equal(t, wantCredentials.UserID, u.UserID())
require.Equal(t, wantUID, u.AuthUID())
require.Equal(t, wantRefresh, u.AuthRef())
require.Equal(t, wantCredentials.MailboxPassword, u.KeyPass())
require.Equal(t,
[]byte(wantCredentials.BridgePassword),
algo.B64RawEncode(u.BridgePass()),
)
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

@ -0,0 +1,70 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package app
import (
"fmt"
"os"
"time"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/proton-bridge/v3/internal/focus"
"github.com/allan-simon/go-singleinstance"
"github.com/sirupsen/logrus"
)
// checkSingleInstance checks if another instance of the application is already running.
// It tries to create a lock file at the given path.
// If it succeeds, it returns the lock file and a nil error.
//
// 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(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.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(settingPath)
if !ok {
return nil, fmt.Errorf("failed to determine version of running instance")
}
if !lastVersion.LessThan(curVersion) {
return nil, fmt.Errorf("running instance is newer than this one")
}
// The other instance is an older version, so we should kill it.
pid, err := getPID(lockFilePath)
if err != nil {
return nil, err
}
if err := killPID(pid); err != nil {
return nil, err
}
// Need to wait some time to release file lock
time.Sleep(time.Second)
return singleinstance.CreateLockFile(lockFilePath)
}

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

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

113
internal/app/vault.go Normal file
View File

@ -0,0 +1,113 @@
// 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 app
import (
"fmt"
"path"
"github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/proton-bridge/v3/internal/certs"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
"github.com/sirupsen/logrus"
)
func WithVault(locations *locations.Locations, keychains *keychain.List, panicHandler async.PanicHandler, fn func(*vault.Vault, bool, bool) error) error {
logrus.Debug("Creating vault")
defer logrus.Debug("Vault stopped")
// Create the encVault.
encVault, insecure, corrupt, err := newVault(locations, keychains, panicHandler)
if err != nil {
return fmt.Errorf("could not create vault: %w", err)
}
logrus.WithFields(logrus.Fields{
"insecure": insecure,
"corrupt": corrupt,
}).Debug("Vault created")
cert, _ := encVault.GetBridgeTLSCert()
certs.NewInstaller().LogCertInstallStatus(cert)
// GODT-1950: Add teardown actions (e.g. to close the vault).
return fn(encVault, insecure, corrupt)
}
func newVault(locations *locations.Locations, keychains *keychain.List, 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)
}
logrus.WithField("vaultDir", vaultDir).Debug("Loading vault from directory")
var (
vaultKey []byte
insecure bool
)
if key, err := loadVaultKey(vaultDir, keychains); err != nil {
logrus.WithError(err).Error("Could not load/create vault key")
insecure = true
// We store the insecure vault in a separate directory
vaultDir = path.Join(vaultDir, "insecure")
} else {
vaultKey = key
}
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, gluonCacheDir, vaultKey, panicHandler)
if err != nil {
return nil, false, false, fmt.Errorf("could not create vault: %w", err)
}
return vault, insecure, corrupt, nil
}
func loadVaultKey(vaultDir string, keychains *keychain.List) ([]byte, error) {
helper, err := vault.GetHelper(vaultDir)
if err != nil {
return nil, fmt.Errorf("could not get keychain helper: %w", err)
}
kc, err := keychain.NewKeychain(helper, constants.KeyChainName, keychains.GetHelpers(), keychains.GetDefaultHelper())
if err != nil {
return nil, fmt.Errorf("could not create keychain: %w", err)
}
has, err := vault.HasVaultKey(kc)
if err != nil {
return nil, fmt.Errorf("could not check for vault key: %w", err)
}
if has {
return vault.GetVaultKey(kc)
}
return vault.NewVaultKey(kc)
}

46
internal/bridge/api.go Normal file
View File

@ -0,0 +1,46 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge
import (
"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"
)
// defaultAPIOptions returns a set of default API options for the given parameters.
func defaultAPIOptions(
apiURL string,
version *semver.Version,
cookieJar http.CookieJar,
transport http.RoundTripper,
panicHandler async.PanicHandler,
) []proton.Option {
return []proton.Option{
proton.WithHostURL(apiURL),
proton.WithAppVersion(constants.AppVersion(version.Original())),
proton.WithCookieJar(cookieJar),
proton.WithTransport(transport),
proton.WithLogger(logrus.StandardLogger()),
proton.WithPanicHandler(panicHandler),
}
}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2022 Proton AG
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
@ -15,22 +15,25 @@
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
//go:build !build_qa
// +build !build_qa
//go:build !build_qa && !test_integration
package pmapi
package bridge
import (
"net/http"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/go-proton-api"
)
func getRootURL() string {
return "https://api.protonmail.ch"
}
func newProxyDialerAndTransport(cfg Config) (*ProxyTLSDialer, http.RoundTripper) {
basicDialer := NewBasicTLSDialer(cfg)
pinningDialer := NewPinningTLSDialer(cfg, basicDialer)
proxyDialer := NewProxyTLSDialer(cfg, pinningDialer)
return proxyDialer, CreateTransportWithDialer(proxyDialer)
// newAPIOptions returns a set of API options for the given parameters.
func newAPIOptions(
apiURL string,
version *semver.Version,
cookieJar http.CookieJar,
transport http.RoundTripper,
panicHandler async.PanicHandler,
) []proton.Option {
return defaultAPIOptions(apiURL, version, cookieJar, transport, panicHandler)
}

63
internal/bridge/api_qa.go Normal file
View File

@ -0,0 +1,63 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
//go:build build_qa || test_integration
package bridge
import (
"crypto/tls"
"net/http"
"os"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/go-proton-api"
)
// newAPIOptions returns a set of API options for the given parameters.
func newAPIOptions(
apiURL string,
version *semver.Version,
cookieJar http.CookieJar,
transport http.RoundTripper,
panicHandler async.PanicHandler,
) []proton.Option {
if allow := os.Getenv("BRIDGE_ALLOW_PROXY"); allow != "" {
transport = &http.Transport{
Proxy: http.ProxyFromEnvironment,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
}
opt := defaultAPIOptions(apiURL, version, cookieJar, transport, panicHandler)
if host := os.Getenv("BRIDGE_API_HOST"); host != "" {
opt = append(opt, proton.WithHostURL(host))
}
if debug := os.Getenv("BRIDGE_API_DEBUG"); debug != "" {
opt = append(opt, proton.WithDebug(true))
}
if skipVerify := os.Getenv("BRIDGE_API_SKIP_VERIFY"); skipVerify != "" {
opt = append(opt, proton.WithSkipVerifyProofs())
}
return opt
}

View File

@ -1,38 +0,0 @@
// Copyright (c) 2022 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 provides core functionality of Bridge app.
package bridge
import "github.com/ProtonMail/proton-bridge/v2/internal/config/settings"
// IsAutostartEnabled checks if link file exits.
func (b *Bridge) IsAutostartEnabled() bool {
return b.autostart.IsEnabled()
}
// EnableAutostart creates link and sets the preferences.
func (b *Bridge) EnableAutostart() error {
b.settings.SetBool(settings.AutostartKey, true)
return b.autostart.Enable()
}
// DisableAutostart removes link and sets the preferences.
func (b *Bridge) DisableAutostart() error {
b.settings.SetBool(settings.AutostartKey, false)
return b.autostart.Disable()
}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2022 Proton AG
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
@ -15,311 +15,542 @@
// 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 provides core functionality of Bridge app.
// Package bridge implements the Bridge, which acts as the backend to the UI.
package bridge
import (
"context"
"crypto/tls"
"errors"
"fmt"
"strconv"
"net/http"
"sync"
"time"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/go-autostart"
"github.com/ProtonMail/proton-bridge/v2/internal/config/settings"
"github.com/ProtonMail/proton-bridge/v2/internal/config/tls"
"github.com/ProtonMail/proton-bridge/v2/internal/config/useragent"
"github.com/ProtonMail/proton-bridge/v2/internal/constants"
"github.com/ProtonMail/proton-bridge/v2/internal/metrics"
"github.com/ProtonMail/proton-bridge/v2/internal/sentry"
"github.com/ProtonMail/proton-bridge/v2/internal/store/cache"
"github.com/ProtonMail/proton-bridge/v2/internal/updater"
"github.com/ProtonMail/proton-bridge/v2/internal/users"
"github.com/ProtonMail/proton-bridge/v2/pkg/message"
"github.com/ProtonMail/proton-bridge/v2/pkg/pmapi"
"github.com/ProtonMail/proton-bridge/v2/pkg/listener"
logrus "github.com/sirupsen/logrus"
"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/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/focus"
"github.com/ProtonMail/proton-bridge/v3/internal/identifier"
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/sentry"
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapsmtpserver"
"github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice"
"github.com/ProtonMail/proton-bridge/v3/internal/telemetry"
"github.com/ProtonMail/proton-bridge/v3/internal/user"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
"github.com/bradenaw/juniper/xslices"
"github.com/go-resty/resty/v2"
"github.com/sirupsen/logrus"
)
var log = logrus.WithField("pkg", "bridge") //nolint:gochecknoglobals
var ErrLocalCacheUnavailable = errors.New("local cache is unavailable")
type Bridge struct {
*users.Users
// vault holds bridge-specific data, such as preferences and known users (authorized or not).
vault *vault.Vault
locations Locator
settings SettingsProvider
clientManager pmapi.Manager
// users holds authorized users.
users map[string]*user.User
usersLock safe.RWMutex
// api manages user API clients.
api *proton.Manager
proxyCtl ProxyController
identifier identifier.Identifier
// tlsConfig holds the bridge TLS config used by the IMAP and SMTP servers.
tlsConfig *tls.Config
// imapServer is the bridge's IMAP server.
imapEventCh chan imapEvents.Event
// updater is the bridge's updater.
updater Updater
versioner Versioner
tls *tls.TLS
userAgent *useragent.UserAgent
cacheProvider CacheProvider
autostart *autostart.App
// Bridge's global errors list.
installCh chan installJob
// heartbeat is the telemetry heartbeat for metrics.
heartbeat *heartBeatState
// curVersion is the current version of the bridge,
// newVersion is the version that was installed by the updater.
curVersion *semver.Version
newVersion *semver.Version
newVersionLock safe.RWMutex
// keychains is the utils that own usable keychains found in the OS.
keychains *keychain.List
// focusService is used to raise the bridge window when needed.
focusService *focus.Service
// autostarter is the bridge's autostarter.
autostarter Autostarter
// locator is the bridge's locator.
locator Locator
// panicHandler
panicHandler async.PanicHandler
// reporter
reporter reporter.Reporter
// watchers holds all registered event watchers.
watchers []*watcher.Watcher[events.Event]
watchersLock sync.RWMutex
// errors contains errors encountered during startup.
errors []error
isAllMailVisible bool
isFirstStart bool
lastVersion string
// These control the bridge's IMAP and SMTP logging behaviour.
logIMAPClient bool
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
// goLoad triggers a load of disconnected users from the vault.
goLoad func()
// goUpdate triggers a check/install of updates.
goUpdate func()
serverManager *imapsmtpserver.Service
syncService *syncservice.Service
}
func New( //nolint:funlen
locations Locator,
cacheProvider CacheProvider,
setting SettingsProvider,
sentryReporter *sentry.Reporter,
panicHandler users.PanicHandler,
eventListener listener.Listener,
tls *tls.TLS,
userAgent *useragent.UserAgent,
cache cache.Cache,
builder *message.Builder,
clientManager pmapi.Manager,
credStorer users.CredentialsStorer,
updater Updater,
versioner Versioner,
autostart *autostart.App,
) *Bridge {
// Allow DoH before starting the app if the user has previously set this setting.
// This allows us to start even if protonmail is blocked.
if setting.GetBool(settings.AllowProxyKey) {
clientManager.AllowProxy()
}
// New creates a new bridge.
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
updater Updater, // the updater to fetch and install updates
curVersion *semver.Version, // the current version of the bridge
keychains *keychain.List, // usable keychains
u := users.New(
locations,
apiURL string, // the URL of the API to use
cookieJar http.CookieJar, // the cookie jar to use
identifier identifier.Identifier, // the identifier to keep track of the user agent
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
panicHandler async.PanicHandler,
reporter reporter.Reporter,
uidValidityGenerator imap.UIDValidityGenerator,
heartBeatManager telemetry.HeartbeatManager,
logIMAPClient, logIMAPServer bool, // whether to log IMAP client/server activity
logSMTP bool, // whether to log SMTP activity
) (*Bridge, <-chan events.Event, error) {
// api is the user's API manager.
api := proton.New(newAPIOptions(apiURL, curVersion, cookieJar, roundTripper, panicHandler)...)
// tasks holds all the bridge's background tasks.
tasks := async.NewGroup(context.Background(), panicHandler)
// imapEventCh forwards IMAP events from gluon instances to the bridge for processing.
imapEventCh := make(chan imapEvents.Event)
// bridge is the bridge.
bridge, err := newBridge(
context.Background(),
tasks,
imapEventCh,
locator,
vault,
autostarter,
updater,
curVersion,
keychains,
panicHandler,
eventListener,
clientManager,
credStorer,
newStoreFactory(cacheProvider, sentryReporter, panicHandler, eventListener, cache, builder),
reporter,
api,
identifier,
proxyCtl,
uidValidityGenerator,
heartBeatManager,
logIMAPClient, logIMAPServer, logSMTP,
)
if err != nil {
return nil, nil, fmt.Errorf("failed to create bridge: %w", err)
}
// Get an event channel for all events (individual events can be subscribed to later).
eventCh, _ := bridge.GetEvents()
// Initialize all of bridge's background tasks and operations.
if err := bridge.init(tlsReporter); err != nil {
return nil, nil, fmt.Errorf("failed to initialize bridge: %w", err)
}
return bridge, eventCh, nil
}
func newBridge(
ctx context.Context,
tasks *async.Group,
imapEventCh chan imapEvents.Event,
locator Locator,
vault *vault.Vault,
autostarter Autostarter,
updater Updater,
curVersion *semver.Version,
keychains *keychain.List,
panicHandler async.PanicHandler,
reporter reporter.Reporter,
api *proton.Manager,
identifier identifier.Identifier,
proxyCtl ProxyController,
uidValidityGenerator imap.UIDValidityGenerator,
heartbeatManager telemetry.HeartbeatManager,
logIMAPClient, logIMAPServer, logSMTP bool,
) (*Bridge, error) {
tlsConfig, err := loadTLSConfig(vault)
if err != nil {
return nil, fmt.Errorf("failed to load TLS config: %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)
}
identifier.SetClientString(vault.GetLastUserAgent())
focusService, err := focus.NewService(locator, curVersion, panicHandler)
if err != nil {
return nil, fmt.Errorf("failed to create focus service: %w", err)
}
bridge := &Bridge{
vault: vault,
users: make(map[string]*user.User),
usersLock: safe.NewRWMutex(),
api: api,
proxyCtl: proxyCtl,
identifier: identifier,
tlsConfig: tlsConfig,
imapEventCh: imapEventCh,
updater: updater,
installCh: make(chan installJob),
curVersion: curVersion,
newVersion: curVersion,
newVersionLock: safe.NewRWMutex(),
keychains: keychains,
panicHandler: panicHandler,
reporter: reporter,
heartbeat: newHeartBeatState(ctx, panicHandler),
focusService: focusService,
autostarter: autostarter,
locator: locator,
logIMAPClient: logIMAPClient,
logIMAPServer: logIMAPServer,
logSMTP: logSMTP,
firstStart: firstStart,
lastVersion: lastVersion,
tasks: tasks,
syncService: syncservice.NewService(reporter, panicHandler),
}
bridge.serverManager = imapsmtpserver.NewService(context.Background(),
&bridgeSMTPSettings{b: bridge},
&bridgeIMAPSettings{b: bridge},
&bridgeEventPublisher{b: bridge},
panicHandler,
reporter,
uidValidityGenerator,
&bridgeIMAPSMTPTelemetry{b: bridge},
)
b := &Bridge{
Users: u,
locations: locations,
settings: setting,
clientManager: clientManager,
updater: updater,
versioner: versioner,
tls: tls,
userAgent: userAgent,
cacheProvider: cacheProvider,
autostart: autostart,
isFirstStart: false,
isAllMailVisible: setting.GetBool(settings.IsAllMailVisible),
if err := bridge.serverManager.Init(context.Background(), bridge.tasks, &bridgeEventSubscription{b: bridge}); err != nil {
return nil, err
}
if setting.GetBool(settings.FirstStartKey) {
b.isFirstStart = true
if err := b.SendMetric(metrics.New(metrics.Setup, metrics.FirstStart, metrics.Label(constants.Version))); err != nil {
logrus.WithError(err).Error("Failed to send metric")
}
setting.SetBool(settings.FirstStartKey, false)
if heartbeatManager == nil {
bridge.heartbeat.init(bridge, bridge)
} else {
bridge.heartbeat.init(bridge, heartbeatManager)
}
// Keep in bridge and update in settings the last used version.
b.lastVersion = b.settings.Get(settings.LastVersionKey)
b.settings.Set(settings.LastVersionKey, constants.Version)
bridge.syncService.Run(bridge.tasks)
go b.heartbeat()
return bridge, nil
}
func (bridge *Bridge) init(tlsReporter TLSReporter) error {
// Enable or disable the proxy at startup.
if bridge.vault.GetProxyAllowed() {
bridge.proxyCtl.AllowProxy()
} else {
bridge.proxyCtl.DisallowProxy()
}
// Handle connection up/down events.
bridge.api.AddStatusObserver(func(status proton.Status) {
logrus.Info("API status changed: ", status)
switch {
case status == proton.StatusUp:
bridge.publish(events.ConnStatusUp{})
bridge.tasks.Once(bridge.onStatusUp)
case status == proton.StatusDown:
bridge.publish(events.ConnStatusDown{})
bridge.tasks.Once(bridge.onStatusDown)
}
})
// If any call returns a bad version code, we need to update.
bridge.api.AddErrorHandler(proton.AppVersionBadCode, func() {
logrus.Warn("App version is bad")
bridge.publish(events.UpdateForced{})
})
// Ensure all outgoing headers have the correct user agent.
bridge.api.AddPreRequestHook(func(_ *resty.Client, req *resty.Request) error {
req.SetHeader("User-Agent", bridge.identifier.GetUserAgent())
return nil
})
// Log all manager API requests (client requests are logged separately).
bridge.api.AddPostRequestHook(func(_ *resty.Client, r *resty.Response) error {
if _, ok := proton.ClientIDFromContext(r.Request.Context()); !ok {
logrus.Infof("[MANAGER] %v: %v %v", r.Status(), r.Request.Method, r.Request.URL)
}
return nil
})
// Publish a TLS issue event if a TLS issue is encountered.
bridge.tasks.Once(func(ctx context.Context) {
async.RangeContext(ctx, tlsReporter.GetTLSIssueCh(), func(struct{}) {
logrus.Warn("TLS issue encountered")
bridge.publish(events.TLSIssue{})
})
})
// Publish a raise event if the focus service is called.
bridge.tasks.Once(func(ctx context.Context) {
async.RangeContext(ctx, bridge.focusService.GetRaiseCh(), func(struct{}) {
logrus.Info("Focus service requested raise")
bridge.publish(events.Raise{})
})
})
// Handle any IMAP events that are forwarded to the bridge from gluon.
bridge.tasks.Once(func(ctx context.Context) {
async.RangeContext(ctx, bridge.imapEventCh, func(event imapEvents.Event) {
logrus.WithField("event", fmt.Sprintf("%T", event)).Debug("Received IMAP event")
bridge.handleIMAPEvent(event)
})
})
// 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")
if netErr := new(proton.NetError); !errors.As(err, &netErr) {
sentry.ReportError(bridge.reporter, "Failed to load users", err)
}
return
}
bridge.publish(events.AllUsersLoaded{})
})
defer bridge.goLoad()
// Check for updates when triggered.
bridge.goUpdate = bridge.tasks.PeriodicOrTrigger(constants.UpdateCheckInterval, 0, func(ctx context.Context) {
logrus.Info("Checking for updates")
version, err := bridge.updater.GetVersionInfo(ctx, bridge.api, bridge.vault.GetUpdateChannel())
if err != nil {
bridge.publish(events.UpdateCheckFailed{Error: err})
} else {
bridge.handleUpdate(version)
}
})
defer bridge.goUpdate()
// Install updates when available.
bridge.tasks.Once(func(ctx context.Context) {
async.RangeContext(ctx, bridge.installCh, func(job installJob) {
bridge.installUpdate(ctx, job)
})
})
return nil
}
// GetEvents returns a channel of events of the given type.
// If no types are supplied, all events are returned.
func (bridge *Bridge) GetEvents(ofType ...events.Event) (<-chan events.Event, context.CancelFunc) {
watcher := bridge.addWatcher(ofType...)
return watcher.GetChannel(), func() { bridge.remWatcher(watcher) }
}
func (bridge *Bridge) PushError(err error) {
bridge.errors = append(bridge.errors, err)
}
func (bridge *Bridge) GetErrors() []error {
return bridge.errors
}
func (bridge *Bridge) Close(ctx context.Context) {
logrus.Info("Closing bridge")
// Stop heart beat before closing users.
bridge.heartbeat.stop()
// Close all users.
safe.Lock(func() {
for _, user := range bridge.users {
user.Close()
}
}, bridge.usersLock)
// Close the servers
if err := bridge.serverManager.CloseServers(ctx); err != nil {
logrus.WithError(err).Error("Failed to close servers")
}
// Stop all ongoing tasks.
bridge.tasks.CancelAndWait()
// Close the focus service.
bridge.focusService.Close()
// Close the watchers.
bridge.watchersLock.Lock()
defer bridge.watchersLock.Unlock()
for _, watcher := range bridge.watchers {
watcher.Close()
}
bridge.watchers = nil
}
func (bridge *Bridge) publish(event events.Event) {
bridge.watchersLock.RLock()
defer bridge.watchersLock.RUnlock()
logrus.WithField("event", event).Debug("Publishing event")
for _, watcher := range bridge.watchers {
if watcher.IsWatching(event) {
if ok := watcher.Send(event); !ok {
logrus.WithField("event", event).Warn("Failed to send event to watcher")
}
}
}
}
func (bridge *Bridge) addWatcher(ofType ...events.Event) *watcher.Watcher[events.Event] {
bridge.watchersLock.Lock()
defer bridge.watchersLock.Unlock()
watcher := watcher.New(bridge.panicHandler, ofType...)
bridge.watchers = append(bridge.watchers, watcher)
return watcher
}
func (bridge *Bridge) remWatcher(watcher *watcher.Watcher[events.Event]) {
bridge.watchersLock.Lock()
defer bridge.watchersLock.Unlock()
idx := xslices.Index(bridge.watchers, watcher)
if idx < 0 {
return
}
bridge.watchers = append(bridge.watchers[:idx], bridge.watchers[idx+1:]...)
watcher.Close()
}
func (bridge *Bridge) onStatusUp(_ context.Context) {
logrus.Info("Handling API status up")
bridge.goLoad()
}
func (bridge *Bridge) onStatusDown(ctx context.Context) {
logrus.Info("Handling API status down")
for backoff := time.Second; ; backoff = min(backoff*2, 30*time.Second) {
select {
case <-ctx.Done():
return
case <-time.After(backoff):
logrus.Info("Pinging API")
if err := bridge.api.Ping(ctx); err != nil {
logrus.WithError(err).Warn("Ping failed, API is still unreachable")
} else {
return
}
}
}
}
func loadTLSConfig(vault *vault.Vault) (*tls.Config, error) {
cert, err := tls.X509KeyPair(vault.GetBridgeTLSCert())
if err != nil {
return nil, err
}
return &tls.Config{
Certificates: []tls.Certificate{cert},
MinVersion: tls.VersionTLS12,
}, nil
}
func min(a, b time.Duration) time.Duration {
if a < b {
return a
}
return b
}
// heartbeat sends a heartbeat signal once a day.
func (b *Bridge) heartbeat() {
for range time.Tick(time.Minute) {
lastHeartbeatDay, err := strconv.ParseInt(b.settings.Get(settings.LastHeartbeatKey), 10, 64)
if err != nil {
continue
}
// If we're still on the same day, don't send a heartbeat.
if time.Now().YearDay() == int(lastHeartbeatDay) {
continue
}
// We're on the next (or a different) day, so send a heartbeat.
if err := b.SendMetric(metrics.New(metrics.Heartbeat, metrics.Daily, metrics.NoLabel)); err != nil {
logrus.WithError(err).Error("Failed to send heartbeat")
continue
}
// Heartbeat was sent successfully so update the last heartbeat day.
b.settings.Set(settings.LastHeartbeatKey, fmt.Sprintf("%v", time.Now().YearDay()))
}
}
// GetUpdateChannel returns currently set update channel.
func (b *Bridge) GetUpdateChannel() updater.UpdateChannel {
return updater.UpdateChannel(b.settings.Get(settings.UpdateChannelKey))
}
// SetUpdateChannel switches update channel.
func (b *Bridge) SetUpdateChannel(channel updater.UpdateChannel) {
b.settings.Set(settings.UpdateChannelKey, string(channel))
}
func (b *Bridge) resetToLatestStable() error {
version, err := b.updater.Check()
if err != nil {
// If we can not check for updates - just remove all local updates and reset to base installer version.
// Not using `b.locations.ClearUpdates()` because `versioner.RemoveOtherVersions` can also handle
// case when it is needed to remove currently running verion.
if err := b.versioner.RemoveOtherVersions(semver.MustParse("0.0.0")); err != nil {
log.WithError(err).Error("Failed to clear updates while downgrading channel")
}
return nil
}
// If current version is same as upstream stable version - do nothing.
if version.Version.Equal(semver.MustParse(constants.Version)) {
return nil
}
if err := b.updater.InstallUpdate(version); err != nil {
return err
}
return b.versioner.RemoveOtherVersions(version.Version)
}
// FactoryReset will remove all local cache and settings.
// It will also downgrade to latest stable version if user is on early version.
func (b *Bridge) FactoryReset() {
wasEarly := b.GetUpdateChannel() == updater.EarlyChannel
b.settings.Set(settings.UpdateChannelKey, string(updater.StableChannel))
if wasEarly {
if err := b.resetToLatestStable(); err != nil {
log.WithError(err).Error("Failed to reset to latest stable version")
}
}
if err := b.Users.ClearData(); err != nil {
log.WithError(err).Error("Failed to remove bridge data")
}
if err := b.Users.ClearUsers(); err != nil {
log.WithError(err).Error("Failed to remove bridge users")
}
}
// GetKeychainApp returns current keychain helper.
func (b *Bridge) GetKeychainApp() string {
return b.settings.Get(settings.PreferredKeychainKey)
}
// SetKeychainApp sets current keychain helper.
func (b *Bridge) SetKeychainApp(helper string) {
b.settings.Set(settings.PreferredKeychainKey, helper)
}
func (b *Bridge) EnableCache() error {
if err := b.Users.EnableCache(); err != nil {
return err
}
b.settings.SetBool(settings.CacheEnabledKey, true)
return nil
}
func (b *Bridge) DisableCache() error {
if err := b.Users.DisableCache(); err != nil {
return err
}
b.settings.SetBool(settings.CacheEnabledKey, false)
// Reset back to the default location when disabling.
b.settings.Set(settings.CacheLocationKey, b.cacheProvider.GetDefaultMessageCacheDir())
return nil
}
func (b *Bridge) MigrateCache(from, to string) error {
if err := b.Users.MigrateCache(from, to); err != nil {
return err
}
b.settings.Set(settings.CacheLocationKey, to)
return nil
}
// SetProxyAllowed instructs the app whether to use DoH to access an API proxy if necessary.
// It also needs to work before the app is initialised (because we may need to use the proxy at startup).
func (b *Bridge) SetProxyAllowed(proxyAllowed bool) {
b.settings.SetBool(settings.AllowProxyKey, proxyAllowed)
if proxyAllowed {
b.clientManager.AllowProxy()
} else {
b.clientManager.DisallowProxy()
}
}
// GetProxyAllowed returns whether use of DoH is enabled to access an API proxy if necessary.
func (b *Bridge) GetProxyAllowed() bool {
return b.settings.GetBool(settings.AllowProxyKey)
}
// AddError add an error to a global error list if it does not contain it yet. Adding nil is noop.
func (b *Bridge) AddError(err error) {
if err == nil {
return
}
if b.HasError(err) {
return
}
b.errors = append(b.errors, err)
}
// DelError removes an error from global error list.
func (b *Bridge) DelError(err error) {
for idx, val := range b.errors {
if val == err {
b.errors = append(b.errors[:idx], b.errors[idx+1:]...)
return
}
}
}
// HasError returnes true if global error list contains an err.
func (b *Bridge) HasError(err error) bool {
for _, val := range b.errors {
if val == err {
return true
}
}
return false
}
// GetLastVersion returns the version which was used in previous execution of
// Bridge.
func (b *Bridge) GetLastVersion() string {
return b.lastVersion
}
// IsFirstStart returns true when Bridge is running for first time or after
// factory reset.
func (b *Bridge) IsFirstStart() bool {
return b.isFirstStart
}
// IsAllMailVisible can be called extensively by IMAP. Therefore, it is better
// to cache the value instead of reading from settings file.
func (b *Bridge) IsAllMailVisible() bool {
return b.isAllMailVisible
}
func (b *Bridge) SetIsAllMailVisible(isVisible bool) {
b.settings.SetBool(settings.IsAllMailVisible, isVisible)
b.isAllMailVisible = isVisible
}

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
// Copyright (c) 2022 Proton AG
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
@ -18,189 +18,149 @@
package bridge
import (
"archive/zip"
"bytes"
"context"
"errors"
"io"
"os"
"path/filepath"
"sort"
"github.com/ProtonMail/proton-bridge/v2/internal/logging"
"github.com/ProtonMail/proton-bridge/v2/pkg/pmapi"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
)
const (
MaxAttachmentSize = 7 * 1024 * 1024 // MaxAttachmentSize 7 MB total limit
MaxCompressedFilesCount = 6
DefaultMaxBugReportZipSize = 7 * 1024 * 1024
DefaultMaxSessionCountForBugReport = 10
)
var ErrSizeTooLarge = errors.New("file is too big")
// ReportBug reports a new bug from the user.
func (b *Bridge) ReportBug(osType, osVersion, description, accountName, address, emailClient string, attachLogs bool) error {
if user, err := b.GetUser(address); err == nil {
accountName = user.Username()
} else if users := b.GetUsers(); len(users) > 0 {
accountName = users[0].Username()
type ReportBugReq struct {
OSType string
OSVersion string
Title string
Description string
Username string
Email string
EmailClient string
IncludeLogs bool
}
report := pmapi.ReportBugReq{
OS: osType,
OSVersion: osVersion,
Browser: emailClient,
Title: "[Bridge] Bug",
Description: description,
Username: accountName,
Email: address,
}
if attachLogs {
logs, err := b.getMatchingLogs(
func(filename string) bool {
return logging.MatchLogName(filename) && !logging.MatchStackTraceName(filename)
},
)
if err != nil {
log.WithError(err).Error("Can't get log files list")
}
crashes, err := b.getMatchingLogs(
func(filename string) bool {
return logging.MatchLogName(filename) && logging.MatchStackTraceName(filename)
},
)
if err != nil {
log.WithError(err).Error("Can't get crash files list")
}
var matchFiles []string
matchFiles = append(matchFiles, logs[max(0, len(logs)-(MaxCompressedFilesCount/2)):]...)
matchFiles = append(matchFiles, crashes[max(0, len(crashes)-(MaxCompressedFilesCount/2)):]...)
archive, err := zipFiles(matchFiles)
if err != nil {
log.WithError(err).Error("Can't zip logs and crashes")
}
if archive != nil {
report.AddAttachment("logs.zip", "application/zip", archive)
func (bridge *Bridge) ReportBug(ctx context.Context, report *ReportBugReq) error {
if info, err := bridge.QueryUserInfo(report.Username); err == nil {
report.Username = info.Username
} else if userIDs := bridge.GetUserIDs(); len(userIDs) > 0 {
if err := bridge.vault.GetUser(userIDs[0], func(user *vault.User) {
report.Username = user.Username()
}); err != nil {
return err
}
}
return b.clientManager.ReportBug(context.Background(), report)
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
func (b *Bridge) getMatchingLogs(filenameMatchFunc func(string) bool) (filenames []string, err error) {
logsPath, err := b.locations.ProvideLogsPath()
if err != nil {
return nil, err
}
files, err := os.ReadDir(logsPath)
if err != nil {
return nil, err
}
var matchFiles []string
for _, file := range files {
if filenameMatchFunc(file.Name()) {
matchFiles = append(matchFiles, filepath.Join(logsPath, file.Name()))
}
}
sort.Strings(matchFiles) // Sorted by timestamp: oldest first.
return matchFiles, nil
}
type LimitedBuffer struct {
capacity int
buf *bytes.Buffer
}
func NewLimitedBuffer(capacity int) *LimitedBuffer {
return &LimitedBuffer{
capacity: capacity,
buf: bytes.NewBuffer(make([]byte, 0, capacity)),
}
}
func (b *LimitedBuffer) Write(p []byte) (n int, err error) {
if len(p)+b.buf.Len() > b.capacity {
return 0, ErrSizeTooLarge
}
return b.buf.Write(p)
}
func (b *LimitedBuffer) Read(p []byte) (n int, err error) {
return b.buf.Read(p)
}
func zipFiles(filenames []string) (io.Reader, error) {
if len(filenames) == 0 {
return nil, nil
}
buf := NewLimitedBuffer(MaxAttachmentSize)
w := zip.NewWriter(buf)
defer w.Close() //nolint:errcheck
for _, file := range filenames {
err := addFileToZip(file, w)
if err != nil {
return nil, err
}
}
if err := w.Close(); err != nil {
return nil, err
}
return buf, nil
}
func addFileToZip(filename string, writer *zip.Writer) error {
fileReader, err := os.Open(filepath.Clean(filename))
var attachments []proton.ReportBugAttachment
if report.IncludeLogs {
logs, err := bridge.CollectLogs()
if err != nil {
return err
}
defer fileReader.Close() //nolint:errcheck,gosec
attachments = append(attachments, logs)
}
fileInfo, err := fileReader.Stat()
var firstAtt proton.ReportBugAttachment
if len(attachments) > 0 && report.IncludeLogs {
firstAtt = attachments[0]
}
attachmentType := proton.AttachmentTypeSync
if len(attachments) > 1 {
attachmentType = proton.AttachmentTypeAsync
}
token, err := bridge.createTicket(ctx, report, attachmentType, firstAtt)
if err != nil || token == "" {
return err
}
safe.RLock(func() {
for _, user := range bridge.users {
user.ReportBugSent()
}
}, bridge.usersLock)
// if we have a token we can append more attachment to the bugReport
for i, att := range attachments {
if i == 0 && report.IncludeLogs {
continue
}
err := bridge.appendComment(ctx, token, att)
if err != nil {
return err
}
}
return err
}
header, err := zip.FileInfoHeader(fileInfo)
func (bridge *Bridge) CollectLogs() (proton.ReportBugAttachment, error) {
logsPath, err := bridge.locator.ProvideLogsPath()
if err != nil {
return err
return proton.ReportBugAttachment{}, err
}
header.Method = zip.Deflate
header.Name = filepath.Base(filename)
fileWriter, err := writer.CreateHeader(header)
buffer, err := logging.ZipLogsForBugReport(logsPath, DefaultMaxSessionCountForBugReport, DefaultMaxBugReportZipSize)
if err != nil {
return err
return proton.ReportBugAttachment{}, err
}
_, err = io.Copy(fileWriter, fileReader)
body, err := io.ReadAll(buffer)
if err != nil {
return err
return proton.ReportBugAttachment{}, err
}
err = fileReader.Close()
return err
return proton.ReportBugAttachment{
Name: "logs.zip",
Filename: "logs.zip",
MIMEType: "application/zip",
Body: body,
}, nil
}
func (bridge *Bridge) createTicket(ctx context.Context, report *ReportBugReq,
asyncAttach proton.AttachmentType, att proton.ReportBugAttachment) (string, error) {
var attachments []proton.ReportBugAttachment
attachments = append(attachments, att)
res, err := bridge.api.ReportBug(ctx, proton.ReportBugReq{
OS: report.OSType,
OSVersion: report.OSVersion,
Title: "[Bridge] Bug - " + report.Title,
Description: report.Description,
Client: report.EmailClient,
ClientType: proton.ClientTypeEmail,
ClientVersion: constants.AppVersion(bridge.curVersion.Original()),
Username: report.Username,
Email: report.Email,
AsyncAttachments: asyncAttach,
}, attachments...)
if err != nil || asyncAttach != proton.AttachmentTypeAsync {
return "", err
}
if asyncAttach == proton.AttachmentTypeAsync && res.Token == nil {
return "", errors.New("no token returns for AsyncAttachments")
}
return *res.Token, nil
}
func (bridge *Bridge) appendComment(ctx context.Context, token string, att proton.ReportBugAttachment) error {
var attachments []proton.ReportBugAttachment
attachments = append(attachments, att)
return bridge.api.ReportBugAttachement(ctx, proton.ReportBugAttachmentReq{
Product: proton.ClientTypeEmail,
Body: "Comment adding attachment: " + att.Filename,
Token: token,
}, attachments...)
}

View File

@ -0,0 +1,46 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge
import (
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
)
func (bridge *Bridge) ReportBugClicked() {
safe.RLock(func() {
for _, user := range bridge.users {
user.ReportBugClicked()
}
}, bridge.usersLock)
}
func (bridge *Bridge) AutoconfigUsed(client string) {
safe.RLock(func() {
for _, user := range bridge.users {
user.AutoconfigUsed(client)
}
}, bridge.usersLock)
}
func (bridge *Bridge) KBArticleOpened(article string) {
safe.RLock(func() {
for _, user := range bridge.users {
user.KBArticleOpened(article)
}
}, bridge.usersLock)
}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2022 Proton AG
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
@ -18,53 +18,73 @@
package bridge
import (
"context"
"errors"
"strings"
"github.com/ProtonMail/proton-bridge/v2/internal/clientconfig"
"github.com/ProtonMail/proton-bridge/v2/internal/config/settings"
"github.com/ProtonMail/proton-bridge/v2/internal/config/useragent"
"github.com/ProtonMail/proton-bridge/v3/internal/clientconfig"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/sirupsen/logrus"
)
func (b *Bridge) ConfigureAppleMail(userID, address string) (bool, error) {
user, err := b.GetUser(userID)
if err != nil {
return false, err
// ConfigureAppleMail configures Apple Mail for the given userID and address.
// If configuring Apple Mail for Catalina or newer, it ensures Bridge is using SSL.
func (bridge *Bridge) ConfigureAppleMail(ctx context.Context, userID, address string) error {
logrus.WithFields(logrus.Fields{
"userID": userID,
"address": logging.Sensitive(address),
}).Info("Configuring Apple Mail")
return safe.RLockRet(func() error {
user, ok := bridge.users[userID]
if !ok {
return ErrNoSuchUser
}
emails := user.Emails()
displayNames := user.DisplayNames()
if (len(emails) == 0) || (len(displayNames) == 0) {
return errors.New("could not retrieve user address info")
}
if address == "" {
address = user.GetPrimaryAddress()
address = emails[0]
}
username := address
addresses := address
if user.IsCombinedAddressMode() {
username = user.GetPrimaryAddress()
addresses = strings.Join(user.GetAddresses(), ",")
var username, displayName, addresses string
if user.GetAddressMode() == vault.CombinedMode {
username = address
displayName = displayNames[username]
addresses = strings.Join(emails, ",")
} else {
username = address
addresses = address
displayName = displayNames[address]
if len(displayName) == 0 {
displayName = address
}
}
var (
restart = false
smtpSSL = b.settings.GetBool(settings.SMTPSSLKey)
if useragent.IsCatalinaOrNewer() && !bridge.vault.GetSMTPSSL() {
if err := bridge.SetSMTPSSL(ctx, true); err != nil {
return err
}
}
return (&clientconfig.AppleMail{}).Configure(
constants.Host,
bridge.vault.GetIMAPPort(),
bridge.vault.GetSMTPPort(),
bridge.vault.GetIMAPSSL(),
bridge.vault.GetSMTPSSL(),
username,
displayName,
addresses,
user.BridgePass(),
)
// If configuring apple mail for Catalina or newer, users should use SSL.
if useragent.IsCatalinaOrNewer() && !smtpSSL {
smtpSSL = true
restart = true
b.settings.SetBool(settings.SMTPSSLKey, true)
}
if err := (&clientconfig.AppleMail{}).Configure(
Host,
b.settings.GetInt(settings.IMAPPortKey),
b.settings.GetInt(settings.SMTPPortKey),
false, smtpSSL,
username, addresses,
user.GetBridgePassword(),
); err != nil {
return false, err
}
return restart, nil
}, bridge.usersLock)
}

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

@ -0,0 +1,297 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge
import (
"context"
"fmt"
"io"
"os"
"path/filepath"
"github.com/ProtonMail/gluon/imap"
"github.com/ProtonMail/gluon/rfc822"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/proton-bridge/v3/internal/user"
"github.com/bradenaw/juniper/iterator"
"github.com/bradenaw/juniper/xslices"
goimap "github.com/emersion/go-imap"
goimapclient "github.com/emersion/go-imap/client"
"github.com/sirupsen/logrus"
"golang.org/x/exp/maps"
)
type CheckClientStateResult struct {
MissingMessages map[string]map[string]user.DiagMailboxMessage
}
func (c *CheckClientStateResult) AddMissingMessage(userID string, message user.DiagMailboxMessage) {
v, ok := c.MissingMessages[userID]
if !ok {
c.MissingMessages[userID] = map[string]user.DiagMailboxMessage{message.ID: message}
} else {
v[message.ID] = message
}
}
// CheckClientState checks the current IMAP client reported state against the proton server state and reports
// anything that is out of place.
func (bridge *Bridge) CheckClientState(ctx context.Context, checkFlags bool, progressCB func(string)) (CheckClientStateResult, error) {
bridge.usersLock.RLock()
defer bridge.usersLock.RUnlock()
users := maps.Values(bridge.users)
result := CheckClientStateResult{
MissingMessages: make(map[string]map[string]user.DiagMailboxMessage),
}
for _, usr := range users {
if progressCB != nil {
progressCB(fmt.Sprintf("Checking state for user %v", usr.Name()))
}
log := logrus.WithField("user", usr.Name()).WithField("diag", "state-check")
log.Debug("Retrieving all server metadata")
meta, err := usr.GetDiagnosticMetadata(ctx)
if err != nil {
return result, err
}
success := true
if len(meta.Metadata) != len(meta.MessageIDs) {
log.Errorf("Metadata (%v) and message(%v) list sizes do not match", len(meta.Metadata), len(meta.MessageIDs))
}
log.Debug("Building state")
state, err := meta.BuildMailboxToMessageMap(ctx, usr)
if err != nil {
log.WithError(err).Error("Failed to build state")
return result, err
}
info, err := bridge.GetUserInfo(usr.ID())
if err != nil {
log.WithError(err).Error("Failed to get user info")
return result, err
}
addr := fmt.Sprintf("127.0.0.1:%v", bridge.GetIMAPPort())
for account, mboxMap := range state {
if progressCB != nil {
progressCB(fmt.Sprintf("Checking state for user %v's account '%v'", usr.Name(), account))
}
if err := func(account string, mboxMap user.AccountMailboxMap) error {
client, err := goimapclient.Dial(addr)
if err != nil {
log.WithError(err).Error("Failed to connect to imap client")
return err
}
defer func() {
_ = client.Logout()
}()
if err := client.Login(account, string(info.BridgePass)); err != nil {
return fmt.Errorf("failed to login for user %v:%w", usr.Name(), err)
}
log := log.WithField("account", account)
for mboxName, messageList := range mboxMap {
log := log.WithField("mbox", mboxName)
status, err := client.Select(mboxName, true)
if err != nil {
log.WithError(err).Errorf("Failed to select mailbox %v", messageList)
return fmt.Errorf("failed to select '%v':%w", mboxName, err)
}
log.Debug("Checking message count")
if int(status.Messages) != len(messageList) {
success = false
log.Errorf("Message count doesn't match, got '%v' expected '%v'", status.Messages, len(messageList))
}
ids, err := clientGetMessageIDs(client, mboxName)
if err != nil {
return fmt.Errorf("failed to get message ids for mbox '%v': %w", mboxName, err)
}
for _, msg := range messageList {
imapFlags, ok := ids[msg.ID]
if !ok {
if meta.FailedMessageIDs.Contains(msg.ID) {
log.Warningf("Missing message '%v', but it is part of failed message set", msg.ID)
} else {
log.Errorf("Missing message '%v'", msg.ID)
}
result.AddMissingMessage(msg.UserID, msg)
continue
}
if checkFlags {
if !imapFlags.Equals(msg.Flags) {
log.Errorf("Message '%v' flags do mot match, got=%v, expected=%v",
msg.ID,
imapFlags.ToSlice(),
msg.Flags.ToSlice(),
)
}
}
}
}
if !success {
log.Errorf("State does not match")
} else {
log.Info("State matches")
}
return nil
}(account, mboxMap); err != nil {
return result, err
}
}
// Check for orphaned messages (only present in All Mail)
if progressCB != nil {
progressCB(fmt.Sprintf("Checking user %v for orphans", usr.Name()))
}
log.Debugf("Checking for orphans")
for _, m := range meta.Metadata {
filteredLabels := xslices.Filter(m.LabelIDs, func(t string) bool {
switch t {
case proton.AllMailLabel:
return false
case proton.AllSentLabel:
return false
case proton.AllDraftsLabel:
return false
case proton.OutboxLabel:
return false
default:
return true
}
})
if len(filteredLabels) == 0 {
log.Warnf("Message %v is only present in All Mail (Subject=%v)", m.ID, m.Subject)
}
}
}
return result, nil
}
func (bridge *Bridge) DebugDownloadFailedMessages(
ctx context.Context,
result CheckClientStateResult,
exportPath string,
progressCB func(string, int, int),
) error {
bridge.usersLock.RLock()
defer bridge.usersLock.RUnlock()
for userID, messages := range result.MissingMessages {
usr, ok := bridge.users[userID]
if !ok {
return fmt.Errorf("failed to find user with id %v", userID)
}
userDir := filepath.Join(exportPath, userID)
if err := os.MkdirAll(userDir, 0o700); err != nil {
return fmt.Errorf("failed to create directory '%v': %w", userDir, err)
}
if err := usr.DebugDownloadMessages(ctx, userDir, messages, progressCB); err != nil {
return err
}
}
return nil
}
func clientGetMessageIDs(client *goimapclient.Client, mailbox string) (map[string]imap.FlagSet, error) {
status, err := client.Select(mailbox, true)
if err != nil {
return nil, err
}
if status.Messages == 0 {
return nil, nil
}
resCh := make(chan *goimap.Message)
section, err := goimap.ParseBodySectionName("BODY[HEADER]")
if err != nil {
return nil, err
}
fetchItems := []goimap.FetchItem{"BODY[HEADER]", goimap.FetchFlags}
seq, err := goimap.ParseSeqSet("1:*")
if err != nil {
return nil, err
}
go func() {
if err := client.Fetch(
seq,
fetchItems,
resCh,
); err != nil {
panic(err)
}
}()
messages := iterator.Collect(iterator.Chan(resCh))
ids := make(map[string]imap.FlagSet, len(messages))
for i, m := range messages {
literal, err := io.ReadAll(m.GetBody(section))
if err != nil {
return nil, err
}
header, err := rfc822.NewHeader(literal)
if err != nil {
return nil, fmt.Errorf("failed to parse header for msg %v: %w", i, err)
}
internalID, ok := header.GetChecked("X-Pm-Internal-Id")
if !ok {
logrus.Errorf("Message %v does not have internal id", internalID)
continue
}
messageFlags := imap.NewFlagSet(m.Flags...)
// Recent and Deleted are not part of the proton flag set.
messageFlags.RemoveFromSelf("\\Recent")
messageFlags.RemoveFromSelf("\\Deleted")
ids[internalID] = messageFlags
}
return ids, nil
}

View File

@ -0,0 +1,175 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge_test
import (
"bytes"
"context"
"crypto/tls"
"fmt"
"io"
"net"
"strings"
"testing"
"time"
"github.com/ProtonMail/gluon/rfc822"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/go-proton-api/server"
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
go_imap "github.com/emersion/go-imap"
"github.com/emersion/go-sasl"
"github.com/emersion/go-smtp"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
)
func TestBridge_HandleDraftsSendFromOtherClient(t *testing.T) {
getGluonHeaderID := func(literal []byte) (string, string) {
h, err := rfc822.NewHeader(literal)
require.NoError(t, err)
gluonID, ok := h.GetChecked("X-Pm-Gluon-Id")
require.True(t, ok)
externalID, ok := h.GetChecked("Message-Id")
require.True(t, ok)
return gluonID, externalID
}
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
_, _, err := s.CreateUser("imap", password)
require.NoError(t, err)
_, _, err = s.CreateUser("bar", password)
require.NoError(t, err)
// The initial user should be fully synced.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
waiter := waitForIMAPServerReady(b)
defer waiter.Done()
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
defer done()
userID, err := b.LoginFull(ctx, "imap", password, nil, nil)
require.NoError(t, err)
require.Equal(t, userID, (<-syncCh).UserID)
waiter.Wait()
info, err := b.GetUserInfo(userID)
require.NoError(t, err)
require.True(t, info.State == bridge.Connected)
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }()
// Create first draft in client.
literal := fmt.Sprintf(`From: %v
To: %v
Date: Fri, 3 Feb 2023 01:04:32 +0100
Subject: Foo
Hello
`, info.Addresses[0], "bar@proton.local")
require.NoError(t, client.Append("Drafts", nil, time.Now(), strings.NewReader(literal)))
// Verify the draft is available in client.
require.Eventually(t, func() bool {
status, err := client.Status("Drafts", []go_imap.StatusItem{go_imap.StatusMessages})
require.NoError(t, err)
return status.Messages == 1
}, 2*time.Second, time.Second)
// Retrieve the new literal so we can have the Proton Message ID.
messages, err := clientFetch(client, "Drafts")
require.NoError(t, err)
require.Equal(t, 1, len(messages))
newLiteral, err := io.ReadAll(messages[0].GetBody(must(go_imap.ParseBodySectionName("BODY[]"))))
require.NoError(t, err)
logrus.Info(string(newLiteral))
newLiteralID, newLiteralExternID := getGluonHeaderID(newLiteral)
// Modify new literal.
newLiteralModified := append(newLiteral, []byte(" world from client2")...) //nolint:gocritic
func() {
smtpClient, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(b.GetSMTPPort())))
require.NoError(t, err)
defer func() { _ = smtpClient.Close() }()
// Upgrade to TLS.
require.NoError(t, smtpClient.StartTLS(&tls.Config{InsecureSkipVerify: true}))
// Authorize with SASL PLAIN.
require.NoError(t, smtpClient.Auth(sasl.NewPlainClient(
info.Addresses[0],
info.Addresses[0],
string(info.BridgePass)),
))
// Send the message.
require.NoError(t, smtpClient.SendMail(
info.Addresses[0],
[]string{"bar@proton.local"},
bytes.NewReader(newLiteralModified),
))
}()
// Append message to Sent as the imap client would.
require.NoError(t, client.Append("Sent", nil, time.Now(), strings.NewReader(literal)))
// Verify the sent message gets updated with the new literal.
require.Eventually(t, func() bool {
// Check if sent message matches the latest draft.
messagesClient1, err := clientFetch(client, "Sent", "BODY[TEXT]", "BODY[]")
require.NoError(t, err)
if len(messagesClient1) != 1 {
return false
}
sentLiteral, err := io.ReadAll(messagesClient1[0].GetBody(must(go_imap.ParseBodySectionName("BODY[]"))))
require.NoError(t, err)
sentLiteralID, sentLiteralExternID := getGluonHeaderID(sentLiteral)
sentLiteralText, err := io.ReadAll(messagesClient1[0].GetBody(must(go_imap.ParseBodySectionName("BODY[TEXT]"))))
require.NoError(t, err)
sentLiteralStr := string(sentLiteralText)
literalMatches := sentLiteralStr == "Hello\r\n world from client2\r\n"
idIsDifferent := sentLiteralID != newLiteralID
externIDMatches := sentLiteralExternID == newLiteralExternID
return literalMatches && idIsDifferent && externIDMatches
}, 2*time.Second, time.Second)
})
}, server.WithMessageDedup())
}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2022 Proton AG
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
@ -15,11 +15,19 @@
// 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 updater
package bridge
import "errors"
var (
ErrDownloadVerify = errors.New("failed to download or verify the update")
ErrInstall = errors.New("failed to install the update")
ErrVaultInsecure = errors.New("the vault is insecure")
ErrVaultCorrupt = errors.New("the vault is corrupt")
ErrWatchUpdates = errors.New("failed to watch for updates")
ErrNoSuchUser = errors.New("no such user")
ErrUserAlreadyExists = errors.New("user already exists")
ErrUserAlreadyLoggedIn = errors.New("the user is already logged in")
ErrNotImplemented = errors.New("not implemented")
ErrSizeTooLarge = errors.New("file is too big")
)

45
internal/bridge/events.go Normal file
View File

@ -0,0 +1,45 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge
import (
"context"
"github.com/ProtonMail/gluon/watcher"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
)
type bridgeEventSubscription struct {
b *Bridge
}
func (b bridgeEventSubscription) Add(ofType ...events.Event) *watcher.Watcher[events.Event] {
return b.b.addWatcher(ofType...)
}
func (b bridgeEventSubscription) Remove(watcher *watcher.Watcher[events.Event]) {
b.b.remWatcher(watcher)
}
type bridgeEventPublisher struct {
b *Bridge
}
func (b bridgeEventPublisher) PublishEvent(_ context.Context, event events.Event) {
b.b.publish(event)
}

View File

@ -0,0 +1,167 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge
import (
"context"
"encoding/json"
"sync"
"time"
"github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/telemetry"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/sirupsen/logrus"
)
const HeartbeatCheckInterval = time.Hour
type heartBeatState struct {
task *async.Group
telemetry.Heartbeat
taskLock sync.Mutex
taskStarted bool
taskInterval time.Duration
}
func newHeartBeatState(ctx context.Context, panicHandler async.PanicHandler) *heartBeatState {
return &heartBeatState{
task: async.NewGroup(ctx, panicHandler),
}
}
func (h *heartBeatState) init(bridge *Bridge, manager telemetry.HeartbeatManager) {
h.Heartbeat = telemetry.NewHeartbeat(manager, 1143, 1025, bridge.GetGluonCacheDir(), bridge.keychains.GetDefaultHelper())
h.taskInterval = manager.GetHeartbeatPeriodicInterval()
h.SetRollout(bridge.GetUpdateRollout())
h.SetAutoStart(bridge.GetAutostart())
h.SetAutoUpdate(bridge.GetAutoUpdate())
h.SetBeta(bridge.GetUpdateChannel())
h.SetDoh(bridge.GetProxyAllowed())
h.SetShowAllMail(bridge.GetShowAllMail())
h.SetIMAPConnectionMode(bridge.GetIMAPSSL())
h.SetSMTPConnectionMode(bridge.GetSMTPSSL())
h.SetIMAPPort(bridge.GetIMAPPort())
h.SetSMTPPort(bridge.GetSMTPPort())
h.SetCacheLocation(bridge.GetGluonCacheDir())
if val, err := bridge.GetKeychainApp(); err != nil {
h.SetKeyChainPref(val)
} else {
h.SetKeyChainPref(bridge.keychains.GetDefaultHelper())
}
h.SetPrevVersion(bridge.GetLastVersion().String())
safe.RLock(func() {
var splitMode = false
for _, user := range bridge.users {
if user.GetAddressMode() == vault.SplitMode {
splitMode = true
break
}
}
var nbAccount = len(bridge.users)
h.SetNbAccount(nbAccount)
h.SetSplitMode(splitMode)
// Do not try to send if there is no user yet.
if nbAccount > 0 {
defer h.start()
}
}, bridge.usersLock)
}
func (h *heartBeatState) start() {
h.taskLock.Lock()
defer h.taskLock.Unlock()
if h.taskStarted {
return
}
h.taskStarted = true
h.task.PeriodicOrTrigger(h.taskInterval, 0, func(ctx context.Context) {
logrus.Debug("Checking for heartbeat")
h.TrySending(ctx)
})
}
func (h *heartBeatState) stop() {
h.taskLock.Lock()
defer h.taskLock.Unlock()
if !h.taskStarted {
return
}
h.task.CancelAndWait()
h.taskStarted = false
}
func (bridge *Bridge) IsTelemetryAvailable(ctx context.Context) bool {
var flag = true
if bridge.GetTelemetryDisabled() {
return false
}
safe.RLock(func() {
for _, user := range bridge.users {
flag = flag && user.IsTelemetryEnabled(ctx)
}
}, bridge.usersLock)
return flag
}
func (bridge *Bridge) SendHeartbeat(ctx context.Context, heartbeat *telemetry.HeartbeatData) bool {
data, err := json.Marshal(heartbeat)
if err != nil {
if err := bridge.reporter.ReportMessageWithContext("Cannot parse heartbeat data.", reporter.Context{
"error": err,
}); err != nil {
logrus.WithError(err).Error("Failed to parse heartbeat data.")
}
return false
}
var sent = false
safe.RLock(func() {
for _, user := range bridge.users {
if err := user.SendTelemetry(ctx, data); err == nil {
sent = true
break
}
}
}, bridge.usersLock)
return sent
}
func (bridge *Bridge) GetLastHeartbeatSent() time.Time {
return bridge.vault.GetLastHeartbeatSent()
}
func (bridge *Bridge) SetLastHeartbeatSent(timestamp time.Time) error {
return bridge.vault.SetLastHeartbeatSent(timestamp)
}
func (bridge *Bridge) GetHeartbeatPeriodicInterval() time.Duration {
return HeartbeatCheckInterval
}

View File

@ -0,0 +1,74 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge
import "github.com/sirupsen/logrus"
func (bridge *Bridge) GetCurrentUserAgent() string {
return bridge.identifier.GetUserAgent()
}
func (bridge *Bridge) SetCurrentPlatform(platform string) {
bridge.identifier.SetPlatform(platform)
}
func (bridge *Bridge) setUserAgent(name, version string) {
currentUserAgent := bridge.identifier.GetClientString()
bridge.identifier.SetClient(name, version)
newUserAgent := bridge.identifier.GetClientString()
if currentUserAgent != newUserAgent {
if err := bridge.vault.SetLastUserAgent(newUserAgent); err != nil {
logrus.WithError(err).Error("Failed to write new user agent to vault")
}
}
}
type bridgeUserAgentUpdater struct {
*Bridge
}
func (b *bridgeUserAgentUpdater) GetUserAgent() string {
return b.identifier.GetUserAgent()
}
func (b *bridgeUserAgentUpdater) HasClient() bool {
return b.identifier.HasClient()
}
func (b *bridgeUserAgentUpdater) SetClient(name, version string) {
b.identifier.SetClient(name, version)
}
func (b *bridgeUserAgentUpdater) SetPlatform(platform string) {
b.identifier.SetPlatform(platform)
}
func (b *bridgeUserAgentUpdater) SetClientString(client string) {
b.identifier.SetClientString(client)
}
func (b *bridgeUserAgentUpdater) GetClientString() string {
return b.identifier.GetClientString()
}
func (b *bridgeUserAgentUpdater) SetUserAgent(name, version string) {
b.setUserAgent(name, version)
}

129
internal/bridge/imap.go Normal file
View File

@ -0,0 +1,129 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge
import (
"context"
"crypto/tls"
"strings"
"github.com/Masterminds/semver/v3"
imapEvents "github.com/ProtonMail/gluon/events"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapsmtpserver"
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
"github.com/sirupsen/logrus"
)
func (bridge *Bridge) restartIMAP(ctx context.Context) error {
return bridge.serverManager.RestartIMAP(ctx)
}
func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) {
switch event := event.(type) {
case imapEvents.UserAdded:
for labelID, count := range event.Counts {
logrus.WithFields(logrus.Fields{
"gluonID": event.UserID,
"labelID": labelID,
"count": count,
}).Info("Received mailbox message count")
}
case imapEvents.IMAPID:
logrus.WithFields(logrus.Fields{
"sessionID": event.SessionID,
"name": event.IMAPID.Name,
"version": event.IMAPID.Version,
}).Info("Received IMAP ID")
if event.IMAPID.Name != "" && event.IMAPID.Version != "" {
bridge.setUserAgent(event.IMAPID.Name, event.IMAPID.Version)
}
case imapEvents.LoginFailed:
logrus.WithFields(logrus.Fields{
"sessionID": event.SessionID,
"username": event.Username,
"pkg": "imap",
}).Error("Incorrect login credentials.")
bridge.publish(events.IMAPLoginFailed{Username: event.Username})
case imapEvents.Login:
if strings.Contains(bridge.GetCurrentUserAgent(), useragent.DefaultUserAgent) {
bridge.setUserAgent(useragent.UnknownClient, useragent.DefaultVersion)
}
}
}
type bridgeIMAPSettings struct {
b *Bridge
}
func (b *bridgeIMAPSettings) EventPublisher() imapsmtpserver.IMAPEventPublisher {
return b
}
func (b *bridgeIMAPSettings) TLSConfig() *tls.Config {
return b.b.tlsConfig
}
func (b *bridgeIMAPSettings) LogClient() bool {
return b.b.logIMAPClient
}
func (b *bridgeIMAPSettings) LogServer() bool {
return b.b.logIMAPServer
}
func (b *bridgeIMAPSettings) Port() int {
return b.b.vault.GetIMAPPort()
}
func (b *bridgeIMAPSettings) SetPort(i int) error {
return b.b.vault.SetIMAPPort(i)
}
func (b *bridgeIMAPSettings) UseSSL() bool {
return b.b.vault.GetIMAPSSL()
}
func (b *bridgeIMAPSettings) CacheDirectory() string {
return b.b.GetGluonCacheDir()
}
func (b *bridgeIMAPSettings) DataDirectory() (string, error) {
return b.b.GetGluonDataDir()
}
func (b *bridgeIMAPSettings) SetCacheDirectory(s string) error {
return b.b.vault.SetGluonDir(s)
}
func (b *bridgeIMAPSettings) Version() *semver.Version {
return b.b.curVersion
}
func (b *bridgeIMAPSettings) PublishIMAPEvent(ctx context.Context, event imapEvents.Event) {
select {
case <-ctx.Done():
return
case b.b.imapEventCh <- event:
// do nothing
}
}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2022 Proton AG
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
@ -17,10 +17,10 @@
package bridge
func (b *Bridge) GetCurrentUserAgent() string {
return b.userAgent.String()
type bridgeIMAPSMTPTelemetry struct {
b *Bridge
}
func (b *Bridge) SetCurrentPlatform(platform string) {
b.userAgent.SetPlatform(platform)
func (b bridgeIMAPSMTPTelemetry) SetCacheLocation(s string) {
b.b.heartbeat.SetCacheLocation(s)
}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2022 Proton AG
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
@ -15,11 +15,10 @@
// 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 cache
package bridge
type Options struct {
MinFreeAbs uint64
MinFreeRat float64
ConcurrentRead int
ConcurrentWrite int
import "golang.org/x/exp/maps"
func (bridge *Bridge) GetHelpersNames() []string {
return maps.Keys(bridge.keychains.GetHelpers())
}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2022 Proton AG
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
@ -17,14 +17,14 @@
package bridge
func (b *Bridge) ProvideLogsPath() (string, error) {
return b.locations.ProvideLogsPath()
func (bridge *Bridge) GetLogsPath() (string, error) {
return bridge.locator.ProvideLogsPath()
}
func (b *Bridge) GetLicenseFilePath() string {
return b.locations.GetLicenseFilePath()
func (bridge *Bridge) GetLicenseFilePath() string {
return bridge.locator.GetLicenseFilePath()
}
func (b *Bridge) GetDependencyLicensesLink() string {
return b.locations.GetDependencyLicensesLink()
func (bridge *Bridge) GetDependencyLicensesLink() string {
return bridge.locator.GetDependencyLicensesLink()
}

View File

@ -0,0 +1,36 @@
// 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 (
"os"
"testing"
"github.com/sirupsen/logrus"
"go.uber.org/goleak"
)
func TestMain(m *testing.M) {
if level := os.Getenv("BRIDGE_LOG_LEVEL"); level != "" {
if parsed, err := logrus.ParseLevel(level); err == nil {
logrus.SetLevel(parsed)
}
}
goleak.VerifyTestMain(m, goleak.IgnoreCurrent())
}

162
internal/bridge/mocks.go Normal file
View File

@ -0,0 +1,162 @@
package bridge
import (
"context"
"net/http"
"net/url"
"os"
"sync"
"testing"
"time"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/proton-bridge/v3/internal/bridge/mocks"
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
"github.com/golang/mock/gomock"
)
type Mocks struct {
ProxyCtl *mocks.MockProxyController
TLSReporter *mocks.MockTLSReporter
TLSIssueCh chan struct{}
Updater *TestUpdater
Autostarter *mocks.MockAutostarter
CrashHandler *mocks.MockPanicHandler
Reporter *mocks.MockReporter
Heartbeat *mocks.MockHeartbeatManager
}
func NewMocks(tb testing.TB, version, minAuto *semver.Version) *Mocks {
ctl := gomock.NewController(tb)
mocks := &Mocks{
ProxyCtl: mocks.NewMockProxyController(ctl),
TLSReporter: mocks.NewMockTLSReporter(ctl),
TLSIssueCh: make(chan struct{}),
Updater: NewTestUpdater(version, minAuto),
Autostarter: mocks.NewMockAutostarter(ctl),
CrashHandler: mocks.NewMockPanicHandler(ctl),
Reporter: mocks.NewMockReporter(ctl),
Heartbeat: mocks.NewMockHeartbeatManager(ctl),
}
// When getting the TLS issue channel, we want to return the test channel.
mocks.TLSReporter.EXPECT().GetTLSIssueCh().Return(mocks.TLSIssueCh).AnyTimes()
// This is called at the end of any go-routine:
mocks.CrashHandler.EXPECT().HandlePanic(gomock.Any()).AnyTimes()
// this is called at start of heartbeat process.
mocks.Heartbeat.EXPECT().IsTelemetryAvailable(gomock.Any()).AnyTimes()
mocks.Heartbeat.EXPECT().GetHeartbeatPeriodicInterval().AnyTimes().Return(500 * time.Millisecond)
return mocks
}
func (mocks *Mocks) Close() {
close(mocks.TLSIssueCh)
}
type TestCookieJar struct {
cookies map[string][]*http.Cookie
}
func NewTestCookieJar() *TestCookieJar {
return &TestCookieJar{
cookies: make(map[string][]*http.Cookie),
}
}
func (j *TestCookieJar) SetCookies(u *url.URL, cookies []*http.Cookie) {
j.cookies[u.Host] = cookies
}
func (j *TestCookieJar) Cookies(u *url.URL) []*http.Cookie {
return j.cookies[u.Host]
}
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
}
type TestUpdater struct {
latest updater.VersionInfo
lock sync.RWMutex
}
func NewTestUpdater(version, minAuto *semver.Version) *TestUpdater {
return &TestUpdater{
latest: updater.VersionInfo{
Version: version,
MinAuto: minAuto,
RolloutProportion: 1.0,
},
}
}
func (testUpdater *TestUpdater) SetLatestVersion(version, minAuto *semver.Version) {
testUpdater.lock.Lock()
defer testUpdater.lock.Unlock()
testUpdater.latest = updater.VersionInfo{
Version: version,
MinAuto: minAuto,
RolloutProportion: 1.0,
}
}
func (testUpdater *TestUpdater) GetVersionInfo(_ context.Context, _ updater.Downloader, _ updater.Channel) (updater.VersionInfo, error) {
testUpdater.lock.RLock()
defer testUpdater.lock.RUnlock()
return testUpdater.latest, nil
}
func (testUpdater *TestUpdater) InstallUpdate(_ context.Context, _ updater.Downloader, _ updater.VersionInfo) error {
return nil
}
func (testUpdater *TestUpdater) RemoveOldUpdates() error {
return nil
}

View File

@ -0,0 +1,46 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/ProtonMail/gluon/async (interfaces: PanicHandler)
// Package mocks is a generated GoMock package.
package mocks
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
)
// MockPanicHandler is a mock of PanicHandler interface.
type MockPanicHandler struct {
ctrl *gomock.Controller
recorder *MockPanicHandlerMockRecorder
}
// MockPanicHandlerMockRecorder is the mock recorder for MockPanicHandler.
type MockPanicHandlerMockRecorder struct {
mock *MockPanicHandler
}
// NewMockPanicHandler creates a new mock instance.
func NewMockPanicHandler(ctrl *gomock.Controller) *MockPanicHandler {
mock := &MockPanicHandler{ctrl: ctrl}
mock.recorder = &MockPanicHandlerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockPanicHandler) EXPECT() *MockPanicHandlerMockRecorder {
return m.recorder
}
// HandlePanic mocks base method.
func (m *MockPanicHandler) HandlePanic(arg0 interface{}) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "HandlePanic", arg0)
}
// HandlePanic indicates an expected call of HandlePanic.
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), arg0)
}

View File

@ -0,0 +1,90 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/ProtonMail/gluon/reporter (interfaces: Reporter)
// Package mocks is a generated GoMock package.
package mocks
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
)
// MockReporter is a mock of Reporter interface.
type MockReporter struct {
ctrl *gomock.Controller
recorder *MockReporterMockRecorder
}
// MockReporterMockRecorder is the mock recorder for MockReporter.
type MockReporterMockRecorder struct {
mock *MockReporter
}
// NewMockReporter creates a new mock instance.
func NewMockReporter(ctrl *gomock.Controller) *MockReporter {
mock := &MockReporter{ctrl: ctrl}
mock.recorder = &MockReporterMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockReporter) EXPECT() *MockReporterMockRecorder {
return m.recorder
}
// ReportException mocks base method.
func (m *MockReporter) ReportException(arg0 interface{}) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReportException", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// ReportException indicates an expected call of ReportException.
func (mr *MockReporterMockRecorder) ReportException(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportException", reflect.TypeOf((*MockReporter)(nil).ReportException), arg0)
}
// ReportExceptionWithContext mocks base method.
func (m *MockReporter) ReportExceptionWithContext(arg0 interface{}, arg1 map[string]interface{}) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReportExceptionWithContext", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// ReportExceptionWithContext indicates an expected call of ReportExceptionWithContext.
func (mr *MockReporterMockRecorder) ReportExceptionWithContext(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportExceptionWithContext", reflect.TypeOf((*MockReporter)(nil).ReportExceptionWithContext), arg0, arg1)
}
// ReportMessage mocks base method.
func (m *MockReporter) ReportMessage(arg0 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReportMessage", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// ReportMessage indicates an expected call of ReportMessage.
func (mr *MockReporterMockRecorder) ReportMessage(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportMessage", reflect.TypeOf((*MockReporter)(nil).ReportMessage), arg0)
}
// ReportMessageWithContext mocks base method.
func (m *MockReporter) ReportMessageWithContext(arg0 string, arg1 map[string]interface{}) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReportMessageWithContext", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// ReportMessageWithContext indicates an expected call of ReportMessageWithContext.
func (mr *MockReporterMockRecorder) ReportMessageWithContext(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportMessageWithContext", reflect.TypeOf((*MockReporter)(nil).ReportMessageWithContext), arg0, arg1)
}

View File

@ -0,0 +1,108 @@
// 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 mocks
import (
"strings"
"github.com/ProtonMail/go-proton-api"
)
type refreshContextMatcher struct {
wantRefresh proton.RefreshFlag
}
func NewRefreshContextMatcher(refreshFlag proton.RefreshFlag) *refreshContextMatcher { //nolint:revive
return &refreshContextMatcher{wantRefresh: refreshFlag}
}
func (m *refreshContextMatcher) Matches(x interface{}) bool {
context, ok := x.(map[string]interface{})
if !ok {
return false
}
i, ok := context["EventLoop"]
if !ok {
return false
}
el, ok := i.(map[string]interface{})
if !ok {
return false
}
vID, ok := el["EventID"]
if !ok {
return false
}
id, ok := vID.(string)
if !ok {
return false
}
if id == "" {
return false
}
vRefresh, ok := el["Refresh"]
if !ok {
return false
}
refresh, ok := vRefresh.(proton.RefreshFlag)
if !ok {
return false
}
return refresh == m.wantRefresh
}
func (m *refreshContextMatcher) String() string {
return `map[string]interface which contains "Refresh" field with value proton.RefreshAll`
}
type closedConnectionMatcher struct{}
func NewClosedConnectionMatcher() *closedConnectionMatcher { //nolint:revive
return &closedConnectionMatcher{}
}
func (m *closedConnectionMatcher) Matches(x interface{}) bool {
context, ok := x.(map[string]interface{})
if !ok {
return false
}
vErr, ok := context["error"]
if !ok {
return false
}
err, ok := vErr.(error)
if !ok {
return false
}
return strings.Contains(err.Error(), "used of closed network connection")
}
func (m *closedConnectionMatcher) String() string {
return "map containing error of closed network connection"
}

View File

@ -0,0 +1,160 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/ProtonMail/proton-bridge/v3/internal/bridge (interfaces: TLSReporter,ProxyController,Autostarter)
// Package mocks is a generated GoMock package.
package mocks
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
)
// MockTLSReporter is a mock of TLSReporter interface.
type MockTLSReporter struct {
ctrl *gomock.Controller
recorder *MockTLSReporterMockRecorder
}
// MockTLSReporterMockRecorder is the mock recorder for MockTLSReporter.
type MockTLSReporterMockRecorder struct {
mock *MockTLSReporter
}
// NewMockTLSReporter creates a new mock instance.
func NewMockTLSReporter(ctrl *gomock.Controller) *MockTLSReporter {
mock := &MockTLSReporter{ctrl: ctrl}
mock.recorder = &MockTLSReporterMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockTLSReporter) EXPECT() *MockTLSReporterMockRecorder {
return m.recorder
}
// GetTLSIssueCh mocks base method.
func (m *MockTLSReporter) GetTLSIssueCh() <-chan struct{} {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetTLSIssueCh")
ret0, _ := ret[0].(<-chan struct{})
return ret0
}
// GetTLSIssueCh indicates an expected call of GetTLSIssueCh.
func (mr *MockTLSReporterMockRecorder) GetTLSIssueCh() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTLSIssueCh", reflect.TypeOf((*MockTLSReporter)(nil).GetTLSIssueCh))
}
// MockProxyController is a mock of ProxyController interface.
type MockProxyController struct {
ctrl *gomock.Controller
recorder *MockProxyControllerMockRecorder
}
// MockProxyControllerMockRecorder is the mock recorder for MockProxyController.
type MockProxyControllerMockRecorder struct {
mock *MockProxyController
}
// NewMockProxyController creates a new mock instance.
func NewMockProxyController(ctrl *gomock.Controller) *MockProxyController {
mock := &MockProxyController{ctrl: ctrl}
mock.recorder = &MockProxyControllerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockProxyController) EXPECT() *MockProxyControllerMockRecorder {
return m.recorder
}
// AllowProxy mocks base method.
func (m *MockProxyController) AllowProxy() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "AllowProxy")
}
// AllowProxy indicates an expected call of AllowProxy.
func (mr *MockProxyControllerMockRecorder) AllowProxy() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AllowProxy", reflect.TypeOf((*MockProxyController)(nil).AllowProxy))
}
// DisallowProxy mocks base method.
func (m *MockProxyController) DisallowProxy() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "DisallowProxy")
}
// DisallowProxy indicates an expected call of DisallowProxy.
func (mr *MockProxyControllerMockRecorder) DisallowProxy() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DisallowProxy", reflect.TypeOf((*MockProxyController)(nil).DisallowProxy))
}
// MockAutostarter is a mock of Autostarter interface.
type MockAutostarter struct {
ctrl *gomock.Controller
recorder *MockAutostarterMockRecorder
}
// MockAutostarterMockRecorder is the mock recorder for MockAutostarter.
type MockAutostarterMockRecorder struct {
mock *MockAutostarter
}
// NewMockAutostarter creates a new mock instance.
func NewMockAutostarter(ctrl *gomock.Controller) *MockAutostarter {
mock := &MockAutostarter{ctrl: ctrl}
mock.recorder = &MockAutostarterMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockAutostarter) EXPECT() *MockAutostarterMockRecorder {
return m.recorder
}
// Disable mocks base method.
func (m *MockAutostarter) Disable() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Disable")
ret0, _ := ret[0].(error)
return ret0
}
// Disable indicates an expected call of Disable.
func (mr *MockAutostarterMockRecorder) Disable() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Disable", reflect.TypeOf((*MockAutostarter)(nil).Disable))
}
// Enable mocks base method.
func (m *MockAutostarter) Enable() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Enable")
ret0, _ := ret[0].(error)
return ret0
}
// Enable indicates an expected call of Enable.
func (mr *MockAutostarterMockRecorder) Enable() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Enable", reflect.TypeOf((*MockAutostarter)(nil).Enable))
}
// IsEnabled mocks base method.
func (m *MockAutostarter) IsEnabled() bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IsEnabled")
ret0, _ := ret[0].(bool)
return ret0
}
// IsEnabled indicates an expected call of IsEnabled.
func (mr *MockAutostarterMockRecorder) IsEnabled() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsEnabled", reflect.TypeOf((*MockAutostarter)(nil).IsEnabled))
}

View File

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

View File

@ -0,0 +1,118 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge_test
import (
"context"
"fmt"
"testing"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/go-proton-api/server"
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/bradenaw/juniper/iterator"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
)
func TestBridge_Refresh(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
userID, _, err := s.CreateUser("imap", password)
require.NoError(t, err)
names := iterator.Collect(iterator.Map(iterator.Counter(10), func(i int) string {
return fmt.Sprintf("folder%v", i)
}))
for _, name := range names {
must(s.CreateLabel(userID, name, "", proton.LabelTypeFolder))
}
// The initial user should be fully synced.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
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)
})
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()
info, err := b.GetUserInfo(userID)
require.NoError(t, err)
require.True(t, info.State == bridge.Connected)
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }()
for _, name := range names {
status, err := client.Select("Folders/"+name, false)
require.NoError(t, err)
uidValidities[name] = status.UidValidity
}
})
// Refresh the user; this will force a resync.
require.NoError(t, s.RefreshUser(userID, proton.RefreshAll))
// 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()
// Wait for refresh event first
refreshCh, refreshChDone := chToType[events.Event, events.UserRefreshed](b.GetEvents(events.UserRefreshed{}))
defer refreshChDone()
require.Equal(t, userID, (<-refreshCh).UserID)
// Then sync event
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
defer done()
require.Equal(t, userID, (<-syncCh).UserID)
})
// After resync, the IMAP client should see all the labels with UID validity of 2.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).AnyTimes()
info, err := b.GetUserInfo(userID)
require.NoError(t, err)
require.True(t, info.State == bridge.Connected)
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }()
for _, name := range names {
status, err := client.Select("Folders/"+name, false)
require.NoError(t, err)
require.Greater(t, status.UidValidity, uidValidities[name])
}
})
})
}

View File

@ -0,0 +1,757 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge_test
import (
"bytes"
"context"
"crypto/tls"
"fmt"
"net"
"os"
"strings"
"testing"
"time"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/go-proton-api/server"
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
smtpservice "github.com/ProtonMail/proton-bridge/v3/internal/services/smtp"
"github.com/emersion/go-imap"
"github.com/emersion/go-sasl"
"github.com/emersion/go-smtp"
"github.com/stretchr/testify/require"
)
func TestBridge_Send(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
_, _, err := s.CreateUser("recipient", password)
require.NoError(t, err)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
smtpWaiter := waitForSMTPServerReady(bridge)
defer smtpWaiter.Done()
senderUserID, err := bridge.LoginFull(ctx, username, password, nil, nil)
require.NoError(t, err)
recipientUserID, err := bridge.LoginFull(ctx, "recipient", password, nil, nil)
require.NoError(t, err)
smtpWaiter.Wait()
senderInfo, err := bridge.GetUserInfo(senderUserID)
require.NoError(t, err)
recipientInfo, err := bridge.GetUserInfo(recipientUserID)
require.NoError(t, err)
for i := 0; i < 10; i++ {
// 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}))
if i%2 == 0 {
// Authorize with SASL PLAIN.
require.NoError(t, client.Auth(sasl.NewPlainClient(
senderInfo.Addresses[0],
senderInfo.Addresses[0],
string(senderInfo.BridgePass)),
))
} else {
// 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(fmt.Sprintf("Subject: Test %v\r\n\r\nHello world!", i)),
))
}
// Connect the sender IMAP client.
senderIMAPClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
require.NoError(t, err)
require.NoError(t, senderIMAPClient.Login(senderInfo.Addresses[0], string(senderInfo.BridgePass)))
defer senderIMAPClient.Logout() //nolint:errcheck
// Connect the recipient IMAP client.
recipientIMAPClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
require.NoError(t, err)
require.NoError(t, recipientIMAPClient.Login(recipientInfo.Addresses[0], string(recipientInfo.BridgePass)))
defer recipientIMAPClient.Logout() //nolint:errcheck
// Sender should have 10 messages in the sent folder.
// 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)
inbox, err := recipientIMAPClient.Status(`Inbox`, []imap.StatusItem{imap.StatusMessages})
require.NoError(t, err)
return sent.Messages == 10 && inbox.Messages == 10
}, 10*time.Second, 100*time.Millisecond)
})
})
}
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.
withBridgeWaitForServers(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
// Get the sender user info.
userInfo, err := bridge.QueryUserInfo(username)
require.NoError(t, err)
// Connect the sender IMAP client.
imapClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
require.NoError(t, err)
require.NoError(t, imapClient.Login(userInfo.Addresses[0], string(userInfo.BridgePass)))
defer imapClient.Logout() //nolint:errcheck
// The message to send.
message := fmt.Sprintf("From: %v\r\nDate: 01 Jan 1980 00:00:00 +0000\r\nSubject: Test\r\n\r\nHello world!", userInfo.Addresses[0])
// Save a draft.
require.NoError(t, imapClient.Append("Drafts", []string{imap.DraftFlag}, time.Now(), strings.NewReader(message)))
// 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.
withBridgeWaitForServers(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
// Get the sender user info.
userInfo, err := bridge.QueryUserInfo(username)
require.NoError(t, err)
// Connect the sender IMAP client.
imapClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
require.NoError(t, err)
require.NoError(t, imapClient.Login(userInfo.Addresses[0], string(userInfo.BridgePass)))
defer imapClient.Logout() //nolint:errcheck
// The message to send.
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) {
// NOTE: Prior to GODT-2887, these tests had inline images, however after the implementation to support
// inline images new parts are injected to reference inline images without content-id set. The images
// in this test have been changed to regular attachments to keep the original checks in place.
const messageMultipartWithoutText = `Content-Type: multipart/mixed;
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
Subject: A new message
Date: Mon, 13 Mar 2023 16:06:16 +0100
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
Content-Disposition: attachment;
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: attachment;
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) {
smtpWaiter := waitForSMTPServerReady(bridge)
defer smtpWaiter.Done()
senderUserID, err := bridge.LoginFull(ctx, username, password, nil, nil)
require.NoError(t, err)
recipientUserID, err := bridge.LoginFull(ctx, "recipient", password, nil, nil)
require.NoError(t, err)
senderInfo, err := bridge.GetUserInfo(senderUserID)
require.NoError(t, err)
recipientInfo, err := bridge.GetUserInfo(recipientUserID)
require.NoError(t, err)
messages := []string{
messageMultipartWithoutText,
messageMultipartWithText,
messageWithTextOnly,
messageMultipartWithoutTextWithTextAttachment,
}
smtpWaiter.Wait()
for _, m := range messages {
// Dial the server.
client, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
require.NoError(t, err)
defer client.Close() //nolint:errcheck
// Upgrade to TLS.
require.NoError(t, client.StartTLS(&tls.Config{InsecureSkipVerify: true}))
// Authorize with SASL LOGIN.
require.NoError(t, client.Auth(sasl.NewLoginClient(
senderInfo.Addresses[0],
string(senderInfo.BridgePass)),
))
// Send the message.
require.NoError(t, client.SendMail(
senderInfo.Addresses[0],
[]string{recipientInfo.Addresses[0]},
strings.NewReader(m),
))
}
// Connect the sender IMAP client.
senderIMAPClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
require.NoError(t, err)
require.NoError(t, senderIMAPClient.Login(senderInfo.Addresses[0], string(senderInfo.BridgePass)))
defer senderIMAPClient.Logout() //nolint:errcheck
// Connect the recipient IMAP client.
recipientIMAPClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
require.NoError(t, err)
require.NoError(t, recipientIMAPClient.Login(recipientInfo.Addresses[0], string(recipientInfo.BridgePass)))
defer recipientIMAPClient.Logout() //nolint:errcheck
require.Eventually(t, func() bool {
messages, err := clientFetch(senderIMAPClient, `Sent`, imap.FetchBodyStructure)
require.NoError(t, err)
if len(messages) != 4 {
return false
}
// messages may not be in order
for _, message := range messages {
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)
})
})
}
func TestBridge_SendInlineImage(t *testing.T) {
const messageInlineImageOnly = `Content-Type: multipart/mixed;
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
Subject: A new message
Date: Mon, 13 Mar 2023 16:06:16 +0100
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
Content-Disposition: inline;
filename=Cat_August_2010-4.jpeg
Content-Type: image/jpeg;
name="Cat_August_2010-4.jpeg"
Content-Transfer-Encoding: base64
SGVsbG8gd29ybGQ=
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84--
`
const messageInlineImageWithHTML = `Content-Type: multipart/mixed;
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
Subject: A new message Part2
Date: Mon, 13 Mar 2023 16:06:16 +0100
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
Content-Type: text/html;charset=utf8
Content-Transfer-Encoding: quoted-printable
Hello world
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
Content-Disposition: inline;
filename=Cat_August_2010-4.jpeg
Content-Type: image/jpeg;
name="Cat_August_2010-4.jpeg"
Content-Transfer-Encoding: base64
SGVsbG8gd29ybGQ=
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84--
`
const messageInlineImageWithText = `Content-Type: multipart/mixed;
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
Subject: A new message Part3
Date: Mon, 13 Mar 2023 16:06:16 +0100
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
Content-Type: text/plain;charset=utf8
Content-Transfer-Encoding: quoted-printable
Hello world
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
Content-Disposition: inline;
filename=Cat_August_2010-4.jpeg
Content-Type: image/jpeg;
name="Cat_August_2010-4.jpeg"
Content-Transfer-Encoding: base64
SGVsbG8gd29ybGQ=
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84--
`
const messageInlineImageFollowedByText = `Content-Type: multipart/mixed;
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
Subject: A new message Part4
Date: Mon, 13 Mar 2023 16:06:16 +0100
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
Content-Disposition: inline;
filename=Cat_August_2010-4.jpeg
Content-Type: image/jpeg;
name="Cat_August_2010-4.jpeg"
Content-Transfer-Encoding: base64
SGVsbG8gd29ybGQ=
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
Content-Type: text/plain;charset=utf8
Content-Transfer-Encoding: quoted-printable
Hello world
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84--
`
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
_, _, err := s.CreateUser("recipient", password)
require.NoError(t, err)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
smtpWaiter := waitForSMTPServerReady(bridge)
defer smtpWaiter.Done()
senderUserID, err := bridge.LoginFull(ctx, username, password, nil, nil)
require.NoError(t, err)
recipientUserID, err := bridge.LoginFull(ctx, "recipient", password, nil, nil)
require.NoError(t, err)
senderInfo, err := bridge.GetUserInfo(senderUserID)
require.NoError(t, err)
recipientInfo, err := bridge.GetUserInfo(recipientUserID)
require.NoError(t, err)
messages := []string{
messageInlineImageOnly,
messageInlineImageWithHTML,
messageInlineImageWithText,
messageInlineImageFollowedByText,
}
smtpWaiter.Wait()
for _, m := range messages {
// Dial the server.
client, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
require.NoError(t, err)
defer client.Close() //nolint:errcheck
// Upgrade to TLS.
require.NoError(t, client.StartTLS(&tls.Config{InsecureSkipVerify: true}))
// Authorize with SASL LOGIN.
require.NoError(t, client.Auth(sasl.NewLoginClient(
senderInfo.Addresses[0],
string(senderInfo.BridgePass)),
))
// Send the message.
require.NoError(t, client.SendMail(
senderInfo.Addresses[0],
[]string{recipientInfo.Addresses[0]},
strings.NewReader(m),
))
}
// Connect the sender IMAP client.
senderIMAPClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
require.NoError(t, err)
require.NoError(t, senderIMAPClient.Login(senderInfo.Addresses[0], string(senderInfo.BridgePass)))
defer senderIMAPClient.Logout() //nolint:errcheck
// Connect the recipient IMAP client.
recipientIMAPClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
require.NoError(t, err)
require.NoError(t, recipientIMAPClient.Login(recipientInfo.Addresses[0], string(recipientInfo.BridgePass)))
defer recipientIMAPClient.Logout() //nolint:errcheck
require.Eventually(t, func() bool {
messages, err := clientFetch(senderIMAPClient, `Sent`, imap.FetchBodyStructure)
require.NoError(t, err)
if len(messages) != 4 {
return false
}
// messages may not be in order
for _, message := range messages {
require.Equal(t, 1, len(message.BodyStructure.Parts))
require.Equal(t, "multipart", message.BodyStructure.MIMEType)
require.Equal(t, "mixed", message.BodyStructure.MIMESubType)
require.Equal(t, "multipart", message.BodyStructure.Parts[0].MIMEType)
require.Equal(t, "related", message.BodyStructure.Parts[0].MIMESubType)
require.Len(t, message.BodyStructure.Parts[0].Parts, 2)
require.Equal(t, "text", message.BodyStructure.Parts[0].Parts[0].MIMEType)
require.Equal(t, "html", message.BodyStructure.Parts[0].Parts[0].MIMESubType)
require.Equal(t, "image", message.BodyStructure.Parts[0].Parts[1].MIMEType)
require.Equal(t, "jpeg", message.BodyStructure.Parts[0].Parts[1].MIMESubType)
}
return true
}, 10*time.Second, 100*time.Millisecond)
})
})
}
func TestBridge_SendAddressDisabled(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
recipientUserID, _, err := s.CreateUser("recipient", password)
require.NoError(t, err)
senderUserID, addrID, err := s.CreateUser("sender", password)
require.NoError(t, err)
require.NoError(t, s.ChangeAddressAllowSend(senderUserID, addrID, false))
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
smtpWaiter := waitForSMTPServerReady(bridge)
defer smtpWaiter.Done()
senderUserID, err := bridge.LoginFull(ctx, "sender", password, nil, nil)
require.NoError(t, err)
_, err = bridge.LoginFull(ctx, "recipient", password, nil, nil)
require.NoError(t, err)
smtpWaiter.Wait()
recipientInfo, err := bridge.GetUserInfo(recipientUserID)
require.NoError(t, err)
senderInfo, err := bridge.GetUserInfo(senderUserID)
require.NoError(t, err)
// Dial the server.
client, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
require.NoError(t, err)
defer client.Close() //nolint:errcheck
// Upgrade to TLS.
require.NoError(t, client.StartTLS(&tls.Config{InsecureSkipVerify: true}))
require.NoError(t, client.Auth(sasl.NewLoginClient(
senderInfo.Addresses[0],
string(senderInfo.BridgePass)),
))
// Send the message.
err = client.SendMail(
senderInfo.Addresses[0],
[]string{recipientInfo.Addresses[0]},
strings.NewReader("Subject: Test 1\r\n\r\nHello world!"),
)
smtpErr := smtpservice.NewErrCanNotSendOnAddress(senderInfo.Addresses[0])
require.Equal(t, fmt.Sprintf("Error: %v", smtpErr.Error()), err.Error())
})
})
}

View File

@ -0,0 +1,83 @@
// 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"
"testing"
"github.com/ProtonMail/gluon/liner"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/go-proton-api/server"
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
)
func TestBridge_Report(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
imapWaiter := waitForIMAPServerReady(b)
defer imapWaiter.Done()
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
defer done()
// Log in the user.
userID, err := b.LoginFull(ctx, username, password, nil, nil)
require.NoError(t, err)
// Wait until the sync has finished.
require.Equal(t, userID, (<-syncCh).UserID)
// Get the IMAP info.
info, err := b.GetUserInfo(userID)
require.NoError(t, err)
require.True(t, info.State == bridge.Connected)
imapWaiter.Wait()
// Dial the IMAP port.
conn, err := net.Dial("tcp", fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
require.NoError(t, err)
defer func() { require.NoError(t, conn.Close()) }()
// Sending garbage to the IMAP port should cause the bridge to report it.
mocks.Reporter.EXPECT().ReportMessageWithContext(
gomock.Eq("Failed to parse IMAP command"),
gomock.Any(),
).Return(nil)
// Read lines from the IMAP port.
lineCh := liner.New(conn).Lines(func() error { return nil })
// On connection, we should get the greeting.
require.Contains(t, string((<-lineCh).Line), "* OK")
// Send garbage data.
must(conn.Write([]byte("tag garbage\r\n")))
// Bridge will reply with BAD.
require.Contains(t, string((<-lineCh).Line), "tag BAD")
})
})
}

View File

@ -0,0 +1,179 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge_test
import (
"context"
"fmt"
"testing"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/go-proton-api/server"
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/stretchr/testify/require"
)
func TestServerManager_NoLoadedUsersNoServers(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
_, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
require.Error(t, err)
})
})
}
func TestServerManager_ServersStartAfterFirstConnectedUser(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
imapWaiter := waitForIMAPServerReady(bridge)
defer imapWaiter.Done()
smtpWaiter := waitForSMTPServerReady(bridge)
defer smtpWaiter.Done()
_, err := bridge.LoginFull(ctx, username, password, nil, nil)
require.NoError(t, err)
imapWaiter.Wait()
smtpWaiter.Wait()
})
})
}
func TestServerManager_ServersStopsAfterUserLogsOut(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
imapWaiter := waitForIMAPServerReady(bridge)
defer imapWaiter.Done()
smtpWaiter := waitForSMTPServerReady(bridge)
defer smtpWaiter.Done()
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
require.NoError(t, err)
imapWaiter.Wait()
smtpWaiter.Wait()
imapWaiterStopped := waitForIMAPServerStopped(bridge)
defer imapWaiterStopped.Done()
require.NoError(t, bridge.LogoutUser(ctx, userID))
imapWaiterStopped.Wait()
})
})
}
func TestServerManager_ServersDoNotStopWhenThereIsStillOneActiveUser(t *testing.T) {
otherPassword := []byte("bar")
otherUser := "foo"
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
_, _, err := s.CreateUser(otherUser, otherPassword)
require.NoError(t, err)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
imapWaiter := waitForIMAPServerReady(bridge)
defer imapWaiter.Done()
smtpWaiter := waitForSMTPServerReady(bridge)
defer smtpWaiter.Done()
_, err := bridge.LoginFull(ctx, username, password, nil, nil)
require.NoError(t, err)
userIDOther, err := bridge.LoginFull(ctx, otherUser, otherPassword, nil, nil)
require.NoError(t, err)
imapWaiter.Wait()
smtpWaiter.Wait()
evtCh, cancel := bridge.GetEvents(events.UserDeauth{})
defer cancel()
require.NoError(t, s.RevokeUser(userIDOther))
waitForEvent(t, evtCh, events.UserDeauth{})
imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, imapClient.Logout())
})
})
}
func TestServerManager_ServersStartIfAtLeastOneUserIsLoggedIn(t *testing.T) {
otherPassword := []byte("bar")
otherUser := "foo"
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
userIDOther, _, err := s.CreateUser(otherUser, otherPassword)
require.NoError(t, err)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
_, err := bridge.LoginFull(ctx, username, password, nil, nil)
require.NoError(t, err)
_, err = bridge.LoginFull(ctx, otherUser, otherPassword, nil, nil)
require.NoError(t, err)
})
require.NoError(t, s.RevokeUser(userIDOther))
withBridgeWaitForServers(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, imapClient.Logout())
})
})
}
func TestServerManager_NetworkLossStopsServers(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
imapWaiter := waitForIMAPServerReady(bridge)
defer imapWaiter.Done()
smtpWaiter := waitForSMTPServerReady(bridge)
defer smtpWaiter.Done()
imapWaiterStop := waitForIMAPServerStopped(bridge)
defer imapWaiterStop.Done()
smtpWaiterStop := waitForSMTPServerStopped(bridge)
defer smtpWaiterStop.Done()
_, err := bridge.LoginFull(ctx, username, password, nil, nil)
require.NoError(t, err)
imapWaiter.Wait()
smtpWaiter.Wait()
netCtl.Disable()
imapWaiterStop.Wait()
smtpWaiterStop.Wait()
netCtl.Enable()
imapWaiter.Wait()
smtpWaiter.Wait()
})
})
}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2022 Proton AG
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
@ -17,28 +17,321 @@
package bridge
import "github.com/ProtonMail/proton-bridge/v2/internal/config/settings"
import (
"context"
"fmt"
func (b *Bridge) Get(key settings.Key) string {
return b.settings.Get(key)
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/services/userevents"
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/sirupsen/logrus"
)
func (bridge *Bridge) GetKeychainApp() (string, error) {
vaultDir, err := bridge.locator.ProvideSettingsPath()
if err != nil {
return "", err
}
func (b *Bridge) Set(key settings.Key, value string) {
b.settings.Set(key, value)
return vault.GetHelper(vaultDir)
}
func (b *Bridge) GetBool(key settings.Key) bool {
return b.settings.GetBool(key)
func (bridge *Bridge) SetKeychainApp(helper string) error {
vaultDir, err := bridge.locator.ProvideSettingsPath()
if err != nil {
return err
}
func (b *Bridge) SetBool(key settings.Key, value bool) {
b.settings.SetBool(key, value)
bridge.heartbeat.SetKeyChainPref(helper)
return vault.SetHelper(vaultDir, helper)
}
func (b *Bridge) GetInt(key settings.Key) int {
return b.settings.GetInt(key)
func (bridge *Bridge) GetIMAPPort() int {
return bridge.vault.GetIMAPPort()
}
func (b *Bridge) SetInt(key settings.Key, value int) {
b.settings.SetInt(key, value)
func (bridge *Bridge) SetIMAPPort(ctx context.Context, newPort int) error {
if newPort == bridge.vault.GetIMAPPort() {
return nil
}
if err := bridge.vault.SetIMAPPort(newPort); err != nil {
return err
}
bridge.heartbeat.SetIMAPPort(newPort)
return bridge.restartIMAP(ctx)
}
func (bridge *Bridge) GetIMAPSSL() bool {
return bridge.vault.GetIMAPSSL()
}
func (bridge *Bridge) SetIMAPSSL(ctx context.Context, newSSL bool) error {
if newSSL == bridge.vault.GetIMAPSSL() {
return nil
}
if err := bridge.vault.SetIMAPSSL(newSSL); err != nil {
return err
}
bridge.heartbeat.SetIMAPConnectionMode(newSSL)
return bridge.restartIMAP(ctx)
}
func (bridge *Bridge) GetSMTPPort() int {
return bridge.vault.GetSMTPPort()
}
func (bridge *Bridge) SetSMTPPort(ctx context.Context, newPort int) error {
if newPort == bridge.vault.GetSMTPPort() {
return nil
}
if err := bridge.vault.SetSMTPPort(newPort); err != nil {
return err
}
bridge.heartbeat.SetSMTPPort(newPort)
return bridge.restartSMTP(ctx)
}
func (bridge *Bridge) GetSMTPSSL() bool {
return bridge.vault.GetSMTPSSL()
}
func (bridge *Bridge) SetSMTPSSL(ctx context.Context, newSSL bool) error {
if newSSL == bridge.vault.GetSMTPSSL() {
return nil
}
if err := bridge.vault.SetSMTPSSL(newSSL); err != nil {
return err
}
bridge.heartbeat.SetSMTPConnectionMode(newSSL)
return bridge.restartSMTP(ctx)
}
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 {
bridge.usersLock.RLock()
defer func() {
logrus.Info("Restarting user event loops")
for _, u := range bridge.users {
u.ResumeEventLoop()
}
bridge.usersLock.RUnlock()
}()
type waiter struct {
w *userevents.EventPollWaiter
id string
}
waiters := make([]waiter, 0, len(bridge.users))
logrus.Info("Pausing user event loops for gluon dir change")
for id, u := range bridge.users {
waiters = append(waiters, waiter{w: u.PauseEventLoopWithWaiter(), id: id})
}
logrus.Info("Waiting on user event loop completion")
for _, waiter := range waiters {
if err := waiter.w.WaitPollFinished(ctx); err != nil {
logrus.WithError(err).Errorf("Failed to wait on event loop pause for user %v", waiter.id)
return fmt.Errorf("failed on event loop pause: %w", err)
}
}
logrus.Info("Changing gluon directory")
return bridge.serverManager.SetGluonDir(ctx, newGluonDir)
}
func (bridge *Bridge) GetProxyAllowed() bool {
return bridge.vault.GetProxyAllowed()
}
func (bridge *Bridge) SetProxyAllowed(allowed bool) error {
if allowed {
bridge.proxyCtl.AllowProxy()
} else {
bridge.proxyCtl.DisallowProxy()
}
bridge.heartbeat.SetDoh(allowed)
return bridge.vault.SetProxyAllowed(allowed)
}
func (bridge *Bridge) GetShowAllMail() bool {
return bridge.vault.GetShowAllMail()
}
func (bridge *Bridge) SetShowAllMail(show bool) error {
return safe.RLockRet(func() error {
for _, user := range bridge.users {
user.SetShowAllMail(show)
}
bridge.heartbeat.SetShowAllMail(show)
return bridge.vault.SetShowAllMail(show)
}, bridge.usersLock)
}
func (bridge *Bridge) GetAutostart() bool {
return bridge.vault.GetAutostart()
}
func (bridge *Bridge) SetAutostart(autostart bool) error {
if autostart != bridge.vault.GetAutostart() {
if err := bridge.vault.SetAutostart(autostart); err != nil {
return err
}
bridge.heartbeat.SetAutoStart(autostart)
}
var err error
if autostart {
// do nothing if already enabled
if bridge.autostarter.IsEnabled() {
return nil
}
err = bridge.autostarter.Enable()
} else {
// do nothing if already disabled
if !bridge.autostarter.IsEnabled() {
return nil
}
err = bridge.autostarter.Disable()
}
return err
}
func (bridge *Bridge) GetUpdateRollout() float64 {
return bridge.vault.GetUpdateRollout()
}
func (bridge *Bridge) GetAutoUpdate() bool {
return bridge.vault.GetAutoUpdate()
}
func (bridge *Bridge) SetAutoUpdate(autoUpdate bool) error {
if bridge.vault.GetAutoUpdate() == autoUpdate {
return nil
}
if err := bridge.vault.SetAutoUpdate(autoUpdate); err != nil {
return err
}
bridge.heartbeat.SetAutoUpdate(autoUpdate)
bridge.goUpdate()
return nil
}
func (bridge *Bridge) GetTelemetryDisabled() bool {
return bridge.vault.GetTelemetryDisabled()
}
func (bridge *Bridge) SetTelemetryDisabled(isDisabled bool) error {
if err := bridge.vault.SetTelemetryDisabled(isDisabled); err != nil {
return err
}
// If telemetry is re-enabled locally, try to send the heartbeat.
if isDisabled {
bridge.heartbeat.stop()
} else {
bridge.heartbeat.start()
}
return nil
}
func (bridge *Bridge) GetUpdateChannel() updater.Channel {
return bridge.vault.GetUpdateChannel()
}
func (bridge *Bridge) SetUpdateChannel(channel updater.Channel) error {
if bridge.vault.GetUpdateChannel() == channel {
return nil
}
if err := bridge.vault.SetUpdateChannel(channel); err != nil {
return err
}
bridge.heartbeat.SetBeta(channel)
bridge.goUpdate()
return nil
}
func (bridge *Bridge) GetCurrentVersion() *semver.Version {
return bridge.curVersion
}
func (bridge *Bridge) GetLastVersion() *semver.Version {
return bridge.lastVersion
}
func (bridge *Bridge) GetFirstStart() bool {
return bridge.firstStart
}
func (bridge *Bridge) GetColorScheme() string {
return bridge.vault.GetColorScheme()
}
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) {
useTelemetry := !bridge.GetTelemetryDisabled()
// Delete all the users.
safe.Lock(func() {
for _, user := range bridge.users {
bridge.logoutUser(ctx, user, true, true, useTelemetry)
}
}, bridge.usersLock)
// Wipe the vault.
gluonCacheDir, err := bridge.locator.ProvideGluonCachePath()
if err != nil {
logrus.WithError(err).Error("Failed to provide gluon dir")
} else if err := bridge.vault.Reset(gluonCacheDir); err != nil {
logrus.WithError(err).Error("Failed to reset vault")
}
// 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")
}
}

View File

@ -0,0 +1,208 @@
// 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"
"os"
"testing"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/go-proton-api/server"
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/stretchr/testify/require"
)
func TestBridge_Settings_GluonDir(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) {
// Create a user.
_, err := bridge.LoginFull(context.Background(), username, password, nil, nil)
require.NoError(t, err)
// Create a new location for the Gluon data.
newGluonDir := t.TempDir()
// Move the gluon dir; it should also move the user's data.
require.NoError(t, bridge.SetGluonDir(context.Background(), newGluonDir))
// Check that the new directory is not empty.
entries, err := os.ReadDir(newGluonDir)
require.NoError(t, err)
// There should be at least one entry.
require.NotEmpty(t, entries)
})
})
}
func TestBridge_Settings_GluonDirWithOnGoingEvents(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
userID, addrID, err := s.CreateUser("imap", password)
require.NoError(t, err)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
defer done()
_, err := bridge.LoginFull(context.Background(), "imap", password, nil, nil)
require.NoError(t, err)
<-syncCh
})
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, 200)
})
withBridgeWaitForServers(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
// Create a new location for the Gluon data.
newGluonDir := t.TempDir()
// Move the gluon dir; it should also move the user's data.
require.NoError(t, bridge.SetGluonDir(context.Background(), newGluonDir))
// Check that the new directory is not empty.
entries, err := os.ReadDir(newGluonDir)
require.NoError(t, err)
// There should be at least one entry.
require.NotEmpty(t, entries)
})
})
}
func TestBridge_Settings_IMAPPort(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) {
curPort := bridge.GetIMAPPort()
// Set the port to 1144.
require.NoError(t, bridge.SetIMAPPort(ctx, 1144))
// Get the new setting.
require.Equal(t, 1144, bridge.GetIMAPPort())
// Assert that it has changed.
require.NotEqual(t, curPort, bridge.GetIMAPPort())
})
})
}
func TestBridge_Settings_IMAPSSL(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, IMAP SSL is disabled.
require.False(t, bridge.GetIMAPSSL())
// Enable IMAP SSL.
require.NoError(t, bridge.SetIMAPSSL(ctx, true))
// Get the new setting.
require.True(t, bridge.GetIMAPSSL())
})
})
}
func TestBridge_Settings_SMTPPort(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) {
curPort := bridge.GetSMTPPort()
// Set the port to 1024.
require.NoError(t, bridge.SetSMTPPort(ctx, 1024))
// Get the new setting.
require.Equal(t, 1024, bridge.GetSMTPPort())
// Assert that it has changed.
require.NotEqual(t, curPort, bridge.GetSMTPPort())
})
})
}
func TestBridge_Settings_SMTPSSL(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, SMTP SSL is disabled.
require.False(t, bridge.GetSMTPSSL())
// Enable SMTP SSL.
require.NoError(t, bridge.SetSMTPSSL(ctx, true))
// Get the new setting.
require.True(t, bridge.GetSMTPSSL())
})
})
}
func TestBridge_Settings_Proxy(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, proxy is allowed.
require.False(t, bridge.GetProxyAllowed())
// Disallow proxy.
mocks.ProxyCtl.EXPECT().AllowProxy()
require.NoError(t, bridge.SetProxyAllowed(true))
// Get the new setting.
require.True(t, bridge.GetProxyAllowed())
})
})
}
func TestBridge_Settings_Autostart(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, autostart is enabled.
require.True(t, bridge.GetAutostart())
// Disable autostart.
mocks.Autostarter.EXPECT().IsEnabled().Return(true)
mocks.Autostarter.EXPECT().Disable().Return(nil)
require.NoError(t, bridge.SetAutostart(false))
// Get the new setting.
require.False(t, bridge.GetAutostart())
// Re Enable autostart.
mocks.Autostarter.EXPECT().IsEnabled().Return(false)
mocks.Autostarter.EXPECT().Enable().Return(nil)
require.NoError(t, bridge.SetAutostart(true))
// Get the new setting.
require.True(t, bridge.GetAutostart())
})
})
}
func TestBridge_Settings_FirstStart(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.GetFirstStart())
// the setting of the first start value is managed by bridge itself, so the setter is not exported.
})
})
}

57
internal/bridge/smtp.go Normal file
View File

@ -0,0 +1,57 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge
import (
"context"
"crypto/tls"
"github.com/ProtonMail/proton-bridge/v3/internal/identifier"
)
func (bridge *Bridge) restartSMTP(ctx context.Context) error {
return bridge.serverManager.RestartSMTP(ctx)
}
type bridgeSMTPSettings struct {
b *Bridge
}
func (b *bridgeSMTPSettings) TLSConfig() *tls.Config {
return b.b.tlsConfig
}
func (b *bridgeSMTPSettings) Log() bool {
return b.b.logSMTP
}
func (b *bridgeSMTPSettings) Port() int {
return b.b.vault.GetSMTPPort()
}
func (b *bridgeSMTPSettings) SetPort(i int) error {
return b.b.vault.SetSMTPPort(i)
}
func (b *bridgeSMTPSettings) UseSSL() bool {
return b.b.vault.GetSMTPSSL()
}
func (b *bridgeSMTPSettings) Identifier() identifier.UserAgentUpdater {
return &bridgeUserAgentUpdater{Bridge: b.b}
}

View File

@ -1,87 +0,0 @@
// Copyright (c) 2022 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge
import (
"fmt"
"path/filepath"
"github.com/ProtonMail/proton-bridge/v2/internal/sentry"
"github.com/ProtonMail/proton-bridge/v2/internal/store"
"github.com/ProtonMail/proton-bridge/v2/internal/store/cache"
"github.com/ProtonMail/proton-bridge/v2/internal/users"
"github.com/ProtonMail/proton-bridge/v2/pkg/listener"
"github.com/ProtonMail/proton-bridge/v2/pkg/message"
)
type storeFactory struct {
cacheProvider CacheProvider
sentryReporter *sentry.Reporter
panicHandler users.PanicHandler
eventListener listener.Listener
events *store.Events
cache cache.Cache
builder *message.Builder
}
func newStoreFactory(
cacheProvider CacheProvider,
sentryReporter *sentry.Reporter,
panicHandler users.PanicHandler,
eventListener listener.Listener,
cache cache.Cache,
builder *message.Builder,
) *storeFactory {
return &storeFactory{
cacheProvider: cacheProvider,
sentryReporter: sentryReporter,
panicHandler: panicHandler,
eventListener: eventListener,
events: store.NewEvents(cacheProvider.GetIMAPCachePath()),
cache: cache,
builder: builder,
}
}
// New creates new store for given user.
func (f *storeFactory) New(user store.BridgeUser) (*store.Store, error) {
return store.New(
f.sentryReporter,
f.panicHandler,
user,
f.eventListener,
f.cache,
f.builder,
getUserStorePath(f.cacheProvider.GetDBDir(), user.ID()),
f.events,
)
}
// Remove removes all store files for given user.
func (f *storeFactory) Remove(userID string) error {
return store.RemoveStore(
f.events,
getUserStorePath(f.cacheProvider.GetDBDir(), userID),
userID,
)
}
// getUserStorePath returns the file path of the store database for the given userID.
func getUserStorePath(storeDir string, userID string) (path string) {
return filepath.Join(storeDir, fmt.Sprintf("mailbox-%v.db", userID))
}

View File

@ -0,0 +1,787 @@
// 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"
"io"
"net/http"
"os"
"path/filepath"
"runtime"
"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/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/services/imapservice"
"github.com/bradenaw/juniper/iterator"
"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/stretchr/testify/require"
)
func TestBridge_Sync(t *testing.T) {
numMsg := 1 << 8
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
userID, addrID, err := s.CreateUser("imap", password)
require.NoError(t, err)
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
require.NoError(t, err)
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
createNumMessages(ctx, t, c, addrID, labelID, numMsg)
})
var total uint64
// The initial user 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()
// Count how many bytes it takes to fully sync the user.
total = countBytesRead(netCtl, func() {
userID, err := bridge.LoginFull(ctx, "imap", password, nil, nil)
require.NoError(t, err)
require.Equal(t, userID, (<-syncCh).UserID)
})
})
// If we then connect an IMAP client, it should see all the messages.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
info, err := b.GetUserInfo(userID)
require.NoError(t, err)
require.True(t, info.State == bridge.Connected)
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }()
status, err := client.Select(`Folders/folder`, false)
require.NoError(t, err)
require.Equal(t, uint32(numMsg), status.Messages)
})
// Now let's remove the user and simulate a network error.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
require.NoError(t, bridge.DeleteUser(ctx, userID))
})
// Pretend we can only sync 2/3 of the original messages.
netCtl.SetReadLimit(2 * total / 3)
// Login the user; its sync should fail.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
{
syncCh, done := chToType[events.Event, events.SyncFailed](b.GetEvents(events.SyncFailed{}))
defer done()
userID, err := b.LoginFull(ctx, "imap", password, nil, nil)
require.NoError(t, err)
require.Equal(t, userID, (<-syncCh).UserID)
info, err := b.GetUserInfo(userID)
require.NoError(t, err)
require.True(t, info.State == bridge.Connected)
}
// Remove the network limit, allowing the sync to finish.
netCtl.SetReadLimit(0)
{
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
defer done()
require.Equal(t, userID, (<-syncCh).UserID)
info, err := b.GetUserInfo(userID)
require.NoError(t, err)
require.True(t, info.State == bridge.Connected)
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }()
status, err := client.Select(`Folders/folder`, false)
require.NoError(t, err)
require.Equal(t, uint32(numMsg), status.Messages)
}
})
}, server.WithTLS(false))
}
// GODT-2215: This test no longer works since it's now possible to import messages into Gluon with bad ContentType header.
func _TestBridge_Sync_BadMessage(t *testing.T) { //nolint:unused,deadcode
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
userID, addrID, err := s.CreateUser("imap", password)
require.NoError(t, err)
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
require.NoError(t, err)
var messageIDs []string
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
messageIDs = createMessages(ctx, t, c, addrID, labelID,
[]byte("To: someone@pm.me\r\nSubject: Good message\r\n\r\nHello!"),
[]byte("To: someone@pm.me\r\nSubject: Bad message\r\nContentType: this is not a valid content type\r\n\r\nHello!"),
)
})
// The initial user should be fully synced and should skip the bad message.
// We should report the bad message to sentry.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
mocks.Reporter.EXPECT().ReportMessageWithContext("Failed to build message (sync)", gomock.Any())
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
defer done()
userID, err := bridge.LoginFull(ctx, "imap", password, nil, nil)
require.NoError(t, err)
require.Equal(t, userID, (<-syncCh).UserID)
})
// If we then connect an IMAP client, it should see the good message but not the bad one.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
info, err := b.GetUserInfo(userID)
require.NoError(t, err)
require.True(t, info.State == bridge.Connected)
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }()
status, err := client.Select(`Folders/folder`, false)
require.NoError(t, err)
require.Equal(t, uint32(1), status.Messages)
messages, err := clientFetch(client, `Folders/folder`)
require.NoError(t, err)
require.Len(t, messages, 1)
// The bad message should have been skipped.
literal, err := io.ReadAll(messages[0].GetBody(must(imap.ParseBodySectionName("BODY[]"))))
require.NoError(t, err)
header, err := rfc822.Parse(literal).ParseHeader()
require.NoError(t, err)
require.Equal(t, "Good message", header.Get("Subject"))
require.Equal(t, messageIDs[0], header.Get("X-Pm-Internal-Id"))
})
})
}
func TestBridge_SyncWithOngoingEvents(t *testing.T) {
numMsg := 1 << 8
messageSplitIndex := numMsg * 2 / 3
renmainingMessageCount := numMsg - messageSplitIndex
messages := make([]string, 0, numMsg)
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
userID, addrID, err := s.CreateUser("imap", password)
require.NoError(t, err)
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
require.NoError(t, err)
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
importResults := createNumMessages(ctx, t, c, addrID, labelID, numMsg)
for _, v := range importResults {
if len(v) != 0 {
messages = append(messages, v)
}
}
})
var total uint64
// The initial user should be fully synced.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
defer done()
// Count how many bytes it takes to fully sync the user.
total = countBytesRead(netCtl, func() {
userID, err := bridge.LoginFull(ctx, "imap", password, nil, nil)
require.NoError(t, err)
require.Equal(t, userID, (<-syncCh).UserID)
})
})
// Now let's remove the user and stop the network at 2/3 of the data.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
require.NoError(t, bridge.DeleteUser(ctx, userID))
})
// Pretend we can only sync 2/3 of the original messages.
netCtl.SetReadLimit(2 * total / 3)
// Login the user; its sync should fail.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
defer done()
{
syncFailedCh, syncFailedDone := chToType[events.Event, events.SyncFailed](b.GetEvents(events.SyncFailed{}))
defer syncFailedDone()
userID, err := b.LoginFull(ctx, "imap", password, nil, nil)
require.NoError(t, err)
require.Equal(t, userID, (<-syncFailedCh).UserID)
info, err := b.GetUserInfo(userID)
require.NoError(t, err)
require.True(t, info.State == bridge.Connected)
}
// Create a new mailbox and move that last 1/3 of the messages into it to simulate user
// actions during sync.
{
newLabelID, err := s.CreateLabel(userID, "folder2", "", proton.LabelTypeFolder)
require.NoError(t, err)
messages := messages[messageSplitIndex:]
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
require.NoError(t, c.UnlabelMessages(ctx, messages, labelID))
require.NoError(t, c.LabelMessages(ctx, messages, newLabelID))
})
}
// Remove the network limit, allowing the sync to finish.
netCtl.SetReadLimit(0)
{
require.Equal(t, userID, (<-syncCh).UserID)
info, err := b.GetUserInfo(userID)
require.NoError(t, err)
require.True(t, info.State == bridge.Connected)
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }()
// Check that the new messages arrive in the right location.
require.Eventually(t, func() bool {
status, err := client.Select(`Folders/folder2`, true)
if err != nil {
return false
}
if status.Messages != uint32(renmainingMessageCount) {
return false
}
return true
}, 10*time.Second, 500*time.Millisecond)
}
})
}, server.WithTLS(false))
}
func TestBridge_CanProcessEventsDuringSync(t *testing.T) {
numMsg := 1 << 8
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
userID, addrID, err := s.CreateUser("imap", password)
require.NoError(t, err)
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
require.NoError(t, err)
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
createNumMessages(ctx, t, c, addrID, labelID, numMsg)
})
// Simulate 429 to prevent sync from progressing.
s.AddStatusHook(func(request *http.Request) (int, bool) {
if strings.Contains(request.URL.Path, "/mail/v4/messages/") {
return http.StatusTooManyRequests, true
}
return 0, false
})
// The initial user should be fully synced.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
syncStartedCh, syncStartedDone := chToType[events.Event, events.SyncStarted](bridge.GetEvents(events.SyncStarted{}))
defer syncStartedDone()
addressCreatedCh, addressCreatedDone := chToType[events.Event, events.UserAddressCreated](bridge.GetEvents(events.UserAddressCreated{}))
defer addressCreatedDone()
userID, err := bridge.LoginFull(ctx, "imap", password, nil, nil)
require.NoError(t, err)
require.Equal(t, userID, (<-syncStartedCh).UserID)
// Create a new address
newAddress := "foo@proton.ch"
addrID, err := s.CreateAddress(userID, newAddress, password)
require.NoError(t, err)
event := <-addressCreatedCh
require.Equal(t, userID, event.UserID)
require.Equal(t, newAddress, event.Email)
require.Equal(t, addrID, event.AddressID)
})
}, server.WithTLS(false))
}
func TestBridge_RefreshDuringSyncRestartSync(t *testing.T) {
numMsg := 1 << 8
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
userID, addrID, err := s.CreateUser("imap", password)
require.NoError(t, err)
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
require.NoError(t, err)
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
createNumMessages(ctx, t, c, addrID, labelID, numMsg)
})
var refreshPerformed atomic.Bool
refreshPerformed.Store(false)
// Simulate 429 to prevent sync from progressing.
s.AddStatusHook(func(request *http.Request) (int, bool) {
if strings.Contains(request.URL.Path, "/mail/v4/messages/") {
if !refreshPerformed.Load() {
return http.StatusTooManyRequests, true
}
}
return 0, false
})
// The initial user 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, "imap", password, nil, nil)
require.NoError(t, err)
syncStartedCh, syncStartedDone := chToType[events.Event, events.SyncStarted](bridge.GetEvents(events.SyncStarted{}))
defer syncStartedDone()
require.Equal(t, userID, (<-syncStartedCh).UserID)
require.NoError(t, err, s.RefreshUser(userID, proton.RefreshMail))
require.Equal(t, userID, (<-syncStartedCh).UserID)
refreshPerformed.Store(true)
require.Equal(t, userID, (<-syncCh).UserID)
})
}, server.WithTLS(false))
}
func TestBridge_EventReplayAfterSyncHasFinished(t *testing.T) {
numMsg := 1 << 8
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
userID, addrID, err := s.CreateUser("imap", password)
require.NoError(t, err)
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
require.NoError(t, err)
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
createNumMessages(ctx, t, c, addrID, labelID, numMsg)
})
addrID1, err := s.CreateAddress(userID, "foo@proton.ch", password)
require.NoError(t, err)
var allowSyncToProgress atomic.Bool
allowSyncToProgress.Store(false)
// Simulate 429 to prevent sync from progressing.
s.AddStatusHook(func(request *http.Request) (int, bool) {
if request.Method == "GET" && strings.Contains(request.URL.Path, "/mail/v4/messages/") {
if !allowSyncToProgress.Load() {
return http.StatusTooManyRequests, true
}
}
return 0, false
})
// The initial user 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()
syncStartedCh, syncStartedDone := chToType[events.Event, events.SyncStarted](bridge.GetEvents(events.SyncStarted{}))
defer syncStartedDone()
addressCreatedCh, addressCreatedDone := chToType[events.Event, events.UserAddressCreated](bridge.GetEvents(events.UserAddressCreated{}))
defer addressCreatedDone()
userID, err := bridge.LoginFull(ctx, "imap", password, nil, nil)
require.NoError(t, err)
require.Equal(t, userID, (<-syncStartedCh).UserID)
// create 20 more messages and move them to inbox
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 20)
})
// User AddrID2 event as a check point to see when the new address was created.
addrID2, err := s.CreateAddress(userID, "bar@proton.ch", password)
require.NoError(t, err)
allowSyncToProgress.Store(true)
require.Equal(t, userID, (<-syncCh).UserID)
// At most two events can be published, one for the first address, then for the second.
// if the second event is not `addrID2` then something went wrong.
event := <-addressCreatedCh
if event.AddressID == addrID1 {
event = <-addressCreatedCh
}
require.Equal(t, addrID2, event.AddressID)
info, err := bridge.GetUserInfo(userID)
require.NoError(t, err)
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }()
// Finally check if the 20 messages are in INBOX.
status, err := client.Status("INBOX", []imap.StatusItem{imap.StatusMessages})
require.NoError(t, err)
require.Equal(t, uint32(20), status.Messages)
// Finally check if the numMsg are in the folder.
status, err = client.Status("Folders/folder", []imap.StatusItem{imap.StatusMessages})
require.NoError(t, err)
require.Equal(t, uint32(numMsg), status.Messages)
})
}, server.WithTLS(false))
}
func TestBridge_MessageCreateDuringSync(t *testing.T) {
numMsg := 1 << 8
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
userID, addrID, err := s.CreateUser("imap", password)
require.NoError(t, err)
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
require.NoError(t, err)
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
createNumMessages(ctx, t, c, addrID, labelID, numMsg)
})
var allowSyncToProgress atomic.Bool
allowSyncToProgress.Store(false)
// Simulate 429 to prevent sync from progressing.
s.AddStatusHook(func(request *http.Request) (int, bool) {
if request.Method == "GET" && strings.Contains(request.URL.Path, "/mail/v4/messages/") {
if !allowSyncToProgress.Load() {
return http.StatusTooManyRequests, true
}
}
return 0, false
})
// The initial user should be fully synced.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
syncStartedCh, syncStartedDone := chToType[events.Event, events.SyncStarted](bridge.GetEvents(events.SyncStarted{}))
defer syncStartedDone()
addressCreatedCh, addressCreatedDone := chToType[events.Event, events.UserAddressCreated](bridge.GetEvents(events.UserAddressCreated{}))
defer addressCreatedDone()
userID, err := bridge.LoginFull(ctx, "imap", password, nil, nil)
require.NoError(t, err)
require.Equal(t, userID, (<-syncStartedCh).UserID)
// create 20 more messages and move them to inbox
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 20)
})
// User AddrID2 event as a check point to see when the new address was created.
addrID, err := s.CreateAddress(userID, "bar@proton.ch", password)
require.NoError(t, err)
// At most two events can be published, one for the first address, then for the second.
// if the second event is not `addrID` then something went wrong.
event := <-addressCreatedCh
require.Equal(t, addrID, event.AddressID)
allowSyncToProgress.Store(true)
info, err := bridge.GetUserInfo(userID)
require.NoError(t, err)
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }()
require.Eventually(t, func() bool {
// Finally check if the 20 messages are in INBOX.
status, err := client.Status("INBOX", []imap.StatusItem{imap.StatusMessages})
require.NoError(t, err)
return uint32(20) == status.Messages
}, 10*time.Second, time.Second)
})
}, server.WithTLS(false))
}
func TestBridge_CorruptedVaultClearsPreviousIMAPSyncState(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
userID, addrID, err := s.CreateUser("imap", password)
require.NoError(t, err)
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
require.NoError(t, err)
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
createNumMessages(ctx, t, c, addrID, labelID, 100)
})
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
defer done()
var err error
userID, err = bridge.LoginFull(context.Background(), "imap", password, nil, nil)
require.NoError(t, err)
// Wait for sync to finish
require.Equal(t, userID, (<-syncCh).UserID)
})
settingsPath, err := locator.ProvideSettingsPath()
require.NoError(t, err)
syncConfigPath, err := locator.ProvideIMAPSyncConfigPath()
require.NoError(t, err)
syncStatePath := imapservice.GetSyncConfigPath(syncConfigPath, userID)
// Check sync state is complete
{
state, err := imapservice.NewSyncState(syncStatePath)
require.NoError(t, err)
syncStatus, err := state.GetSyncStatus(context.Background())
require.NoError(t, err)
require.True(t, syncStatus.IsComplete())
}
// corrupt the vault
require.NoError(t, os.WriteFile(filepath.Join(settingsPath, "vault.enc"), []byte("Trash!"), 0o600))
// Bridge starts but can't find the gluon database dir; there should be no error.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
_, err := bridge.LoginFull(context.Background(), "imap", password, nil, nil)
require.NoError(t, err)
})
// Check sync state is reset.
{
state, err := imapservice.NewSyncState(syncStatePath)
require.NoError(t, err)
syncStatus, err := state.GetSyncStatus(context.Background())
require.NoError(t, err)
require.False(t, syncStatus.IsComplete())
}
})
}
func withClient(ctx context.Context, t *testing.T, s *server.Server, username string, password []byte, fn func(context.Context, *proton.Client)) { //nolint:unparam
m := proton.New(
proton.WithHostURL(s.GetHostURL()),
proton.WithTransport(proton.InsecureTransport()),
)
c, _, err := m.NewClientWithLogin(ctx, username, password)
require.NoError(t, err)
defer c.Close()
fn(ctx, c)
}
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
}
if status.Messages == 0 {
return nil, nil
}
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}}},
fetchItems,
resCh,
); err != nil {
panic(err)
}
}()
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)
return createMessages(ctx, t, c, addrID, labelID, xslices.Repeat(literal, count)...)
}
func createMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID, labelID string, messages ...[]byte) []string {
return createMessagesWithFlags(ctx, t, c, addrID, labelID, 0, messages...)
}
func createMessagesWithFlags(ctx context.Context, t *testing.T, c *proton.Client, addrID, labelID string, flags proton.MessageFlag, messages ...[]byte) []string {
user, err := c.GetUser(ctx)
require.NoError(t, err)
addr, err := c.GetAddresses(ctx)
require.NoError(t, err)
salt, err := c.GetSalts(ctx)
require.NoError(t, err)
keyPass, err := salt.SaltForKey(password, user.Keys.Primary().ID)
require.NoError(t, err)
_, addrKRs, err := proton.Unlock(user, addr, keyPass, async.NoopPanicHandler{})
require.NoError(t, err)
_, ok := addrKRs[addrID]
require.True(t, ok)
var msgFlags proton.MessageFlag
if flags == 0 {
msgFlags = proton.MessageFlagReceived
} else {
msgFlags = flags
}
str, err := c.ImportMessages(
ctx,
addrKRs[addrID],
runtime.NumCPU(),
runtime.NumCPU(),
xslices.Map(messages, func(message []byte) proton.ImportReq {
return proton.ImportReq{
Metadata: proton.ImportMetadata{
AddressID: addrID,
LabelIDs: []string{labelID},
Flags: msgFlags,
},
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 {
return res.MessageID
})
}
func countBytesRead(ctl *proton.NetCtl, fn func()) uint64 {
var read uint64
ctl.OnRead(func(b []byte) {
atomic.AddUint64(&read, uint64(len(b)))
})
fn()
return read
}

View File

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

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

@ -0,0 +1,6 @@
To: recipient@pm.me
From: sender@pm.me
Subject: Test
Content-Type: text/plain; charset=utf-8
Test

View File

@ -1,4 +1,4 @@
// Copyright (c) 2022 Proton AG
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
@ -17,48 +17,10 @@
package bridge
import (
"crypto/tls"
pkgTLS "github.com/ProtonMail/proton-bridge/v2/internal/config/tls"
"github.com/pkg/errors"
logrus "github.com/sirupsen/logrus"
)
func (b *Bridge) GetTLSConfig() (*tls.Config, error) {
if !b.tls.HasCerts() {
if err := b.generateTLSCerts(); err != nil {
return nil, err
}
func (bridge *Bridge) GetBridgeTLSCert() ([]byte, []byte) {
return bridge.vault.GetBridgeTLSCert()
}
tlsConfig, err := b.tls.GetConfig()
if err == nil {
return tlsConfig, nil
}
logrus.WithError(err).Error("Failed to load TLS config, regenerating certificates")
if err := b.generateTLSCerts(); err != nil {
return nil, err
}
return b.tls.GetConfig()
}
func (b *Bridge) generateTLSCerts() error {
template, err := pkgTLS.NewTLSTemplate()
if err != nil {
return errors.Wrap(err, "failed to generate TLS template")
}
if err := b.tls.GenerateCerts(template); err != nil {
return errors.Wrap(err, "failed to generate TLS certs")
}
if err := b.tls.InstallCerts(); err != nil {
return errors.Wrap(err, "failed to install TLS certs")
}
return nil
func (bridge *Bridge) SetBridgeTLSCertPath(certPath, keyPath string) error {
return bridge.vault.SetBridgeTLSCertPath(certPath, keyPath)
}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2022 Proton AG
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
@ -18,45 +18,40 @@
package bridge
import (
"github.com/Masterminds/semver/v3"
"context"
"github.com/ProtonMail/proton-bridge/v2/internal/config/settings"
"github.com/ProtonMail/proton-bridge/v2/internal/updater"
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
)
type Locator interface {
ProvideSettingsPath() (string, error)
ProvideLogsPath() (string, error)
ProvideGluonCachePath() (string, error)
ProvideGluonDataPath() (string, error)
ProvideStatsPath() (string, error)
GetLicenseFilePath() string
GetDependencyLicensesLink() string
Clear() error
ClearUpdates() error
Clear(...string) error
ProvideIMAPSyncConfigPath() (string, error)
}
type CacheProvider interface {
GetIMAPCachePath() string
GetDBDir() string
GetDefaultMessageCacheDir() string
type ProxyController interface {
AllowProxy()
DisallowProxy()
}
type SettingsProvider interface {
Get(key settings.Key) string
Set(key settings.Key, value string)
type TLSReporter interface {
GetTLSIssueCh() <-chan struct{}
}
GetBool(key settings.Key) bool
SetBool(key settings.Key, val bool)
GetInt(key settings.Key) int
SetInt(key settings.Key, val int)
type Autostarter interface {
Enable() error
Disable() error
IsEnabled() bool
}
type Updater interface {
Check() (updater.VersionInfo, error)
IsDowngrade(updater.VersionInfo) bool
InstallUpdate(updater.VersionInfo) error
}
type Versioner interface {
RemoveOtherVersions(*semver.Version) error
GetVersionInfo(context.Context, updater.Downloader, updater.Channel) (updater.VersionInfo, error)
InstallUpdate(context.Context, updater.Downloader, updater.VersionInfo) error
RemoveOldUpdates() error
}

147
internal/bridge/updates.go Normal file
View File

@ -0,0 +1,147 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge
import (
"context"
"errors"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
"github.com/sirupsen/logrus"
)
func (bridge *Bridge) CheckForUpdates() {
bridge.goUpdate()
}
func (bridge *Bridge) InstallUpdate(version updater.VersionInfo) {
bridge.installCh <- installJob{version: version, silent: false}
}
func (bridge *Bridge) handleUpdate(version updater.VersionInfo) {
log := logrus.WithFields(logrus.Fields{
"version": version.Version,
"current": bridge.curVersion,
"channel": bridge.vault.GetUpdateChannel(),
})
bridge.publish(events.UpdateLatest{
Version: version,
})
switch {
case !version.Version.GreaterThan(bridge.curVersion):
log.Debug("No update available")
bridge.publish(events.UpdateNotAvailable{})
case version.RolloutProportion < bridge.vault.GetUpdateRollout():
log.Info("An update is available but has not been rolled out yet")
bridge.publish(events.UpdateNotAvailable{})
case bridge.curVersion.LessThan(version.MinAuto):
log.Info("An update is available but is incompatible with this version")
bridge.publish(events.UpdateAvailable{
Version: version,
Compatible: false,
Silent: false,
})
case !bridge.vault.GetAutoUpdate():
log.Info("An update is available but auto-update is disabled")
bridge.publish(events.UpdateAvailable{
Version: version,
Compatible: true,
Silent: false,
})
default:
safe.RLock(func() {
bridge.installCh <- installJob{version: version, silent: true}
}, bridge.newVersionLock)
}
}
type installJob struct {
version updater.VersionInfo
silent bool
}
func (bridge *Bridge) installUpdate(ctx context.Context, job installJob) {
safe.Lock(func() {
log := logrus.WithFields(logrus.Fields{
"version": job.version.Version,
"current": bridge.curVersion,
"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,
Silent: job.silent,
})
bridge.publish(events.UpdateInstalling{
Version: job.version,
Silent: job.silent,
})
err := bridge.updater.InstallUpdate(ctx, bridge.api, job.version)
switch {
case errors.Is(err, updater.ErrUpdateAlreadyInstalled):
log.Info("The update was already installed")
case err != nil:
log.WithError(err).Error("The update could not be installed")
bridge.publish(events.UpdateFailed{
Version: job.version,
Silent: job.silent,
Error: err,
})
default:
log.Info("The update was installed successfully")
bridge.publish(events.UpdateInstalled{
Version: job.version,
Silent: job.silent,
})
bridge.newVersion = job.version.Version
}
}, bridge.newVersionLock)
}
func (bridge *Bridge) RemoveOldUpdates() {
if err := bridge.updater.RemoveOldUpdates(); err != nil {
logrus.WithError(err).Error("Remove old updates fails")
}
}

669
internal/bridge/user.go Normal file
View File

@ -0,0 +1,669 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge
import (
"context"
"errors"
"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/events"
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice"
"github.com/ProtonMail/proton-bridge/v3/internal/try"
"github.com/ProtonMail/proton-bridge/v3/internal/user"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/go-resty/resty/v2"
"github.com/sirupsen/logrus"
)
type UserState int
const (
SignedOut UserState = iota
Locked
Connected
)
var ErrFailedToUnlock = errors.New("failed to unlock user keys")
type UserInfo struct {
// UserID is the user's API ID.
UserID string
// Username is the user's API username.
Username string
// Signed Out is true if the user is signed out (no AuthUID, user will need to provide credentials to log in again)
State UserState
// Addresses holds the user's email addresses. The first address is the primary address.
Addresses []string
// AddressMode is the user's address mode.
AddressMode vault.AddressMode
// BridgePass is the user's bridge password.
BridgePass []byte
// UsedSpace is the amount of space used by the user.
UsedSpace uint64
// MaxSpace is the total amount of space available to the user.
MaxSpace uint64
}
// GetUserIDs returns the IDs of all known users (authorized or not).
func (bridge *Bridge) GetUserIDs() []string {
return bridge.vault.GetUserIDs()
}
// HasUser returns true iff the given user is known (authorized or not).
func (bridge *Bridge) HasUser(userID string) bool {
return bridge.vault.HasUser(userID)
}
// GetUserInfo returns info about the given user.
func (bridge *Bridge) GetUserInfo(userID string) (UserInfo, error) {
return safe.RLockRetErr(func() (UserInfo, error) {
if user, ok := bridge.users[userID]; ok {
return getConnUserInfo(user), nil
}
var info UserInfo
if err := bridge.vault.GetUser(userID, func(user *vault.User) {
state := Locked
if len(user.AuthUID()) == 0 {
state = SignedOut
}
info = getUserInfo(user.UserID(), user.Username(), user.PrimaryEmail(), state, user.AddressMode())
}); err != nil {
return UserInfo{}, fmt.Errorf("failed to get user info: %w", err)
}
return info, nil
}, bridge.usersLock)
}
// QueryUserInfo queries the user info by username or address.
func (bridge *Bridge) QueryUserInfo(query string) (UserInfo, error) {
return safe.RLockRetErr(func() (UserInfo, error) {
for _, user := range bridge.users {
if user.Match(query) {
return getConnUserInfo(user), nil
}
}
return UserInfo{}, ErrNoSuchUser
}, bridge.usersLock)
}
// LoginAuth begins the login process. It returns an authorized client that might need 2FA.
func (bridge *Bridge) LoginAuth(ctx context.Context, username string, password []byte) (*proton.Client, proton.Auth, error) {
logrus.WithField("username", logging.Sensitive(username)).Info("Authorizing user for login")
if username == "crash@bandicoot" {
panic("Your wish is my command.. I crash!")
}
client, auth, err := bridge.api.NewClientWithLogin(ctx, username, password)
if err != nil {
return nil, proton.Auth{}, fmt.Errorf("failed to create new API client: %w", err)
}
if ok := safe.RLockRet(func() bool { return mapHas(bridge.users, auth.UserID) }, bridge.usersLock); ok {
logrus.WithField("userID", auth.UserID).Warn("User already logged in")
if err := client.AuthDelete(ctx); err != nil {
logrus.WithError(err).Warn("Failed to delete auth")
}
return nil, proton.Auth{}, ErrUserAlreadyLoggedIn
}
return client, auth, nil
}
// LoginUser finishes the user login process using the client and auth received from LoginAuth.
func (bridge *Bridge) LoginUser(
ctx context.Context,
client *proton.Client,
auth proton.Auth,
keyPass []byte,
) (string, error) {
logrus.WithField("userID", auth.UserID).Info("Logging in authorized user")
userID, err := try.CatchVal(
func() (string, error) {
return bridge.loginUser(ctx, client, auth.UID, auth.RefreshToken, keyPass)
},
)
if err != nil {
// Failure to unlock will allow retries, so we do not delete auth.
if !errors.Is(err, ErrFailedToUnlock) {
if deleteErr := client.AuthDelete(ctx); deleteErr != nil {
logrus.WithError(deleteErr).Error("Failed to delete auth")
}
}
return "", fmt.Errorf("failed to login user: %w", err)
}
bridge.publish(events.UserLoggedIn{
UserID: userID,
})
return userID, nil
}
// LoginFull authorizes a new bridge user with the given username and password.
// If necessary, a TOTP and mailbox password are requested via the callbacks.
// This is equivalent to doing LoginAuth and LoginUser separately.
func (bridge *Bridge) LoginFull(
ctx context.Context,
username string,
password []byte,
getTOTP func() (string, error),
getKeyPass func() ([]byte, error),
) (string, error) {
logrus.WithField("username", logging.Sensitive(username)).Info("Performing full user login")
client, auth, err := bridge.LoginAuth(ctx, username, password)
if err != nil {
return "", fmt.Errorf("failed to begin login process: %w", err)
}
if auth.TwoFA.Enabled&proton.HasTOTP != 0 {
logrus.WithField("userID", auth.UserID).Info("Requesting TOTP")
totp, err := getTOTP()
if err != nil {
return "", fmt.Errorf("failed to get TOTP: %w", err)
}
if err := client.Auth2FA(ctx, proton.Auth2FAReq{TwoFactorCode: totp}); err != nil {
return "", fmt.Errorf("failed to authorize 2FA: %w", err)
}
}
var keyPass []byte
if auth.PasswordMode == proton.TwoPasswordMode {
logrus.WithField("userID", auth.UserID).Info("Requesting mailbox password")
userKeyPass, err := getKeyPass()
if err != nil {
return "", fmt.Errorf("failed to get key password: %w", err)
}
keyPass = userKeyPass
} else {
keyPass = password
}
userID, err := bridge.LoginUser(ctx, client, auth, keyPass)
if err != nil {
if deleteErr := client.AuthDelete(ctx); deleteErr != nil {
logrus.WithError(err).Error("Failed to delete auth")
}
return "", err
}
return userID, nil
}
// LogoutUser logs out the given user.
func (bridge *Bridge) LogoutUser(ctx context.Context, userID string) error {
logrus.WithField("userID", userID).Info("Logging out user")
return safe.LockRet(func() error {
user, ok := bridge.users[userID]
if !ok {
return ErrNoSuchUser
}
bridge.logoutUser(ctx, user, true, false, false)
bridge.publish(events.UserLoggedOut{
UserID: userID,
})
return nil
}, bridge.usersLock)
}
// DeleteUser deletes the given user.
func (bridge *Bridge) DeleteUser(ctx context.Context, userID string) error {
logrus.WithField("userID", userID).Info("Deleting user")
syncConfigDir, err := bridge.locator.ProvideIMAPSyncConfigPath()
if err != nil {
return fmt.Errorf("failed to get sync config path")
}
return safe.LockRet(func() error {
if !bridge.vault.HasUser(userID) {
return ErrNoSuchUser
}
if user, ok := bridge.users[userID]; ok {
bridge.logoutUser(ctx, user, true, true, !bridge.GetTelemetryDisabled())
}
if err := imapservice.DeleteSyncState(syncConfigDir, userID); err != nil {
return fmt.Errorf("failed to delete use sync config")
}
if err := bridge.vault.DeleteUser(userID); err != nil {
logrus.WithError(err).Error("Failed to delete vault user")
}
bridge.publish(events.UserDeleted{
UserID: userID,
})
return nil
}, bridge.usersLock)
}
// SetAddressMode sets the address mode for the given user.
func (bridge *Bridge) SetAddressMode(ctx context.Context, userID string, mode vault.AddressMode) error {
logrus.WithField("userID", userID).WithField("mode", mode).Info("Setting address mode")
return safe.RLockRet(func() error {
user, ok := bridge.users[userID]
if !ok {
return ErrNoSuchUser
}
if user.GetAddressMode() == mode {
return fmt.Errorf("address mode is already %q", mode)
}
if err := user.SetAddressMode(ctx, mode); err != nil {
return fmt.Errorf("failed to set address mode: %w", err)
}
bridge.publish(events.AddressModeChanged{
UserID: userID,
AddressMode: mode,
})
var splitMode = false
for _, user := range bridge.users {
if user.GetAddressMode() == vault.SplitMode {
splitMode = true
break
}
}
bridge.heartbeat.SetSplitMode(splitMode)
return nil
}, bridge.usersLock)
}
// 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.RLockRet(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")
}
return user.BadEventFeedbackResync(ctx)
}
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, 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 {
return "", fmt.Errorf("failed to get API user: %w", err)
}
salts, err := client.GetSalts(ctx)
if err != nil {
return "", fmt.Errorf("failed to get key salts: %w", err)
}
saltedKeyPass, err := salts.SaltForKey(keyPass, apiUser.Keys.Primary().ID)
if err != nil {
return "", fmt.Errorf("failed to salt key password: %w", err)
}
if userKR, err := apiUser.Keys.Unlock(saltedKeyPass, nil); err != nil {
return "", fmt.Errorf("%w: %w", ErrFailedToUnlock, err)
} else if userKR.CountDecryptionEntities() == 0 {
return "", ErrFailedToUnlock
}
if err := bridge.addUser(ctx, client, apiUser, authUID, authRef, saltedKeyPass, true); err != nil {
return "", fmt.Errorf("failed to add bridge user: %w", err)
}
return apiUser.ID, nil
}
// 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
}
log.WithField("mode", user.AddressMode()).Info("Loading connected user")
bridge.publish(events.UserLoading{
UserID: user.UserID(),
})
if err := bridge.loadUser(ctx, user); err != nil {
log.WithError(err).Error("Failed to load connected user")
bridge.publish(events.UserLoadFail{
UserID: user.UserID(),
Error: err,
})
} else {
log.Info("Successfully loaded connected user")
bridge.publish(events.UserLoadSuccess{
UserID: user.UserID(),
})
}
return nil
})
}
// loadUser loads an existing user from the vault.
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.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)
}
if err := user.SetAuth(auth.UID, auth.RefreshToken); err != nil {
return fmt.Errorf("failed to set auth: %w", err)
}
apiUser, err := client.GetUser(ctx)
if err != nil {
return fmt.Errorf("failed to get user: %w", err)
}
if err := bridge.addUser(ctx, client, apiUser, auth.UID, auth.RefreshToken, user.KeyPass(), false); err != nil {
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
}
// addUser adds a new user with an already salted mailbox password.
func (bridge *Bridge) addUser(
ctx context.Context,
client *proton.Client,
apiUser proton.User,
authUID, authRef string,
saltedKeyPass []byte,
isLogin bool,
) error {
vaultUser, isNew, err := bridge.newVaultUser(apiUser, authUID, authRef, saltedKeyPass)
if err != nil {
return fmt.Errorf("failed to add vault user: %w", err)
}
if err := bridge.addUserWithVault(ctx, client, apiUser, vaultUser, isNew); err != nil {
if _, ok := err.(*resty.ResponseError); ok || isLogin {
logrus.WithError(err).Error("Failed to add user, clearing its secrets from vault")
if err := vaultUser.Clear(); err != nil {
logrus.WithError(err).Error("Failed to clear user secrets")
}
} else {
logrus.WithError(err).Error("Failed to add user")
}
if err := vaultUser.Close(); err != nil {
logrus.WithError(err).Error("Failed to close vault user")
}
if isNew {
logrus.Warn("Deleting newly added vault user")
if err := bridge.vault.DeleteUser(apiUser.ID); err != nil {
logrus.WithError(err).Error("Failed to delete vault user")
}
}
return fmt.Errorf("failed to add user with vault: %w", err)
}
return nil
}
// addUserWithVault adds a new user to bridge with the given vault.
func (bridge *Bridge) addUserWithVault(
ctx context.Context,
client *proton.Client,
apiUser proton.User,
vault *vault.User,
isNew bool,
) error {
statsPath, err := bridge.locator.ProvideStatsPath()
if err != nil {
return fmt.Errorf("failed to get Statistics directory: %w", err)
}
syncSettingsPath, err := bridge.locator.ProvideIMAPSyncConfigPath()
if err != nil {
return fmt.Errorf("failed to get IMAP sync config path: %w", err)
}
user, err := user.New(
ctx,
vault,
client,
bridge.reporter,
apiUser,
bridge.panicHandler,
bridge.vault.GetShowAllMail(),
bridge.vault.GetMaxSyncMemory(),
statsPath,
bridge,
bridge.serverManager,
bridge.serverManager,
&bridgeEventSubscription{b: bridge},
bridge.syncService,
syncSettingsPath,
isNew,
)
if err != nil {
return fmt.Errorf("failed to create user: %w", err)
}
// Handle events coming from the user before forwarding them to the bridge.
// For example, if the user's addresses change, we need to update them in gluon.
bridge.tasks.Once(func(ctx context.Context) {
async.RangeContext(ctx, user.GetEventCh(), func(event events.Event) {
logrus.WithFields(logrus.Fields{
"userID": apiUser.ID,
"event": event,
}).Debug("Received user event")
bridge.handleUserEvent(ctx, user, event)
bridge.publish(event)
})
})
// Gluon will set the IMAP ID in the context, if known, before making requests on behalf of this user.
// As such, if we find this ID in the context, we should use it to update our user agent.
client.AddPreRequestHook(func(_ *resty.Client, r *resty.Request) error {
if imapID, ok := imap.GetIMAPIDFromContext(r.Context()); ok {
bridge.setUserAgent(imapID.Name, imapID.Version)
}
return nil
})
// Finally, save the user in the bridge.
safe.Lock(func() {
bridge.users[apiUser.ID] = user
bridge.heartbeat.SetNbAccount(len(bridge.users))
}, bridge.usersLock)
// As we need at least one user to send heartbeat, try to send it.
bridge.heartbeat.start()
return nil
}
// newVaultUser creates a new vault user from the given auth information.
// If one already exists in the vault, its data will be updated.
func (bridge *Bridge) newVaultUser(
apiUser proton.User,
authUID, authRef string,
saltedKeyPass []byte,
) (*vault.User, bool, error) {
return bridge.vault.GetOrAddUser(apiUser.ID, apiUser.Name, apiUser.Email, authUID, authRef, saltedKeyPass)
}
// logout logs out the given user, optionally logging them out from the API too.
func (bridge *Bridge) logoutUser(ctx context.Context, user *user.User, withAPI, withData, withTelemetry bool) {
defer delete(bridge.users, user.ID())
// if this is actually a remove account
if withData && withAPI {
user.SendConfigStatusAbort(ctx, withTelemetry)
}
logrus.WithFields(logrus.Fields{
"userID": user.ID(),
"withAPI": withAPI,
"withData": withData,
}).Debug("Logging out user")
if err := user.Logout(ctx, withAPI); err != nil {
logrus.WithError(err).Error("Failed to logout user")
}
bridge.heartbeat.SetNbAccount(len(bridge.users))
user.Close()
}
// getUserInfo returns information about a disconnected user.
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,
}
}
// getConnUserInfo returns information about a connected user.
func getConnUserInfo(user *user.User) UserInfo {
return UserInfo{
State: Connected,
UserID: user.ID(),
Username: user.Name(),
Addresses: user.Emails(),
AddressMode: user.GetAddressMode(),
BridgePass: user.BridgePass(),
UsedSpace: user.UsedSpace(),
MaxSpace: user.MaxSpace(),
}
}
func mapHas[Key comparable, Val any](m map[Key]Val, key Key) bool {
_, ok := m[key]
return ok
}

View File

@ -0,0 +1,938 @@
// 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{}))
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) {
smtpWaiter := waitForSMTPServerReady(bridge)
defer smtpWaiter.Done()
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)
smtpWaiter.Wait()
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) {
smtpWaiter := waitForSMTPServerReady(bridge)
defer smtpWaiter.Done()
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...))
})
smtpWaiter.Wait()
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) {
var count int32
// The first 10 times bridge attempts to sync any of the messages, drop the connection.
s.AddStatusHook(func(req *http.Request) (int, bool) {
if strings.Contains(req.URL.Path, "/mail/v4/messages") {
if atomic.AddInt32(&count, 1) < 10 {
dropListener.DropAll()
}
}
return 0, false
})
userLoginAndSync(ctx, t, bridge, "user", password)
mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).AnyTimes()
// 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)
})
info, err := bridge.QueryUserInfo("user")
require.NoError(t, err)
cli, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, cli.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = cli.Logout() }()
// The IMAP client will eventually see 20 messages.
require.Eventually(t, func() bool {
status, err := cli.Status("INBOX", []imap.StatusItem{imap.StatusMessages})
return err == nil && status.Messages == 20
}, 10*time.Second, 100*time.Millisecond)
})
}, server.WithListener(dropListener))
}
func TestBridge_User_UpdateDraft(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
// Create a bridge user.
_, _, err := s.CreateUser("user", password)
require.NoError(t, err)
// Initially sync the user.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
userLoginAndSync(ctx, t, bridge, "user", password)
})
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
user, err := c.GetUser(ctx)
require.NoError(t, err)
addrs, err := c.GetAddresses(ctx)
require.NoError(t, err)
salts, err := c.GetSalts(ctx)
require.NoError(t, err)
keyPass, err := salts.SaltForKey(password, user.Keys.Primary().ID)
require.NoError(t, err)
_, addrKRs, err := proton.Unlock(user, addrs, keyPass, async.NoopPanicHandler{})
require.NoError(t, err)
// Create a draft (generating a "create draft message" event).
draft, err := c.CreateDraft(ctx, addrKRs[addrs[0].ID], proton.CreateDraftReq{
Message: proton.DraftTemplate{
Subject: "subject",
Sender: &mail.Address{Name: "sender", Address: addrs[0].Email},
Body: "body",
MIMEType: rfc822.TextPlain,
},
})
require.NoError(t, err)
require.Empty(t, draft.ReplyTos)
// Process those events
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
userContinueEventProcess(ctx, t, s, bridge)
})
// Update the draft (generating an "update draft message" event).
draft2, err := c.UpdateDraft(ctx, draft.ID, addrKRs[addrs[0].ID], proton.UpdateDraftReq{
Message: proton.DraftTemplate{
Subject: "subject 2",
Sender: &mail.Address{Name: "sender", Address: addrs[0].Email},
Body: "body 2",
MIMEType: rfc822.TextPlain,
},
})
require.NoError(t, err)
require.Empty(t, draft2.ReplyTos)
})
})
}
func TestBridge_User_UpdateDraftAndCreateOtherMessage(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
// Create a bridge user.
_, _, 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)
cli, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, cli.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = cli.Logout() }()
messages, err := clientFetch(cli, "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)
cli, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, cli.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = cli.Logout() }()
messages, err := clientFetch(cli, "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) {
imapWaiter := waitForIMAPServerReady(bridge)
defer imapWaiter.Done()
smtpWaiter := waitForSMTPServerReady(bridge)
defer smtpWaiter.Done()
require.NoError(t, getErr(bridge.LoginFull(ctx, username, password, nil, nil)))
info, err := bridge.QueryUserInfo(username)
require.NoError(t, err)
imapWaiter.Wait()
smtpWaiter.Wait()
cli, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, cli.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = cli.Logout() }()
withClient(ctx, t, s, username, password, func(ctx context.Context, c *proton.Client) {
parentName := uuid.NewString()
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(cli), 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(cli), 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(cli), func(mailbox *imap.MailboxInfo) bool {
return mailbox.Name == fmt.Sprintf("Folders/%v", newParentName)
}) >= 0
}, 100*user.EventPeriod, user.EventPeriod)
// Wait for the child folder to be renamed.
require.Eventually(t, func() bool {
return xslices.IndexFunc(clientList(cli), 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)
cli, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, cli.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = cli.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(cli), func(mailbox *imap.MailboxInfo) bool {
return mailbox.Name == "Labels/"+randomLabel
}) >= 0
}, 100*user.EventPeriod, user.EventPeriod)
}
func eventuallyDial(addr string) (cli *client.Client, err error) {
var sleep = 1 * time.Second
for i := 0; i < 5; i++ {
cli, err := client.Dial(addr)
if err == nil {
return cli, nil
}
time.Sleep(sleep)
sleep *= 2
}
return nil, fmt.Errorf("after 5 attempts, last error: %s", err)
}

View File

@ -0,0 +1,75 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge
import (
"context"
"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/sirupsen/logrus"
)
func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, event events.Event) {
switch event := event.(type) {
case events.UserDeauth:
bridge.handleUserDeauth(ctx, user)
case events.UserBadEvent:
bridge.handleUserBadEvent(ctx, user, event)
case events.UncategorizedEventError:
bridge.handleUncategorizedErrorEvent(event)
}
}
func (bridge *Bridge) handleUserDeauth(ctx context.Context, user *user.User) {
safe.Lock(func() {
bridge.logoutUser(ctx, user, false, false, false)
user.ReportConfigStatusFailure("User deauth.")
}, bridge.usersLock)
}
func (bridge *Bridge) handleUserBadEvent(ctx context.Context, user *user.User, event events.UserBadEvent) {
safe.RLock(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.OnBadEvent(ctx)
}, 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

@ -0,0 +1,726 @@
// 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"
"testing"
"time"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/go-proton-api/server"
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/stretchr/testify/require"
)
func TestBridge_WithoutUsers(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.Empty(t, bridge.GetUserIDs())
require.Empty(t, getConnectedUserIDs(t, bridge))
})
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
require.Empty(t, bridge.GetUserIDs())
require.Empty(t, getConnectedUserIDs(t, bridge))
})
})
}
func TestBridge_Login(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) {
// 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))
})
})
}
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) {
// 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))
// Additional login should fail.
_, err = bridge.LoginFull(ctx, username, password, nil, nil)
require.Error(t, err)
})
})
}
func TestBridge_LoginLogoutLogin(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) {
// Login the user.
userID := must(bridge.LoginFull(ctx, username, password, nil, nil))
// The user is now connected.
require.Equal(t, []string{userID}, bridge.GetUserIDs())
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
// Logout the user.
require.NoError(t, bridge.LogoutUser(ctx, userID))
// The user is now disconnected.
require.Equal(t, []string{userID}, bridge.GetUserIDs())
require.Empty(t, getConnectedUserIDs(t, bridge))
// Login the user again.
newUserID := must(bridge.LoginFull(ctx, username, password, nil, nil))
require.Equal(t, userID, newUserID)
// The user is connected again.
require.Equal(t, []string{userID}, bridge.GetUserIDs())
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
})
})
}
func TestBridge_LoginDeleteLogin(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) {
// Login the user.
userID := must(bridge.LoginFull(ctx, username, password, nil, nil))
// The user is now connected.
require.Equal(t, []string{userID}, bridge.GetUserIDs())
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
// Delete the user.
require.NoError(t, bridge.DeleteUser(ctx, userID))
// The user is now gone.
require.Empty(t, bridge.GetUserIDs())
require.Empty(t, getConnectedUserIDs(t, bridge))
// Login the user again.
newUserID := must(bridge.LoginFull(ctx, username, password, nil, nil))
require.Equal(t, userID, newUserID)
// The user is connected again.
require.Equal(t, []string{userID}, bridge.GetUserIDs())
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
})
})
}
func TestBridge_LoginDeauthLogin(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) {
// Login the user.
userID := must(bridge.LoginFull(ctx, username, password, nil, nil))
// Get a channel to receive the deauth event.
eventCh, done := bridge.GetEvents(events.UserDeauth{})
defer done()
// Deauth the user.
require.NoError(t, s.RevokeUser(userID))
// The user is eventually disconnected.
require.Eventually(t, func() bool {
return len(getConnectedUserIDs(t, bridge)) == 0
}, 10*time.Second, time.Second)
// We should get a deauth event.
require.IsType(t, events.UserDeauth{}, <-eventCh)
// Login the user after the disconnection.
newUserID := must(bridge.LoginFull(ctx, username, password, nil, nil))
require.Equal(t, userID, newUserID)
// The user is connected again.
require.Equal(t, []string{userID}, bridge.GetUserIDs())
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
})
})
}
func TestBridge_LoginDeauthRestartLogin(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
var userID string
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
// Login the user.
userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
// Get a channel to receive the deauth event.
eventCh, done := bridge.GetEvents(events.UserDeauth{})
defer done()
// Deauth the user.
require.NoError(t, s.RevokeUser(userID))
// The user is eventually disconnected.
require.Eventually(t, func() bool {
return len(getConnectedUserIDs(t, bridge)) == 0
}, 10*time.Second, time.Second)
// We should get a deauth event.
require.IsType(t, events.UserDeauth{}, <-eventCh)
})
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
// The user should be disconnected at startup.
require.Equal(t, []string{userID}, bridge.GetUserIDs())
require.Empty(t, getConnectedUserIDs(t, bridge))
// Login the user after the disconnection.
newUserID := must(bridge.LoginFull(ctx, username, password, nil, nil))
require.Equal(t, userID, newUserID)
// The user is connected again.
require.Equal(t, []string{userID}, bridge.GetUserIDs())
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
})
})
}
func TestBridge_LoginExpireLogin(t *testing.T) {
const authLife = 2 * time.Second
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
s.SetAuthLife(authLife)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
// Login the user. Its auth will only be valid for a short time.
userID := must(bridge.LoginFull(ctx, username, password, nil, nil))
// Wait until the auth expires.
time.Sleep(authLife)
// The user will have to refresh but the logout will still succeed.
require.NoError(t, bridge.LogoutUser(ctx, userID))
})
})
}
func TestBridge_FailToLoad(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
var userID string
// Login the user.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
})
// Deauth the user while bridge is stopped.
require.NoError(t, s.RevokeUser(userID))
// When bridge starts, the user will not be logged in.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
require.Equal(t, []string{userID}, bridge.GetUserIDs())
require.Empty(t, getConnectedUserIDs(t, bridge))
})
})
}
func TestBridge_LoadWithoutInternet(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
var userID string
// Login the user.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
})
// Simulate loss of internet connection.
netCtl.Disable()
// Start bridge without internet.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
// Initially, users are not connected.
require.Equal(t, []string{userID}, bridge.GetUserIDs())
require.Empty(t, getConnectedUserIDs(t, bridge))
time.Sleep(5 * time.Second)
// Simulate internet connection.
netCtl.Enable()
// The user will eventually be connected.
require.Eventually(t, func() bool {
return len(getConnectedUserIDs(t, bridge)) == 1 && getConnectedUserIDs(t, bridge)[0] == userID
}, 10*time.Second, time.Second)
})
})
}
func TestBridge_LoginRestart(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
var userID string
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
})
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
require.Equal(t, []string{userID}, bridge.GetUserIDs())
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
})
})
}
func TestBridge_LoginLogoutRestart(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
var userID string
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
// Login the user.
userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
// Logout the user.
require.NoError(t, bridge.LogoutUser(ctx, userID))
})
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
// The user is still disconnected.
require.Equal(t, []string{userID}, bridge.GetUserIDs())
require.Empty(t, getConnectedUserIDs(t, bridge))
})
})
}
func TestBridge_LoginDeleteRestart(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
var userID string
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
// Login the user.
userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
// Delete the user.
require.NoError(t, bridge.DeleteUser(ctx, userID))
})
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
// The user is still gone.
require.Empty(t, bridge.GetUserIDs())
require.Empty(t, getConnectedUserIDs(t, bridge))
})
})
}
func TestBridge_FailLoginRecover(t *testing.T) {
for i := uint64(1); i < 10; i++ {
t.Run(fmt.Sprintf("read %v%% of the data", 100*i/10), func(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
var userID string
// Log the user in, wait for it to sync, then log it out.
// (We don't want to count message sync data in the test.)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
defer done()
userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
require.Equal(t, userID, (<-syncCh).UserID)
require.NoError(t, bridge.LogoutUser(ctx, userID))
})
var total uint64
// Now that the user is synced, we can measure exactly how much data is needed during login.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
total = countBytesRead(netCtl, func() {
must(bridge.LoginFull(ctx, username, password, nil, nil))
})
require.NoError(t, bridge.LogoutUser(ctx, userID))
})
// Now simulate failing to login.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
// Simulate a partial read.
netCtl.SetReadLimit(i * total / 10)
// We should fail to log the user in because we can't fully read its data.
require.Error(t, getErr(bridge.LoginFull(ctx, username, password, nil, nil)))
// The user should still be there (but disconnected).
require.Equal(t, []string{userID}, bridge.GetUserIDs())
require.Empty(t, getConnectedUserIDs(t, bridge))
})
// Simulate the network recovering.
netCtl.SetReadLimit(0)
// We should now be able to log the user in.
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)))
// The user should be there, now connected.
require.Equal(t, []string{userID}, bridge.GetUserIDs())
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
})
})
})
}
}
func TestBridge_FailLoadRecover(t *testing.T) {
for i := uint64(1); i < 10; i++ {
t.Run(fmt.Sprintf("read %v%% of the data", 100*i/10), func(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
var userID string
// Log the user in and wait for it to sync.
// (We don't want to count message sync data in the test.)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
defer done()
userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
require.Equal(t, userID, (<-syncCh).UserID)
})
// See how much data it takes to load the user at startup.
total := countBytesRead(netCtl, func() {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
// ...
})
})
// Simulate a partial read.
netCtl.SetReadLimit(i * total / 10)
// We should fail to load the user; it should be listed but disconnected.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
require.Equal(t, []string{userID}, bridge.GetUserIDs())
require.Empty(t, getConnectedUserIDs(t, bridge))
})
// Simulate the network recovering.
netCtl.SetReadLimit(0)
// We should now be able to load the user.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
require.Equal(t, []string{userID}, bridge.GetUserIDs())
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
})
})
})
}
}
func TestBridge_BridgePass(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
var userID string
var pass []byte
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
// Login the user.
userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
// Retrieve the bridge pass.
pass = must(bridge.GetUserInfo(userID)).BridgePass
// Log the user out.
require.NoError(t, bridge.LogoutUser(ctx, userID))
// Log the user back in.
must(bridge.LoginFull(ctx, username, password, nil, nil))
// The bridge pass should be the same.
require.Equal(t, pass, pass)
})
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
// The bridge should load the user.
require.Equal(t, []string{userID}, bridge.GetUserIDs())
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
// The bridge pass should be the same.
require.Equal(t, pass, must(bridge.GetUserInfo(userID)).BridgePass)
})
})
}
func TestBridge_AddressMode(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) {
// Login the user.
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
require.NoError(t, err)
// Get the user's info.
info, err := bridge.GetUserInfo(userID)
require.NoError(t, err)
// The user is in combined mode by default.
require.Equal(t, vault.CombinedMode, info.AddressMode)
// Repeatedly switch between address modes.
for i := 1; i <= 10; i++ {
var target vault.AddressMode
if i%2 == 0 {
target = vault.CombinedMode
} else {
target = vault.SplitMode
}
// Put the user in the target mode.
require.NoError(t, bridge.SetAddressMode(ctx, userID, target))
// Get the user's info.
info, err = bridge.GetUserInfo(userID)
require.NoError(t, err)
// The user is in the target mode.
require.Equal(t, target, info.AddressMode)
}
})
})
}
func TestBridge_LoginLogoutRepeated(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) {
for i := 0; i < 10; i++ {
// Log the user in.
userID := must(bridge.LoginFull(ctx, username, password, nil, nil))
// Log the user out.
require.NoError(t, bridge.LogoutUser(ctx, userID))
}
})
})
}
func TestBridge_LogoutOffline(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
var userID string
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
// Login the user.
userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
// The user is now connected.
require.Equal(t, []string{userID}, bridge.GetUserIDs())
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
// Go offline.
netCtl.Disable()
// We can still log the user out.
require.NoError(t, bridge.LogoutUser(ctx, userID))
// The user is now disconnected.
require.Equal(t, []string{userID}, bridge.GetUserIDs())
require.Empty(t, getConnectedUserIDs(t, bridge))
})
// Go back online.
netCtl.Enable()
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
// The user is still disconnected.
require.Equal(t, []string{userID}, bridge.GetUserIDs())
require.Empty(t, getConnectedUserIDs(t, bridge))
})
})
}
func TestBridge_DeleteDisconnected(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) {
// 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))
// Logout the user.
require.NoError(t, bridge.LogoutUser(ctx, userID))
// The user is now disconnected.
require.Equal(t, []string{userID}, bridge.GetUserIDs())
require.Empty(t, getConnectedUserIDs(t, bridge))
// Delete the user.
require.NoError(t, bridge.DeleteUser(ctx, userID))
// The user is now deleted.
require.Empty(t, bridge.GetUserIDs())
require.Empty(t, getConnectedUserIDs(t, bridge))
})
})
}
func TestBridge_DeleteOffline(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) {
// 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))
// Go offline.
netCtl.Disable()
// We can still log the user out.
require.NoError(t, bridge.DeleteUser(ctx, userID))
// The user is now gone.
require.Empty(t, bridge.GetUserIDs())
require.Empty(t, getConnectedUserIDs(t, bridge))
})
})
}
func TestBridge_UserInfo_Alias(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) {
// Create a new user.
userID, _, err := s.CreateUser("primary", []byte("password"))
require.NoError(t, err)
// Give the new user an alias.
require.NoError(t, getErr(s.CreateAddress(userID, "alias@pm.me", []byte("password"))))
// Login the user.
require.NoError(t, getErr(bridge.LoginFull(ctx, "primary", []byte("password"), nil, nil)))
// Get user info.
info, err := bridge.GetUserInfo(userID)
require.NoError(t, err)
// The user should have two addresses, the primary should be first.
require.Equal(t, []string{"primary@" + s.GetDomain(), "alias@pm.me"}, info.Addresses)
})
})
}
func TestBridge_User_Refresh(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
// Get a channel of sync started events.
syncStartCh, done := chToType[events.Event, events.SyncStarted](bridge.GetEvents(events.SyncStarted{}))
defer done()
// Get a channel of sync finished events.
syncFinishCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
defer done()
// Log the user in.
userID := must(bridge.LoginFull(ctx, username, password, nil, nil))
// The sync should start and finish.
require.Equal(t, userID, (<-syncStartCh).UserID)
require.Equal(t, userID, (<-syncFinishCh).UserID)
// Trigger a refresh.
require.NoError(t, s.RefreshUser(userID, proton.RefreshAll))
// The sync should start and finish again.
require.Equal(t, userID, (<-syncStartCh).UserID)
require.Equal(t, userID, (<-syncFinishCh).UserID)
})
})
}
func TestBridge_User_GetAddresses(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
// Create a user.
userID, _, err := s.CreateUser("user", password)
require.NoError(t, err)
addrID2, err := s.CreateAddress(userID, "user@external.com", []byte("password"))
require.NoError(t, err)
require.NoError(t, s.ChangeAddressType(userID, addrID2, proton.AddressTypeExternal))
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
userLoginAndSync(ctx, t, bridge, "user", password)
info, err := bridge.GetUserInfo(userID)
require.NoError(t, err)
require.Equal(t, 1, len(info.Addresses))
require.Equal(t, info.Addresses[0], "user@proton.local")
})
})
}
// getErr returns the error that was passed to it.
func getErr[T any](_ T, err error) error {
return err
}

View File

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

View File

@ -0,0 +1,98 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
//go:build darwin
package certs
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestCertInKeychain(t *testing.T) {
// no trust settings change is performed, so this test will not trigger an OS security prompt.
certPEM := generatePEMCertificate(t)
require.True(t, osSupportCertInstall())
require.False(t, isCertInKeychain(certPEM))
require.NoError(t, addCertToKeychain(certPEM))
require.True(t, isCertInKeychain(certPEM))
require.Error(t, addCertToKeychain(certPEM))
require.True(t, isCertInKeychain(certPEM))
require.NoError(t, removeCertFromKeychain(certPEM))
require.False(t, isCertInKeychain(certPEM))
require.Error(t, removeCertFromKeychain(certPEM))
require.False(t, isCertInKeychain(certPEM))
}
// This test require human interaction (macOS security prompts), and is disabled by default.
func _TestCertificateTrust(t *testing.T) { //nolint:unused
certPEM := generatePEMCertificate(t)
require.False(t, isCertTrusted(certPEM))
require.NoError(t, addCertToKeychain(certPEM))
require.NoError(t, setCertTrusted(certPEM))
require.True(t, isCertTrusted(certPEM))
require.NoError(t, removeCertTrust(certPEM))
require.False(t, isCertTrusted(certPEM))
require.NoError(t, removeCertFromKeychain(certPEM))
}
// This test require human interaction (macOS security prompts), and is disabled by default.
func _TestInstallAndRemove(t *testing.T) { //nolint:unused
certPEM := generatePEMCertificate(t)
// fresh install
require.False(t, isCertInstalled(certPEM))
require.NoError(t, installCert(certPEM))
require.True(t, isCertInKeychain(certPEM))
require.True(t, isCertTrusted(certPEM))
require.True(t, isCertInstalled(certPEM))
require.NoError(t, uninstallCert(certPEM))
require.False(t, isCertInKeychain(certPEM))
require.False(t, isCertTrusted(certPEM))
require.False(t, isCertInstalled(certPEM))
// Install where certificate is already in Keychain, but not trusted.
require.NoError(t, addCertToKeychain(certPEM))
require.False(t, isCertInstalled(certPEM))
require.NoError(t, installCert(certPEM))
require.True(t, isCertInstalled(certPEM))
// Install where certificate is already installed
require.NoError(t, installCert(certPEM))
// Remove when certificate is not trusted.
require.NoError(t, removeCertTrust(certPEM))
require.NoError(t, uninstallCert(certPEM))
require.False(t, isCertInstalled(certPEM))
// Remove when certificate has already been removed.
require.NoError(t, uninstallCert(certPEM))
require.False(t, isCertTrusted(certPEM))
require.False(t, isCertInKeychain(certPEM))
}
func generatePEMCertificate(t *testing.T) []byte {
template, err := NewTLSTemplate()
require.NoError(t, err)
certPEM, _, err := GenerateCert(template)
require.NoError(t, err)
return certPEM
}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2022 Proton AG
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
@ -15,12 +15,20 @@
// 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 tls
package certs
func (t *TLS) InstallCerts() error {
func osSupportCertInstall() bool {
return false
}
func installCert([]byte) error {
return nil // Linux doesn't have a root cert store.
}
func (t *TLS) UninstallCerts() error {
func uninstallCert([]byte) error {
return nil // Linux doesn't have a root cert store.
}
func isCertInstalled([]byte) bool {
return false
}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2022 Proton AG
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
@ -15,12 +15,20 @@
// 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 tls
package certs
func (t *TLS) InstallCerts() error {
func osSupportCertInstall() bool {
return false
}
func installCert([]byte) error {
return nil // NOTE(GODT-986): Install certs to root cert store?
}
func (t *TLS) UninstallCerts() error {
func uninstallCert([]byte) error {
return nil // NOTE(GODT-986): Uninstall certs from root cert store?
}
func isCertInstalled([]byte) bool {
return false
}

View File

@ -0,0 +1,82 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package certs
import (
"errors"
"github.com/sirupsen/logrus"
)
var (
ErrUserCanceledCertificateInstall = errors.New("the user cancelled the authorization dialog")
)
type Installer struct {
log *logrus.Entry
}
func NewInstaller() *Installer {
return &Installer{
log: logrus.WithField("pkg", "certs"),
}
}
func (installer *Installer) OSSupportCertInstall() bool {
return osSupportCertInstall()
}
func (installer *Installer) InstallCert(certPEM []byte) error {
installer.log.Info("Installing the Bridge TLS certificate in the OS keychain")
if err := installCert(certPEM); err != nil {
installer.log.WithError(err).Error("The Bridge TLS certificate could not be installed in the OS keychain")
return err
}
installer.log.Info("The Bridge TLS certificate was successfully installed in the OS keychain")
return nil
}
func (installer *Installer) UninstallCert(certPEM []byte) error {
installer.log.Info("Uninstalling the Bridge TLS certificate from the OS keychain")
if err := uninstallCert(certPEM); err != nil {
installer.log.WithError(err).Error("The Bridge TLS certificate could not be uninstalled from the OS keychain")
return err
}
installer.log.Info("The Bridge TLS certificate was successfully uninstalled from the OS keychain")
return nil
}
func (installer *Installer) IsCertInstalled(certPEM []byte) bool {
return isCertInstalled(certPEM)
}
// LogCertInstallStatus reports the current status of the certificate installation in the log.
// If certificate installation is not supported on the platform, this function does nothing.
func (installer *Installer) LogCertInstallStatus(certPEM []byte) {
if installer.OSSupportCertInstall() {
if installer.IsCertInstalled(certPEM) {
installer.log.Info("The Bridge TLS certificate is installed in the OS keychain")
} else {
installer.log.Info("The Bridge TLS certificate is not installed in the OS keychain")
}
}
}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2022 Proton AG
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
@ -15,9 +15,10 @@
// 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 tls
package certs
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
@ -27,22 +28,13 @@ import (
"fmt"
"math/big"
"net"
"os"
"path/filepath"
"time"
"github.com/pkg/errors"
)
type TLS struct {
settingsPath string
}
func New(settingsPath string) *TLS {
return &TLS{
settingsPath: settingsPath,
}
}
// ErrTLSCertExpiresSoon is returned when the TLS certificate is about to expire.
var ErrTLSCertExpiresSoon = fmt.Errorf("TLS certificate will expire soon")
// NewTLSTemplate creates a new TLS template certificate with a random serial number.
func NewTLSTemplate() (*x509.Certificate, error) {
@ -69,108 +61,40 @@ func NewTLSTemplate() (*x509.Certificate, error) {
}, nil
}
// NewPEMKeyPair return a new TLS private key and certificate in PEM encoded format.
func NewPEMKeyPair() (pemCert, pemKey []byte, err error) {
template, err := NewTLSTemplate()
if err != nil {
return nil, nil, errors.Wrap(err, "failed to generate TLS template")
}
// GenerateCert generates a new TLS certificate and returns it as PEM.
var GenerateCert = func(template *x509.Certificate) ([]byte, []byte, error) { //nolint:gochecknoglobals
priv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, nil, errors.Wrap(err, "failed to generate private key")
}
pemKey = pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
derBytes, err := x509.CreateCertificate(rand.Reader, template, template, &priv.PublicKey, priv)
if err != nil {
return nil, nil, errors.Wrap(err, "failed to create certificate")
}
pemCert = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
certPEM := new(bytes.Buffer)
return pemCert, pemKey, nil
if err := pem.Encode(certPEM, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
return nil, nil, err
}
var ErrTLSCertExpiresSoon = fmt.Errorf("TLS certificate will expire soon")
keyPEM := new(bytes.Buffer)
// getTLSCertPath returns path to certificate; used for TLS servers (IMAP, SMTP).
func (t *TLS) getTLSCertPath() string {
return filepath.Join(t.settingsPath, "cert.pem")
if err := pem.Encode(keyPEM, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}); err != nil {
return nil, nil, err
}
// getTLSKeyPath returns path to private key; used for TLS servers (IMAP, SMTP).
func (t *TLS) getTLSKeyPath() string {
return filepath.Join(t.settingsPath, "key.pem")
}
// HasCerts returns whether TLS certs have been generated.
func (t *TLS) HasCerts() bool {
if _, err := os.Stat(t.getTLSCertPath()); err != nil {
return false
}
if _, err := os.Stat(t.getTLSKeyPath()); err != nil {
return false
}
return true
}
// GenerateCerts generates certs from the given template.
func (t *TLS) GenerateCerts(template *x509.Certificate) error {
priv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return errors.Wrap(err, "failed to generate private key")
}
derBytes, err := x509.CreateCertificate(rand.Reader, template, template, &priv.PublicKey, priv)
if err != nil {
return errors.Wrap(err, "failed to create certificate")
}
certOut, err := os.Create(t.getTLSCertPath())
if err != nil {
return err
}
defer certOut.Close() //nolint:errcheck,gosec
if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
return err
}
keyOut, err := os.OpenFile(t.getTLSKeyPath(), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
if err != nil {
return err
}
defer keyOut.Close() //nolint:errcheck,gosec
return pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
return certPEM.Bytes(), keyPEM.Bytes(), nil
}
// GetConfig tries to load TLS config or generate new one which is then returned.
func (t *TLS) GetConfig() (*tls.Config, error) {
c, err := tls.LoadX509KeyPair(t.getTLSCertPath(), t.getTLSKeyPath())
func GetConfig(certPEM, keyPEM []byte) (*tls.Config, error) {
c, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil {
return nil, errors.Wrap(err, "failed to load keypair")
}
return getConfigFromKeyPair(c)
}
// GetConfigFromPEMKeyPair load a TLS config from PEM encoded certificate and key.
func GetConfigFromPEMKeyPair(permCert, pemKey []byte) (*tls.Config, error) {
c, err := tls.X509KeyPair(permCert, pemKey)
if err != nil {
return nil, errors.Wrap(err, "failed to load keypair")
}
return getConfigFromKeyPair(c)
}
func getConfigFromKeyPair(c tls.Certificate) (*tls.Config, error) {
var err error
c.Leaf, err = x509.ParseCertificate(c.Certificate[0])
if err != nil {
return nil, errors.Wrap(err, "failed to parse certificate")

View File

@ -1,4 +1,4 @@
// Copyright (c) 2022 Proton AG
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
@ -15,10 +15,10 @@
// 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 tls
package certs
import (
"os"
"crypto/tls"
"testing"
"time"
@ -26,12 +26,6 @@ import (
)
func TestGetOldConfig(t *testing.T) {
dir, err := os.MkdirTemp("", "test-tls")
require.NoError(t, err)
// Create new tls object.
tls := New(dir)
// Create new TLS template.
tlsTemplate, err := NewTLSTemplate()
require.NoError(t, err)
@ -41,20 +35,15 @@ func TestGetOldConfig(t *testing.T) {
tlsTemplate.NotAfter = time.Now()
// Generate the certs from the template.
require.NoError(t, tls.GenerateCerts(tlsTemplate))
certPEM, keyPEM, err := GenerateCert(tlsTemplate)
require.NoError(t, err)
// Generate the config from the certs -- it's going to expire soon so we don't want to use it.
_, err = tls.GetConfig()
_, err = GetConfig(certPEM, keyPEM)
require.Equal(t, err, ErrTLSCertExpiresSoon)
}
func TestGetValidConfig(t *testing.T) {
dir, err := os.MkdirTemp("", "test-tls")
require.NoError(t, err)
// Create new tls object.
tls := New(dir)
// Create new TLS template.
tlsTemplate, err := NewTLSTemplate()
require.NoError(t, err)
@ -64,10 +53,11 @@ func TestGetValidConfig(t *testing.T) {
tlsTemplate.NotAfter = time.Now().Add(2 * 365 * 24 * time.Hour)
// Generate the certs from the template.
require.NoError(t, tls.GenerateCerts(tlsTemplate))
certPEM, keyPEM, err := GenerateCert(tlsTemplate)
require.NoError(t, err)
// Generate the config from the certs -- it's not going to expire soon so we want to use it.
config, err := tls.GetConfig()
config, err := GetConfig(certPEM, keyPEM)
require.NoError(t, err)
require.Equal(t, len(config.Certificates), 1)
@ -77,9 +67,13 @@ func TestGetValidConfig(t *testing.T) {
}
func TestNewConfig(t *testing.T) {
pemCert, pemKey, err := NewPEMKeyPair()
tlsTemplate, err := NewTLSTemplate()
require.NoError(t, err)
_, err = GetConfigFromPEMKeyPair(pemCert, pemKey)
pemCert, pemKey, err := GenerateCert(tlsTemplate)
require.NoError(t, err)
cert, err := tls.X509KeyPair(pemCert, pemKey)
require.NoError(t, err)
require.NotNil(t, cert)
}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2022 Proton AG
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
@ -23,8 +23,8 @@ import (
"strconv"
"time"
"github.com/ProtonMail/proton-bridge/v2/internal/config/useragent"
"github.com/ProtonMail/proton-bridge/v2/pkg/mobileconfig"
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
"github.com/ProtonMail/proton-bridge/v3/pkg/mobileconfig"
"golang.org/x/sys/execabs"
)
@ -39,9 +39,10 @@ func (c *AppleMail) Configure(
hostname string,
imapPort, smtpPort int,
imapSSL, smtpSSL bool,
username, addresses, password string,
username, displayName, addresses string,
password []byte,
) error {
mc := prepareMobileConfig(hostname, imapPort, smtpPort, imapSSL, smtpSSL, username, addresses, password)
mc := prepareMobileConfig(hostname, imapPort, smtpPort, imapSSL, smtpSSL, username, displayName, addresses, password)
confPath, err := saveConfigTemporarily(mc)
if err != nil {
@ -65,24 +66,28 @@ func prepareMobileConfig(
hostname string,
imapPort, smtpPort int,
imapSSL, smtpSSL bool,
username, addresses, password string,
username, displayName, addresses string,
password []byte,
) *mobileconfig.Config {
return &mobileconfig.Config{
DisplayName: username,
EmailAddress: addresses,
AccountName: displayName,
AccountDescription: username,
Identifier: "protonmail " + username + strconv.FormatInt(time.Now().Unix(), 10),
IMAP: &mobileconfig.IMAP{
Hostname: hostname,
Port: imapPort,
TLS: imapSSL,
Username: username,
Password: password,
Password: string(password),
},
SMTP: &mobileconfig.SMTP{
Hostname: hostname,
Port: smtpPort,
TLS: smtpSSL,
Username: username,
Password: string(password),
},
}
}
@ -95,6 +100,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

@ -1,70 +0,0 @@
// Copyright (c) 2022 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 cache provides access to contents inside a cache directory.
package cache
import (
"os"
"path/filepath"
"github.com/ProtonMail/proton-bridge/v2/pkg/files"
)
type Cache struct {
dir, version string
}
func New(dir, version string) (*Cache, error) {
if err := os.MkdirAll(filepath.Join(dir, version), 0o700); err != nil {
return nil, err
}
return &Cache{
dir: dir,
version: version,
}, nil
}
// GetDBDir returns folder for db files.
func (c *Cache) GetDBDir() string {
return c.getCurrentCacheDir()
}
// GetDefaultMessageCacheDir returns folder for cached messages files.
func (c *Cache) GetDefaultMessageCacheDir() string {
return filepath.Join(c.getCurrentCacheDir(), "messages")
}
// GetIMAPCachePath returns path to file with IMAP status.
func (c *Cache) GetIMAPCachePath() string {
return filepath.Join(c.getCurrentCacheDir(), "user_info.json")
}
// GetTransferDir returns folder for import-export rules files.
func (c *Cache) GetTransferDir() string {
return c.getCurrentCacheDir()
}
// RemoveOldVersions removes any cache dirs that are not the current version.
func (c *Cache) RemoveOldVersions() error {
return files.Remove(c.dir).Except(c.getCurrentCacheDir()).Do()
}
func (c *Cache) getCurrentCacheDir() string {
return filepath.Join(c.dir, c.version)
}

View File

@ -1,69 +0,0 @@
// Copyright (c) 2022 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 cache
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRemoveOldVersions(t *testing.T) {
dir, err := os.MkdirTemp("", "test-cache")
require.NoError(t, err)
cache, err := New(dir, "c4")
require.NoError(t, err)
createFilesInDir(t, dir,
"unexpected1.txt",
"c1/unexpected1.txt",
"c2/unexpected2.txt",
"c3/unexpected3.txt",
"something.txt",
)
require.DirExists(t, filepath.Join(dir, "c4"))
require.FileExists(t, filepath.Join(dir, "unexpected1.txt"))
require.FileExists(t, filepath.Join(dir, "c1", "unexpected1.txt"))
require.FileExists(t, filepath.Join(dir, "c2", "unexpected2.txt"))
require.FileExists(t, filepath.Join(dir, "c3", "unexpected3.txt"))
require.FileExists(t, filepath.Join(dir, "something.txt"))
assert.NoError(t, cache.RemoveOldVersions())
assert.DirExists(t, filepath.Join(dir, "c4"))
assert.NoFileExists(t, filepath.Join(dir, "unexpected1.txt"))
assert.NoFileExists(t, filepath.Join(dir, "c1", "unexpected1.txt"))
assert.NoFileExists(t, filepath.Join(dir, "c2", "unexpected2.txt"))
assert.NoFileExists(t, filepath.Join(dir, "c3", "unexpected3.txt"))
assert.NoFileExists(t, filepath.Join(dir, "something.txt"))
}
func createFilesInDir(t *testing.T, dir string, files ...string) {
for _, target := range files {
require.NoError(t, os.MkdirAll(filepath.Dir(filepath.Join(dir, target)), 0o700))
f, err := os.Create(filepath.Join(dir, target))
require.NoError(t, err)
require.NoError(t, f.Close())
}
}

View File

@ -1,151 +0,0 @@
// Copyright (c) 2022 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 settings
import (
"encoding/json"
"errors"
"fmt"
"os"
"strconv"
"sync"
"github.com/sirupsen/logrus"
)
type keyValueStore struct {
vals map[Key]string
path string
lock *sync.RWMutex
}
// newKeyValueStore returns loaded preferences.
func newKeyValueStore(path string) *keyValueStore {
p := &keyValueStore{
path: path,
lock: &sync.RWMutex{},
}
if err := p.load(); err != nil {
logrus.WithError(err).Warn("Cannot load preferences file, creating new one")
}
return p
}
func (p *keyValueStore) load() error {
if p.vals != nil {
return nil
}
p.lock.Lock()
defer p.lock.Unlock()
p.vals = make(map[Key]string)
f, err := os.Open(p.path)
if err != nil {
return err
}
defer f.Close() //nolint:errcheck,gosec
return json.NewDecoder(f).Decode(&p.vals)
}
func (p *keyValueStore) save() error {
if p.vals == nil {
return errors.New("cannot save preferences: cache is nil")
}
p.lock.Lock()
defer p.lock.Unlock()
b, err := json.MarshalIndent(p.vals, "", "\t")
if err != nil {
return err
}
return os.WriteFile(p.path, b, 0o600)
}
func (p *keyValueStore) setDefault(key Key, value string) {
if p.Get(key) == "" {
p.Set(key, value)
}
}
func (p *keyValueStore) Get(key Key) string {
p.lock.RLock()
defer p.lock.RUnlock()
return p.vals[key]
}
func (p *keyValueStore) GetBool(key Key) bool {
return p.Get(key) == "true"
}
func (p *keyValueStore) GetInt(key Key) int {
if p.Get(key) == "" {
return 0
}
value, err := strconv.Atoi(p.Get(key))
if err != nil {
logrus.WithError(err).Error("Cannot parse int")
}
return value
}
func (p *keyValueStore) GetFloat64(key Key) float64 {
if p.Get(key) == "" {
return 0
}
value, err := strconv.ParseFloat(p.Get(key), 64)
if err != nil {
logrus.WithError(err).Error("Cannot parse float64")
}
return value
}
func (p *keyValueStore) Set(key Key, value string) {
p.lock.Lock()
p.vals[key] = value
p.lock.Unlock()
if err := p.save(); err != nil {
logrus.WithError(err).Warn("Cannot save preferences")
}
}
func (p *keyValueStore) SetBool(key Key, value bool) {
if value {
p.Set(key, "true")
} else {
p.Set(key, "false")
}
}
func (p *keyValueStore) SetInt(key Key, value int) {
p.Set(key, strconv.Itoa(value))
}
func (p *keyValueStore) SetFloat64(key Key, value float64) {
p.Set(key, fmt.Sprintf("%v", value))
}

View File

@ -1,141 +0,0 @@
// Copyright (c) 2022 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 settings
import (
"os"
"testing"
"github.com/stretchr/testify/require"
)
func TestLoadNoKeyValueStore(t *testing.T) {
r := require.New(t)
pref, clean := newTestEmptyKeyValueStore(r)
defer clean()
r.Equal("", pref.Get("key"))
}
func TestLoadBadKeyValueStore(t *testing.T) {
r := require.New(t)
path, clean := newTmpFile(r)
defer clean()
r.NoError(os.WriteFile(path, []byte("{\"key\":\"MISSING_QUOTES"), 0o700))
pref := newKeyValueStore(path)
r.Equal("", pref.Get("key"))
}
func TestKeyValueStor(t *testing.T) {
r := require.New(t)
pref, clean := newTestKeyValueStore(r)
defer clean()
r.Equal("value", pref.Get("str"))
r.Equal("42", pref.Get("int"))
r.Equal("true", pref.Get("bool"))
r.Equal("t", pref.Get("falseBool"))
}
func TestKeyValueStoreGetInt(t *testing.T) {
r := require.New(t)
pref, clean := newTestKeyValueStore(r)
defer clean()
r.Equal(0, pref.GetInt("str"))
r.Equal(42, pref.GetInt("int"))
r.Equal(0, pref.GetInt("bool"))
r.Equal(0, pref.GetInt("falseBool"))
}
func TestKeyValueStoreGetBool(t *testing.T) {
r := require.New(t)
pref, clean := newTestKeyValueStore(r)
defer clean()
r.Equal(false, pref.GetBool("str"))
r.Equal(false, pref.GetBool("int"))
r.Equal(true, pref.GetBool("bool"))
r.Equal(false, pref.GetBool("falseBool"))
}
func TestKeyValueStoreSetDefault(t *testing.T) {
r := require.New(t)
pref, clean := newTestEmptyKeyValueStore(r)
defer clean()
pref.setDefault("key", "value")
pref.setDefault("key", "othervalue")
r.Equal("value", pref.Get("key"))
}
func TestKeyValueStoreSet(t *testing.T) {
r := require.New(t)
pref, clean := newTestEmptyKeyValueStore(r)
defer clean()
pref.Set("str", "value")
checkSavedKeyValueStore(r, pref.path, "{\n\t\"str\": \"value\"\n}")
}
func TestKeyValueStoreSetInt(t *testing.T) {
r := require.New(t)
pref, clean := newTestEmptyKeyValueStore(r)
defer clean()
pref.SetInt("int", 42)
checkSavedKeyValueStore(r, pref.path, "{\n\t\"int\": \"42\"\n}")
}
func TestKeyValueStoreSetBool(t *testing.T) {
r := require.New(t)
pref, clean := newTestEmptyKeyValueStore(r)
defer clean()
pref.SetBool("trueBool", true)
pref.SetBool("falseBool", false)
checkSavedKeyValueStore(r, pref.path, "{\n\t\"falseBool\": \"false\",\n\t\"trueBool\": \"true\"\n}")
}
func newTmpFile(r *require.Assertions) (path string, clean func()) {
tmpfile, err := os.CreateTemp("", "pref.*.json")
r.NoError(err)
defer r.NoError(tmpfile.Close())
return tmpfile.Name(), func() {
r.NoError(os.Remove(tmpfile.Name()))
}
}
func newTestEmptyKeyValueStore(r *require.Assertions) (*keyValueStore, func()) {
path, clean := newTmpFile(r)
return newKeyValueStore(path), clean
}
func newTestKeyValueStore(r *require.Assertions) (*keyValueStore, func()) {
path, clean := newTmpFile(r)
r.NoError(os.WriteFile(path, []byte("{\"str\":\"value\",\"int\":\"42\",\"bool\":\"true\",\"falseBool\":\"t\"}"), 0o700))
return newKeyValueStore(path), clean
}
func checkSavedKeyValueStore(r *require.Assertions, path, expected string) {
data, err := os.ReadFile(path)
r.NoError(err)
r.Equal(expected, string(data))
}

View File

@ -1,116 +0,0 @@
// Copyright (c) 2022 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 settings provides access to persistent user settings.
package settings
import (
"fmt"
"math/rand"
"path/filepath"
"time"
)
type Key string
// Keys of preferences in JSON file.
const (
FirstStartKey Key = "first_time_start"
FirstStartGUIKey Key = "first_time_start_gui"
LastHeartbeatKey Key = "last_heartbeat"
APIPortKey Key = "user_port_api"
IMAPPortKey Key = "user_port_imap"
SMTPPortKey Key = "user_port_smtp"
SMTPSSLKey Key = "user_ssl_smtp"
AllowProxyKey Key = "allow_proxy"
AutostartKey Key = "autostart"
AutoUpdateKey Key = "autoupdate"
CookiesKey Key = "cookies"
LastVersionKey Key = "last_used_version"
UpdateChannelKey Key = "update_channel"
RolloutKey Key = "rollout"
PreferredKeychainKey Key = "preferred_keychain"
CacheEnabledKey Key = "cache_enabled"
CacheCompressionKey Key = "cache_compression"
CacheLocationKey Key = "cache_location"
CacheMinFreeAbsKey Key = "cache_min_free_abs"
CacheMinFreeRatKey Key = "cache_min_free_rat"
CacheConcurrencyRead Key = "cache_concurrent_read"
CacheConcurrencyWrite Key = "cache_concurrent_write"
IMAPWorkers Key = "imap_workers"
FetchWorkers Key = "fetch_workers"
AttachmentWorkers Key = "attachment_workers"
ColorScheme Key = "color_scheme"
RebrandingMigrationKey Key = "rebranding_migrated"
IsAllMailVisible Key = "is_all_mail_visible"
)
type Settings struct {
*keyValueStore
settingsPath string
}
func New(settingsPath string) *Settings {
s := &Settings{
keyValueStore: newKeyValueStore(filepath.Join(settingsPath, "prefs.json")),
settingsPath: settingsPath,
}
s.setDefaultValues()
return s
}
const (
DefaultIMAPPort = "1143"
DefaultSMTPPort = "1025"
DefaultAPIPort = "1042"
)
func (s *Settings) setDefaultValues() {
s.setDefault(FirstStartKey, "true")
s.setDefault(FirstStartGUIKey, "true")
s.setDefault(LastHeartbeatKey, fmt.Sprintf("%v", time.Now().YearDay()))
s.setDefault(AllowProxyKey, "true")
s.setDefault(AutostartKey, "true")
s.setDefault(AutoUpdateKey, "true")
s.setDefault(LastVersionKey, "")
s.setDefault(UpdateChannelKey, "")
s.setDefault(RolloutKey, fmt.Sprintf("%v", rand.Float64())) //nolint:gosec // G404 It is OK to use weak random number generator here
s.setDefault(PreferredKeychainKey, "")
s.setDefault(CacheEnabledKey, "true")
s.setDefault(CacheCompressionKey, "true")
s.setDefault(CacheLocationKey, "")
s.setDefault(CacheMinFreeAbsKey, "250000000")
s.setDefault(CacheMinFreeRatKey, "")
s.setDefault(CacheConcurrencyRead, "16")
s.setDefault(CacheConcurrencyWrite, "16")
s.setDefault(IMAPWorkers, "16")
s.setDefault(FetchWorkers, "16")
s.setDefault(AttachmentWorkers, "16")
s.setDefault(ColorScheme, "")
s.setDefault(APIPortKey, DefaultAPIPort)
s.setDefault(IMAPPortKey, DefaultIMAPPort)
s.setDefault(SMTPPortKey, DefaultSMTPPort)
// By default, stick to STARTTLS. If the user uses catalina+applemail they'll have to change to SSL.
s.setDefault(SMTPSSLKey, "false")
s.setDefault(IsAllMailVisible, "true")
}

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