Compare commits

...

257 Commits

Author SHA1 Message Date
b7ef6e1486 chore: Umshiang Bridge 3.5.1 changelog. 2023-09-27 13:18:23 +02:00
0d03f84711 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 12:34:07 +02:00
949666724d chore: Umshiang Bridge 3.5.1 changelog. 2023-09-27 10:54:50 +02:00
bbe19bf960 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 14:06:31 +02:00
bfe25e3a46 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 13:58:46 +02:00
236c958703 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 13:58:18 +02:00
e6b312b437 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 13:58:07 +02:00
45d2e9ea63 chore: update changelog. 2023-09-13 10:25:47 +02:00
86e8a566c7 chore: Umshiang Bridge 3.5.0 changelog. 2023-09-12 07:45:08 +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
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
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
eaa673c4e4 fix(GODT-2708): fix dimensions event format + handling of ReportClicked event. 2023-07-04 13:04:30 +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
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
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
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
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
4e080b59d3 feat(GODT-2750): disable raise on main window when a notification is clicked on Linux. 2023-06-30 10:05:07 +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
650158ea8a feat(GODT-2749): manual windows-test. 2023-06-29 11:37:57 +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
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
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
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
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
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
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
fc06665d2b feat(GODT-2655): display internal build time tag in log and GUI. 2023-06-01 05:24:51 +00: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
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
afef730870 chore: notes 2023-05-15 17:16:44 +02:00
f691795ca1 doc: Add 3.2.0 release notes 2023-05-15 11:26:03 +00: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
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
c1918e2b1b doc: release notes 3.1.1 2023-04-11 13:21:53 +02:00
60a779c653 doc: Added 3.1.0 2023-04-06 06:50:15 +00: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
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
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
9c1b9e8df2 doc: Add 3.0.18 and remove non-beta 3.0.17 2023-02-24 08:36:02 +00:00
fb44de4f18 doc: Add 3.0.18 release notes 2023-02-23 16:16:26 +00: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
ec2a4f9111 doc: release notes v3 live 2023-02-20 05:59:57 +01:00
968a01053f chore: release notes 2023-02-17 12:02:44 +01:00
6f914a4973 chore: early 3.0.15 release notes 2023-02-14 12:24:50 +01:00
76bdc21fef doc: Add 3.0.14 notes 2023-02-09 09:56:45 +00: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
314 changed files with 27826 additions and 60013 deletions

4
.gitignore vendored
View File

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

View File

@ -30,13 +30,6 @@ stages:
- test
- build
.rules-branch-and-MR-always:
rules:
- if: $CI_COMMIT_BRANCH || $CI_PIPELINE_SOURCE == "merge_request_event"
when: always
allow_failure: false
- when: never
.rules-branch-and-MR-manual:
rules:
- if: $CI_COMMIT_BRANCH || $CI_PIPELINE_SOURCE == "merge_request_event"
@ -44,16 +37,6 @@ stages:
allow_failure: true
- when: never
.rules-branch-manual-MR-always:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
when: always
allow_failure: false
- if: $CI_COMMIT_BRANCH
when: manual
allow_failure: true
- when: never
.rules-branch-manual-MR-and-devel-always:
rules:
- if: $CI_COMMIT_BRANCH == "devel" || $CI_PIPELINE_SOURCE == "merge_request_event"
@ -64,173 +47,23 @@ stages:
allow_failure: true
- when: never
.after-script-code-coverage:
after_script:
- go get github.com/boumenot/gocover-cobertura
- go run github.com/boumenot/gocover-cobertura < /tmp/coverage.out > coverage.xml
- "go tool cover -func=/tmp/coverage.out | grep total:"
coverage: '/total:.*\(statements\).*\d+\.\d+%/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
# Stage: TEST
lint:
stage: test
extends:
- .rules-branch-and-MR-always
script:
- make lint
tags:
- medium
.test-base:
stage: test
script:
- make test
test-linux:
extends:
- .test-base
- .rules-branch-manual-MR-and-devel-always
- .after-script-code-coverage
tags:
- large
test-linux-race:
extends:
- test-linux
- .rules-branch-and-MR-manual
script:
- make test-race
test-integration:
extends:
- test-linux
script:
- make test-integration
tags:
- large
test-integration-race:
extends:
- test-integration
- .rules-branch-and-MR-manual
script:
- make test-integration-race
.windows-base:
before_script:
- 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=
tags:
- windows-bridge
#test-windows:
# extends:
# - .rules-branch-manual-MR-always
# - .windows-base
# stage: test
# script:
# - make test
# 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
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
.linux-build-setup:
image: gitlab.protontech.ch:4567/go/bridge-internal:build-go1.20-qt6.3.2
variables:
VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.cache
cache:
key: linux-vcpkg
paths:
- .cache
when: 'always'
before_script:
- mkdir -p .cache/bin
- export PATH=$(pwd)/.cache/bin:$PATH
- export GOPATH="$CI_PROJECT_DIR/.cache"
- export PATH=$PATH:$QT6DIR/bin
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
tags:
- large
build-linux:
extends:
- .build-base
- .linux-build-setup
build-linux-qa:
extends:
- build-linux
variables:
BUILD_TAGS: "build_qa"
.darwin-build-setup:
before_script:
- export PATH=/usr/local/bin:$PATH
- export PATH=/usr/local/opt/git/bin:$PATH
- export PATH=/usr/local/opt/make/libexec/gnubin:$PATH
- export PATH=/usr/local/opt/go@1.13/bin:$PATH
- export PATH=/usr/local/opt/gnu-sed/libexec/gnubin:$PATH
- export GOPATH=~/go1.20
- export PATH=$GOPATH/bin:$PATH
- export CGO_CPPFLAGS='-Wno-error -Wno-nullability-completeness -Wno-expansion-to-defined -Wno-builtin-requires-header'
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
cache: {}
tags:
- macOS
build-darwin:
extends:
- .build-base
- .darwin-build-setup
build-darwin-qa:
extends:
- build-darwin
variables:
BUILD_TAGS: "build_qa"
.windows-build-setup:
# 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
@ -249,15 +82,204 @@ build-darwin-qa:
tags:
- windows-bridge
#build-windows:
# extends:
# - .build-base
# - .windows-build-setup
#
##build-windows-qa:
# extends:
# - build-windows
# variables:
# BUILD_TAGS: "build_qa"
#
.env-darwin:
before_script:
- export BRIDGE_SYNC_FORCE_MINIMUM_SPEC=1
- export PATH=/usr/local/bin:$PATH
- export PATH=/usr/local/opt/git/bin:$PATH
- export PATH=/usr/local/opt/make/libexec/gnubin:$PATH
- export PATH=/usr/local/opt/gnu-sed/libexec/gnubin:$PATH
- export GOROOT=~/local/opt/go@1.20
- export PATH="${GOROOT}/bin:$PATH"
- export GOPATH=~/go1.20
- export PATH="${GOPATH}/bin:$PATH"
- export QT6DIR=/opt/Qt/6.3.2/macos
- export PATH="${QT6DIR}/bin:$PATH"
- uname -a
cache: {}
tags:
- macos-m1-bridge
.env-linux-build:
image: gitlab.protontech.ch:4567/go/bridge-internal:build-go1.20-qt6.3.2
variables:
VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.cache
cache:
key: linux-vcpkg
paths:
- .cache
when: 'always'
before_script:
- mkdir -p .cache/bin
- export BRIDGE_SYNC_FORCE_MINIMUM_SPEC=1
- export PATH=$(pwd)/.cache/bin:$PATH
- export GOPATH="$CI_PROJECT_DIR/.cache"
- export PATH=$PATH:$QT6DIR/bin
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
tags:
- large
# Stage: TEST
lint:
stage: test
extends:
- .rules-branch-manual-MR-and-devel-always
script:
- make lint
tags:
- medium
bug-report-preview:
stage: test
extends:
- .rules-branch-and-MR-manual
script:
- make lint-bug-report-preview
tags:
- medium
.script-test:
stage: test
extends:
- .rules-branch-manual-MR-and-devel-always
script:
- make test
artifacts:
paths:
- coverage/**
test-linux:
extends:
- .script-test
tags:
- large
fuzz-linux:
stage: test
extends:
- .rules-branch-manual-MR-and-devel-always
script:
- make fuzz
tags:
- large
test-linux-race:
extends:
- test-linux
- .rules-branch-and-MR-manual
script:
- make test-race
test-integration:
extends:
- test-linux
script:
- make test-integration
test-integration-race:
extends:
- test-integration
- .rules-branch-and-MR-manual
script:
- make test-integration-race
test-integration-nightly:
extends:
- test-integration
- .rules-branch-manual-scheduled-and-test-branch-always
needs:
- test-integration
script:
- make test-integration-nightly
test-windows:
extends:
- .env-windows
- .script-test
test-darwin:
extends:
- .env-darwin
- .script-test
test-coverage:
stage: test
extends:
- .rules-branch-manual-scheduled-and-test-branch-always
script:
- ./utils/coverage.sh
coverage: '/total:.*\(statements\).*\d+\.\d+%/'
needs:
- test-linux
- test-windows
- test-darwin
- test-integration
- test-integration-nightly
tags:
- small
artifacts:
paths:
- coverage*
- coverage/**
when: 'always'
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
# Stage: BUILD
.script-build:
stage: build
needs: ["lint"]
extends:
- .rules-branch-and-MR-manual
script:
- make build
- git diff && git diff-index --quiet HEAD
- make vault-editor
artifacts:
expire_in: 1 day
when: always
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
paths:
- bridge_*.tgz
- vault-editor
build-linux:
extends:
- .script-build
- .env-linux-build
build-linux-qa:
extends:
- build-linux
- .rules-branch-manual-MR-and-devel-always
variables:
BUILD_TAGS: "build_qa"
build-darwin:
extends:
- .script-build
- .env-darwin
build-darwin-qa:
extends:
- build-darwin
variables:
BUILD_TAGS: "build_qa"
build-windows:
extends:
- .script-build
- .env-windows
build-windows-qa:
extends:
- build-windows
variables:
BUILD_TAGS: "build_qa"
# TODO: PUT BACK ALL THE JOBS! JUST DID THIS FOR NOW TO GET CI WORKING AGAIN...

View File

@ -36,6 +36,14 @@ issues:
- gosec
- goconst
- dogsled
- path: utils/smtp-send
linters:
- dupl
- gochecknoglobals
- gochecknoinits
- gosec
- goconst
- dogsled
linters-settings:
godox:

View File

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

View File

@ -3,9 +3,150 @@
Changelog [format](http://keepachangelog.com/en/1.0.0/)
## Stone Bridge 3.3.1
## Umshiang Bridge 3.5.1
### Fixed
* GODT-2963: Use multi error to report file removal errors.
* GODT-2956: Restore old deletion rules.
* GODT-2951: Negative WaitGroup Counter.
* GODT-2590: Fix send on closed channel.
* GODT-2949: Fix close of close channel in event service.
## Umshiang Bridge 3.5.0
### Added
* GODT-2734: Add testing steps to modify account settings.
* GODT-2746: Integration tests for reporting a problem.
* GODT-2891: Allow message create & delete during sync.
* GODT-2848: Decouple IMAP service from Event Loop.
* Add trace profiling option.
* GODT-2829: New Sync Service.
* Test: oss-fuzz support for fuzzing.
* GODT-2799: SMTP Service.
* GODT-2800: User Event Service.
* GODT-2801: Identity Service.
* GODT-2802: IMAP Serivce.
* GODT-2788: Add preview to bug report validation and JSON file validator.
* GODT-2803: Bridge Database access.
### Changed
* GODT-2909: Remove Timeout on event publish.
* GODT-2913: Reduce the number of configuration failure detected.
* GODT-2828: Increase sync progress report frequency.
* Test: Fix TestBridge_SyncWithOnGoingEvents.
* GODT-2871: Is telemetry enabled as service.
* Test(GODT-2873): Wait for Gluon Watcher to finish.
* Test(GODT-2744): Add integration tests for moving messages (with MOVE support).
* Test(GODT-2872): Fix nightly job.
* Test(GODT-2742): Add more integration tests regarding drafts.
* GODT-2787: Force Scrollview to top when re-opening questions set.
* GODT-2787: Tweaking Bug Report form with last Review.
* Ci(GODT-2717): Create a job that will run on schedule.
* GODT-2787: Fix vertical alignement on CategoryItem.
* GODT-2842: Implement Bug Report Fallback notification.
* Chore(GODT-2848): Simplify User Event Service.
* GODT-2808: Apply comment from Bug Report content review.
* Test(GODT-2743): Sync high number of messages.
* GODT-2814: Standalone Server Manager.
* GODT-2808: Initial list of categories and questions.
* GODT-2787: Replace the PathTracker by a more visual NavigationIndicator.
* GODT-2816: Wait until mandatory fields are filled then fill body and title.
* GODT-2794: Clear cached answers when report is sent.
* GODT-2793: Feed the bug report body with the answered questions.
* GODT-2791: Parse the Bug Report Flow description file and ensure forward compatibility (GODT-2789).
* GODT-2821: Display questions in one page.
* GODT-2786: Init bug report flow description file.
* GODT-2792: Implement display of question set for bug report.
* Use qmlformat on qml files, and removed deprecated tests.
### Fixed
* GODT-2828: Fix negative report time.
* GODT-2828: Fix sync progress report after restart.
* GODT-2867: Do not crash on timeout or context cancel.
* GODT-2693: Duplicate messages in sent folder.
* GODT-2867: Get attachment returns API error on network problem.
* GODT-2805: Ignore Contact Group Labels.
* GODT-2866: Add 429/5xx Retry to Event Service.
* GODT-2855: Fix for text overlapping in settings view.
* Test: Verify leaks at end of WithEnv.
* Test: Fix event registration in TestBridge_SyncWithOngoingEvents.
* Test: Fix deadlock in chToType.
* GODT-2865: Add error on failed unlock.
* GODT-2857: Do not check changed values in clear recent flag.
* GODT-2827: Restore ticker to event poller.
* Test: TestBridge_SendAddTextBodyPartIfNotExists eventually fix.
* GODT-2813: Write new vault to temporary file first.
* GODT-2807: Fix issue where sessionID would not be removed from command-line on restart by bridge-gui.
* GODT-2687: Tabs after header field colon.
* GODT-2764: Allow perma-delete for messages which still have labels.
* GODT-2693: Fix message appearing twice after sent.
* GODT-2781: Try to remove stale lock file before failing in checkSingleInstance.
* GODT-2780: Fix 'QSystemTrayIcon::setVisible: No Icon set' warning in bridge-gui log on startup.
* GODT-2778: Fix login screen being disabled after an 'already logged in' error.
* Fix typos found by codespell.
* GODT-2577: Answered flag should only be applied to replied messages.
## Trift Bridge 3.4.1
### Fixed
* GODT-2859: Trigger user resync while updating from 3.4.0 to 3.4.1.
* GODT-2833: Fix migration of message flags.
* GODT-2759: Use examine rather than select for fetching.
## Trift Bridge 3.4.0
### Added
### Changed
* Test: Add require.Eventually to TestBridge_UserAgentFromSMTPClient.
* Test: Add smtp-send utility.
* GODT-2759: Check for oprhan messages.
* GODT-2759: Add prompt to download missing messages for analysis.
* GODT-2759: CLI debug commands.
* Remove gRPC auto-generated C++ source files.
* Test: Force all unit test to use minimum sync spec.
* Test: Force sync limits to minimum with env variable.
* GODT-2691: Close logrus output file on exit.
* GODT-2522: New Gluon database layout.
* GODT-2678: When internet is off, do not display status dot icon for the user in the context menu.
* GODT-2686: Change the orientation of the expand/collapse arrow for Advanced settings.
* Test(GODT-2636): Add step for sending from EML.
* Log failed message ids during sync.
* GODT-2510: Remove Ent.
* Test(GODT-2600): Changing state (read/unread, starred/unstarred) of a message in integration tests.
* GODT-2703: Got rid of account details dialog with Apple Mail autoconf.
* GODT-2685: Update to bug report log attachment logic.
* GODT-2690: Update sentry reporting in GUI for new log file naming.
* GODT-2668: Implemented new log retention policy.
* Test(GODT-2683): Save Draft without "Date" & "From" in headers.
* GODT-2666: Feat(GODT-2667): introduce sessionID in bridge.
* GODT-2660: Calculate bridge coverage and refactor CI yaml file.
* Fix dependency_license script to handle dot formated version.
### Fixed
* GODT-2812: Fix rare sync deadlock.
* GODT-2822: Better handling 429 during sync and event loop.
* GODT-2763: Missing Answered flag on Sync and Message Create.
* GODT-2758: Fix panic in SetFlagsOnMessages.
* GODT-2578: Refresh literals appended to Sent folder.
* GODT-2753: Vault test now check that value auto-assigned is first available port.
* GODT-2522: Handle migration with unreferenced db values.
* GODT-2670: Allow missing whitespace after header field colon.
* GODT-2653: Only log when err is not nil.
* GODT-2680: Fix for C++ debugger not working on ARM64 because of OpenSSL 3.1.
* GODT-2675: Update GPA to applye togin-gonic/gin patch + update COPYING_NOTES.
## Stone Bridge 3.3.2
### Fixed
* GODT-2782: Filter all labels when doing perma delete check.
## Stone Bridge 3.3.1
### Changed
* GODT-2707: Set bridge-gui default log level to 'debug'.
@ -78,7 +219,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* GODT-2437: Silence harmless report to sentry.
* GODT-2649: Clean up cache files after failed connector create (Gluon).
* GODT-2638: Validate messages before import.
* GODT-2646: Bump GPA and Gluon dependecy after CIRCL upgrade.
* GODT-2646: Bump GPA and Gluon dependency after CIRCL upgrade.
* GODT-2454: Only Send status update if transaction succeeded.
* Test: fix flaky tests.
* GODT-2628: Attempt to fix closed channel panic on logout.
@ -138,7 +279,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* GODT-2574: Fix label/unlabel of large amounts of messages.
* GODT-2573: Handle invalid header fields in message.
* GODT-2573: Crash on null update.
* GODT-2407: Replace invalid email addresses with emtpy for new Drafts.
* GODT-2407: Replace invalid email addresses with empty for new Drafts.
## [Bridge 3.1.3] Quebec
@ -279,7 +420,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* GODT-2429: Do not report context cancel to sentry.
### Fixed
* GODT-2467: elide long email adresses in 'bad event' QML notification dialog.
* GODT-2467: elide long email addresses in 'bad event' QML notification dialog.
* GODT-2449: fix bug in Bridge-GUI's Exception::what().
* GODT-2427: Parsing header issues.
* GODT-2426: Fix crash on user delete.
@ -296,7 +437,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* GODT-2404: Handle unexpected EOF.
* GODT-2400: Allow state updates to be applied if command fails.
* GODT-2399: Fix immediate message deletion during updates.
* GODT-2390: Missing changes from pervious commit.
* GODT-2390: Missing changes from previous commit.
* GODT-2390: Add reports for uncaught json and net.opErr.
* GODT-2414: Multiple deletion bug in WriteControlledStore.
@ -361,7 +502,7 @@ GODT-1804: Preserve MIME parameters when uploading attachments.
* GODT-2223: Improve event handling.
* GODT-2305: Detect missing gluon DB.
* GODT-2291: Change gluon store default location from Cache to Data.
* Other: Disable dialer test until badssl cert is bumbed.
* Other: Disable dialer test until badssl cert is bumped.
* GODT-2292: Updated BUILDS.md doc.
* GODT-2258: suggest email as login when signing in via status window.
* Other: Report corrupt and/or insecure vaults to sentry.
@ -641,7 +782,7 @@ GODT-1804: Preserve MIME parameters when uploading attachments.
## [Bridge 2.4.6] Osney
### Changed
* GODT-2019: When signing out and a single user is connecte* we do not go back to the welcome screen.
* GODT-2019: When signing out and a single user is connected we do not go back to the welcome screen.
* GODT-2071: Bridge-gui report error if an orphan bridge is detected.
* GODT-2046: Bridge-gui log is included in optional archive sent with bug reports.
* GODT-2039: Bridge monitors bridge-gui via its PID.
@ -795,7 +936,7 @@ GODT-1804: Preserve MIME parameters when uploading attachments.
* GODT-1260: Renaming.
* GODT-1502: Rebranding: color and radius.
* GODT-1549: Add notification when address list changes.
* GODT-1560: Dependecy licenses update and link.
* GODT-1560: Dependency licenses update and link.
### Changed
* GODT-1543: Using one buffered event for off and on connection.
@ -892,7 +1033,7 @@ GODT-1537: Manual in-app update mechanism.
* GODT-1338: GODT-1343 Help view buttons.
* GODT-1340: Not crashing, user list updating in main thread.
* GODT-1345: Adding panic handlers.
* GODT-1271: Fix Status margings.
* GODT-1271: Fix Status margins.
* GODT-1320: Add loading property to each action within a notification.
* GODT-1210: Add "free user" banner.
* GODT-1314: Limit description field length within 150/800 bounds.
@ -934,7 +1075,7 @@ GODT-1537: Manual in-app update mechanism.
* GODT-1381 Treat readonly folder as failure for cache on disk.
* GODT-1431 Prevent watcher when not using disk on cache.
* GODT-1381: Use in-memory cache in case local cache is unavailable.
* GODT-1356 GODT-1302: Cache on disk concurency and API retries.
* GODT-1356 GODT-1302: Cache on disk concurrency and API retries.
* GODT-1332 Added tests for cache move functions.
* GODT-1332: moved cache related functions to separate file.
* GODT-1332 moving cache does not work on Windows.
@ -1185,7 +1326,7 @@ GODT-1537: Manual in-app update mechanism.
### Fixed
* GODT-1029 Fix tray icon not updating under certain conditions.
* GODT-1062 Fix lost notification bar when window is closed.
* GODT-1058 Install version after chaning channel right away only in case of downgrade.
* GODT-1058 Install version after changing channel right away only in case of downgrade.
* GODT-1073 Re-write autostart link on every start if turned on in preferences.
* GODT-1055 Fix flaky empty trash test.
@ -1275,7 +1416,7 @@ GODT-1537: Manual in-app update mechanism.
* GODT-820 Added GUI notification on impossibility of update installation (both silent and manual).
* GODT-870 Added GUI notification on error during silent update.
* GODT-805 Added GUI notification on update available.
* GODT-804 Added GUI notification on silent update installed (promt to restart).
* GODT-804 Added GUI notification on silent update installed (prompt to restart).
* GODT-275 Added option to disable autoupdates in settings (default autoupdate is enabled).
* GODT-874 Added manual triggers to Updater module.
* GODT-851 Added support of UID EXPUNGE.
@ -1599,7 +1740,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
### Changed
* GODT-360 Detect charset embedded in html/xml.
* GODT-354 Do not label/unlabel messsages from `All Mail` folder.
* GODT-354 Do not label/unlabel messages from `All Mail` folder.
* GODT-388 Support for both bridge and import/export credentials by package users.
* GODT-387 Store factory to make store optional.
* GODT-386 Renamed bridge to general users and keep bridge only for bridge stuff.
@ -1764,13 +1905,13 @@ CSB-331 Fix sending error due to mixed case in sender address.
* GODT-88 Run mbox sync in parallel when switch password mode (re-init not user).
* GODT-95 Do not throw error when trying to create new mailbox in IMAP root.
* GODT-75 Do not fail on unlabel inside delete.
* #1095 always delete IMAP USER including wrong pasword.
* #1095 always delete IMAP USER including wrong password.
* Unique pmapi client userID (including #1098).
* Using go.enmime@v0.6.1 snapshot.
* Better detection of non-auth-error.
* Reset `hasAuthChannel` during logout for proper login functionality (set up auth channel and unlock keys).
* Allow `APPEND` messages without parsable email address in sender field.
* #1060 avoid `Append` after internal message ID was found and message was copyed to mailbox using `MessageLabel`.
* #1060 avoid `Append` after internal message ID was found and message was copied to mailbox using `MessageLabel`.
* #1049 Basic usage of store in SMTP package to poll event loop during sending message.
* #1050 pollNow waits for events to be processed.
* #1047 Fix fetch of empty mailbox.
@ -1896,7 +2037,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
* #903 added http.Client timeout to not hang out forever.
* Closing body after checking internet connection.
* Pedantic lint for bridgeUtils.
* Selected events are buffered and emited again when frontend loop is ready.
* Selected events are buffered and emitted again when frontend loop is ready.
* #890 implemented 2FA endpoint (auth split).
* #888 TLS Cert.
* Error bar and modal with explanation in GUI.
@ -1904,7 +2045,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
* Add pinning to bridge (only for live API builds).
* #887 #883:
* Wait before clearing data.
* Configer which provides pmapi.ClientConfig and app directories.
* Configure which provides pmapi.ClientConfig and app directories.
* #861 restart after clear data.
* Panic handler for all goroutines.
* CD for linux.
@ -1952,7 +2093,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
* #882 unassign PMAPI client after logout and force to run garbage collector.
* #880, #884, #885, #886 fix of informing user about outgoing non-encrypted e-mail.
* #838 `Sirupsen` -> `sirupsen`.
* #893 save panic report file everytime.
* #893 save panic report file every time.
* #880 fix of informing user about outgoing non-encrypted e-mail.
* Fix aliases in split mode.
* Fix decrypted data in log notification.
@ -2026,7 +2167,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
### Changed
* Fix custom message format.
* #802 acumulated long lines while parsing body structure.
* #802 accumulated long lines while parsing body structure.
* Process `AddressEvent` before `MessageEvent`.
* #791 updated crypto: fix wrong signature format.
* #793 fix returning size.
@ -2048,7 +2189,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
### Changed
* #748 when charset missing assume utf8 and check the validity.
* #750 before sync check that events are uptodate, if not poll events instead of sync.
* #750 before sync check that events are up-to-date, if not poll events instead of sync.
* Use pmapi with support of decrypted access token.
* #750 Status is using DB status instead of API.
* Format panic error as string instead of struct dump.
@ -2065,7 +2206,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
* Full version of program visible on release notes.
### Changed
* #720 only one concurent DB sync.
* #720 only one concurrent DB sync.
* #720 sync every 3 pages.
* #512 extending list of charsets go-pm-mime!4.
@ -2089,7 +2230,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
* Fix srp modulus issue with new `ProtonMail/crypto`.
* Generate version files from main file.
* Be able to set update set on build.
* #597 check on start that certificat will be still valid after one month and generate new cert if not.
* #597 check on start that certificate will be still valid after one month and generate new cert if not.
* #597 extended certificate validity to 2 years.
* Copyright 2019.
* Exclude `protontech` repos from credits.
@ -2108,7 +2249,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
* #592 internal references are added only when not present already.
* #592 field `Date` changed to m.Time only when wrong format or missing `Date`.
* #645 pmapi#26 `Message.Flags` instead of `IsEncrypted`, `Type`, `IsReplied`, `IsRepliedAll`, `IsForwarded`.
* DB: do not allow to put Body or Attachements to db.
* DB: do not allow to put Body or Attachments to db.
* #574 SMTP: can now send more than one email.
* #671 Verbosity levels: `debug` (only bridge), `debug-client` (bridge and client communication), `debug-server` (bridge, whole SMTP/IMAP communication).
* #644 Return rfc.size 0 or correct size of fetched body (stored in DB).
@ -2180,7 +2321,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
* Start with new versioning.
1.1.0
| | `--- bug fix number (internal, irregular, beta relases)
| | `--- bug fix number (internal, irregular, beta releases)
| `----- minor version (features, release once per month, live release, milestones)
`------- major version (big changes, once per year, breaking changes, api force upgrade)
@ -2246,7 +2387,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
* All `client.Do` errors are interpreted as connection issue.
* Moved to internal gitlab.
* Typo `frontend-qml`.
* Better message for case when server is not reacheable.
* Better message for case when server is not reachable.
* Setting 1min timeout to IMAP connection.
### Changed
@ -2278,12 +2419,12 @@ CSB-331 Fix sending error due to mixed case in sender address.
* Keychain format and function refactor.
* Create crash file on panic with full trace.
* Clear old data only in main process (no double keychain typing).
* Create label udpate API route.
* Create label update API route.
* Selectable text in release notes.
### Added
* Support sending to external PGP recipients.
* Return error codes: `0: Ok`, `2: Frontend crashed`, `3: Bridge already running`, `4: Uknown argument`, `42: Restart application`.
* Return error codes: `0: Ok`, `2: Frontend crashed`, `3: Bridge already running`, `4: Unknown argument`, `42: Restart application`.
### Release notes
* Support of encryption to external PGP recipients using contacts created on beta.protonmail.com (see https://protonmail.com/blog/pgp-vulnerability-efail/ to understand the vulnerabilities that may be associated with sending to other PGP clients).
@ -2308,7 +2449,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
* Bug report window.
* Checkbox and with label (only I/E).
* Error dialog and Info tooltip (only I/E).
* Add user modal formating (colors, text).
* Add user modal formatting (colors, text).
* Account view style.
* Input box style (used in bug report).
* Input field style (used in add account and change port).

View File

@ -11,7 +11,7 @@ ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
.PHONY: build build-gui build-nogui build-launcher versioner hasher
# Keep version hardcoded so app build works also without Git repository.
BRIDGE_APP_VERSION?=3.3.1+git
BRIDGE_APP_VERSION?=3.5.1+git
APP_VERSION:=${BRIDGE_APP_VERSION}
APP_FULL_NAME:=Proton Mail Bridge
APP_VENDOR:=Proton AG
@ -229,14 +229,28 @@ add-license:
change-copyright-year:
./utils/missing_license.sh change-year
GOCOVERAGE=-covermode=count -coverpkg=github.com/ProtonMail/proton-bridge/v3/internal/...,github.com/ProtonMail/proton-bridge/v3/pkg/...,
GOCOVERDIR=-args -test.gocoverdir=$$PWD/coverage
test: gofiles
go test -v -timeout=20m -p=1 -count=1 -coverprofile=/tmp/coverage.out -run=${TESTRUN} ./internal/... ./pkg/...
mkdir -p coverage/unit-${GOOS}
go test \
-v -timeout=20m -p=1 -count=1 \
${GOCOVERAGE} \
-run=${TESTRUN} ./internal/... ./pkg/... \
${GOCOVERDIR}/unit-${GOOS}
test-race: gofiles
go test -v -timeout=40m -p=1 -count=1 -race -failfast -run=${TESTRUN} ./internal/... ./pkg/...
test-integration: gofiles
go test -v -timeout=60m -p=1 -count=1 github.com/ProtonMail/proton-bridge/v3/tests
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
@ -244,6 +258,22 @@ test-integration-debug: gofiles
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
go tool pprof -png -output bench_mem.png bench_mem.pprof
@ -260,8 +290,22 @@ mocks:
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
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
@ -277,6 +321,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}

View File

@ -18,6 +18,7 @@
package main
import (
"io"
"os"
"path/filepath"
"runtime"
@ -43,9 +44,10 @@ import (
)
const (
appName = "Proton Mail Launcher"
exeName = "bridge"
guiName = "bridge-gui"
appName = "Proton Mail Launcher"
exeName = "bridge"
guiName = "bridge-gui"
launcherName = "launcher"
FlagCLI = "cli"
FlagCLIShort = "c"
@ -53,6 +55,7 @@ const (
FlagNonInteractiveShort = "n"
FlagLauncher = "--launcher"
FlagWait = "--wait"
FlagSessionID = "--session-id"
)
func main() { //nolint:funlen
@ -75,12 +78,26 @@ func main() { //nolint:funlen
if err != nil {
l.WithError(err).Fatal("Failed to get logs path")
}
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath))
if err := logging.Init(logsPath, os.Getenv("VERBOSITY")); err != nil {
sessionID := logging.NewSessionID()
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath, sessionID, launcherName))
var closer io.Closer
if closer, err = logging.Init(
logsPath,
sessionID,
logging.LauncherShortAppName,
logging.DefaultMaxLogFileSize,
logging.NoPruning,
os.Getenv("VERBOSITY"),
); err != nil {
l.WithError(err).Fatal("Failed to setup logging")
}
defer func() {
_ = logging.Close(closer)
}()
updatesPath, err := locations.ProvideUpdatesPath()
if err != nil {
l.WithError(err).Fatal("Failed to get updates path")
@ -134,7 +151,7 @@ func main() { //nolint:funlen
}
}
cmd := execabs.Command(exe, appendLauncherPath(launcher, args)...) //nolint:gosec
cmd := execabs.Command(exe, appendLauncherPath(launcher, append(args, FlagSessionID, string(sessionID)))...) //nolint:gosec
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout

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

14
go.mod
View File

@ -5,9 +5,9 @@ go 1.20
require (
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
github.com/Masterminds/semver/v3 v3.2.0
github.com/ProtonMail/gluon v0.16.1-0.20230706112359-3146d8312d12
github.com/ProtonMail/gluon v0.17.1-0.20230829112217-5d5c25c504b5
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
github.com/ProtonMail/go-proton-api v0.4.1-0.20230628092916-81cb3f87f184
github.com/ProtonMail/go-proton-api v0.4.1-0.20230831064234-0e3a549b3f36
github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton
github.com/PuerkitoBio/goquery v1.8.1
github.com/abiosoft/ishell v2.0.0+incompatible
@ -51,16 +51,12 @@ require (
)
require (
ariga.io/atlas v0.9.1-0.20230119145809-92243f7c55cb // indirect
entgo.io/ent v0.11.8 // indirect
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 // indirect
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect
github.com/ProtonMail/go-srp v0.0.7 // indirect
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect
github.com/agext/levenshtein v1.2.3 // indirect
github.com/andybalholm/cascadia v1.3.2 // indirect
github.com/apparentlymart/go-textseg/v13 v13.0.0 // 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
@ -78,7 +74,6 @@ require (
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-openapi/inflect v0.19.0 // 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
@ -90,7 +85,6 @@ require (
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/go-memdb v1.3.3 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/hashicorp/hcl/v2 v2.16.1 // indirect
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
@ -98,8 +92,7 @@ require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/mattn/go-sqlite3 v1.14.16 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/mattn/go-sqlite3 v1.14.17 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
@ -115,7 +108,6 @@ require (
github.com/ugorji/go/codec v1.2.11 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
github.com/zclconf/go-cty v1.12.1 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.9.0 // indirect
golang.org/x/mod v0.8.0 // indirect

42
go.sum
View File

@ -1,5 +1,3 @@
ariga.io/atlas v0.9.1-0.20230119145809-92243f7c55cb h1:mbsFtavDqGdYwdDpP50LGOOZ2hgyGoJcZeOpbgKMyu4=
ariga.io/atlas v0.9.1-0.20230119145809-92243f7c55cb/go.mod h1:T230JFcENj4ZZzMkZrXFDSkv+2kXkUgpJ5FQQ5hMcKU=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
@ -13,13 +11,10 @@ cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqCl
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
entgo.io/ent v0.11.8 h1:M/M0QL1CYCUSdqGRXUrXhFYSDRJPsOOrr+RLEej/gyQ=
entgo.io/ent v0.11.8/go.mod h1:ericBi6Q8l3wBH1wEIDfKxw7rcQEuRPyBfbIzjtxJ18=
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557 h1:l6surSnJ3RP4qA1qmKJ+hQn3UjytosdoG27WGjrDlVs=
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557/go.mod h1:sTrmvD/TxuypdOERsDOS7SndZg0rzzcCi1b6wQMXUYM=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
@ -28,10 +23,8 @@ github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
github.com/ProtonMail/gluon v0.16.1-0.20230607122549-dbdb8e1cc0c3 h1:VMbbJD3dcGPPIgbdQTS5Z4nX0QU/SsVZWdmsMVVBBsI=
github.com/ProtonMail/gluon v0.16.1-0.20230607122549-dbdb8e1cc0c3/go.mod h1:ERZikuN+2i/oTeSwS5fq7J0Fms76uUcBlTAwT4KaEAk=
github.com/ProtonMail/gluon v0.16.1-0.20230706112359-3146d8312d12 h1:a4mVvmGGojclWgbQ6g4eW/XquioHJ/iYF4OFk70265Q=
github.com/ProtonMail/gluon v0.16.1-0.20230706112359-3146d8312d12/go.mod h1:ERZikuN+2i/oTeSwS5fq7J0Fms76uUcBlTAwT4KaEAk=
github.com/ProtonMail/gluon v0.17.1-0.20230829112217-5d5c25c504b5 h1:C/8P5NHAKi2yCKez+OZ5rSR8SsL7k8si4pK4SE2QtV8=
github.com/ProtonMail/gluon v0.17.1-0.20230829112217-5d5c25c504b5/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo=
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
@ -42,8 +35,8 @@ github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7 h1:+j+Kd/
github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
github.com/ProtonMail/go-proton-api v0.4.1-0.20230628092916-81cb3f87f184 h1:gw8sgQMCIDS/lw5xbF2iqlTfvY0HhuafjlGsKcN3VsE=
github.com/ProtonMail/go-proton-api v0.4.1-0.20230628092916-81cb3f87f184/go.mod h1:+aTJoYu8bqzGECXL2DOdiZTZ64bGn3w0NC8VcFpJrFM=
github.com/ProtonMail/go-proton-api v0.4.1-0.20230831064234-0e3a549b3f36 h1:JVMK2w90bCWayUCXJIb3wkQ5+j2P/NbnrX3BrDoLzsc=
github.com/ProtonMail/go-proton-api v0.4.1-0.20230831064234-0e3a549b3f36/go.mod h1:nS8hMGjJLgC0Iej0JMYbsI388LesEkM1Hj/jCCxQeaQ=
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton h1:YS6M20yvjCJPR1r4ADW5TPn6rahs4iAyZaACei86bEc=
@ -54,8 +47,6 @@ github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma
github.com/abiosoft/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzgHf/aS6+zVXRj14cVk9qg=
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8=
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db/go.mod h1:rB3B4rKii8V21ydCbIzH5hZiCQE7f5E9SzUb/ZZx530=
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37 h1:28uU3TtuvQ6KRndxg9TrC868jBWmSKgh0GTXkACCXmA=
@ -63,9 +54,6 @@ github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37/go.m
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
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/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM=
github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw=
github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
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=
@ -160,8 +148,6 @@ github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4=
github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
@ -172,8 +158,6 @@ github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QX
github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
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=
@ -250,8 +234,6 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/hcl/v2 v2.16.1 h1:BwuxEMD/tsYgbhIW7UuI3crjovf3MzuFWiVgiv57iHg=
github.com/hashicorp/hcl/v2 v2.16.1/go.mod h1:JRmR89jycNkrrqnMmvPDMd56n1rQJ2Q6KocSLCMCXng=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
@ -284,9 +266,6 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
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=
@ -303,8 +282,8 @@ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
@ -313,8 +292,6 @@ github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceT
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
@ -369,8 +346,6 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y=
@ -415,10 +390,8 @@ github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4d
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/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4=
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 v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
@ -426,9 +399,6 @@ github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRT
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zclconf/go-cty v1.12.1 h1:PcupnljUm9EIvbgSHQnHhUr3fO6oFmkOrvs2BAFNXXY=
github.com/zclconf/go-cty v1.12.1/go.mod h1:s9IfD1LK5ccNMSWCVFCE2rJfHiZgi7JijgeWIMfhLvA=
github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=

View File

@ -19,6 +19,7 @@ package app
import (
"fmt"
"io"
"net/http"
"net/http/cookiejar"
"net/url"
@ -51,6 +52,9 @@ const (
flagCPUProfile = "cpu-prof"
flagCPUProfileShort = "p"
flagTraceProfile = "trace-prof"
flagTraceProfileShort = "t"
flagMemProfile = "mem-prof"
flagMemProfileShort = "m"
@ -76,10 +80,12 @@ const (
flagNoWindow = "no-window"
flagParentPID = "parent-pid"
flagSoftwareRenderer = "software-renderer"
flagSessionID = "session-id"
)
const (
appUsage = "Proton Mail IMAP and SMTP Bridge"
appUsage = "Proton Mail IMAP and SMTP Bridge"
appShortName = "bridge"
)
func New() *cli.App {
@ -93,6 +99,11 @@ func New() *cli.App {
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},
@ -150,6 +161,10 @@ func New() *cli.App {
Hidden: true,
Value: false,
},
&cli.StringFlag{
Name: flagSessionID,
Hidden: true,
},
}
app.Action = run
@ -183,6 +198,11 @@ func run(c *cli.Context) error {
exe = os.Args[0]
}
var logCloser io.Closer
defer func() {
_ = logging.Close(logCloser)
}()
// Restart the app if requested.
return withRestarter(exe, func(restarter *restarter.Restarter) error {
// Handle crashes with various actions.
@ -199,7 +219,9 @@ func run(c *cli.Context) error {
}
// Initialize logging.
return withLogging(c, crashHandler, locations, func() error {
return withLogging(c, crashHandler, locations, func(closer io.Closer) error {
logCloser = closer
// If there was an error during migration, log it now.
if migrationErr != nil {
logrus.WithError(migrationErr).Error("Failed to migrate old app data")
@ -298,7 +320,7 @@ func withSingleInstance(settingPath, lockFile string, version *semver.Version, f
}
// Initialize our logging system.
func withLogging(c *cli.Context, crashHandler *crash.Handler, locations *locations.Locations, fn func() error) error {
func withLogging(c *cli.Context, crashHandler *crash.Handler, locations *locations.Locations, fn func(closer io.Closer) error) error {
logrus.Debug("Initializing logging")
defer logrus.Debug("Logging stopped")
@ -311,12 +333,21 @@ func withLogging(c *cli.Context, crashHandler *crash.Handler, locations *locatio
logrus.WithField("path", logsPath).Debug("Received logs path")
// Initialize logging.
if err := logging.Init(logsPath, c.String(flagLogLevel)); err != nil {
sessionID := logging.NewSessionIDFromString(c.String(flagSessionID))
var closer io.Closer
if closer, err = logging.Init(
logsPath,
sessionID,
logging.BridgeShortAppName,
logging.DefaultMaxLogFileSize,
logging.DefaultPruningSize,
c.String(flagLogLevel),
); err != nil {
return fmt.Errorf("could not initialize logging: %w", err)
}
// Ensure we dump a stack trace if we crash.
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath))
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath, sessionID, appShortName))
logrus.
WithField("appName", constants.FullAppName).
@ -329,7 +360,7 @@ func withLogging(c *cli.Context, crashHandler *crash.Handler, locations *locatio
WithField("SentryID", sentry.GetProtectedHostname()).
Info("Run app")
return fn()
return fn(closer)
}
// WithLocations provides access to locations where we store our files.
@ -356,6 +387,11 @@ func withProfiler(c *cli.Context, fn func() error) error {
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()

View File

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

View File

@ -15,7 +15,7 @@
// 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
//go:build !build_qa && !test_integration
package bridge

View File

@ -15,7 +15,7 @@
// 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
//go:build build_qa || test_integration
package bridge

View File

@ -23,7 +23,6 @@ import (
"crypto/tls"
"errors"
"fmt"
"net"
"net/http"
"sync"
"time"
@ -38,8 +37,11 @@ import (
"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"
@ -59,7 +61,7 @@ type Bridge struct {
// api manages user API clients.
api *proton.Manager
proxyCtl ProxyController
identifier Identifier
identifier identifier.Identifier
// tlsConfig holds the bridge TLS config used by the IMAP and SMTP servers.
tlsConfig *tls.Config
@ -125,9 +127,8 @@ type Bridge struct {
// goHeartbeat triggers a check/sending if heartbeat is needed.
goHeartbeat func()
uidValidityGenerator imap.UIDValidityGenerator
serverManager *ServerManager
serverManager *imapsmtpserver.Service
syncService *syncservice.Service
}
// New creates a new bridge.
@ -140,7 +141,7 @@ func New(
apiURL string, // the URL of the API to use
cookieJar http.CookieJar, // the cookie jar to use
identifier Identifier, // the identifier to keep track of the user agent
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
@ -207,7 +208,7 @@ func newBridge(
reporter reporter.Reporter,
api *proton.Manager,
identifier Identifier,
identifier identifier.Identifier,
proxyCtl ProxyController,
uidValidityGenerator imap.UIDValidityGenerator,
@ -269,17 +270,26 @@ func newBridge(
firstStart: firstStart,
lastVersion: lastVersion,
tasks: tasks,
uidValidityGenerator: uidValidityGenerator,
serverManager: newServerManager(),
tasks: tasks,
syncService: syncservice.NewService(reporter, panicHandler),
}
if err := bridge.serverManager.Init(bridge); err != nil {
bridge.serverManager = imapsmtpserver.NewService(context.Background(),
&bridgeSMTPSettings{b: bridge},
&bridgeIMAPSettings{b: bridge},
&bridgeEventPublisher{b: bridge},
panicHandler,
reporter,
uidValidityGenerator,
&bridgeIMAPSMTPTelemetry{b: bridge},
)
if err := bridge.serverManager.Init(context.Background(), bridge.tasks, &bridgeEventSubscription{b: bridge}); err != nil {
return nil, err
}
bridge.syncService.Run(bridge.tasks)
return bridge, nil
}
@ -407,11 +417,6 @@ func (bridge *Bridge) GetErrors() []error {
func (bridge *Bridge) Close(ctx context.Context) {
logrus.Info("Closing bridge")
// Close the servers
if err := bridge.serverManager.CloseServers(ctx); err != nil {
logrus.WithError(err).Error("Failed to close servers")
}
// Close all users.
safe.Lock(func() {
for _, user := range bridge.users {
@ -419,6 +424,11 @@ func (bridge *Bridge) Close(ctx context.Context) {
}
}, 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()
@ -527,24 +537,6 @@ func loadTLSConfig(vault *vault.Vault) (*tls.Config, error) {
}, nil
}
func newListener(port int, useTLS bool, tlsConfig *tls.Config) (net.Listener, error) {
if useTLS {
tlsListener, err := tls.Listen("tcp", fmt.Sprintf("%v:%v", constants.Host, port), tlsConfig)
if err != nil {
return nil, err
}
return tlsListener, nil
}
netListener, err := net.Listen("tcp", fmt.Sprintf("%v:%v", constants.Host, port))
if err != nil {
return nil, err
}
return netListener, nil
}
func min(a, b time.Duration) time.Duration {
if a < b {
return a

View File

@ -25,6 +25,7 @@ import (
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"time"
@ -43,6 +44,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/focus"
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapsmtpserver"
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
"github.com/ProtonMail/proton-bridge/v3/internal/user"
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
@ -53,6 +55,7 @@ import (
"github.com/emersion/go-sasl"
"github.com/emersion/go-smtp"
"github.com/stretchr/testify/require"
"go.uber.org/goleak"
)
var (
@ -300,8 +303,11 @@ func TestBridge_UserAgentFromSMTPClient(t *testing.T) {
string(info.BridgePass)),
))
currentUserAgent = b.GetCurrentUserAgent()
require.Contains(t, currentUserAgent, "UnknownClient/0.0.1")
require.Eventually(t, func() bool {
currentUserAgent = b.GetCurrentUserAgent()
return strings.Contains(currentUserAgent, "UnknownClient/0.0.1")
}, time.Minute, 5*time.Second)
})
})
}
@ -617,6 +623,10 @@ func TestBridge_AddressWithoutKeys(t *testing.T) {
defer m.Close()
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
// Watch for sync finished event.
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
defer done()
// Create a user which will have an address without keys.
userID, _, err := s.CreateUser("nokeys", []byte("password"))
require.NoError(t, err)
@ -637,10 +647,6 @@ func TestBridge_AddressWithoutKeys(t *testing.T) {
// Remove the address keys.
require.NoError(t, s.RemoveAddressKey(userID, aliasAddrID, aliasAddr.Keys[0].ID))
// Watch for sync finished event.
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
defer done()
// We should be able to log the user in.
require.NoError(t, getErr(bridge.LoginFull(context.Background(), "nokeys", []byte("password"), nil, nil)))
require.NoError(t, err)
@ -696,10 +702,10 @@ func TestBridge_InitGluonDirectory(t *testing.T) {
configDir, err := b.GetGluonDataDir()
require.NoError(t, err)
_, err = os.ReadDir(bridge.ApplyGluonCachePathSuffix(b.GetGluonCacheDir()))
_, err = os.ReadDir(imapsmtpserver.ApplyGluonCachePathSuffix(b.GetGluonCacheDir()))
require.False(t, os.IsNotExist(err))
_, err = os.ReadDir(bridge.ApplyGluonConfigPathSuffix(configDir))
_, err = os.ReadDir(imapsmtpserver.ApplyGluonConfigPathSuffix(configDir))
require.False(t, os.IsNotExist(err))
})
})
@ -772,16 +778,16 @@ func TestBridge_ChangeCacheDirectory(t *testing.T) {
require.NoError(t, err)
// Old store should no more exists.
_, err = os.ReadDir(bridge.ApplyGluonCachePathSuffix(currentCacheDir))
_, err = os.ReadDir(imapsmtpserver.ApplyGluonCachePathSuffix(currentCacheDir))
require.True(t, os.IsNotExist(err))
// Database should not have changed.
_, err = os.ReadDir(bridge.ApplyGluonConfigPathSuffix(configDir))
_, err = os.ReadDir(imapsmtpserver.ApplyGluonConfigPathSuffix(configDir))
require.False(t, os.IsNotExist(err))
// New path should have Gluon sub-folder.
require.Equal(t, filepath.Join(newCacheDir, "gluon"), b.GetGluonCacheDir())
// And store should be inside it.
_, err = os.ReadDir(bridge.ApplyGluonCachePathSuffix(b.GetGluonCacheDir()))
_, err = os.ReadDir(imapsmtpserver.ApplyGluonCachePathSuffix(b.GetGluonCacheDir()))
require.False(t, os.IsNotExist(err))
// We should be able to fetch.
@ -869,6 +875,9 @@ func TestBridge_ChangeAddressOrder(t *testing.T) {
// withEnv creates the full test environment and runs the tests.
func withEnv(t *testing.T, tests func(context.Context, *server.Server, *proton.NetCtl, bridge.Locator, []byte), opts ...server.Option) {
opt := goleak.IgnoreCurrent()
defer goleak.VerifyNone(t, opt)
server := server.New(opts...)
defer server.Close()
@ -1053,6 +1062,7 @@ func getConnectedUserIDs(t *testing.T, b *bridge.Bridge) []string {
func chToType[In, Out any](inCh <-chan In, done func()) (<-chan Out, func()) {
outCh := make(chan Out)
ctx, cancel := context.WithCancel(context.Background())
go func() {
defer close(outCh)
@ -1062,11 +1072,19 @@ func chToType[In, Out any](inCh <-chan In, done func()) (<-chan Out, func()) {
panic(fmt.Sprintf("unexpected type %T", in))
}
outCh <- out
select {
case <-ctx.Done():
return
case outCh <- out:
}
}
}()
return outCh, done
return outCh, func() {
cancel()
done()
}
}
type eventWaiter struct {

View File

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

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

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

@ -40,3 +40,35 @@ func (bridge *Bridge) setUserAgent(name, version string) {
}
}
}
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)
}

View File

@ -20,23 +20,12 @@ package bridge
import (
"context"
"crypto/tls"
"io"
"os"
"path/filepath"
"strings"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/gluon"
"github.com/ProtonMail/gluon/async"
imapEvents "github.com/ProtonMail/gluon/events"
"github.com/ProtonMail/gluon/imap"
"github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/gluon/store"
"github.com/ProtonMail/gluon/store/fallback_v0"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
"github.com/ProtonMail/proton-bridge/v3/internal/user"
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapsmtpserver"
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
"github.com/sirupsen/logrus"
)
@ -45,16 +34,6 @@ func (bridge *Bridge) restartIMAP(ctx context.Context) error {
return bridge.serverManager.RestartIMAP(ctx)
}
// addIMAPUser connects the given user to gluon.
func (bridge *Bridge) addIMAPUser(ctx context.Context, user *user.User) error {
return bridge.serverManager.AddIMAPUser(ctx, user)
}
// removeIMAPUser disconnects the given user from gluon, optionally also removing its files.
func (bridge *Bridge) removeIMAPUser(ctx context.Context, user *user.User, withData bool) error {
return bridge.serverManager.RemoveIMAPUser(ctx, user, withData)
}
func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) {
switch event := event.(type) {
case imapEvents.UserAdded:
@ -92,108 +71,59 @@ func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) {
}
}
func ApplyGluonCachePathSuffix(basePath string) string {
return filepath.Join(basePath, "backend", "store")
type bridgeIMAPSettings struct {
b *Bridge
}
func ApplyGluonConfigPathSuffix(basePath string) string {
return filepath.Join(basePath, "backend", "db")
func (b *bridgeIMAPSettings) EventPublisher() imapsmtpserver.IMAPEventPublisher {
return b
}
func newIMAPServer(
gluonCacheDir, gluonConfigDir string,
version *semver.Version,
tlsConfig *tls.Config,
reporter reporter.Reporter,
logClient, logServer bool,
eventCh chan<- imapEvents.Event,
tasks *async.Group,
uidValidityGenerator imap.UIDValidityGenerator,
panicHandler async.PanicHandler,
) (*gluon.Server, error) {
gluonCacheDir = ApplyGluonCachePathSuffix(gluonCacheDir)
gluonConfigDir = ApplyGluonConfigPathSuffix(gluonConfigDir)
func (b *bridgeIMAPSettings) TLSConfig() *tls.Config {
return b.b.tlsConfig
}
logrus.WithFields(logrus.Fields{
"gluonStore": gluonCacheDir,
"gluonDB": gluonConfigDir,
"version": version,
"logClient": logClient,
"logServer": logServer,
}).Info("Creating IMAP server")
func (b *bridgeIMAPSettings) LogClient() bool {
return b.b.logIMAPClient
}
if logClient || logServer {
log := logrus.WithField("protocol", "IMAP")
log.Warning("================================================")
log.Warning("THIS LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA")
log.Warning("================================================")
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
}
var imapClientLog io.Writer
if logClient {
imapClientLog = logging.NewIMAPLogger()
} else {
imapClientLog = io.Discard
}
var imapServerLog io.Writer
if logServer {
imapServerLog = logging.NewIMAPLogger()
} else {
imapServerLog = io.Discard
}
imapServer, err := gluon.New(
gluon.WithTLS(tlsConfig),
gluon.WithDataDir(gluonCacheDir),
gluon.WithDatabaseDir(gluonConfigDir),
gluon.WithStoreBuilder(new(storeBuilder)),
gluon.WithLogger(imapClientLog, imapServerLog),
getGluonVersionInfo(version),
gluon.WithReporter(reporter),
gluon.WithUIDValidityGenerator(uidValidityGenerator),
gluon.WithPanicHandler(panicHandler),
)
if err != nil {
return nil, err
}
tasks.Once(func(ctx context.Context) {
async.ForwardContext(ctx, eventCh, imapServer.AddWatcher())
})
tasks.Once(func(ctx context.Context) {
async.RangeContext(ctx, imapServer.GetErrorCh(), func(err error) {
logrus.WithError(err).Error("IMAP server error")
})
})
return imapServer, nil
}
func getGluonVersionInfo(version *semver.Version) gluon.Option {
return gluon.WithVersionInfo(
int(version.Major()),
int(version.Minor()),
int(version.Patch()),
constants.FullAppName,
"TODO",
"TODO",
)
}
type storeBuilder struct{}
func (*storeBuilder) New(path, userID string, passphrase []byte) (store.Store, error) {
return store.NewOnDiskStore(
filepath.Join(path, userID),
passphrase,
store.WithFallback(fallback_v0.NewOnDiskStoreV0WithCompressor(&fallback_v0.GZipCompressor{})),
)
}
func (*storeBuilder) Delete(path, userID string) error {
return os.RemoveAll(filepath.Join(path, userID))
}

View File

@ -0,0 +1,26 @@
// 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
type bridgeIMAPSMTPTelemetry struct {
b *Bridge
}
func (b bridgeIMAPSMTPTelemetry) SetCacheLocation(s string) {
b.b.heartbeat.SetCacheLocation(s)
}

View File

@ -84,6 +84,11 @@ func TestBridge_Refresh(t *testing.T) {
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()

View File

@ -467,7 +467,9 @@ SGVsbG8gd29ybGQK
require.Eventually(t, func() bool {
messages, err := clientFetch(senderIMAPClient, `Sent`, imap.FetchBodyStructure)
require.NoError(t, err)
require.Equal(t, 4, len(messages))
if len(messages) != 4 {
return false
}
// messages may not be in order
for _, message := range messages {

View File

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

View File

@ -20,11 +20,10 @@ package bridge
import (
"context"
"fmt"
"net"
"os"
"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"
@ -131,26 +130,41 @@ func (bridge *Bridge) GetGluonDataDir() (string, error) {
}
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) moveGluonCacheDir(oldGluonDir, newGluonDir string) error {
logrus.Infof("gluon cache moving from %s to %s", oldGluonDir, newGluonDir)
oldCacheDir := ApplyGluonCachePathSuffix(oldGluonDir)
if err := copyDir(oldCacheDir, ApplyGluonCachePathSuffix(newGluonDir)); err != nil {
return fmt.Errorf("failed to copy gluon dir: %w", err)
}
if err := bridge.vault.SetGluonDir(newGluonDir); err != nil {
return fmt.Errorf("failed to set new gluon cache dir: %w", err)
}
if err := os.RemoveAll(oldCacheDir); err != nil {
logrus.WithError(err).Error("failed to remove old gluon cache dir")
}
return nil
}
func (bridge *Bridge) GetProxyAllowed() bool {
return bridge.vault.GetProxyAllowed()
}
@ -318,16 +332,3 @@ func (bridge *Bridge) FactoryReset(ctx context.Context) {
logrus.WithError(err).Error("Failed to clear data paths")
}
}
func getPort(addr net.Addr) int {
switch addr := addr.(type) {
case *net.TCPAddr:
return addr.Port
case *net.UDPAddr:
return addr.Port
default:
return 0
}
}

View File

@ -25,6 +25,7 @@ import (
"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"
)
@ -51,6 +52,45 @@ func TestBridge_Settings_GluonDir(t *testing.T) {
})
}
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) {

View File

@ -21,43 +21,37 @@ import (
"context"
"crypto/tls"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
"github.com/emersion/go-sasl"
"github.com/emersion/go-smtp"
"github.com/sirupsen/logrus"
"github.com/ProtonMail/proton-bridge/v3/internal/identifier"
)
func (bridge *Bridge) restartSMTP(ctx context.Context) error {
return bridge.serverManager.RestartSMTP(ctx)
}
func newSMTPServer(bridge *Bridge, tlsConfig *tls.Config, logSMTP bool) *smtp.Server {
logrus.WithField("logSMTP", logSMTP).Info("Creating SMTP server")
smtpServer := smtp.NewServer(&smtpBackend{Bridge: bridge})
smtpServer.TLSConfig = tlsConfig
smtpServer.Domain = constants.Host
smtpServer.AllowInsecureAuth = true
smtpServer.MaxLineLength = 1 << 16
smtpServer.ErrorLog = logging.NewSMTPLogger()
// go-smtp suppors SASL PLAIN but not LOGIN. We need to add LOGIN support ourselves.
smtpServer.EnableAuth(sasl.Login, func(conn *smtp.Conn) sasl.Server {
return sasl.NewLoginServer(func(username, password string) error {
return conn.Session().AuthPlain(username, password)
})
})
if logSMTP {
log := logrus.WithField("protocol", "SMTP")
log.Warning("================================================")
log.Warning("THIS LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA")
log.Warning("================================================")
smtpServer.Debug = logging.NewSMTPDebugLogger()
}
return smtpServer
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

@ -21,9 +21,11 @@ import (
"context"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
"sync/atomic"
"testing"
"time"
@ -252,14 +254,17 @@ func TestBridge_SyncWithOngoingEvents(t *testing.T) {
// 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()
{
syncCh, done := chToType[events.Event, events.SyncFailed](b.GetEvents(events.SyncFailed{}))
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, (<-syncCh).UserID)
require.Equal(t, userID, (<-syncFailedCh).UserID)
info, err := b.GetUserInfo(userID)
require.NoError(t, err)
@ -282,11 +287,7 @@ func TestBridge_SyncWithOngoingEvents(t *testing.T) {
// 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)
@ -298,12 +299,6 @@ func TestBridge_SyncWithOngoingEvents(t *testing.T) {
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)
// Original folder should have more than 0 messages and less than the total.
require.Greater(t, status.Messages, uint32(0))
require.Less(t, status.Messages, uint32(numMsg))
// Check that the new messages arrive in the right location.
require.Eventually(t, func() bool {
status, err := client.Select(`Folders/folder2`, true)
@ -321,6 +316,269 @@ func TestBridge_SyncWithOngoingEvents(t *testing.T) {
}, 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 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()),
@ -399,6 +657,10 @@ func createNumMessages(ctx context.Context, t *testing.T, c *proton.Client, addr
}
func createMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID, labelID string, messages ...[]byte) []string {
return createMessagesWithFlags(ctx, t, c, addrID, labelID, 0, messages...)
}
func createMessagesWithFlags(ctx context.Context, t *testing.T, c *proton.Client, addrID, labelID string, flags proton.MessageFlag, messages ...[]byte) []string {
user, err := c.GetUser(ctx)
require.NoError(t, err)
@ -417,6 +679,13 @@ func createMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID,
_, ok := addrKRs[addrID]
require.True(t, ok)
var msgFlags proton.MessageFlag
if flags == 0 {
msgFlags = proton.MessageFlagReceived
} else {
msgFlags = flags
}
str, err := c.ImportMessages(
ctx,
addrKRs[addrID],
@ -427,7 +696,7 @@ func createMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID,
Metadata: proton.ImportMetadata{
AddressID: addrID,
LabelIDs: []string{labelID},
Flags: proton.MessageFlagReceived,
Flags: msgFlags,
},
Message: message,
}

View File

@ -31,7 +31,7 @@ import (
"github.com/stretchr/testify/require"
)
// Disabled due to flakyness.
// Disabled due to flakiness.
func _TestBridge_SyncExistsWithErrorWhenTooManyFilesAreOpen(t *testing.T) { //nolint:unused
var rlimitCurrent syscall.Rlimit

View File

@ -32,15 +32,7 @@ type Locator interface {
GetLicenseFilePath() string
GetDependencyLicensesLink() string
Clear(...string) error
}
type Identifier interface {
GetUserAgent() string
HasClient() bool
SetClient(name, version string)
SetPlatform(platform string)
SetClientString(client string)
GetClientString() string
ProvideIMAPSyncConfigPath() (string, error)
}
type ProxyController interface {

View File

@ -30,6 +30,7 @@ import (
"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"
@ -243,6 +244,11 @@ func (bridge *Bridge) LogoutUser(ctx context.Context, userID string) error {
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
@ -252,6 +258,10 @@ func (bridge *Bridge) DeleteUser(ctx context.Context, userID string) error {
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")
}
@ -278,18 +288,10 @@ func (bridge *Bridge) SetAddressMode(ctx context.Context, userID string, mode va
return fmt.Errorf("address mode is already %q", mode)
}
if err := bridge.removeIMAPUser(ctx, user, true); err != nil {
return fmt.Errorf("failed to remove IMAP user: %w", err)
}
if err := user.SetAddressMode(ctx, mode); err != nil {
return fmt.Errorf("failed to set address mode: %w", err)
}
if err := bridge.addIMAPUser(ctx, user); err != nil {
return fmt.Errorf("failed to add IMAP user: %w", err)
}
bridge.publish(events.AddressModeChanged{
UserID: userID,
AddressMode: mode,
@ -335,13 +337,7 @@ func (bridge *Bridge) SendBadEventUserFeedback(_ context.Context, userID string,
logrus.WithError(rerr).Error("Failed to report feedback failure")
}
if err := bridge.addIMAPUser(ctx, user); err != nil {
return fmt.Errorf("failed to add IMAP user: %w", err)
}
user.BadEventFeedbackResync(ctx)
return nil
return user.BadEventFeedbackResync(ctx)
}
if rerr := bridge.reporter.ReportMessageWithContext(
@ -524,6 +520,11 @@ func (bridge *Bridge) addUserWithVault(
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,
@ -535,16 +536,16 @@ func (bridge *Bridge) addUserWithVault(
bridge.vault.GetMaxSyncMemory(),
statsPath,
bridge,
bridge.serverManager,
bridge.serverManager,
&bridgeEventSubscription{b: bridge},
bridge.syncService,
syncSettingsPath,
)
if err != nil {
return fmt.Errorf("failed to create user: %w", err)
}
// Connect the user's address(es) to gluon.
if err := bridge.addIMAPUser(ctx, user); err != nil {
return fmt.Errorf("failed to add IMAP 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) {
@ -554,11 +555,8 @@ func (bridge *Bridge) addUserWithVault(
"event": event,
}).Debug("Received user event")
if err := bridge.handleUserEvent(ctx, user, event); err != nil {
logrus.WithError(err).Error("Failed to handle user event")
} else {
bridge.publish(event)
}
bridge.handleUserEvent(ctx, user, event)
bridge.publish(event)
})
})
@ -609,10 +607,6 @@ func (bridge *Bridge) logoutUser(ctx context.Context, user *user.User, withAPI,
"withData": withData,
}).Debug("Logging out user")
if err := bridge.removeIMAPUser(ctx, user, withData); err != nil {
logrus.WithError(err).Error("Failed to remove IMAP user")
}
if err := user.Logout(ctx, withAPI); err != nil {
logrus.WithError(err).Error("Failed to logout user")
}

View File

@ -19,44 +19,17 @@ package bridge
import (
"context"
"fmt"
"github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/proton-bridge/v3/internal"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/user"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/sirupsen/logrus"
)
func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, event events.Event) error {
func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, event events.Event) {
switch event := event.(type) {
case events.UserAddressCreated:
if err := bridge.handleUserAddressCreated(ctx, user, event); err != nil {
return fmt.Errorf("failed to handle user address created event: %w", err)
}
case events.UserAddressEnabled:
if err := bridge.handleUserAddressEnabled(ctx, user, event); err != nil {
return fmt.Errorf("failed to handle user address enabled event: %w", err)
}
case events.UserAddressDisabled:
if err := bridge.handleUserAddressDisabled(ctx, user, event); err != nil {
return fmt.Errorf("failed to handle user address disabled event: %w", err)
}
case events.UserAddressDeleted:
if err := bridge.handleUserAddressDeleted(ctx, user, event); err != nil {
return fmt.Errorf("failed to handle user address deleted event: %w", err)
}
case events.UserRefreshed:
if err := bridge.handleUserRefreshed(ctx, user, event); err != nil {
return fmt.Errorf("failed to handle user refreshed event: %w", err)
}
case events.UserDeauth:
bridge.handleUserDeauth(ctx, user)
@ -66,102 +39,6 @@ func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, even
case events.UncategorizedEventError:
bridge.handleUncategorizedErrorEvent(event)
}
return nil
}
func (bridge *Bridge) handleUserAddressCreated(ctx context.Context, user *user.User, event events.UserAddressCreated) error {
if user.GetAddressMode() == vault.CombinedMode {
return nil
}
gluonID, err := bridge.serverManager.AddGluonUser(ctx, user.NewIMAPConnector(event.AddressID), user.GluonKey())
if err != nil {
return fmt.Errorf("failed to add user to IMAP server: %w", err)
}
if err := user.SetGluonID(event.AddressID, gluonID); err != nil {
return fmt.Errorf("failed to set gluon ID: %w", err)
}
return nil
}
func (bridge *Bridge) handleUserAddressEnabled(ctx context.Context, user *user.User, event events.UserAddressEnabled) error {
if user.GetAddressMode() == vault.CombinedMode {
return nil
}
gluonID, err := bridge.serverManager.AddGluonUser(ctx, user.NewIMAPConnector(event.AddressID), user.GluonKey())
if err != nil {
return fmt.Errorf("failed to add user to IMAP server: %w", err)
}
if err := user.SetGluonID(event.AddressID, gluonID); err != nil {
return fmt.Errorf("failed to set gluon ID: %w", err)
}
return nil
}
func (bridge *Bridge) handleUserAddressDisabled(ctx context.Context, user *user.User, event events.UserAddressDisabled) error {
if user.GetAddressMode() == vault.CombinedMode {
return nil
}
gluonID, ok := user.GetGluonID(event.AddressID)
if !ok {
return fmt.Errorf("gluon ID not found for address %s", event.AddressID)
}
if err := bridge.serverManager.RemoveGluonUser(ctx, gluonID); err != nil {
return fmt.Errorf("failed to remove user from IMAP server: %w", err)
}
if err := user.RemoveGluonID(event.AddressID, gluonID); err != nil {
return fmt.Errorf("failed to remove gluon ID for address: %w", err)
}
return nil
}
func (bridge *Bridge) handleUserAddressDeleted(ctx context.Context, user *user.User, event events.UserAddressDeleted) error {
if user.GetAddressMode() == vault.CombinedMode {
return nil
}
gluonID, ok := user.GetGluonID(event.AddressID)
if !ok {
return fmt.Errorf("gluon ID not found for address %s", event.AddressID)
}
if err := bridge.serverManager.handleRemoveGluonUser(ctx, gluonID); err != nil {
return fmt.Errorf("failed to remove user from IMAP server: %w", err)
}
if err := user.RemoveGluonID(event.AddressID, gluonID); err != nil {
return fmt.Errorf("failed to remove gluon ID for address: %w", err)
}
return nil
}
func (bridge *Bridge) handleUserRefreshed(ctx context.Context, user *user.User, event events.UserRefreshed) error {
return safe.RLockRet(func() error {
if event.CancelEventPool {
user.CancelSyncAndEventPoll()
}
if err := bridge.removeIMAPUser(ctx, user, true); err != nil {
return fmt.Errorf("failed to remove IMAP user: %w", err)
}
if err := bridge.addIMAPUser(ctx, user); err != nil {
return fmt.Errorf("failed to add IMAP user: %w", err)
}
return nil
}, bridge.usersLock)
}
func (bridge *Bridge) handleUserDeauth(ctx context.Context, user *user.User) {
@ -171,7 +48,7 @@ func (bridge *Bridge) handleUserDeauth(ctx context.Context, user *user.User) {
}, bridge.usersLock)
}
func (bridge *Bridge) handleUserBadEvent(_ context.Context, user *user.User, event events.UserBadEvent) {
func (bridge *Bridge) handleUserBadEvent(ctx context.Context, user *user.User, event events.UserBadEvent) {
safe.Lock(func() {
if rerr := bridge.reporter.ReportMessageWithContext("Failed to handle event", reporter.Context{
"user_id": user.ID(),
@ -184,12 +61,7 @@ func (bridge *Bridge) handleUserBadEvent(_ context.Context, user *user.User, eve
logrus.WithError(rerr).Error("Failed to report failed event handling")
}
user.CancelSyncAndEventPoll()
// Disable IMAP user
if err := bridge.removeIMAPUser(context.Background(), user, false); err != nil {
logrus.WithError(err).Error("Failed to remove IMAP user")
}
user.OnBadEvent(ctx)
}, bridge.usersLock)
}

View File

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

View File

@ -17,7 +17,13 @@
package events
import "fmt"
import (
"context"
"fmt"
"github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/gluon/watcher"
)
type Event interface {
fmt.Stringer
@ -28,3 +34,30 @@ type Event interface {
type eventBase struct{}
func (eventBase) _isEvent() {}
type EventPublisher interface {
PublishEvent(ctx context.Context, event Event)
}
type NullEventPublisher struct{}
func (NullEventPublisher) PublishEvent(_ context.Context, _ Event) {}
type Subscription interface {
Add(ofType ...Event) *watcher.Watcher[Event]
Remove(watcher *watcher.Watcher[Event])
}
type NullSubscription struct{}
func (n NullSubscription) Add(ofType ...Event) *watcher.Watcher[Event] {
return watcher.New[Event](&async.NoopPanicHandler{}, ofType...)
}
func (n NullSubscription) Remove(watcher *watcher.Watcher[Event]) {
watcher.Close()
}
func NewNullSubscription() *NullSubscription {
return &NullSubscription{}
}

View File

@ -0,0 +1,48 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/ProtonMail/proton-bridge/v3/internal/events (interfaces: EventPublisher)
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
reflect "reflect"
events "github.com/ProtonMail/proton-bridge/v3/internal/events"
gomock "github.com/golang/mock/gomock"
)
// MockEventPublisher is a mock of EventPublisher interface.
type MockEventPublisher struct {
ctrl *gomock.Controller
recorder *MockEventPublisherMockRecorder
}
// MockEventPublisherMockRecorder is the mock recorder for MockEventPublisher.
type MockEventPublisherMockRecorder struct {
mock *MockEventPublisher
}
// NewMockEventPublisher creates a new mock instance.
func NewMockEventPublisher(ctrl *gomock.Controller) *MockEventPublisher {
mock := &MockEventPublisher{ctrl: ctrl}
mock.recorder = &MockEventPublisherMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockEventPublisher) EXPECT() *MockEventPublisherMockRecorder {
return m.recorder
}
// PublishEvent mocks base method.
func (m *MockEventPublisher) PublishEvent(arg0 context.Context, arg1 events.Event) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "PublishEvent", arg0, arg1)
}
// PublishEvent indicates an expected call of PublishEvent.
func (mr *MockEventPublisherMockRecorder) PublishEvent(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PublishEvent", reflect.TypeOf((*MockEventPublisher)(nil).PublishEvent), arg0, arg1)
}

View File

@ -37,6 +37,22 @@ func (event IMAPServerStopped) String() string {
return "IMAPServerStopped"
}
type IMAPServerClosed struct {
eventBase
}
func (event IMAPServerClosed) String() string {
return "IMAPServerClosed"
}
type IMAPServerCreated struct {
eventBase
}
func (event IMAPServerCreated) String() string {
return "IMAPServerCreated"
}
type IMAPServerError struct {
eventBase

View File

@ -15,7 +15,7 @@
// 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
package files
import (
"fmt"
@ -24,7 +24,7 @@ import (
"path/filepath"
)
func moveDir(from, to string) error {
func MoveDir(from, to string) error {
entries, err := os.ReadDir(from)
if err != nil {
return err
@ -36,7 +36,7 @@ func moveDir(from, to string) error {
return err
}
if err := moveDir(filepath.Join(from, entry.Name()), filepath.Join(to, entry.Name())); err != nil {
if err := MoveDir(filepath.Join(from, entry.Name()), filepath.Join(to, entry.Name())); err != nil {
return err
}
@ -61,12 +61,12 @@ func moveFile(from, to string) error {
return os.Rename(from, to)
}
func copyDir(from, to string) error {
func CopyDir(from, to string) error {
entries, err := os.ReadDir(from)
if err != nil {
return err
}
if err := createIfNotExists(to, 0o700); err != nil {
if err := CreateIfNotExists(to, 0o700); err != nil {
return err
}
for _, entry := range entries {
@ -74,11 +74,11 @@ func copyDir(from, to string) error {
destPath := filepath.Join(to, entry.Name())
if entry.IsDir() {
if err := copyDir(sourcePath, destPath); err != nil {
if err := CopyDir(sourcePath, destPath); err != nil {
return err
}
} else {
if err := copyFile(sourcePath, destPath); err != nil {
if err := CopyFile(sourcePath, destPath); err != nil {
return err
}
}
@ -86,7 +86,7 @@ func copyDir(from, to string) error {
return nil
}
func copyFile(srcFile, dstFile string) error {
func CopyFile(srcFile, dstFile string) error {
out, err := os.Create(filepath.Clean(dstFile))
defer func(out *os.File) {
_ = out.Close()
@ -113,7 +113,7 @@ func copyFile(srcFile, dstFile string) error {
return nil
}
func exists(filePath string) bool {
func Exists(filePath string) bool {
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return false
}
@ -121,8 +121,8 @@ func exists(filePath string) bool {
return true
}
func createIfNotExists(dir string, perm os.FileMode) error {
if exists(dir) {
func CreateIfNotExists(dir string, perm os.FileMode) error {
if Exists(dir) {
return nil
}

View File

@ -15,7 +15,7 @@
// 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
package files
import (
"os"
@ -41,7 +41,7 @@ func TestMoveDir(t *testing.T) {
}
// Move the files.
if err := moveDir(from, to); err != nil {
if err := MoveDir(from, to); err != nil {
t.Fatal(err)
}

View File

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

View File

@ -385,6 +385,14 @@ Status GRPCService::Login(ServerContext *, LoginRequest const *request, Empty *)
app().log().debug(__FUNCTION__);
UsersTab &usersTab = app().mainWindow().usersTab();
loginUsername_ = QString::fromStdString(request->username());
SPUser const& user = usersTab.userTable().userWithUsernameOrEmail(QString::fromStdString(request->username()));
if (user) {
qtProxy_.sendDelayedEvent(newLoginAlreadyLoggedInEvent(user->id()));
return Status::OK;
}
if (usersTab.nextUserUsernamePasswordError()) {
qtProxy_.sendDelayedEvent(newLoginError(LoginErrorType::USERNAME_PASSWORD_ERROR, usersTab.usernamePasswordErrorMessage()));
return Status::OK;
@ -826,7 +834,7 @@ bool GRPCService::sendEvent(SPStreamEvent const &event) {
//****************************************************************************************************************************************************
void GRPCService::finishLogin() {
UsersTab &usersTab = app().mainWindow().usersTab();
SPUser user = usersTab.userWithUsername(loginUsername_);
SPUser user = usersTab.userWithUsernameOrEmail(loginUsername_);
bool const alreadyExist = user.get();
if (!user) {
user = randomUser();

View File

@ -272,8 +272,8 @@ bridgepp::SPUser UsersTab::userWithID(QString const &userID) {
/// \return The user with the given username.
/// \return A null pointer if the user is not in the list.
//****************************************************************************************************************************************************
bridgepp::SPUser UsersTab::userWithUsername(QString const &username) {
return users_.userWithUsername(username);
bridgepp::SPUser UsersTab::userWithUsernameOrEmail(QString const &username) {
return users_.userWithUsernameOrEmail(username);
}

View File

@ -38,7 +38,7 @@ public: // member functions.
UsersTab &operator=(UsersTab &&) = delete; ///< Disabled move assignment operator.
UserTable &userTable(); ///< Returns a reference to the user table.
bridgepp::SPUser userWithID(QString const &userID); ///< Get the user with the given ID.
bridgepp::SPUser userWithUsername(QString const &username); ///< Get the user with the given username.
bridgepp::SPUser userWithUsernameOrEmail(QString const &username); ///< Get the user with the given username.
bool nextUserUsernamePasswordError() const; ///< Check if next user login should trigger a username/password error.
bool nextUserFreeUserError() const; ///< Check if next user login should trigger a Free user error.
bool nextUserTFARequired() const; ///< Check if next user login should requires 2FA.

View File

@ -150,13 +150,16 @@ bridgepp::SPUser UserTable::userWithID(QString const &userID) {
//****************************************************************************************************************************************************
/// \param[in] username The username.
/// \param[in] username The username, or any email address attached to the account.
/// \return The user with the given username.
/// \return A null pointer if the user is not in the list.
//****************************************************************************************************************************************************
bridgepp::SPUser UserTable::userWithUsername(QString const &username) {
bridgepp::SPUser UserTable::userWithUsernameOrEmail(QString const &username) {
QList<SPUser>::const_iterator it = std::find_if(users_.constBegin(), users_.constEnd(), [&username](SPUser const &user) -> bool {
return user->username() == username;
if (user->username().compare(username, Qt::CaseInsensitive) == 0) {
return true;
}
return user->addresses().contains(username, Qt::CaseInsensitive);
});
return it == users_.end() ? nullptr : *it;

View File

@ -40,7 +40,7 @@ public: // member functions.
void append(bridgepp::SPUser const &user); ///< Append a user.
bridgepp::SPUser userAtIndex(qint32 index); ///< Return the user at the given index.
bridgepp::SPUser userWithID(QString const &userID); ///< Return the user with a given id.
bridgepp::SPUser userWithUsername(QString const &username); ///< Return the user with a given username.
bridgepp::SPUser userWithUsernameOrEmail(QString const &username); ///< Return the user with a given username.
qint32 indexOfUser(QString const &userID); ///< Return the index of a given User.
void touch(qint32 index); ///< touch the user at a given index (indicates it has been modified).
void touch(QString const& userID); ///< touch the user with the given userID (indicates it has been modified).

View File

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

View File

@ -20,6 +20,7 @@
#include "QMLBackend.h"
#include "SentryUtils.h"
#include "Settings.h"
#include <bridgepp/CLI/CLIUtils.h>
#include <bridgepp/GRPC/GRPCClient.h>
#include <bridgepp/Exception/Exception.h>
#include <bridgepp/ProcessMonitor.h>
@ -101,23 +102,47 @@ void AppController::onFatalError(Exception const &exception) {
qApp->exit(EXIT_FAILURE);
}
//****************************************************************************************************************************************************
/// \param[in] isCrashing Is the restart triggered by a crash.
//****************************************************************************************************************************************************
void AppController::restart(bool isCrashing) {
if (!launcher_.isEmpty()) {
QProcess p;
log_->info(QString("Restarting - App : %1 - Args : %2").arg(launcher_, launcherArgs_.join(" ")));
QStringList args = launcherArgs_;
if (isCrashing) {
args.append(noWindowFlag);
}
p.startDetached(launcher_, args);
p.waitForStarted();
if (launcher_.isEmpty()) {
return;
}
QProcess p;
QStringList args = stripStringParameterFromCommandLine("--session-id", launcherArgs_);
if (isCrashing) {
args.append(noWindowFlag);
}
log_->info(QString("Restarting - App : %1 - Args : %2").arg(launcher_, args.join(" ")));
p.startDetached(launcher_, args);
p.waitForStarted();
}
//****************************************************************************************************************************************************
/// \param[in] launcher The launcher.
/// \param[in] args The launcher arguments.
//****************************************************************************************************************************************************
void AppController::setLauncherArgs(const QString &launcher, const QStringList &args) {
launcher_ = launcher;
launcherArgs_ = args;
}
//****************************************************************************************************************************************************
/// \param[in] sessionID The sessionID.
//****************************************************************************************************************************************************
void AppController::setSessionID(const QString &sessionID) {
sessionID_ = sessionID;
}
//****************************************************************************************************************************************************
/// \return The sessionID.
//****************************************************************************************************************************************************
QString AppController::sessionID() {
return sessionID_;
}

View File

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

View File

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

View File

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

View File

@ -37,6 +37,15 @@
using namespace bridgepp;
namespace {
QString const bugReportFile = ":qml/Resources/bug_report_flow.json";
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
@ -61,7 +70,7 @@ void QMLBackend::init(GRPCConfig const &serviceConfig) {
app().grpc().setLog(&log);
this->connectGrpcEvents();
app().grpc().connectToServer(bridgepp::userConfigDir(), serviceConfig, app().bridgeMonitor());
app().grpc().connectToServer(app().sessionID(), bridgepp::userConfigDir(), serviceConfig, app().bridgeMonitor());
app().log().info("Connected to backend via gRPC service.");
QString bridgeVer;
@ -89,6 +98,8 @@ void QMLBackend::init(GRPCConfig const &serviceConfig) {
this->setUseSSLForIMAP(sslForIMAP);
this->setUseSSLForSMTP(sslForSMTP);
this->retrieveUserList();
if (!reportFlow_.parse(bugReportFile))
app().log().error(QString("Cannot parse BugReportFlow description file: %1").arg(bugReportFile));
}
@ -109,6 +120,12 @@ UserList const &QMLBackend::users() const {
return *users_;
}
//****************************************************************************************************************************************************
/// \return the if bridge considers internet is on.
//****************************************************************************************************************************************************
bool QMLBackend::isInternetOn() const {
return isInternetOn_;
}
//****************************************************************************************************************************************************
@ -204,6 +221,63 @@ bool QMLBackend::areSameFileOrFolder(QUrl const &lhs, QUrl const &rhs) const {
}
//****************************************************************************************************************************************************
/// \param[in] categoryId The id of the bug category.
/// \return Set of question for this category.
//****************************************************************************************************************************************************
QString QMLBackend::getBugCategory(quint8 categoryId) const {
return reportFlow_.getCategory(categoryId);
}
//****************************************************************************************************************************************************
/// \param[in] categoryId The id of the bug category.
/// \return Set of question for this category.
//****************************************************************************************************************************************************
QVariantList QMLBackend::getQuestionSet(quint8 categoryId) const {
QVariantList list = reportFlow_.questionSet(categoryId);
if (list.count() == 0)
app().log().error(QString("Bug category not found (id: %1)").arg(categoryId));
return list;
};
//****************************************************************************************************************************************************
/// \param[in] questionId The id of the question.
/// \param[in] answer The answer to that question.
//****************************************************************************************************************************************************
void QMLBackend::setQuestionAnswer(quint8 questionId, QString const &answer) {
if (!reportFlow_.setAnswer(questionId, answer))
app().log().error(QString("Bug Report Question not found (id: %1)").arg(questionId));
}
//****************************************************************************************************************************************************
/// \param[in] questionId The id of the question.
/// \return answer for the given question.
//****************************************************************************************************************************************************
QString QMLBackend::getQuestionAnswer(quint8 questionId) const {
return reportFlow_.getAnswer(questionId);
}
//****************************************************************************************************************************************************
/// \param[in] categoryId The id of the question set.
/// \return concatenate answers for set of questions.
//****************************************************************************************************************************************************
QString QMLBackend::collectAnswers(quint8 categoryId) const {
return reportFlow_.collectAnswers(categoryId);
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void QMLBackend::clearAnswers() {
reportFlow_.clearAnswers();
}
//****************************************************************************************************************************************************
/// \return The value for the 'showOnStartup' property.
//****************************************************************************************************************************************************
@ -575,6 +649,21 @@ QStringList QMLBackend::availableKeychain() const {
}
//****************************************************************************************************************************************************
/// \return The value for the 'bugCategories' property.
//****************************************************************************************************************************************************
QVariantList QMLBackend::bugCategories() const {
return reportFlow_.categories();
}
//****************************************************************************************************************************************************
/// \return The value for the 'bugQuestions' property.
//****************************************************************************************************************************************************
QVariantList QMLBackend::bugQuestions() const {
return reportFlow_.questions();
}
//****************************************************************************************************************************************************
/// \return The value for the 'currentKeychain' property.
//****************************************************************************************************************************************************
@ -680,7 +769,7 @@ void QMLBackend::login(QString const &username, QString const &password) const {
HANDLE_EXCEPTION(
if (username.compare("coco@bandicoot", Qt::CaseInsensitive) == 0) {
throw Exception("User requested bridge-gui to crash by trying to log as coco@bandicoot",
"This error exists for test purposes and should be ignored.", __func__, tailOfLatestBridgeLog());
"This error exists for test purposes and should be ignored.", __func__, tailOfLatestBridgeLog(app().sessionID()));
}
app().grpc().login(username, password);
)
@ -839,14 +928,15 @@ void QMLBackend::triggerReset() const {
//****************************************************************************************************************************************************
/// \param[in] category The category of the bug.
/// \param[in] description The description of the bug.
/// \param[in] address The email address.
/// \param[in] emailClient The email client.
/// \param[in] includeLogs Should the logs be included in the report.
//****************************************************************************************************************************************************
void QMLBackend::reportBug(QString const &description, QString const &address, QString const &emailClient, bool includeLogs) const {
void QMLBackend::reportBug(QString const &category, QString const &description, QString const &address, QString const &emailClient, bool includeLogs) const {
HANDLE_EXCEPTION(
app().grpc().reportBug(description, address, emailClient, includeLogs);
app().grpc().reportBug(category, description, address, emailClient, includeLogs);
)
}
@ -914,7 +1004,6 @@ void QMLBackend::sendBadEventUserFeedback(QString const &userID, bool doResync)
if (!badEventDisplayQueue_.isEmpty()) {
// we introduce a small delay here, so that the user notices the dialog disappear and pops up again.
QTimer::singleShot(500, [&]() { this->displayBadEventDialog(badEventDisplayQueue_.front()); });
}
)
}
@ -989,6 +1078,25 @@ void QMLBackend::setUpdateTrayIcon(QString const &stateString, QString const &st
}
//****************************************************************************************************************************************************
/// \param[in] isOn Does bridge consider internet as on.
//****************************************************************************************************************************************************
void QMLBackend::internetStatusChanged(bool isOn) {
HANDLE_EXCEPTION(
if (isInternetOn_ == isOn) {
return;
}
isInternetOn_ = isOn;
if (isOn) {
emit internetOn();
} else {
emit internetOff();
}
)
}
//****************************************************************************************************************************************************
/// \param[in] imapPort The IMAP port.
/// \param[in] smtpPort The SMTP port.
@ -1152,11 +1260,12 @@ void QMLBackend::connectGrpcEvents() {
GRPCClient *client = &app().grpc();
// app events
connect(client, &GRPCClient::internetStatus, this, [&](bool isOn) { if (isOn) { emit internetOn(); } else { emit internetOff(); }});
connect(client, &GRPCClient::internetStatus, this, &QMLBackend::internetStatusChanged);
connect(client, &GRPCClient::toggleAutostartFinished, this, &QMLBackend::toggleAutostartFinished);
connect(client, &GRPCClient::resetFinished, this, &QMLBackend::onResetFinished);
connect(client, &GRPCClient::reportBugFinished, this, &QMLBackend::reportBugFinished);
connect(client, &GRPCClient::reportBugSuccess, this, &QMLBackend::bugReportSendSuccess);
connect(client, &GRPCClient::reportBugFallback, this, &QMLBackend::bugReportSendFallback);
connect(client, &GRPCClient::reportBugError, this, &QMLBackend::bugReportSendError);
connect(client, &GRPCClient::showMainWindow, [&]() { this->showMainWindow("gRPC showMainWindow event"); });

View File

@ -24,6 +24,7 @@
#include "BuildConfig.h"
#include "TrayIcon.h"
#include "UserList.h"
#include <bridgepp/BugReportFlow/BugReportFlow.h>
#include <bridgepp/GRPC/GRPCClient.h>
#include <bridgepp/GRPC/GRPCUtils.h>
#include <bridgepp/Worker/Overseer.h>
@ -45,17 +46,24 @@ public: // member functions.
void init(GRPCConfig const &serviceConfig); ///< Initialize the backend.
bool waitForEventStreamReaderToFinish(qint32 timeoutMs); ///< Wait for the event stream reader to finish.
UserList const& users() const; ///< Return the list of users
bool isInternetOn() const; ///< Check if bridge considers internet as on.
void showMainWindow(QString const &reason); ///< Show the main window.
void showHelp(QString const &reason); ///< Show the help page.
void showSettings(QString const &reason); ///< Show the settings page.
void selectUser(QString const &userID, bool forceShowWindow, QString const &reason); ///< Select the user and display its account details (or login screen).
// invokable methods can be called from QML. They generally return a value, which slots cannot do.
// invocable methods can be called from QML. They generally return a value, which slots cannot do.
Q_INVOKABLE static QString buildYear(); ///< Return the application build year.
Q_INVOKABLE QPoint getCursorPos() const; ///< Retrieve the cursor position.
Q_INVOKABLE bool isPortFree(int port) const; ///< Check if a given network port is available.
Q_INVOKABLE QString nativePath(QUrl const &url) const; ///< Retrieve the native path of a local URL.
Q_INVOKABLE bool areSameFileOrFolder(QUrl const &lhs, QUrl const &rhs) const; ///< Check if two local URL point to the same file.
Q_INVOKABLE QString getBugCategory(quint8 categoryId) const; ///< Get a Category name.
Q_INVOKABLE QVariantList getQuestionSet(quint8 categoryId) const; ///< Retrieve the set of question for a given bug category.
Q_INVOKABLE void setQuestionAnswer(quint8 questionId, QString const &answer); ///< Feed an answer for a given question.
Q_INVOKABLE QString getQuestionAnswer(quint8 questionId) const; ///< Get the answer for a given question.
Q_INVOKABLE QString collectAnswers(quint8 categoryId) const; ///< Collect answer for a given set of questions.
Q_INVOKABLE void clearAnswers(); ///< Clear all collected answers.
public: // Qt/QML properties. Note that the NOTIFY-er signal is required even for read-only properties (QML warning otherwise)
Q_PROPERTY(bool showOnStartup READ showOnStartup NOTIFY showOnStartupChanged)
@ -86,10 +94,11 @@ public: // Qt/QML properties. Note that the NOTIFY-er signal is required even fo
Q_PROPERTY(QString currentEmailClient READ currentEmailClient NOTIFY currentEmailClientChanged)
Q_PROPERTY(QStringList availableKeychain READ availableKeychain NOTIFY availableKeychainChanged)
Q_PROPERTY(QString currentKeychain READ currentKeychain NOTIFY currentKeychainChanged)
Q_PROPERTY(QVariantList bugCategories READ bugCategories NOTIFY bugCategoriesChanged)
Q_PROPERTY(QVariantList bugQuestions READ bugQuestions NOTIFY bugQuestionsChanged)
Q_PROPERTY(UserList *users MEMBER users_ NOTIFY usersChanged)
Q_PROPERTY(bool dockIconVisible READ dockIconVisible WRITE setDockIconVisible NOTIFY dockIconVisibleChanged)
// Qt Property system setters & getters.
bool showOnStartup() const; ///< Getter for the 'showOnStartup' property.
void setShowSplashScreen(bool show); ///< Setter for the 'showSplashScreen' property.
@ -124,6 +133,8 @@ public: // Qt/QML properties. Note that the NOTIFY-er signal is required even fo
QString currentEmailClient() const; ///< Getter for the 'currentEmail' property.
QStringList availableKeychain() const; ///< Getter for the 'availableKeychain' property.
QString currentKeychain() const; ///< Getter for the 'currentKeychain' property.
QVariantList bugCategories() const; ///< Getter for the 'bugCategories' property.
QVariantList bugQuestions() const; ///< Getter for the 'bugQuestions' property.
void setDockIconVisible(bool visible); ///< Setter for the 'dockIconVisible' property.
bool dockIconVisible() const;; ///< Getter for the 'dockIconVisible' property.
@ -153,6 +164,8 @@ signals: // Signal used by the Qt property system. Many of them are unused but r
void tagChanged(QString const &tag); ///<Signal for the change of the 'tag' property.
void currentEmailClientChanged(QString const &email); ///<Signal for the change of the 'currentEmailClient' property.
void currentKeychainChanged(QString const &keychain); ///<Signal for the change of the 'currentKeychain' property.
void bugCategoriesChanged(QVariantList const &bugCategories); ///<Signal for the change of the 'bugCategories' property.
void bugQuestionsChanged(QVariantList const &bugQuestions); ///<Signal for the change of the 'bugQuestions' property.
void availableKeychainChanged(QStringList const &keychains); ///<Signal for the change of the 'availableKeychain' property.
void hostnameChanged(QString const &hostname); ///<Signal for the change of the 'hostname' property.
void isAutostartOnChanged(bool value); ///<Signal for the change of the 'isAutostartOn' property.
@ -181,7 +194,7 @@ public slots: // slot for signals received from QML -> To be forwarded to Bridge
void checkUpdates() const; ///< Slot for the update check.
void installUpdate() const; ///< Slot for the update install.
void triggerReset() const; ///< Slot for the triggering of reset.
void reportBug(QString const &description, QString const &address, QString const &emailClient, bool includeLogs) const; ///< Slot for the bug report.
void reportBug(QString const &category, QString const &description, QString const &address, QString const &emailClient, bool includeLogs) const; ///< Slot for the bug report.
void exportTLSCertificates() const; ///< Slot for the export of the TLS certificates.
void onResetFinished(); ///< Slot for the reset finish signal.
void onVersionChanged(); ///< Slot for the version change signal.
@ -198,6 +211,7 @@ public slots: // slots for functions that need to be processed locally.
void setUpdateTrayIcon(QString const& stateString, QString const &statusIcon); ///< Set the tray icon to 'update' state.
public slots: // slot for signals received from gRPC that need transformation instead of simple forwarding
void internetStatusChanged(bool isOn); ///< Check if bridge considers internet as on.
void onMailServerSettingsChanged(int imapPort, int smtpPort, bool useSSLForIMAP, bool useSSLForSMTP); ///< Slot for the ConnectionModeChanged gRPC event.
void onGenericError(bridgepp::ErrorInfo const &info); ///< Slot for generic errors received from the gRPC service.
void onLoginFinished(QString const &userID, bool wasSignedOut); ///< Slot for LoginFinished gRPC event.
@ -252,6 +266,7 @@ signals: // Signals received from the Go backend, to be forwarded to QML
void resetFinished(); ///< Signal for the 'resetFinished' gRPC stream event.
void reportBugFinished(); ///< Signal for the 'reportBugFinished' gRPC stream event.
void bugReportSendSuccess(); ///< Signal for the 'bugReportSendSuccess' gRPC stream event.
void bugReportSendFallback(); ///< Signal for the 'bugReportSendFallback' gRPC stream event.
void bugReportSendError(); ///< Signal for the 'bugReportSendError' gRPC stream event.
void showMainWindow(); ///< Signal for the 'showMainWindow' gRPC stream event.
void hideMainWindow(); ///< Signal for the 'hideMainWindow' gRPC stream event.
@ -280,8 +295,10 @@ private: // data members
int smtpPort_ { 0 }; ///< The cached value for the SMTP port.
bool useSSLForIMAP_ { false }; ///< The cached value for useSSLForIMAP.
bool useSSLForSMTP_ { false }; ///< The cached value for useSSLForSMTP.
bool isInternetOn_ { true }; ///< Does bridge consider internet as on?
QList<QString> badEventDisplayQueue_; ///< THe queue for displaying 'bad event feedback request dialog'.
std::unique_ptr<TrayIcon> trayIcon_;
std::unique_ptr<TrayIcon> trayIcon_; ///< The tray icon for the application.
bridgepp::BugReportFlow reportFlow_; ///< The bug report flow.
friend class AppController;
};

View File

@ -6,7 +6,11 @@
<file>qml/Banner.qml</file>
<file>qml/Bridge.qml</file>
<file>qml/bridgeqml.qmlproject</file>
<file>qml/BugCategoryView.qml</file>
<file>qml/BugQuestionView.qml</file>
<file>qml/BugReportFlow.qml</file>
<file>qml/BugReportView.qml</file>
<file>qml/CategoryItem.qml</file>
<file>qml/Configuration.qml</file>
<file>qml/ConfigurationItem.qml</file>
<file>qml/ContentWrapper.qml</file>
@ -19,6 +23,7 @@
<file>qml/icons/ic-card-identity.svg</file>
<file>qml/icons/ic-check.svg</file>
<file>qml/icons/ic-chevron-down.svg</file>
<file>qml/icons/ic-chevron-right.svg</file>
<file>qml/icons/ic-chevron-up.svg</file>
<file>qml/icons/ic-cog-wheel.svg</file>
<file>qml/icons/ic-connected.svg</file>
@ -31,6 +36,7 @@
<file>qml/icons/ic-eye-slash.svg</file>
<file>qml/icons/ic-eye.svg</file>
<file>qml/icons/ic-illustrative-view-html-code.svg</file>
<file>qml/icons/ic-info-circle.svg</file>
<file>qml/icons/ic-info-circle-filled.svg</file>
<file>qml/icons/ic-info.svg</file>
<file>qml/icons/ic-microsoft-outlook.svg</file>
@ -94,6 +100,8 @@
<file>qml/Proton/TextArea.qml</file>
<file>qml/Proton/TextField.qml</file>
<file>qml/Proton/Toggle.qml</file>
<file>qml/QuestionItem.qml</file>
<file>qml/Resources/bug_report_flow.json</file>
<file>qml/SettingsItem.qml</file>
<file>qml/SettingsView.qml</file>
<file>qml/SetupGuide.qml</file>

View File

@ -192,6 +192,8 @@ TrayIcon::TrayIcon()
if (!onLinux()) { // we disable this on linux because of a Qt bug that causes the signal to be emitted for other apps (GODT-2750)
connect(this, &TrayIcon::messageClicked, []() { app().backend().showMainWindow("tray icon popup notification clicked"); });
}
this->setIcon();
this->show();
// TrayIcon does not expose its screen, so we connect relevant screen events to our DPI change handler.
@ -348,6 +350,7 @@ void TrayIcon::refreshContextMenu() {
return;
}
bool const internetOn = app().backend().isInternetOn();
menu_->clear();
menu_->addAction(statusIcon_, stateString_, []() {app().backend().showMainWindow("tray menu status clicked");});
menu_->addSeparator();
@ -359,7 +362,9 @@ void TrayIcon::refreshContextMenu() {
User const &user = *users.get(i);
UserState const state = user.state();
auto action = new QAction(user.primaryEmailOrUsername());
action->setIcon((UserState::Connected == state) ? greenDot_ : (UserState::Locked == state ? orangeDot_ : greyDot_));
if (internetOn) {
action->setIcon((UserState::Connected == state) ? greenDot_ : (UserState::Locked == state ? orangeDot_ : greyDot_));
}
action->setData(user.id());
connect(action, &QAction::triggered, this, &TrayIcon::onUserClicked);
if ((i < 10) && onMac) {

View File

@ -63,7 +63,7 @@ BRIDGE_BUILD_ENV= ${BRIDGE_BUILD_ENV:-"dev"}
git submodule update --init --recursive ${VCPKG_ROOT}
check_exit "Failed to initialize vcpkg as a submodule."
echo submodule udpated
echo submodule updated
VCPKG_EXE="${VCPKG_ROOT}/vcpkg"
VCPKG_BOOTSTRAP="${VCPKG_ROOT}/bootstrap-vcpkg.sh"

View File

@ -137,12 +137,21 @@ bool checkSingleInstance(QLockFile &lock) {
if (lock.getLockInfo(&pid, &hostname, &appName)) {
details = QString("(PID : %1 - Host : %2 - App : %3)").arg(pid).arg(hostname, appName);
}
if (lock.error() == QLockFile::LockFailedError) {
// This happens if a stale lock file exists and another process uses that PID.
// Try removing the stale file, which will fail if a real process is holding a
// file-level lock. A false error is more problematic than not locking properly
// on corner-case systems.
if (lock.removeStaleLockFile() && lock.tryLock()) {
app().log().info("Removed stale lock file");
app().log().info(QString("lock file created %1").arg(lock.fileName()));
return true;
}
}
app().log().error(QString("Instance already exists %1 %2").arg(lock.fileName(), details));
return false;
} else {
app().log().info(QString("lock file created %1").arg(lock.fileName()));
}
app().log().info(QString("lock file created %1").arg(lock.fileName()));
return true;
}
@ -305,22 +314,23 @@ int main(int argc, char *argv[]) {
// these outputs and output them on the command-line.
log.info(QString("New Sentry reporter - id: %1.").arg(getProtectedHostname()));
QString bridgeexec;
QString const &sessionID = app().sessionID();
QString bridgeExe;
if (!cliOptions.attach) {
if (isBridgeRunning()) {
throw Exception("An orphan instance of bridge is already running. Please terminate it and relaunch the application.",
QString(), __FUNCTION__, tailOfLatestBridgeLog());
QString(), __FUNCTION__, tailOfLatestBridgeLog(sessionID));
}
// before launching bridge, we remove any trailing service config file, because we need to make sure we get a newly generated one.
FocusGRPCClient::removeServiceConfigFile(configDir);
GRPCClient::removeServiceConfigFile(configDir);
bridgeexec = launchBridge(cliOptions.bridgeArgs);
bridgeExe = launchBridge(cliOptions.bridgeArgs);
}
log.info(QString("Retrieving gRPC service configuration from '%1'").arg(QDir::toNativeSeparators(grpcServerConfigPath(configDir))));
app().backend().init(GRPCClient::waitAndRetrieveServiceConfig(configDir, cliOptions.attach ? 0 : grpcServiceConfigWaitDelayMs,
app().bridgeMonitor()));
app().backend().init(GRPCClient::waitAndRetrieveServiceConfig(sessionID, configDir,
cliOptions.attach ? 0 : grpcServiceConfigWaitDelayMs, app().bridgeMonitor()));
if (!cliOptions.attach) {
GRPCClient::removeServiceConfigFile(configDir);
}
@ -378,9 +388,9 @@ int main(int argc, char *argv[]) {
QStringList args = cliOptions.bridgeGuiArgs;
args.append(waitFlag);
args.append(mainexec);
if (!bridgeexec.isEmpty()) {
if (!bridgeExe.isEmpty()) {
args.append(waitFlag);
args.append(bridgeexec);
args.append(bridgeExe);
}
app().setLauncherArgs(cliOptions.launcher, args);
result = QGuiApplication::exec();

View File

@ -1,251 +1,252 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Proton
Item {
id: root
property ColorScheme colorScheme
property var user
enum ViewType {
SmallView,
LargeView
}
property var _spacing: 12 * ProtonStyle.px
property color progressColor : {
if (!root.enabled) return root.colorScheme.text_weak
if (root.type == AccountDelegate.SmallView) return root.colorScheme.text_weak
if (root.user && root.user.isSyncing) return root.colorScheme.text_weak
if (root.progressRatio < .50) return root.colorScheme.signal_success
if (root.progressRatio < .75) return root.colorScheme.signal_warning
return root.colorScheme.signal_danger
property ColorScheme colorScheme
property color progressColor: {
if (!root.enabled)
return root.colorScheme.text_weak;
if (root.type === AccountDelegate.SmallView)
return root.colorScheme.text_weak;
if (root.user && root.user.isSyncing)
return root.colorScheme.text_weak;
if (root.progressRatio < .50)
return root.colorScheme.signal_success;
if (root.progressRatio < .75)
return root.colorScheme.signal_warning;
return root.colorScheme.signal_danger;
}
property real progressRatio: {
if (!root.user)
return 0
return root.user.isSyncing ? root.user.syncProgress : reasonableFraction(root.user.usedBytes, root.user.totalBytes)
return 0;
return root.user.isSyncing ? root.user.syncProgress : reasonableFraction(root.user.usedBytes, root.user.totalBytes);
}
property string totalSpace: root.spaceWithUnits(root.user ? root.reasonableBytes(root.user.totalBytes) : 0)
property var type: AccountDelegate.SmallView
property string usedSpace: root.spaceWithUnits(root.user ? root.reasonableBytes(root.user.usedBytes) : 0)
function reasonableFraction(used, total){
var usedSafe = root.reasonableBytes(used)
var totalSafe = root.reasonableBytes(total)
if (totalSafe == 0 || usedSafe == 0) return 0
if (totalSafe <= usedSafe) return 1
return usedSafe / totalSafe
}
function reasonableBytes(bytes){
var safeBytes = bytes+0
if (safeBytes != bytes) return 0
if (safeBytes < 0) return 0
return Math.ceil(safeBytes)
}
function spaceWithUnits(bytes){
if (bytes*1 !== bytes || bytes == 0 ) return "0 kB"
var units = ['B',"kB", "MB", "GB", "TB"];
var i = parseInt(Math.floor(Math.log(bytes)/Math.log(1024)));
return Math.round(bytes*10 / Math.pow(1024, i))/10 + " " + units[i]
}
property var user
function primaryEmail() {
return root.user ? root.user.primaryEmailOrUsername() : ""
return root.user ? root.user.primaryEmailOrUsername() : "";
}
function reasonableBytes(bytes) {
const safeBytes = bytes + 0;
if (safeBytes !== bytes)
return 0;
if (safeBytes < 0)
return 0;
return Math.ceil(safeBytes);
}
function reasonableFraction(used, total) {
const usedSafe = root.reasonableBytes(used);
const totalSafe = root.reasonableBytes(total);
if (totalSafe === 0 || usedSafe === 0)
return 0;
if (totalSafe <= usedSafe)
return 1;
return usedSafe / totalSafe;
}
function spaceWithUnits(bytes) {
if (bytes * 1 !== bytes || bytes === 0)
return "0 kB";
const units = ['B', "kB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return Math.round(bytes * 10 / Math.pow(1024, i)) / 10 + " " + units[i];
}
// width expected to be set by parent object
implicitHeight : children[0].implicitHeight
enum ViewType{
SmallView, LargeView
}
property var type : AccountDelegate.SmallView
implicitHeight: children[0].implicitHeight
RowLayout {
spacing: root._spacing
anchors {
top: root.top
left: root.left
right: root.right
top: root.top
}
Rectangle {
id: avatar
Layout.fillHeight: true
Layout.preferredWidth: height
color: root.colorScheme.background_avatar
radius: ProtonStyle.avatar_radius
color: root.colorScheme.background_avatar
Label {
colorScheme: root.colorScheme
anchors.fill: parent
text: root.user ? root.user.avatarText.toUpperCase(): ""
color: "#FFFFFF"
colorScheme: root.colorScheme
font.weight: Font.Normal
horizontalAlignment: Qt.AlignHCenter
text: root.user ? root.user.avatarText.toUpperCase() : ""
type: {
switch (root.type) {
case AccountDelegate.SmallView: return Label.Body
case AccountDelegate.LargeView: return Label.Title
case AccountDelegate.SmallView:
return Label.Body;
case AccountDelegate.LargeView:
return Label.Title;
}
}
font.weight: Font.Normal
color: "#FFFFFF"
horizontalAlignment: Qt.AlignHCenter
verticalAlignment: Qt.AlignVCenter
}
}
ColumnLayout {
id: account
Layout.fillHeight: true
Layout.fillWidth: true
spacing: 0
Label {
id: labelEmail
Layout.maximumWidth: root.width - (root._spacing + avatar.width)
colorScheme: root.colorScheme
elide: Text.ElideMiddle
text: primaryEmail()
type: {
switch (root.type) {
case AccountDelegate.SmallView: return Label.Body
case AccountDelegate.LargeView: return Label.Title
case AccountDelegate.SmallView:
return Label.Body;
case AccountDelegate.LargeView:
return Label.Title;
}
}
elide: Text.ElideMiddle
MouseArea {
id: labelArea
anchors.fill:parent
anchors.fill: parent
hoverEnabled: true
}
ToolTip {
id: toolTipEmail
visible: labelArea.containsMouse && labelEmail.truncated
text: primaryEmail()
delay: 1000
text: primaryEmail()
visible: labelArea.containsMouse && labelEmail.truncated
background: Rectangle {
border.color: root.colorScheme.background_strong
color: root.colorScheme.background_norm
}
contentItem: Text {
color: root.colorScheme.text_norm
text: toolTipEmail.text
}
}
}
Item { implicitHeight: root.type == AccountDelegate.LargeView ? 6 * ProtonStyle.px : 0 }
Item {
implicitHeight: root.type === AccountDelegate.LargeView ? 6 * ProtonStyle.px : 0
}
RowLayout {
spacing: 0
Label {
color: root.progressColor
colorScheme: root.colorScheme
text: {
if (!root.user)
return qsTr("Signed out")
return qsTr("Signed out");
switch (root.user.state) {
case EUserState.SignedOut:
default:
return qsTr("Signed out")
return qsTr("Signed out");
case EUserState.Locked:
return qsTr("Connecting") + dotsTimer.dots
return qsTr("Connecting") + dotsTimer.dots;
case EUserState.Connected:
if (root.user.isSyncing)
return qsTr("Synchronizing (%1%)").arg(Math.floor(root.user.syncProgress * 100)) + dotsTimer.dots
return qsTr("Synchronizing (%1%)").arg(Math.floor(root.user.syncProgress * 100)) + dotsTimer.dots;
else
return root.usedSpace
return root.usedSpace;
}
}
Timer { // dots animation while connecting & syncing.
id:dotsTimer
property string dots: ""
interval: 500;
repeat: true;
running: (root.user != null) && ((root.user.state === EUserState.Locked) || (root.user.isSyncing))
onTriggered: {
dots += "."
if (dots.length > 3)
dots = ""
}
onRunningChanged: {
dots = ""
}
}
color: root.progressColor
type: {
switch (root.type) {
case AccountDelegate.SmallView: return Label.Caption
case AccountDelegate.LargeView: return Label.Body
case AccountDelegate.SmallView:
return Label.Caption;
case AccountDelegate.LargeView:
return Label.Body;
}
}
Timer {
// dots animation while connecting & syncing.
id: dotsTimer
property string dots: ""
interval: 500
repeat: true
running: (root.user != null) && ((root.user.state === EUserState.Locked) || (root.user.isSyncing))
onRunningChanged: {
dots = "";
}
onTriggered: {
dots += ".";
if (dots.length > 3)
dots = "";
}
}
}
Label {
colorScheme: root.colorScheme
text: root.user && root.user.state == EUserState.Connected && !root.user.isSyncing ? " / " + root.totalSpace : ""
color: root.colorScheme.text_weak
colorScheme: root.colorScheme
text: root.user && root.user.state === EUserState.Connected && !root.user.isSyncing ? " / " + root.totalSpace : ""
type: {
switch (root.type) {
case AccountDelegate.SmallView: return Label.Caption
case AccountDelegate.LargeView: return Label.Body
case AccountDelegate.SmallView:
return Label.Caption;
case AccountDelegate.LargeView:
return Label.Body;
}
}
}
}
Item { implicitHeight: root.type == AccountDelegate.LargeView ? 3 * ProtonStyle.px : 0 }
Item {
implicitHeight: root.type === AccountDelegate.LargeView ? 3 * ProtonStyle.px : 0
}
Rectangle {
id: progress_bar
visible: root.user ? root.type == AccountDelegate.LargeView : false
width: 140 * ProtonStyle.px
color: root.colorScheme.border_weak
height: 4 * ProtonStyle.px
radius: ProtonStyle.progress_bar_radius
color: root.colorScheme.border_weak
visible: root.user ? root.type === AccountDelegate.LargeView : false
width: 140 * ProtonStyle.px
Rectangle {
id: progress_bar_filled
radius: ProtonStyle.progress_bar_radius
color: root.progressColor
visible: root.user ? parent.visible && (root.user.state == EUserState.Connected): false
radius: ProtonStyle.progress_bar_radius
visible: root.user ? parent.visible && (root.user.state === EUserState.Connected) : false
width: Math.min(1, Math.max(0.02, root.progressRatio)) * parent.width
anchors {
top : parent.top
bottom : parent.bottom
left : parent.left
bottom: parent.bottom
left: parent.left
top: parent.top
}
width: Math.min(1,Math.max(0.02,root.progressRatio)) * parent.width
}
}
}
Item {
Layout.fillWidth: true
}

View File

@ -1,42 +1,35 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Proton
Item {
id: root
property bool _connected: root.user ? root.user.state === EUserState.Connected : false
property int _contentWidth: 640
property int _detailsMargin: 25
property int _lineThickness: 1
property int _spacing: 20
property int _topMargin: 32
property ColorScheme colorScheme
property var notifications
property var user
signal showSignIn
signal showSetupGuide(var user, string address)
property int _contentWidth: 640
property int _topMargin: 32
property int _detailsMargin: 25
property int _spacing: 20
property int _lineThickness: 1
property bool _connected: root.user ? root.user.state === EUserState.Connected : false
signal showSignIn
Rectangle {
anchors.fill: parent
@ -45,6 +38,7 @@ Item {
ScrollView {
id: scrollView
anchors.fill: parent
Component.onCompleted: contentItem.boundsBehavior = Flickable.StopAtBounds
ColumnLayout {
@ -54,16 +48,16 @@ Item {
Rectangle {
id: topArea
color: root.colorScheme.background_norm
clip: true
Layout.fillWidth: true
clip: true
color: root.colorScheme.background_norm
implicitHeight: childrenRect.height
ColumnLayout {
id: topLayout
width: _contentWidth
anchors.horizontalCenter: parent.horizontalCenter
spacing: _spacing
width: _contentWidth
RowLayout {
// account delegate with action buttons
@ -73,83 +67,82 @@ Item {
AccountDelegate {
Layout.fillWidth: true
colorScheme: root.colorScheme
user: root.user
type: AccountDelegate.LargeView
enabled: _connected
type: AccountDelegate.LargeView
user: root.user
}
Button {
Layout.alignment: Qt.AlignTop
colorScheme: root.colorScheme
text: qsTr("Sign out")
secondary: true
text: qsTr("Sign out")
visible: _connected
onClicked: {
if (!root.user)
return;
root.user.logout();
}
}
Button {
Layout.alignment: Qt.AlignTop
colorScheme: root.colorScheme
text: qsTr("Sign in")
secondary: true
text: qsTr("Sign in")
visible: root.user ? (root.user.state === EUserState.SignedOut) : false
onClicked: {
if (!root.user)
return;
root.showSignIn();
}
}
Button {
Layout.alignment: Qt.AlignTop
colorScheme: root.colorScheme
icon.source: "/qml/icons/ic-trash.svg"
secondary: true
visible: root.user ? root.user.state !== EUserState.Locked : false
onClicked: {
if (!root.user)
return;
root.notifications.askDeleteAccount(root.user);
}
visible: root.user ? root.user.state !== EUserState.Locked : false
}
}
Rectangle {
Layout.fillWidth: true
height: root._lineThickness
color: root.colorScheme.border_weak
height: root._lineThickness
}
SettingsItem {
colorScheme: root.colorScheme
text: qsTr("Email clients")
Layout.fillWidth: true
actionText: qsTr("Configure")
colorScheme: root.colorScheme
description: qsTr("Using the mailbox details below (re)configure your client.")
showSeparator: splitMode.visible
text: qsTr("Email clients")
type: SettingsItem.Button
visible: _connected && (!root.user.splitMode) || (root.user.addresses.length === 1)
showSeparator: splitMode.visible
onClicked: {
if (!root.user)
return;
root.showSetupGuide(root.user, user.addresses[0]);
}
Layout.fillWidth: true
}
SettingsItem {
id: splitMode
colorScheme: root.colorScheme
text: qsTr("Split addresses")
description: qsTr("Setup multiple email addresses individually.")
type: SettingsItem.Toggle
Layout.fillWidth: true
checked: root.user ? root.user.splitMode : false
visible: _connected && root.user.addresses.length > 1
colorScheme: root.colorScheme
description: qsTr("Setup multiple email addresses individually.")
showSeparator: addressSelector.visible
text: qsTr("Split addresses")
type: SettingsItem.Toggle
visible: _connected && root.user.addresses.length > 1
onClicked: {
if (!splitMode.checked) {
root.notifications.askEnableSplitMode(user);
@ -158,26 +151,23 @@ Item {
root.user.toggleSplitMode(!splitMode.checked);
}
}
Layout.fillWidth: true
}
RowLayout {
Layout.fillWidth: true
Layout.bottomMargin: _spacing
Layout.fillWidth: true
visible: _connected && root.user.splitMode
ComboBox {
id: addressSelector
colorScheme: root.colorScheme
Layout.fillWidth: true
colorScheme: root.colorScheme
model: root.user ? root.user.addresses : null
}
Button {
colorScheme: root.colorScheme
text: qsTr("Configure")
secondary: true
text: qsTr("Configure")
onClicked: {
if (!root.user)
return;
@ -185,25 +175,23 @@ Item {
}
}
}
Rectangle {
height: 0
} // just for some extra space before separator
}
}
Rectangle {
id: bottomArea
Layout.fillWidth: true
implicitHeight: bottomLayout.implicitHeight
color: root.colorScheme.background_weak
implicitHeight: bottomLayout.implicitHeight
ColumnLayout {
id: bottomLayout
width: _contentWidth
anchors.horizontalCenter: parent.horizontalCenter
spacing: _spacing
visible: _connected
width: _contentWidth
Label {
Layout.topMargin: _detailsMargin
@ -211,35 +199,34 @@ Item {
text: qsTr("Mailbox details")
type: Label.Body_semibold
}
RowLayout {
id: configuration
spacing: _spacing
Layout.fillWidth: true
Layout.fillHeight: true
property string currentAddress: addressSelector.displayText
Configuration {
Layout.fillWidth: true
colorScheme: root.colorScheme
title: qsTr("IMAP")
hostname: Backend.hostname
port: Backend.imapPort.toString()
username: configuration.currentAddress
password: root.user ? root.user.password : ""
security: Backend.useSSLForIMAP ? "SSL" : "STARTTLS"
}
Layout.fillHeight: true
Layout.fillWidth: true
spacing: _spacing
Configuration {
Layout.fillWidth: true
colorScheme: root.colorScheme
title: qsTr("SMTP")
hostname: Backend.hostname
port: Backend.smtpPort.toString()
username: configuration.currentAddress
password: root.user ? root.user.password : ""
port: Backend.imapPort.toString()
security: Backend.useSSLForIMAP ? "SSL" : "STARTTLS"
title: qsTr("IMAP")
username: configuration.currentAddress
}
Configuration {
Layout.fillWidth: true
colorScheme: root.colorScheme
hostname: Backend.hostname
password: root.user ? root.user.password : ""
port: Backend.smtpPort.toString()
security: Backend.useSSLForSMTP ? "SSL" : "STARTTLS"
title: qsTr("SMTP")
username: configuration.currentAddress
}
}
}

View File

@ -1,25 +1,19 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import QtQuick.Controls.impl
import Proton
import Notifications
@ -27,34 +21,28 @@ Popup {
id: root
property ColorScheme colorScheme
property Notification notification
property var mainWindow
topMargin: 37
leftMargin: (mainWindow.width - root.implicitWidth)/2
property Notification notification
implicitHeight: contentLayout.implicitHeight + contentLayout.anchors.topMargin + contentLayout.anchors.bottomMargin
implicitWidth: 600 // contentLayout.implicitWidth + contentLayout.anchors.leftMargin + contentLayout.anchors.rightMargin
popupType: ApplicationWindow.PopupType.Banner
shouldShow: notification ? (notification.active && !notification.dismissed) : false
leftMargin: (mainWindow.width - root.implicitWidth) / 2
modal: false
popupType: ApplicationWindow.PopupType.Banner
shouldShow: notification ? (notification.active && !notification.dismissed) : false
topMargin: 37
Action {
id: defaultDismissAction
text: qsTr("OK")
onTriggered: {
if (!root.notification) {
return
return;
}
root.notification.dismissed = true
root.notification.dismissed = true;
}
}
RowLayout {
id: contentLayout
anchors.fill: parent
@ -63,170 +51,148 @@ Popup {
Item {
Layout.fillHeight: true
Layout.fillWidth: true
clip: true
implicitHeight: children[1].implicitHeight + children[1].anchors.topMargin + children[1].anchors.bottomMargin
implicitWidth: children[1].implicitWidth + children[1].anchors.leftMargin + children[1].anchors.rightMargin
Rectangle {
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left
width: parent.width + 10
radius: ProtonStyle.banner_radius
anchors.top: parent.top
color: {
if (!root.notification) {
return "transparent"
return "transparent";
}
switch (root.notification.type) {
case Notification.NotificationType.Info:
return root.colorScheme.signal_info
case Notification.NotificationType.Success:
return root.colorScheme.signal_success
case Notification.NotificationType.Warning:
return root.colorScheme.signal_warning
case Notification.NotificationType.Danger:
return root.colorScheme.signal_danger
case Notification.NotificationType.Info:
return root.colorScheme.signal_info;
case Notification.NotificationType.Success:
return root.colorScheme.signal_success;
case Notification.NotificationType.Warning:
return root.colorScheme.signal_warning;
case Notification.NotificationType.Danger:
return root.colorScheme.signal_danger;
}
}
radius: ProtonStyle.banner_radius
width: parent.width + 10
}
RowLayout {
anchors.fill: parent
anchors.topMargin: 14
anchors.bottomMargin: 14
anchors.fill: parent
anchors.leftMargin: 16
anchors.topMargin: 14
spacing: 8
ColorImage {
color: root.colorScheme.text_invert
width: 24
height: 24
sourceSize.width: 24
sourceSize.height: 24
Layout.preferredHeight: 24
Layout.preferredWidth: 24
color: root.colorScheme.text_invert
height: 24
source: {
if (!root.notification) {
return ""
return "";
}
switch (root.notification.type) {
case Notification.NotificationType.Info:
return "/qml/icons/ic-info-circle-filled.svg"
case Notification.NotificationType.Success:
return "/qml/icons/ic-info-circle-filled.svg"
case Notification.NotificationType.Warning:
return "/qml/icons/ic-exclamation-circle-filled.svg"
case Notification.NotificationType.Danger:
return "/qml/icons/ic-exclamation-circle-filled.svg"
case Notification.NotificationType.Info:
return "/qml/icons/ic-info-circle-filled.svg";
case Notification.NotificationType.Success:
return "/qml/icons/ic-info-circle-filled.svg";
case Notification.NotificationType.Warning:
return "/qml/icons/ic-exclamation-circle-filled.svg";
case Notification.NotificationType.Danger:
return "/qml/icons/ic-exclamation-circle-filled.svg";
}
}
sourceSize.height: 24
sourceSize.width: 24
width: 24
}
Label {
colorScheme: root.colorScheme
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
Layout.leftMargin: 16
color: root.colorScheme.text_invert
colorScheme: root.colorScheme
text: root.notification ? root.notification.description : ""
wrapMode: Text.WordWrap
}
}
}
Rectangle {
Layout.fillHeight: true
width: 1
color: {
if (!root.notification) {
return "transparent"
return "transparent";
}
switch (root.notification.type) {
case Notification.NotificationType.Info:
return root.colorScheme.signal_info_active
case Notification.NotificationType.Success:
return root.colorScheme.signal_success_active
case Notification.NotificationType.Warning:
return root.colorScheme.signal_warning_active
case Notification.NotificationType.Danger:
return root.colorScheme.signal_danger_active
case Notification.NotificationType.Info:
return root.colorScheme.signal_info_active;
case Notification.NotificationType.Success:
return root.colorScheme.signal_success_active;
case Notification.NotificationType.Warning:
return root.colorScheme.signal_warning_active;
case Notification.NotificationType.Danger:
return root.colorScheme.signal_danger_active;
}
}
width: 1
}
Button {
colorScheme: root.colorScheme
Layout.fillHeight: true
id: actionButton
Layout.fillHeight: true
action: (root.notification && root.notification.action.length > 0) ? root.notification.action[0] : defaultDismissAction
colorScheme: root.colorScheme
background: Item {
clip: true
Rectangle {
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: parent.right
width: parent.width + 10
radius: ProtonStyle.banner_radius
anchors.top: parent.top
color: {
if (!root.notification) {
return "transparent"
return "transparent";
}
var norm
var hover
var active
let norm;
let hover;
let active;
switch (root.notification.type) {
case Notification.NotificationType.Info:
norm = root.colorScheme.signal_info
hover = root.colorScheme.signal_info_hover
active = root.colorScheme.signal_info_active
case Notification.NotificationType.Info:
norm = root.colorScheme.signal_info;
hover = root.colorScheme.signal_info_hover;
active = root.colorScheme.signal_info_active;
break;
case Notification.NotificationType.Success:
norm = root.colorScheme.signal_success
hover = root.colorScheme.signal_success_hover
active = root.colorScheme.signal_success_active
case Notification.NotificationType.Success:
norm = root.colorScheme.signal_success;
hover = root.colorScheme.signal_success_hover;
active = root.colorScheme.signal_success_active;
break;
case Notification.NotificationType.Warning:
norm = root.colorScheme.signal_warning
hover = root.colorScheme.signal_warning_hover
active = root.colorScheme.signal_warning_active
case Notification.NotificationType.Warning:
norm = root.colorScheme.signal_warning;
hover = root.colorScheme.signal_warning_hover;
active = root.colorScheme.signal_warning_active;
break;
case Notification.NotificationType.Danger:
norm = root.colorScheme.signal_danger
hover = root.colorScheme.signal_danger_hover
active = root.colorScheme.signal_danger_active
case Notification.NotificationType.Danger:
norm = root.colorScheme.signal_danger;
hover = root.colorScheme.signal_danger_hover;
active = root.colorScheme.signal_danger_active;
break;
}
if (actionButton.down) {
return active
return active;
}
if (actionButton.enabled && (actionButton.highlighted || actionButton.hovered || actionButton.checked)) {
return hover
return hover;
}
if (actionButton.loading) {
return hover
return hover;
}
return norm
return norm;
}
radius: ProtonStyle.banner_radius
width: parent.width + 10
}
}
}

View File

@ -1,129 +1,112 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQml
import QtQuick
import QtQuick.Window
import Qt.labs.platform
import Proton
import Notifications
QtObject {
id: root
function bound(num, lowerLimit, upperLimit) {
return Math.max(lowerLimit, Math.min(upperLimit, num))
property MainWindow _mainWindow: MainWindow {
id: mainWindow
notifications: root._notifications
title: root.title
visible: false
onVisibleChanged: {
Backend.dockIconVisible = visible;
}
Connections {
function onColorSchemeNameChanged(scheme) {
root.setColorScheme();
}
function onDiskCacheUnavailable() {
mainWindow.showAndRise();
}
function onHideMainWindow() {
mainWindow.hide();
}
target: Backend
}
}
property var title: Backend.appname
property Notifications _notifications: Notifications {
id: notifications
frontendMain: mainWindow
}
property NotificationFilter _trayNotificationFilter: NotificationFilter {
id: trayNotificationFilter
source: root._notifications ? root._notifications.all : undefined
onTopmostChanged: {
if (topmost) {
switch (topmost.type) {
case Notification.NotificationType.Danger:
Backend.setErrorTrayIcon(topmost.brief, topmost.icon)
return
case Notification.NotificationType.Warning:
Backend.setWarnTrayIcon(topmost.brief, topmost.icon)
return
case Notification.NotificationType.Info:
Backend.setUpdateTrayIcon(topmost.brief, topmost.icon)
return
id: trayNotificationFilter
source: root._notifications ? root._notifications.all : undefined
onTopmostChanged: {
if (topmost) {
switch (topmost.type) {
case Notification.NotificationType.Danger:
Backend.setErrorTrayIcon(topmost.brief, topmost.icon);
return;
case Notification.NotificationType.Warning:
Backend.setWarnTrayIcon(topmost.brief, topmost.icon);
return;
case Notification.NotificationType.Info:
Backend.setUpdateTrayIcon(topmost.brief, topmost.icon);
return;
}
}
Backend.setNormalTrayIcon()
Backend.setNormalTrayIcon();
}
}
property var title: Backend.appname
property MainWindow _mainWindow: MainWindow {
id: mainWindow
visible: false
title: root.title
notifications: root._notifications
onVisibleChanged: {
Backend.dockIconVisible = visible
}
Connections {
target: Backend
function onDiskCacheUnavailable() {
mainWindow.showAndRise()
}
function onColorSchemeNameChanged(scheme) { root.setColorScheme() }
function onHideMainWindow() {
mainWindow.hide();
}
}
function bound(num, lowerLimit, upperLimit) {
return Math.max(lowerLimit, Math.min(upperLimit, num));
}
function setColorScheme() {
if (Backend.colorSchemeName === "light")
ProtonStyle.currentStyle = ProtonStyle.lightStyle;
if (Backend.colorSchemeName === "dark")
ProtonStyle.currentStyle = ProtonStyle.darkStyle;
}
Component.onCompleted: {
if (!Backend) {
console.log("Backend not loaded")
console.log("Backend not loaded");
}
root.setColorScheme()
root.setColorScheme();
if (!Backend.users) {
console.log("users not loaded")
console.log("users not loaded");
}
var c = Backend.users.count
var u = Backend.users.get(0)
const c = Backend.users.count;
const u = Backend.users.get(0);
// DEBUG
if (c !== 0) {
console.log("users non zero", c)
console.log("first user", u )
console.log("users non zero", c);
console.log("first user", u);
}
if (c === 0) {
mainWindow.showAndRise()
mainWindow.showAndRise();
}
if (u) {
if (c === 1 && (u.state === EUserState.SignedOut)) {
mainWindow.showAndRise()
mainWindow.showAndRise();
}
}
Backend.guiReady()
if (Backend.showOnStartup || Backend.showSplashScreen) {
mainWindow.showAndRise()
Backend.guiReady();
if (Backend.showOnStartup || Backend.showSplashScreen) {
mainWindow.showAndRise();
}
}
function setColorScheme() {
if (Backend.colorSchemeName === "light") ProtonStyle.currentStyle = ProtonStyle.lightStyle
if (Backend.colorSchemeName === "dark") ProtonStyle.currentStyle = ProtonStyle.darkStyle
}
}

View File

@ -0,0 +1,52 @@
// Copyright (c) 2023 Proton AG
// This file is part of Proton Mail Bridge.
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Proton
SettingsView {
id: root
signal categorySelected(int categoryId)
fillHeight: true
property var categories: Backend.bugCategories
Label {
Layout.fillWidth: true
colorScheme: root.colorScheme
text: qsTr("What do you want to report?")
type: Label.Heading
}
Repeater {
model: root.categories
CategoryItem {
Layout.fillWidth: true
actionIcon: "/qml/icons/ic-chevron-right.svg"
colorScheme: root.colorScheme
text: modelData.name
hint: modelData.hint ? modelData.hint: ""
onClicked: root.categorySelected(index)
}
}
// fill height so the footer label will always be attached to the bottom
Item {
Layout.fillHeight: true
}
}

View File

@ -0,0 +1,130 @@
// Copyright (c) 2023 Proton AG
// This file is part of Proton Mail Bridge.
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Proton
SettingsView {
id: root
property var questions:Backend.bugQuestions
property var categoryId:0
property var questionSet:ListModel{}
property bool error: questionRepeater.error
signal questionAnswered
function setCategoryId(catId) {
root.categoryId = catId;
}
function submit() {
root.questionAnswered();
}
fillHeight: true
onCategoryIdChanged: {
root.questionSet = Backend.getQuestionSet(root.categoryId)
}
Label {
Layout.fillWidth: true
colorScheme: root.colorScheme
text: qsTr("Provide more details")
type: Label.Heading
}
Label {
Layout.fillWidth: true
colorScheme: root.colorScheme
text: qsTr(Backend.getBugCategory(root.categoryId))
type: Label.Title
}
TextEdit {
Layout.fillWidth: true
color: root.colorScheme.text_weak
font.family: ProtonStyle.font_family
font.letterSpacing: ProtonStyle.caption_letter_spacing
font.pixelSize: ProtonStyle.caption_font_size
font.weight: ProtonStyle.fontWeight_400
textFormat: Text.MarkdownText
readOnly: true
selectByMouse: true
selectedTextColor: root.colorScheme.text_invert
// No way to set lineHeight: ProtonStyle.caption_line_height
selectionColor: root.colorScheme.interaction_norm
text: qsTr("* Mandatory questions")
wrapMode: Text.WordWrap
}
Repeater {
id: questionRepeater
model: root.questionSet
property bool error :{
for (var i = 0; i < questionRepeater.count; i++) {
if (questionRepeater.itemAt(i).error)
return true;
}
return false;
}
function validate(){
for (var i = 0; i < questionRepeater.count; i++) {
questionRepeater.itemAt(i).validate()
}
}
QuestionItem {
Layout.fillWidth: true
colorScheme: root.colorScheme
showSeparator: index < (root.questionSet.length - 1)
text: root.questions[modelData].text
tips: root.questions[modelData].tips ? root.questions[modelData].tips : ""
label: root.questions[modelData].label ? root.questions[modelData].label : ""
type: root.questions[modelData].type
mandatory: root.questions[modelData].mandatory ? root.questions[modelData].mandatory : false
answerList: root.questions[modelData].answerList ? root.questions[modelData].answerList : []
maxChar: root.questions[modelData].maxChar ? root.questions[modelData].maxChar : 150
onAnswerChanged: {
Backend.setQuestionAnswer(modelData, answer);
}
Connections {
function onVisibleChanged() {
setDefaultValue(Backend.getQuestionAnswer(modelData))
}
target: root
}
}
}
// fill height so the footer label will always be attached to the bottom
Item {
Layout.fillHeight: true
}
Button {
id: continueButton
colorScheme: root.colorScheme
enabled: !loading && !root.error
text: qsTr("Continue")
onClicked: {
questionRepeater.validate()
if (!root.error)
submit();
}
}
}

View File

@ -0,0 +1,99 @@
// Copyright (c) 2023 Proton AG
// This file is part of Proton Mail Bridge.
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Proton
import Notifications
Item {
id: root
property ColorScheme colorScheme
property string selectedAddress
property int categoryId: -1
signal back
signal bugReportWasSent
onVisibleChanged: {
root.showBugCategory();
}
function showBugCategory() {
bugReportFlow.currentIndex = 0;
}
function showBugQuestion() {
bugQuestion.setCategoryId(root.categoryId);
bugQuestion.positionViewAtBegining();
bugReportFlow.currentIndex = 1;
}
function showBugReport() {
bugReport.setCategoryId(root.categoryId);
bugReportFlow.currentIndex = 2;
}
Rectangle {
anchors.fill: parent
Layout.fillHeight: true // right content background
Layout.fillWidth: true
color: colorScheme.background_norm
StackLayout {
id: bugReportFlow
anchors.fill: parent
BugCategoryView {
// 0
id: bugCategory
colorScheme: root.colorScheme
onBack: {
root.back()
}
onCategorySelected: function(categoryId){
root.categoryId = categoryId
root.showBugQuestion();
}
}
BugQuestionView {
// 1
id: bugQuestion
colorScheme: root.colorScheme
onBack: {
root.showBugCategory();
}
onQuestionAnswered: {
root.showBugReport();
}
}
BugReportView {
// 2
id: bugReport
colorScheme: root.colorScheme
selectedAddress: root.selectedAddress
onBack: {
root.showBugQuestion();
}
onBugReportWasSent: {
Backend.clearAnswers();
root.bugReportWasSent();
}
}
}
}
}

View File

@ -1,202 +1,161 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Proton
SettingsView {
id: root
fillHeight: true
property var selectedAddress
property var categoryId:-1
property string category: Backend.getBugCategory(root.categoryId)
signal bugReportWasSent()
signal bugReportWasSent
Label {
text: qsTr("Report a problem")
colorScheme: root.colorScheme
type: Label.Heading
function isValidEmail(text) {
const reEmail = /^[^@]+@[^@]+\.[A-Za-z]+\s*$/;
return reEmail.test(text);
}
function setCategoryId(catId) {
root.categoryId = catId;
}
function setDefaultValue() {
description.text = Backend.collectAnswers(root.categoryId);
address.text = root.selectedAddress;
emailClient.text = Backend.currentEmailClient;
includeLogs.checked = true;
}
function submit() {
sendButton.loading = true;
Backend.reportBug(root.category, description.text, address.text, emailClient.text, includeLogs.checked);
}
fillHeight: true
onVisibleChanged: {
root.setDefaultValue();
}
Label {
colorScheme: root.colorScheme
text: qsTr("Send report")
type: Label.Heading
}
TextArea {
id: description
property int _minLength: 150
property int _maxLength: 800
label: qsTr("Description")
colorScheme: root.colorScheme
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumHeight: heightForLinesVisible(4)
hint: description.text.length + "/" + _maxLength
placeholderText: qsTr("Tell us what went wrong or isn't working (min. %1 characters).").arg(_minLength)
validator: function(text) {
if (description.text.length < description._minLength) {
return qsTr("Enter a problem description (min. %1 characters).").arg(_minLength)
}
if (description.text.length > description._maxLength) {
return qsTr("Enter a problem description (max. %1 characters).").arg(_maxLength)
}
return
}
onTextChanged: {
// Rise max length error immediately while typing
if (description.text.length > description._maxLength) {
validate()
}
}
KeyNavigation.priority: KeyNavigation.BeforeItem
KeyNavigation.tab: address
Layout.fillHeight: true
Layout.fillWidth: true
Layout.minimumHeight: heightForLinesVisible(4)
colorScheme: root.colorScheme
textFormat: Text.MarkdownText
// set implicitHeight to explicit height because se don't
// want TextArea implicitHeight (which is height of all text)
// to be considered in SettingsView internal scroll view
implicitHeight: height
label: "Your answers to: " + qsTr(root.category);
readOnly : true
}
TextField {
id: address
label: qsTr("Your contact email")
colorScheme: root.colorScheme
Layout.fillWidth: true
colorScheme: root.colorScheme
label: qsTr("Your contact email")
placeholderText: qsTr("e.g. jane.doe@protonmail.com")
validator: function(str) {
validator: function (str) {
if (!isValidEmail(str)) {
return qsTr("Enter valid email address")
return qsTr("Enter valid email address");
}
return
return;
}
}
TextField {
id: emailClient
label: qsTr("Your email client (including version)")
colorScheme: root.colorScheme
Layout.fillWidth: true
colorScheme: root.colorScheme
label: qsTr("Your email client (including version)")
placeholderText: qsTr("e.g. Apple Mail 14.0")
validator: function(str) {
validator: function (str) {
if (str.length === 0) {
return qsTr("Enter an email client name and version")
return qsTr("Enter an email client name and version");
}
return
return;
}
}
RowLayout {
CheckBox {
id: includeLogs
text: qsTr("Include my recent logs")
colorScheme: root.colorScheme
checked: true
colorScheme: root.colorScheme
text: qsTr("Include my recent logs")
}
Button {
Layout.leftMargin: 12
text: qsTr("View logs")
secondary: true
colorScheme: root.colorScheme
secondary: true
text: qsTr("View logs")
onClicked: Qt.openUrlExternally(Backend.logsPath)
}
}
TextEdit {
text: qsTr("Reports are not end-to-end encrypted, please do not send any sensitive information.")
readOnly: true
Layout.fillWidth: true
color: root.colorScheme.text_weak
font.family: ProtonStyle.font_family
font.weight: ProtonStyle.fontWeight_400
font.pixelSize: ProtonStyle.caption_font_size
font.letterSpacing: ProtonStyle.caption_letter_spacing
font.pixelSize: ProtonStyle.caption_font_size
font.weight: ProtonStyle.fontWeight_400
readOnly: true
selectByMouse: true
selectedTextColor: root.colorScheme.text_invert
// No way to set lineHeight: ProtonStyle.caption_line_height
selectionColor: root.colorScheme.interaction_norm
selectedTextColor: root.colorScheme.text_invert
text: qsTr("Reports are not end-to-end encrypted, please do not send any sensitive information.")
wrapMode: Text.WordWrap
selectByMouse: true
}
Button {
id: sendButton
text: qsTr("Send")
colorScheme: root.colorScheme
enabled: !loading
text: qsTr("Send")
onClicked: {
description.validate()
address.validate()
emailClient.validate()
description.validate();
address.validate();
emailClient.validate();
if (description.error || address.error || emailClient.error) {
return
return;
}
submit()
submit();
}
Connections {
function onBugReportSendSuccess() {
root.bugReportWasSent();
}
function onReportBugFinished() {
sendButton.loading = false;
}
target: Backend
function onReportBugFinished() { sendButton.loading = false }
function onBugReportSendSuccess() { root.bugReportWasSent() }
}
}
function setDescription(message) {
description.text = message
}
function setDefaultValue() {
description.text = ""
address.text = root.selectedAddress
emailClient.text = Backend.currentEmailClient
includeLogs.checked = true
}
function isValidEmail(text){
var reEmail = /^[^@]+@[^@]+\.[A-Za-z]+\s*$/
return reEmail.test(text)
}
function submit() {
sendButton.loading = true
Backend.reportBug(
description.text,
address.text,
emailClient.text,
includeLogs.checked
)
}
onVisibleChanged: {
root.setDefaultValue()
}
}
}

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/>.
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Proton
Item {
id: root
property var _bottomMargin: 20
property var _lineHeight: 1
property string actionIcon: ""
property var colorScheme
property bool showSeparator: true
property string text: "Text"
property string hint: ""
signal clicked
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
RowLayout {
anchors.fill: parent
spacing: 16
Label {
id: mainLabel
colorScheme: root.colorScheme
text: root.text
type: Label.Body
Layout.alignment: Qt.AlignVCenter
Layout.bottomMargin: root._bottomMargin
wrapMode: Text.WordWrap
}
ColorImage {
id: infoImage
Layout.alignment: Qt.AlignVCenter
Layout.bottomMargin: root._bottomMargin
color: root.colorScheme.interaction_norm
height: 21
width: 21
source: "/qml/icons/ic-info-circle.svg"
sourceSize.height: 21
sourceSize.width: 21
visible: root.hint !== ""
MouseArea {
id: imageArea
anchors.fill: infoImage
hoverEnabled: true
}
ToolTip {
id: toolTipinfo
text: root.hint
visible: imageArea.containsMouse
implicitWidth: Math.min(400, tooltipText.implicitWidth)
background: Rectangle {
radius: 4
border.color: root.colorScheme.border_weak
color: root.colorScheme.background_weak
}
contentItem: Text {
id: tooltipText
color: root.colorScheme.text_hint
text: toolTipinfo.text
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
}
// fill height so the footer label will always be attached to the bottom
Item {
Layout.fillWidth: true
}
Button {
id: button
Layout.alignment: Qt.AlignVCenter
Layout.bottomMargin: root._bottomMargin
colorScheme: root.colorScheme
icon.source: root.actionIcon
text: ""
secondary: true
visible: root.actionIcon !== ""
onClicked: {
if (!root.loading)
root.clicked();
}
}
}
Rectangle {
anchors.bottom: root.bottom
anchors.left: root.left
anchors.right: root.right
color: colorScheme.border_weak
height: root._lineHeight
visible: root.showSeparator
}
}

View File

@ -1,71 +1,80 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import QtQuick.Controls.impl
import Proton
Rectangle {
id: root
property int _margin: 24
property ColorScheme colorScheme
property string title
property string hostname
property string port
property string username
property string password
property string port
property string security
implicitWidth: 304
implicitHeight: content.height + 2*root._margin
property string title
property string username
color: root.colorScheme.background_norm
implicitHeight: content.height + 2 * root._margin
implicitWidth: 304
radius: ProtonStyle.card_radius
property int _margin: 24
ColumnLayout {
id: content
width: root.width - 2*root._margin
anchors{
top: root.top
left: root.left
leftMargin : root._margin
rightMargin : root._margin
topMargin : root._margin
bottomMargin : root._margin
}
spacing: 12
width: root.width - 2 * root._margin
anchors {
bottomMargin: root._margin
left: root.left
leftMargin: root._margin
rightMargin: root._margin
top: root.top
topMargin: root._margin
}
Label {
colorScheme: root.colorScheme
text: root.title
type: Label.Body_semibold
}
ConfigurationItem{ colorScheme: root.colorScheme; label: qsTr("Hostname") ; value: root.hostname }
ConfigurationItem{ colorScheme: root.colorScheme; label: qsTr("Port") ; value: root.port }
ConfigurationItem{ colorScheme: root.colorScheme; label: qsTr("Username") ; value: root.username }
ConfigurationItem{ colorScheme: root.colorScheme; label: qsTr("Password") ; value: root.password }
ConfigurationItem{ colorScheme: root.colorScheme; label: qsTr("Security") ; value: root.security }
ConfigurationItem {
colorScheme: root.colorScheme
label: qsTr("Hostname")
value: root.hostname
}
ConfigurationItem {
colorScheme: root.colorScheme
label: qsTr("Port")
value: root.port
}
ConfigurationItem {
colorScheme: root.colorScheme
label: qsTr("Username")
value: root.username
}
ConfigurationItem {
colorScheme: root.colorScheme
label: qsTr("Password")
value: root.password
}
ConfigurationItem {
colorScheme: root.colorScheme
label: qsTr("Security")
value: root.security
}
}
}

View File

@ -1,35 +1,29 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import QtQuick.Controls.impl
import Proton
Item {
id: root
Layout.fillWidth: true
property var colorScheme
property string label
property string value
Layout.fillWidth: true
implicitHeight: children[0].implicitHeight
implicitWidth: children[0].implicitWidth
@ -47,45 +41,42 @@ Item {
}
TextEdit {
id: valueText
text: root.value
Layout.fillWidth: true
color: root.colorScheme.text_weak
readOnly: true
selectByMouse: true
selectByKeyboard: true
selectByMouse: true
selectionColor: root.colorScheme.text_weak
text: root.value
wrapMode: Text.WrapAnywhere
Layout.fillWidth: true
}
}
Item {
Layout.fillWidth: true
}
ColorImage {
source: "/qml/icons/ic-copy.svg"
color: root.colorScheme.text_norm
height: root.colorScheme.body_font_size
source: "/qml/icons/ic-copy.svg"
sourceSize.height: root.colorScheme.body_font_size
MouseArea {
anchors.fill: parent
onClicked : {
valueText.select(0, valueText.length)
valueText.copy()
valueText.deselect()
onClicked: {
valueText.select(0, valueText.length);
valueText.copy();
valueText.deselect();
}
onPressed: parent.scale = 0.90
onReleased: parent.scale = 1
}
}
}
Rectangle {
Layout.fillWidth: true
height: 1
color: root.colorScheme.border_norm
height: 1
}
}
}

View File

@ -1,155 +1,138 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import QtQuick.Controls.impl
import Proton
SettingsView {
id: root
function setDefaultValues() {
imapSSLButton.checked = Backend.useSSLForIMAP;
imapSTARTTLSButton.checked = !Backend.useSSLForIMAP;
smtpSSLButton.checked = Backend.useSSLForSMTP;
smtpSTARTTLSButton.checked = !Backend.useSSLForSMTP;
}
function submit() {
submitButton.loading = true;
Backend.setMailServerSettings(Backend.imapPort, Backend.smtpPort, imapSSLButton.checked, smtpSSLButton.checked);
}
fillHeight: false
onVisibleChanged: {
root.setDefaultValues();
}
Label {
Layout.fillWidth: true
colorScheme: root.colorScheme
text: qsTr("Connection mode")
type: Label.Heading
Layout.fillWidth: true
}
Label {
Layout.fillWidth: true
color: root.colorScheme.text_weak
colorScheme: root.colorScheme
text: qsTr("Change the protocol Bridge and the email client use to connect for IMAP and SMTP.")
type: Label.Body
color: root.colorScheme.text_weak
Layout.fillWidth: true
wrapMode: Text.WordWrap
}
ColumnLayout {
spacing: 16
ButtonGroup{ id: imapProtocolSelection }
ButtonGroup {
id: imapProtocolSelection
}
Label {
colorScheme: root.colorScheme
text: qsTr("IMAP connection")
}
RadioButton {
id: imapSSLButton
colorScheme: root.colorScheme
ButtonGroup.group: imapProtocolSelection
colorScheme: root.colorScheme
text: qsTr("SSL")
}
RadioButton {
id: imapSTARTTLSButton
colorScheme: root.colorScheme
ButtonGroup.group: imapProtocolSelection
colorScheme: root.colorScheme
text: qsTr("STARTTLS")
}
}
Rectangle {
Layout.fillWidth: true
height: 1
color: root.colorScheme.border_weak
height: 1
}
ColumnLayout {
spacing: 16
ButtonGroup{ id: smtpProtocolSelection }
ButtonGroup {
id: smtpProtocolSelection
}
Label {
colorScheme: root.colorScheme
text: qsTr("SMTP connection")
}
RadioButton {
id: smtpSSLButton
colorScheme: root.colorScheme
ButtonGroup.group: smtpProtocolSelection
colorScheme: root.colorScheme
text: qsTr("SSL")
}
RadioButton {
id: smtpSTARTTLSButton
colorScheme: root.colorScheme
ButtonGroup.group: smtpProtocolSelection
colorScheme: root.colorScheme
text: qsTr("STARTTLS")
}
}
Rectangle {
Layout.fillWidth: true
height: 1
color: root.colorScheme.border_weak
height: 1
}
RowLayout {
spacing: 12
Button {
id: submitButton
colorScheme: root.colorScheme
text: qsTr("Save")
onClicked: {
submitButton.loading = true
root.submit()
}
enabled: (!loading) && ((imapSSLButton.checked !== Backend.useSSLForIMAP) || (smtpSSLButton.checked !== Backend.useSSLForSMTP))
}
text: qsTr("Save")
onClicked: {
submitButton.loading = true;
root.submit();
}
}
Button {
colorScheme: root.colorScheme
text: qsTr("Cancel")
onClicked: root.back()
secondary: true
}
text: qsTr("Cancel")
onClicked: root.back()
}
Connections {
target: Backend
function onChangeMailServerSettingsFinished() {
submitButton.loading = false
root.back()
submitButton.loading = false;
root.back();
}
target: Backend
}
}
function submit(){
submitButton.loading = true
Backend.setMailServerSettings(Backend.imapPort, Backend.smtpPort, imapSSLButton.checked, smtpSSLButton.checked)
}
function setDefaultValues(){
imapSSLButton.checked = Backend.useSSLForIMAP
imapSTARTTLSButton.checked = !Backend.useSSLForIMAP
smtpSSLButton.checked = Backend.useSSLForSMTP
smtpSTARTTLSButton.checked = !Backend.useSSLForSMTP
}
onVisibleChanged: {
root.setDefaultValues()
}
}

View File

@ -1,36 +1,58 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Proton
import Notifications
Item {
id: root
property ColorScheme colorScheme
property ColorScheme colorScheme
property var notifications
signal closeWindow
signal quitBridge
signal showSetupGuide(var user, string address)
signal closeWindow()
signal quitBridge()
function selectUser(userID) {
const users = Backend.users;
for (let i = 0; i < users.count; i++) {
const user = users.get(i);
if (user.id !== userID) {
continue;
}
accounts.currentIndex = i;
if (user.state === EUserState.SignedOut)
showSignIn(user.primaryEmailOrUsername());
return;
}
console.error("User with ID ", userID, " was not found in the account list");
}
function showHelp() {
rightContent.showHelpView();
}
function showLocalCacheSettings() {
rightContent.showLocalCacheSettings();
}
function showSettings() {
rightContent.showGeneralSettings();
}
function showSignIn(username) {
signIn.username = username;
rightContent.showSignIn();
}
RowLayout {
anchors.fill: parent
@ -38,13 +60,13 @@ Item {
Rectangle {
id: leftBar
property ColorScheme colorScheme: root.colorScheme.prominent
Layout.minimumWidth: 264
Layout.maximumWidth: 320
Layout.preferredWidth: 320
Layout.fillHeight: true
Layout.maximumWidth: 320
Layout.minimumWidth: 264
Layout.preferredWidth: 320
color: colorScheme.background_norm
ColumnLayout {
@ -52,24 +74,21 @@ Item {
spacing: 0
RowLayout {
id:topLeftBar
id: topLeftBar
Layout.fillWidth: true
Layout.minimumHeight: 60
Layout.maximumHeight: 60
Layout.minimumHeight: 60
Layout.preferredHeight: 60
spacing: 0
Status {
Layout.alignment: Qt.AlignHCenter
Layout.bottomMargin: 17
Layout.leftMargin: 16
Layout.topMargin: 24
Layout.bottomMargin: 17
Layout.alignment: Qt.AlignHCenter
colorScheme: leftBar.colorScheme
notifications: root.notifications
notificationWhitelist: Notifications.Group.Connection | Notifications.Group.ForceUpdate
notifications: root.notifications
}
// just a placeholder
@ -77,47 +96,38 @@ Item {
Layout.fillHeight: true
Layout.fillWidth: true
}
Button {
colorScheme: leftBar.colorScheme
Layout.minimumHeight: 36
Layout.maximumHeight: 36
Layout.preferredHeight: 36
Layout.minimumWidth: 36
Layout.maximumWidth: 36
Layout.preferredWidth: 36
Layout.topMargin: 16
Layout.bottomMargin: 9
Layout.maximumHeight: 36
Layout.maximumWidth: 36
Layout.minimumHeight: 36
Layout.minimumWidth: 36
Layout.preferredHeight: 36
Layout.preferredWidth: 36
Layout.rightMargin: 4
Layout.topMargin: 16
colorScheme: leftBar.colorScheme
horizontalPadding: 0
icon.source: "/qml/icons/ic-question-circle.svg"
onClicked: rightContent.showHelpView()
}
Button {
colorScheme: leftBar.colorScheme
Layout.minimumHeight: 36
Layout.maximumHeight: 36
Layout.preferredHeight: 36
Layout.minimumWidth: 36
Layout.maximumWidth: 36
Layout.preferredWidth: 36
Layout.topMargin: 16
Layout.bottomMargin: 9
Layout.maximumHeight: 36
Layout.maximumWidth: 36
Layout.minimumHeight: 36
Layout.minimumWidth: 36
Layout.preferredHeight: 36
Layout.preferredWidth: 36
Layout.rightMargin: 4
Layout.topMargin: 16
colorScheme: leftBar.colorScheme
horizontalPadding: 0
icon.source: "/qml/icons/ic-cog-wheel.svg"
onClicked: rightContent.showGeneralSettings()
}
Button {
id: dotMenuButton
Layout.bottomMargin: 9
@ -134,7 +144,7 @@ Item {
icon.source: "/qml/icons/ic-three-dots-vertical.svg"
onClicked: {
dotMenu.open()
dotMenu.open();
}
Menu {
@ -143,332 +153,319 @@ Item {
modal: true
y: dotMenuButton.Layout.preferredHeight + dotMenuButton.Layout.bottomMargin
onClosed: {
parent.checked = false;
}
onOpened: {
parent.checked = true;
}
MenuItem {
colorScheme: root.colorScheme
text: qsTr("Close window")
onClicked: {
root.closeWindow()
root.closeWindow();
}
}
MenuItem {
colorScheme: root.colorScheme
text: qsTr("Quit Bridge")
onClicked: {
root.quitBridge()
}
}
onClosed: {
parent.checked = false
}
onOpened: {
parent.checked = true
onClicked: {
root.quitBridge();
}
}
}
}
}
Item {implicitHeight:10}
Item {
implicitHeight: 10
}
// Separator line
Rectangle {
Layout.fillWidth: true
Layout.minimumHeight: 1
Layout.maximumHeight: 1
Layout.minimumHeight: 1
color: leftBar.colorScheme.border_weak
}
ListView {
id: accounts
property var _topBottomMargins: 24
property var _leftRightMargins: 16
property var _topBottomMargins: 24
Layout.fillWidth: true
Layout.bottomMargin: accounts._topBottomMargins
Layout.fillHeight: true
Layout.fillWidth: true
Layout.leftMargin: accounts._leftRightMargins
Layout.rightMargin: accounts._leftRightMargins
Layout.topMargin: accounts._topBottomMargins
Layout.bottomMargin: accounts._topBottomMargins
spacing: 12
clip: true
boundsBehavior: Flickable.StopAtBounds
clip: true
model: Backend.users
spacing: 12
header: Rectangle {
height: headerLabel.height+16
// color: ProtonStyle.transparent
Label{
delegate: Item {
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
width: leftBar.width - 2 * accounts._leftRightMargins
AccountDelegate {
id: accountDelegate
anchors.bottomMargin: 8
anchors.fill: parent
anchors.leftMargin: 12
anchors.rightMargin: 12
anchors.topMargin: 8
colorScheme: leftBar.colorScheme
user: Backend.users.get(index)
}
MouseArea {
anchors.fill: parent
onClicked: {
const user = Backend.users.get(index);
accounts.currentIndex = index;
if (!user)
return;
if (user.state !== EUserState.SignedOut) {
rightContent.showAccount();
} else {
signIn.username = user.primaryEmailOrUsername();
rightContent.showSignIn();
}
}
}
}
header: Rectangle {
height: headerLabel.height + 16
// color: ProtonStyle.transparent
Label {
id: headerLabel
colorScheme: leftBar.colorScheme
text: qsTr("Accounts")
type: Label.LabelType.Body
}
}
highlight: Rectangle {
color: leftBar.colorScheme.interaction_default_active
radius: ProtonStyle.account_row_radius
}
model: Backend.users
delegate: Item {
width: leftBar.width - 2*accounts._leftRightMargins
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
AccountDelegate {
id: accountDelegate
anchors.fill: parent
anchors.topMargin: 8
anchors.bottomMargin: 8
anchors.leftMargin: 12
anchors.rightMargin: 12
colorScheme: leftBar.colorScheme
user: Backend.users.get(index)
}
MouseArea {
anchors.fill: parent
onClicked: {
var user = Backend.users.get(index)
accounts.currentIndex = index
if (!user) return
if (user.state !== EUserState.SignedOut) {
rightContent.showAccount()
} else {
signIn.username = user.primaryEmailOrUsername()
rightContent.showSignIn()
}
}
}
}
}
// Separator
Rectangle {
Layout.fillWidth: true
Layout.minimumHeight: 1
Layout.maximumHeight: 1
Layout.minimumHeight: 1
color: leftBar.colorScheme.border_weak
}
Item {
id: bottomLeftBar
Layout.fillWidth: true
Layout.minimumHeight: 52
Layout.maximumHeight: 52
Layout.minimumHeight: 52
Layout.preferredHeight: 52
Button {
colorScheme: leftBar.colorScheme
width: 36
height: 36
anchors.left: parent.left
anchors.top: parent.top
anchors.leftMargin: 16
anchors.top: parent.top
anchors.topMargin: 7
colorScheme: leftBar.colorScheme
height: 36
horizontalPadding: 0
icon.source: "/qml/icons/ic-plus.svg"
width: 36
onClicked: {
signIn.username = ""
rightContent.showSignIn()
signIn.username = "";
rightContent.showSignIn();
}
}
}
}
}
Rectangle { // right content background
Rectangle {
Layout.fillHeight: true // right content background
Layout.fillWidth: true
Layout.fillHeight: true
color: colorScheme.background_norm
StackLayout {
id: rightContent
function showAccount(index) {
if (index !== undefined && index >= 0) {
accounts.currentIndex = index;
}
rightContent.currentIndex = 0;
}
function showBugReport() {
rightContent.currentIndex = 8;
}
function showConnectionModeSettings() {
rightContent.currentIndex = 5;
}
function showGeneralSettings() {
rightContent.currentIndex = 2;
}
function showHelpView() {
rightContent.currentIndex = 7;
}
function showKeychainSettings() {
rightContent.currentIndex = 3;
}
function showLocalCacheSettings() {
rightContent.currentIndex = 6;
}
function showPortSettings() {
rightContent.currentIndex = 4;
}
function showSignIn() {
rightContent.currentIndex = 1;
signIn.focus = true;
}
anchors.fill: parent
AccountView { // 0
AccountView {
// 0
colorScheme: root.colorScheme
notifications: root.notifications
user: {
if (accounts.currentIndex < 0) return undefined
if (Backend.users.count == 0) return undefined
return Backend.users.get(accounts.currentIndex)
if (accounts.currentIndex < 0)
return undefined;
if (Backend.users.count === 0)
return undefined;
return Backend.users.get(accounts.currentIndex);
}
onShowSetupGuide: function (user, address) {
root.showSetupGuide(user, address);
}
onShowSignIn: {
var user = this.user
signIn.username = user ? user.primaryEmailOrUsername() : ""
rightContent.showSignIn()
}
onShowSetupGuide: function(user, address) {
root.showSetupGuide(user,address)
const user = this.user;
signIn.username = user ? user.primaryEmailOrUsername() : "";
rightContent.showSignIn();
}
}
GridLayout { // 1 Sign In
GridLayout {
// 1 Sign In
columns: 2
Button {
id: backButton
Layout.alignment: Qt.AlignTop
Layout.leftMargin: 18
Layout.topMargin: 10
Layout.alignment: Qt.AlignTop
colorScheme: root.colorScheme
onClicked: {
signIn.abort()
rightContent.showAccount()
}
horizontalPadding: 8
icon.source: "/qml/icons/ic-arrow-left.svg"
secondary: true
horizontalPadding: 8
}
onClicked: {
signIn.abort();
rightContent.showAccount();
}
}
SignIn {
id: signIn
Layout.topMargin: 68
Layout.leftMargin: 80 - backButton.width - 18
Layout.rightMargin: 80
Layout.bottomMargin: 68
Layout.preferredWidth: 320
Layout.fillWidth: true
Layout.fillHeight: true
Layout.fillWidth: true
Layout.leftMargin: 80 - backButton.width - 18
Layout.preferredWidth: 320
Layout.rightMargin: 80
Layout.topMargin: 68
colorScheme: root.colorScheme
}
}
GeneralSettings { // 2
GeneralSettings {
// 2
colorScheme: root.colorScheme
notifications: root.notifications
onBack: {
rightContent.showAccount()
rightContent.showAccount();
}
}
KeychainSettings { // 3
KeychainSettings {
// 3
colorScheme: root.colorScheme
onBack: {
rightContent.showGeneralSettings()
rightContent.showGeneralSettings();
}
}
PortSettings { // 4
colorScheme: root.colorScheme
notifications: root.notifications
onBack: {
rightContent.showGeneralSettings()
}
}
ConnectionModeSettings { // 5
colorScheme: root.colorScheme
onBack: {
rightContent.showGeneralSettings()
}
}
LocalCacheSettings { // 6
PortSettings {
// 4
colorScheme: root.colorScheme
notifications: root.notifications
onBack: {
rightContent.showGeneralSettings()
rightContent.showGeneralSettings();
}
}
HelpView { // 7
ConnectionModeSettings {
// 5
colorScheme: root.colorScheme
onBack: {
rightContent.showAccount()
rightContent.showGeneralSettings();
}
}
LocalCacheSettings {
// 6
colorScheme: root.colorScheme
notifications: root.notifications
BugReportView { // 8
onBack: {
rightContent.showGeneralSettings();
}
}
HelpView {
// 7
colorScheme: root.colorScheme
onBack: {
rightContent.showAccount();
}
}
BugReportFlow {
// 8
id: bugReport
colorScheme: root.colorScheme
selectedAddress: {
if (accounts.currentIndex < 0) return ""
if (Backend.users.count == 0) return ""
var user = Backend.users.get(accounts.currentIndex)
if (!user) return ""
return user.addresses[0]
if (accounts.currentIndex < 0)
return "";
if (Backend.users.count === 0)
return "";
const user = Backend.users.get(accounts.currentIndex);
if (!user)
return "";
return user.addresses[0];
}
onBack: {
rightContent.showHelpView()
rightContent.showHelpView();
}
onBugReportWasSent: {
rightContent.showAccount()
rightContent.showAccount();
}
}
function showAccount(index) {
if (index !== undefined && index >= 0){
accounts.currentIndex = index
}
rightContent.currentIndex = 0
}
function showSignIn () { rightContent.currentIndex = 1; signIn.focus = true }
function showGeneralSettings () { rightContent.currentIndex = 2 }
function showKeychainSettings () { rightContent.currentIndex = 3 }
function showPortSettings () { rightContent.currentIndex = 4 }
function showConnectionModeSettings() { rightContent.currentIndex = 5 }
function showLocalCacheSettings () { rightContent.currentIndex = 6 }
function showHelpView () { rightContent.currentIndex = 7 }
function showBugReport () { rightContent.currentIndex = 8 }
Connections {
target: Backend
function onLoginAlreadyLoggedIn(index) {
rightContent.showAccount(index);
}
function onLoginFinished(index) {
rightContent.showAccount(index);
}
function onLoginFinished(index) { rightContent.showAccount(index) }
function onLoginAlreadyLoggedIn(index) { rightContent.showAccount(index) }
target: Backend
}
}
}
}
function showLocalCacheSettings(){rightContent.showLocalCacheSettings() }
function showSettings(){rightContent.showGeneralSettings() }
function showHelp(){rightContent.showHelpView() }
function showSignIn(username){
signIn.username = username
rightContent.showSignIn()
}
function selectUser(userID) {
var users = Backend.users;
for (var i = 0; i < users.count; i++) {
var user = users.get(i)
if (user.id !== userID) {
continue;
}
accounts.currentIndex = i;
if (user.state === EUserState.SignedOut)
showSignIn(user.primaryEmailOrUsername())
return;
}
console.error("User with ID ", userID, " was not found in the account list")
}
function showBugReportAndPrefill(description) {
rightContent.showBugReport()
bugReport.setDescription(description)
}
}
}

View File

@ -1,54 +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/>.
import QtQuick
import QtQuick.Controls
import "."
import "./Proton"
import "Proton"
Rectangle {
property var target: parent
x: target.x
y: target.y
width: target.width
height: target.height
color: "transparent"
border.color: "red"
border.width: 1
color: "transparent"
height: target.height
width: target.width
x: target.x
y: target.y
//z: parent.z - 1
z: 10000000
Label {
text: parent.width + "x" + parent.height
anchors.centerIn: parent
color: "black"
colorScheme: ProtonStyle.currentStyle
text: parent.width + "x" + parent.height
}
Rectangle {
width: target.implicitWidth
height: target.implicitHeight
color: "transparent"
border.color: "green"
border.width: 1
color: "transparent"
height: target.implicitHeight
width: target.implicitWidth
//z: parent.z - 1
z: 10000000
}

View File

@ -1,25 +1,19 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import QtQuick.Controls.impl
import Proton
SettingsView {
@ -31,144 +25,138 @@ SettingsView {
fillHeight: false
Label {
Layout.fillWidth: true
colorScheme: root.colorScheme
text: qsTr("Settings")
type: Label.Heading
Layout.fillWidth: true
}
SettingsItem {
id: autoUpdate
colorScheme: root.colorScheme
text: qsTr("Automatic updates")
description: qsTr("Bridge will automatically update in the background.")
type: SettingsItem.Toggle
checked: Backend.isAutomaticUpdateOn
onClicked: Backend.toggleAutomaticUpdate(!autoUpdate.checked)
Layout.fillWidth: true
}
checked: Backend.isAutomaticUpdateOn
colorScheme: root.colorScheme
description: qsTr("Bridge will automatically update in the background.")
text: qsTr("Automatic updates")
type: SettingsItem.Toggle
onClicked: Backend.toggleAutomaticUpdate(!autoUpdate.checked)
}
SettingsItem {
id: autostart
colorScheme: root.colorScheme
text: qsTr("Open on startup")
description: qsTr("Bridge will open upon startup.")
type: SettingsItem.Toggle
checked: Backend.isAutostartOn
onClicked: {
autostart.loading = true
Backend.toggleAutostart(!autostart.checked)
}
Connections{
target: Backend
function onToggleAutostartFinished() {
autostart.loading = false
}
}
Layout.fillWidth: true
}
checked: Backend.isAutostartOn
colorScheme: root.colorScheme
description: qsTr("Bridge will open upon startup.")
text: qsTr("Open on startup")
type: SettingsItem.Toggle
onClicked: {
autostart.loading = true;
Backend.toggleAutostart(!autostart.checked);
}
Connections {
function onToggleAutostartFinished() {
autostart.loading = false;
}
target: Backend
}
}
SettingsItem {
id: beta
colorScheme: root.colorScheme
text: qsTr("Beta access")
description: qsTr("Be among the first to try new features.")
type: SettingsItem.Toggle
Layout.fillWidth: true
checked: Backend.isBetaEnabled
colorScheme: root.colorScheme
description: qsTr("Be among the first to try new features.")
text: qsTr("Beta access")
type: SettingsItem.Toggle
onClicked: {
if (!beta.checked) {
root.notifications.askEnableBeta()
root.notifications.askEnableBeta();
} else {
Backend.toggleBeta(false)
Backend.toggleBeta(false);
}
}
Layout.fillWidth: true
}
RowLayout {
ColorImage {
Layout.alignment: Qt.AlignTop
source: root._isAdvancedShown ? "/qml/icons/ic-chevron-up.svg" : "/qml/icons/ic-chevron-down.svg"
Layout.alignment: Qt.AlignCenter
color: root.colorScheme.interaction_norm
height: root.colorScheme.body_font_size
source: root._isAdvancedShown ? "/qml/icons/ic-chevron-down.svg" : "/qml/icons/ic-chevron-right.svg"
sourceSize.height: root.colorScheme.body_font_size
MouseArea {
anchors.fill: parent
onClicked: root._isAdvancedShown = !root._isAdvancedShown
}
}
Label {
id: advSettLabel
color: root.colorScheme.interaction_norm
colorScheme: root.colorScheme
text: qsTr("Advanced settings")
color: root.colorScheme.interaction_norm
type: Label.Body
MouseArea {
anchors.fill: parent
onClicked: root._isAdvancedShown = !root._isAdvancedShown
}
}
}
SettingsItem {
id: keychains
visible: root._isAdvancedShown && Backend.availableKeychain.length > 1
colorScheme: root.colorScheme
text: qsTr("Change keychain")
description: qsTr("Change which keychain Bridge uses as default")
actionText: qsTr("Change")
type: SettingsItem.Button
checked: Backend.isDoHEnabled
onClicked: root.parent.showKeychainSettings()
Layout.fillWidth: true
}
actionText: qsTr("Change")
checked: Backend.isDoHEnabled
colorScheme: root.colorScheme
description: qsTr("Change which keychain Bridge uses as default")
text: qsTr("Change keychain")
type: SettingsItem.Button
visible: root._isAdvancedShown && Backend.availableKeychain.length > 1
onClicked: root.parent.showKeychainSettings()
}
SettingsItem {
id: doh
visible: root._isAdvancedShown
colorScheme: root.colorScheme
text: qsTr("Alternative routing")
description: qsTr("If Protons servers are blocked in your location, alternative network routing will be used to reach Proton.")
type: SettingsItem.Toggle
checked: Backend.isDoHEnabled
onClicked: Backend.toggleDoH(!doh.checked)
Layout.fillWidth: true
}
checked: Backend.isDoHEnabled
colorScheme: root.colorScheme
description: qsTr("If Protons servers are blocked in your location, alternative network routing will be used to reach Proton.")
text: qsTr("Alternative routing")
type: SettingsItem.Toggle
visible: root._isAdvancedShown
onClicked: Backend.toggleDoH(!doh.checked)
}
SettingsItem {
id: darkMode
visible: root._isAdvancedShown
colorScheme: root.colorScheme
text: qsTr("Dark mode")
description: qsTr("Choose dark color theme.")
type: SettingsItem.Toggle
checked: Backend.colorSchemeName == "dark"
onClicked: Backend.changeColorScheme( darkMode.checked ? "light" : "dark")
Layout.fillWidth: true
}
checked: Backend.colorSchemeName === "dark"
colorScheme: root.colorScheme
description: qsTr("Choose dark color theme.")
text: qsTr("Dark mode")
type: SettingsItem.Toggle
visible: root._isAdvancedShown
onClicked: Backend.changeColorScheme(darkMode.checked ? "light" : "dark")
}
SettingsItem {
id: allMail
visible: root._isAdvancedShown
colorScheme: root.colorScheme
text: qsTr("Show All Mail")
description: qsTr("Choose to list the All Mail folder in your local client.")
type: SettingsItem.Toggle
checked: Backend.isAllMailVisible
onClicked: root.notifications.askChangeAllMailVisibility(Backend.isAllMailVisible)
Layout.fillWidth: true
}
checked: Backend.isAllMailVisible
colorScheme: root.colorScheme
description: qsTr("Choose to list the All Mail folder in your local client.")
text: qsTr("Show All Mail")
type: SettingsItem.Toggle
visible: root._isAdvancedShown
onClicked: root.notifications.askChangeAllMailVisibility(Backend.isAllMailVisible)
}
SettingsItem {
id: telemetry
Layout.fillWidth: true
@ -181,73 +169,68 @@ SettingsView {
onClicked: Backend.toggleIsTelemetryDisabled(telemetry.checked)
}
SettingsItem {
id: ports
visible: root._isAdvancedShown
colorScheme: root.colorScheme
text: qsTr("Default ports")
actionText: qsTr("Change")
description: qsTr("Choose which ports are used by default.")
type: SettingsItem.Button
onClicked: root.parent.showPortSettings()
Layout.fillWidth: true
}
actionText: qsTr("Change")
colorScheme: root.colorScheme
description: qsTr("Choose which ports are used by default.")
text: qsTr("Default ports")
type: SettingsItem.Button
visible: root._isAdvancedShown
onClicked: root.parent.showPortSettings()
}
SettingsItem {
id: imap
visible: root._isAdvancedShown
colorScheme: root.colorScheme
text: qsTr("Connection mode")
actionText: qsTr("Change")
description: qsTr("Change the protocol Bridge and the email client use to connect for IMAP and SMTP.")
type: SettingsItem.Button
onClicked: root.parent.showConnectionModeSettings()
Layout.fillWidth: true
}
actionText: qsTr("Change")
colorScheme: root.colorScheme
description: qsTr("Change the protocol Bridge and the email client use to connect for IMAP and SMTP.")
text: qsTr("Connection mode")
type: SettingsItem.Button
visible: root._isAdvancedShown
onClicked: root.parent.showConnectionModeSettings()
}
SettingsItem {
id: cache
visible: root._isAdvancedShown
colorScheme: root.colorScheme
text: qsTr("Local cache")
actionText: qsTr("Configure")
description: qsTr("Configure Bridge's local cache.")
type: SettingsItem.Button
onClicked: root.parent.showLocalCacheSettings()
Layout.fillWidth: true
}
actionText: qsTr("Configure")
colorScheme: root.colorScheme
description: qsTr("Configure Bridge's local cache.")
text: qsTr("Local cache")
type: SettingsItem.Button
visible: root._isAdvancedShown
onClicked: root.parent.showLocalCacheSettings()
}
SettingsItem {
id: exportTLSCertificates
visible: root._isAdvancedShown
colorScheme: root.colorScheme
text: qsTr("Export TLS certificates")
actionText: qsTr("Export")
description: qsTr("Export the TLS private key and certificate used by the IMAP and SMTP servers.")
type: SettingsItem.Button
onClicked: {
Backend.exportTLSCertificates()
}
Layout.fillWidth: true
actionText: qsTr("Export")
colorScheme: root.colorScheme
description: qsTr("Export the TLS private key and certificate used by the IMAP and SMTP servers.")
text: qsTr("Export TLS certificates")
type: SettingsItem.Button
visible: root._isAdvancedShown
onClicked: {
Backend.exportTLSCertificates();
}
}
SettingsItem {
id: reset
visible: root._isAdvancedShown
colorScheme: root.colorScheme
text: qsTr("Reset Bridge")
actionText: qsTr("Reset")
description: qsTr("Remove all accounts, clear cached data, and restore the original settings.")
type: SettingsItem.Button
onClicked: {
root.notifications.askResetBridge()
}
Layout.fillWidth: true
actionText: qsTr("Reset")
colorScheme: root.colorScheme
description: qsTr("Remove all accounts, clear cached data, and restore the original settings.")
text: qsTr("Reset Bridge")
type: SettingsItem.Button
visible: root._isAdvancedShown
onClicked: {
root.notifications.askResetBridge();
}
}
}

View File

@ -1,126 +1,110 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Proton
SettingsView {
id: root
fillHeight: true
Label {
Layout.fillWidth: true
colorScheme: root.colorScheme
text: qsTr("Help")
type: Label.Heading
Layout.fillWidth: true
}
SettingsItem {
id: setupPage
colorScheme: root.colorScheme
text: qsTr("Installation and setup")
actionText: qsTr("Go to help topics")
Layout.fillWidth: true
actionIcon: "/qml/icons/ic-external-link.svg"
actionText: qsTr("Go to help topics")
colorScheme: root.colorScheme
description: qsTr("Get help setting up your client with our instructions and FAQs.")
text: qsTr("Installation and setup")
type: SettingsItem.PrimaryButton
onClicked: {
Backend.notifyKBArticleClicked("https://proton.me/support/bridge");
Qt.openUrlExternally("https://proton.me/support/bridge")}
Layout.fillWidth: true
Qt.openUrlExternally("https://proton.me/support/bridge");
}
}
SettingsItem {
id: checkUpdates
colorScheme: root.colorScheme
text: qsTr("Updates")
Layout.fillWidth: true
actionText: qsTr("Check now")
description: qsTr("Check that you're using the latest version of Bridge. To stay up to date, enable auto-updates in settings.")
colorScheme: root.colorScheme
description: qsTr("Check that you're using the latest version of Bridge.\nTo stay up to date, enable auto-updates in settings.")
text: qsTr("Updates")
type: SettingsItem.Button
onClicked: {
checkUpdates.loading = true
Backend.checkUpdates()
checkUpdates.loading = true;
Backend.checkUpdates();
}
Connections {
function onCheckUpdatesFinished() {
checkUpdates.loading = false;
}
target: Backend
function onCheckUpdatesFinished() { checkUpdates.loading = false }
}
Layout.fillWidth: true
}
SettingsItem {
id: logs
colorScheme: root.colorScheme
text: qsTr("Logs")
actionText: qsTr("View logs")
description: qsTr("Open and review logs to troubleshoot.")
type: SettingsItem.Button
onClicked: Qt.openUrlExternally(Backend.logsPath)
Layout.fillWidth: true
}
actionText: qsTr("View logs")
colorScheme: root.colorScheme
description: qsTr("Open and review logs to troubleshoot.")
text: qsTr("Logs")
type: SettingsItem.Button
onClicked: Qt.openUrlExternally(Backend.logsPath)
}
SettingsItem {
id: reportBug
colorScheme: root.colorScheme
text: qsTr("Report a problem")
actionText: qsTr("Report a problem")
description: qsTr("Something not working as expected? Let us know.")
type: SettingsItem.Button
onClicked: {
Backend.updateCurrentMailClient()
Backend.notifyReportBugClicked()
root.parent.showBugReport()
}
Layout.fillWidth: true
actionText: qsTr("Report problem")
colorScheme: root.colorScheme
description: qsTr("Something not working as expected? Let us know.")
text: qsTr("Report a problem")
type: SettingsItem.Button
onClicked: {
Backend.updateCurrentMailClient();
Backend.notifyReportBugClicked();
root.parent.showBugReport();
}
}
// fill height so the footer label will be always attached to the bottom
// fill height so the footer label will always be attached to the bottom
Item {
Layout.fillHeight: true
Layout.fillWidth: true
}
Label {
Layout.alignment: Qt.AlignHCenter
colorScheme: root.colorScheme
type: Label.Caption
color: root.colorScheme.text_weak
textFormat: Text.StyledText
colorScheme: root.colorScheme
horizontalAlignment: Text.AlignHCenter
text: qsTr("%1 v%2 (%3)<br>© 2017-%4 %5<br>%6 %7<br>%8").arg(Backend.appname).arg(Backend.version).arg(Backend.tag).arg(Backend.buildYear()).arg(Backend.vendor).arg(link(Backend.licensePath, qsTr("License"))).arg(link(Backend.dependencyLicensesLink, qsTr("Dependencies"))).arg(link(Backend.releaseNotesLink, qsTr("Release notes")))
textFormat: Text.StyledText
type: Label.Caption
text: qsTr("%1 v%2 (%3)<br>© 2017-%4 %5<br>%6 %7<br>%8").
arg(Backend.appname).
arg(Backend.version).
arg(Backend.tag).
arg(Backend.buildYear()).
arg(Backend.vendor).
arg(link(Backend.licensePath, qsTr("License"))).
arg(link(Backend.dependencyLicensesLink, qsTr("Dependencies"))).
arg(link(Backend.releaseNotesLink, qsTr("Release notes")))
onLinkActivated: function(link) { Qt.openUrlExternally(link) }
onLinkActivated: function (link) {
Qt.openUrlExternally(link);
}
}
}

View File

@ -1,116 +1,105 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import QtQuick.Controls.impl
import Proton
SettingsView {
id: root
fillHeight: false
property bool _valuesChanged: keychainSelection.checkedButton && keychainSelection.checkedButton.text !== Backend.currentKeychain
property bool _valuesChanged: keychainSelection.checkedButton && keychainSelection.checkedButton.text != Backend.currentKeychain
Label {
colorScheme: root.colorScheme
text: qsTr("Default keychain")
type: Label.Heading
Layout.fillWidth: true
}
Label {
colorScheme: root.colorScheme
text: qsTr("Change which keychain Bridge uses as default")
type: Label.Body
color: root.colorScheme.text_weak
Layout.fillWidth: true
wrapMode: Text.WordWrap
}
ColumnLayout {
spacing: 16
ButtonGroup{ id: keychainSelection }
Repeater {
model: Backend.availableKeychain
RadioButton {
colorScheme: root.colorScheme
ButtonGroup.group: keychainSelection
text: modelData
function setDefaultValues() {
for (const bi in keychainSelection.buttons) {
const button = keychainSelection.buttons[bi];
if (button.text === Backend.currentKeychain) {
button.checked = true;
break;
}
}
}
fillHeight: false
Rectangle {
Layout.fillWidth: true
height: 1
color: root.colorScheme.border_weak
Component.onCompleted: root.setDefaultValues()
onBack: {
root.setDefaultValues();
}
Label {
Layout.fillWidth: true
colorScheme: root.colorScheme
text: qsTr("Default keychain")
type: Label.Heading
}
Label {
Layout.fillWidth: true
color: root.colorScheme.text_weak
colorScheme: root.colorScheme
text: qsTr("Change which keychain Bridge uses as default")
type: Label.Body
wrapMode: Text.WordWrap
}
ColumnLayout {
spacing: 16
ButtonGroup {
id: keychainSelection
}
Repeater {
model: Backend.availableKeychain
RadioButton {
ButtonGroup.group: keychainSelection
colorScheme: root.colorScheme
text: modelData
}
}
}
Rectangle {
Layout.fillWidth: true
color: root.colorScheme.border_weak
height: 1
}
RowLayout {
spacing: 12
Button {
id: submitButton
colorScheme: root.colorScheme
text: qsTr("Save and restart")
enabled: root._valuesChanged
text: qsTr("Save and restart")
onClicked: {
Backend.changeKeychain(keychainSelection.checkedButton.text)
Backend.changeKeychain(keychainSelection.checkedButton.text);
}
}
Button {
colorScheme: root.colorScheme
text: qsTr("Cancel")
onClicked: root.back()
secondary: true
}
text: qsTr("Cancel")
onClicked: root.back()
}
Connections {
target: Backend
function onChangeKeychainFinished() {
submitButton.loading = false
root.back()
submitButton.loading = false;
root.back();
}
target: Backend
}
}
onBack: {
root.setDefaultValues()
}
function setDefaultValues(){
for (var bi in keychainSelection.buttons){
var button = keychainSelection.buttons[bi]
if (button.text == Backend.currentKeychain) {
button.checked = true
break;
}
}
}
Component.onCompleted: root.setDefaultValues()
}

View File

@ -1,81 +1,88 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import QtQuick.Controls.impl
import QtQuick.Dialogs
import Proton
SettingsView {
id: root
fillHeight: false
property var notifications
property url diskCachePath: pathDialog.shortcuts.home
property var notifications
function refresh() {
diskCacheSetting.description = Backend.nativePath(root.diskCachePath)
submitButton.enabled = (!submitButton.loading) && !Backend.areSameFileOrFolder(Backend.diskCachePath, root.diskCachePath)
diskCacheSetting.description = Backend.nativePath(root.diskCachePath);
submitButton.enabled = (!submitButton.loading) && !Backend.areSameFileOrFolder(Backend.diskCachePath, root.diskCachePath);
}
function setDefaultValues() {
root.diskCachePath = Backend.diskCachePath;
root.refresh();
}
function submit() {
submitButton.loading = true;
Backend.setDiskCachePath(root.diskCachePath);
}
fillHeight: false
onBack: {
root.setDefaultValues();
}
onVisibleChanged: {
root.setDefaultValues();
}
Label {
Layout.fillWidth: true
colorScheme: root.colorScheme
text: qsTr("Local cache")
type: Label.Heading
Layout.fillWidth: true
}
Label {
Layout.fillWidth: true
color: root.colorScheme.text_weak
colorScheme: root.colorScheme
text: qsTr("Bridge stores your encrypted messages locally to optimize communication with your client.")
type: Label.Body
color: root.colorScheme.text_weak
Layout.fillWidth: true
wrapMode: Text.WordWrap
}
SettingsItem {
id: diskCacheSetting
colorScheme: root.colorScheme
text: qsTr("Current cache location")
actionText: qsTr("Change location")
descriptionWrap: Text.WrapAnywhere
type: SettingsItem.Button
onClicked: {
pathDialog.open()
}
Layout.fillWidth: true
actionText: qsTr("Change location")
colorScheme: root.colorScheme
descriptionWrap: Text.WrapAnywhere
text: qsTr("Current cache location")
type: SettingsItem.Button
onClicked: {
pathDialog.open();
}
FolderDialog {
id: pathDialog
title: qsTr("Select cache location")
currentFolder: root.diskCachePath
onAccepted: {
root.diskCachePath = pathDialog.selectedFolder
root.refresh()
}
}
}
title: qsTr("Select cache location")
onAccepted: {
root.diskCachePath = pathDialog.selectedFolder;
root.refresh();
}
}
}
RowLayout {
spacing: 12
@ -83,43 +90,25 @@ SettingsView {
id: submitButton
colorScheme: root.colorScheme
text: qsTr("Save")
onClicked: {
root.submit()
root.submit();
}
}
Button {
colorScheme: root.colorScheme
text: qsTr("Cancel")
onClicked: root.back()
secondary: true
}
text: qsTr("Cancel")
onClicked: root.back()
}
Connections {
target: Backend
function onDiskCachePathChangeFinished() {
submitButton.loading = false
root.setDefaultValues()
submitButton.loading = false;
root.setDefaultValues();
}
target: Backend
}
}
onBack: {
root.setDefaultValues()
}
function submit() {
submitButton.loading = true
Backend.setDiskCachePath(root.diskCachePath)
}
function setDefaultValues(){
root.diskCachePath = Backend.diskCachePath
root.refresh();
}
onVisibleChanged: {
root.setDefaultValues()
}
}

View File

@ -1,232 +1,199 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQml
import QtQuick
import QtQuick.Window
import QtQuick.Layouts
import QtQuick.Controls
import Proton
import Notifications
ApplicationWindow {
id: root
colorScheme: ProtonStyle.currentStyle
visible: true
property int _defaultWidth: 1080
property int _defaultHeight: 780
width: _defaultWidth
property int _defaultWidth: 1080
property var notifications
function selectUser(userID) {
contentWrapper.selectUser(userID);
}
function showAndRise() {
root.show();
root.raise();
if (!root.active) {
root.requestActivate();
}
}
function showHelp() {
contentWrapper.showHelp();
}
function showLocalCacheSettings() {
contentWrapper.showLocalCacheSettings();
}
function showSettings() {
contentWrapper.showSettings();
}
function showSetup(user, address) {
setupGuide.user = user;
setupGuide.address = address;
setupGuide.reset();
contentLayout._showSetup = !!setupGuide.user;
}
function showSignIn(username) {
if (contentLayout.currentIndex === 1)
return;
contentWrapper.showSignIn(username);
}
colorScheme: ProtonStyle.currentStyle
height: _defaultHeight
minimumWidth: _defaultWidth
property var notifications
visible: true
width: _defaultWidth
// show Setup Guide on every new user
Connections {
target: Backend.users
function onRowsInserted(parent, first, last) {
// considering that users are added one-by-one
var user = Backend.users.get(first)
if (user.state === EUserState.SignedOut) {
return
}
if (user.setupGuideSeen) {
return
}
root.showSetup(user,user.addresses[0])
}
function onRowsAboutToBeRemoved(parent, first, last) {
for (var i = first; i <= last; i++ ) {
var user = Backend.users.get(i)
for (let i = first; i <= last; i++) {
const user = Backend.users.get(i);
if (setupGuide.user === user) {
setupGuide.user = null
contentLayout._showSetup = false
return
setupGuide.user = null;
contentLayout._showSetup = false;
return;
}
}
}
}
function onRowsInserted(parent, first, _) {
// considering that users are added one-by-one
const user = Backend.users.get(first);
if (user.state === EUserState.SignedOut) {
return;
}
if (user.setupGuideSeen) {
return;
}
root.showSetup(user, user.addresses[0]);
}
target: Backend.users
}
Connections {
target: Backend
function onShowMainWindow() {
root.showAndRise()
}
function onLoginFinished(index, wasSignedOut) {
var user = Backend.users.get(index)
const user = Backend.users.get(index);
if (user && !wasSignedOut) {
root.showSetup(user, user.addresses[0])
root.showSetup(user, user.addresses[0]);
}
console.debug("Login finished", index)
console.debug("Login finished", index);
}
function onShowHelp() {
root.showHelp()
root.showAndRise()
}
function onShowSettings() {
root.showSettings()
root.showAndRise()
}
function onSelectUser(userID, forceShowWindow) {
contentWrapper.selectUser(userID)
contentWrapper.selectUser(userID);
if (forceShowWindow) {
root.showAndRise()
root.showAndRise();
}
}
}
function onShowHelp() {
root.showHelp();
root.showAndRise();
}
function onShowMainWindow() {
root.showAndRise();
}
function onShowSettings() {
root.showSettings();
root.showAndRise();
}
target: Backend
}
StackLayout {
id: contentLayout
anchors.fill: parent
property bool _showSetup: false
anchors.fill: parent
currentIndex: {
// show welcome when there are no users
if (Backend.users.count === 0) {
return 1
return 1;
}
var u = Backend.users.get(0)
const u = Backend.users.get(0);
if (!u) {
console.trace()
console.log("empty user")
return 1
console.trace();
console.log("empty user");
return 1;
}
if ((Backend.users.count === 1) && (u.state === EUserState.SignedOut)) {
showSignIn(u.primaryEmailOrUsername())
return 0
showSignIn(u.primaryEmailOrUsername());
return 0;
}
if (contentLayout._showSetup) {
return 2
return 2;
}
return 0
return 0;
}
ContentWrapper { // 0
ContentWrapper {
// 0
id: contentWrapper
Layout.fillHeight: true
Layout.fillWidth: true
colorScheme: root.colorScheme
notifications: root.notifications
Layout.fillHeight: true
Layout.fillWidth: true
onShowSetupGuide: function(user, address) {
root.showSetup(user,address)
}
onCloseWindow: {
root.close()
root.close();
}
onQuitBridge: {
// If we ever want to add a confirmation dialog before quitting:
//root.notifications.askQuestion("Quit Bridge", "Insert warning message here.", "Quit", "Cancel", Backend.quit, null)
root.close()
Backend.quit()
root.close();
Backend.quit();
}
onShowSetupGuide: function (user, address) {
root.showSetup(user, address);
}
}
WelcomeGuide { // 1
colorScheme: root.colorScheme
WelcomeGuide {
Layout.fillHeight: true
Layout.fillWidth: true
Layout.fillWidth: true // 1
colorScheme: root.colorScheme
}
SetupGuide { // 2
SetupGuide {
// 2
id: setupGuide
colorScheme: root.colorScheme
Layout.fillHeight: true
Layout.fillWidth: true
colorScheme: root.colorScheme
onDismissed: {
root.showSetup(null,"")
root.showSetup(null, "");
}
onFinished: {
// TODO: Do not close window. Trigger Backend to check that
// there is a successfully connected client. Then Backend
// should send another signal to close the setup guide.
root.showSetup(null,"")
root.showSetup(null, "");
}
}
}
NotificationPopups {
colorScheme: root.colorScheme
notifications: root.notifications
mainWindow: root
notifications: root.notifications
}
SplashScreen {
id: splashScreen
colorScheme: root.colorScheme
}
function showLocalCacheSettings() { contentWrapper.showLocalCacheSettings() }
function showSettings() { contentWrapper.showSettings() }
function showHelp() { contentWrapper.showHelp() }
function selectUser(userID) { contentWrapper.selectUser(userID) }
function showBugReportAndPrefill(message) {
contentWrapper.showBugReportAndPrefill(message)
}
function showSignIn(username) {
if (contentLayout.currentIndex == 1) return
contentWrapper.showSignIn(username)
}
function showSetup(user, address) {
setupGuide.user = user
setupGuide.address = address
setupGuide.reset()
if (setupGuide.user) {
contentLayout._showSetup = true
} else {
contentLayout._showSetup = false
}
}
function showAndRise() {
root.show()
root.raise()
if (!root.active) {
root.requestActivate()
}
}
}

View File

@ -1,118 +1,99 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQml
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Proton
import Notifications
Dialog {
id: root
default property alias data: additionalChildrenContainer.children
property var notification
shouldShow: notification && notification.active && !notification.dismissed
modal: true
default property alias data: additionalChildrenContainer.children
shouldShow: notification && notification.active && !notification.dismissed
ColumnLayout {
spacing: 0
Image {
Layout.alignment: Qt.AlignHCenter
sourceSize.width: 64
sourceSize.height: 64
Layout.bottomMargin: 16
Layout.preferredHeight: 64
Layout.preferredWidth: 64
Layout.bottomMargin: 16
visible: source != ""
source: {
if (!root.notification) {
return ""
return "";
}
switch (root.notification.type) {
case Notification.NotificationType.Info:
return "/qml/icons/ic-info.svg"
case Notification.NotificationType.Success:
return "/qml/icons/ic-success.svg"
case Notification.NotificationType.Warning:
case Notification.NotificationType.Danger:
return "/qml/icons/ic-alert.svg"
case Notification.NotificationType.Info:
return "/qml/icons/ic-info.svg";
case Notification.NotificationType.Success:
return "/qml/icons/ic-success.svg";
case Notification.NotificationType.Warning:
case Notification.NotificationType.Danger:
return "/qml/icons/ic-alert.svg";
}
}
sourceSize.height: 64
sourceSize.width: 64
visible: source != ""
}
Label {
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
Layout.bottomMargin: 8
colorScheme: root.colorScheme
horizontalAlignment: Text.AlignHCenter
text: root.notification.title
type: Label.LabelType.Title
}
Label {
Layout.bottomMargin: 16
Layout.fillWidth: true
Layout.preferredWidth: 240
Layout.bottomMargin: 16
colorScheme: root.colorScheme
text: root.notification.description
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
text: root.notification.description
type: Label.LabelType.Body
onLinkActivated: function(link) { Qt.openUrlExternally(link) }
}
wrapMode: Text.WordWrap
onLinkActivated: function (link) {
Qt.openUrlExternally(link);
}
}
Item {
id: additionalChildrenContainer
Layout.fillWidth: true
Layout.bottomMargin: 16
visible: children.length > 0
Layout.fillWidth: true
implicitHeight: additionalChildrenContainer.childrenRect.height
implicitWidth: additionalChildrenContainer.childrenRect.width
visible: children.length > 0
}
ColumnLayout {
spacing: 8
Repeater {
model: root.notification.action
delegate: Button {
Layout.fillWidth: true
colorScheme: root.colorScheme
action: modelData
secondary: index > 0
colorScheme: root.colorScheme
loading: modelData.loading
secondary: index > 0
}
}
}

View File

@ -1,25 +1,19 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQml
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Proton
import Notifications
@ -27,118 +21,98 @@ Item {
id: root
property ColorScheme colorScheme
property var notifications
property var mainWindow
property int notificationWhitelist: NotificationFilter.FilterConsts.All
property int notificationBlacklist: NotificationFilter.FilterConsts.None
property int notificationWhitelist: NotificationFilter.FilterConsts.All
property var notifications
NotificationFilter {
id: bannerNotificationFilter
source: root.notifications.all
blacklist: Notifications.Group.Dialogs
source: root.notifications.all
}
Banner {
colorScheme: root.colorScheme
notification: bannerNotificationFilter.topmost
mainWindow: root.mainWindow
notification: bannerNotificationFilter.topmost
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.updateManualReady
Switch {
id:autoUpdate
id: autoUpdate
checked: Backend.isAutomaticUpdateOn
colorScheme: root.colorScheme
text: qsTr("Update automatically in the future")
checked: Backend.isAutomaticUpdateOn
onClicked: Backend.toggleAutomaticUpdate(autoUpdate.checked)
}
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.updateForce
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.updateForceError
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.enableBeta
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.cacheUnavailable
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.cacheCantMove
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.diskFull
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.enableSplitMode
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.resetBridge
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.changeAllMailVisibility
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.deleteAccount
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.noKeychain
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.rebuildKeychain
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.apiCertIssue
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.noActiveKeyForRecipient
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.userBadEvent
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.genericError
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.genericQuestion

View File

@ -1,54 +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/>.
import QtQml
import QtQuick.Controls
QtObject {
id: root
default property var children
enum NotificationType {
Info = 0,
Success = 1,
Warning = 2,
Danger = 3
Info,
Success,
Warning,
Danger
}
property list<Action> action
property bool active: false
// brief is used in status view only
property string brief
default property var children
property var data
// description is used in banners and in dialogs as description
property string description
property bool dismissed: false
property int group
property string icon
readonly property var occurred: active ? new Date() : undefined
// title is used in dialogs only
property string title
// description is used in banners and in dialogs as description
property string description
// brief is used in status view only
property string brief
property string icon
property list<Action> action
property int type
property int group
property bool dismissed: false
property bool active: false
readonly property var occurred: active ? new Date() : undefined
property var data
onActiveChanged: {
dismissed = false
dismissed = false;
}
}

View File

@ -1,114 +1,95 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQml
import QtQml.Models
// contains notifications that satisfy black- and whitelist and are sorted in time-occurred order
ListModel {
id: root
enum FilterConsts {
None = 0,
None,
All = 255
}
property int whitelist: NotificationFilter.FilterConsts.All
property int blacklist: NotificationFilter.FilterConsts.None
property Notification topmost
property var source
property bool componentCompleted: false
Component.onCompleted: {
root.componentCompleted = true
root.rebuildList()
}
property var source
property Notification topmost
property int whitelist: NotificationFilter.FilterConsts.All
// overriding get method to ignore any role and return directly object itself
function get(row) {
if (row < 0 || row >= count) {
return undefined
return undefined;
}
return data(index(row, 0), Qt.DisplayRole)
return data(index(row, 0), Qt.DisplayRole);
}
function rebuildList() {
let i;
// avoid evaluation of the list before Component.onCompleted
if (!root.componentCompleted) {
return
return;
}
for (var i = 0; i < root.count; i++) {
root.get(i).onActiveChanged.disconnect( root.updateList )
for (i = 0; i < root.count; i++) {
root.get(i).onActiveChanged.disconnect(root.updateList);
}
root.clear()
root.clear();
if (!root.source) {
return
return;
}
for (i = 0; i < root.source.length; i++) {
var obj = root.source[i]
const obj = root.source[i];
if (obj.group & root.blacklist) {
continue
continue;
}
if (!(obj.group & root.whitelist)) {
continue
continue;
}
root.append({obj})
obj.onActiveChanged.connect( root.updateList )
root.append({
"obj": obj
});
obj.onActiveChanged.connect(root.updateList);
}
}
function updateList() {
var topmost = null
for (var i = 0; i < root.count; i++) {
var obj = root.get(i)
let topmost = null;
for (let i = 0; i < root.count; i++) {
const obj = root.get(i);
if (!obj.active) {
continue
continue;
}
if (topmost && (topmost.type > obj.type)) {
continue
continue;
}
if (topmost && (topmost.type === obj.type) && (topmost.occurred > obj.occurred)) {
continue
continue;
}
topmost = obj
topmost = obj;
}
root.topmost = topmost
root.topmost = topmost;
}
onWhitelistChanged: {
root.rebuildList()
Component.onCompleted: {
root.componentCompleted = true;
root.rebuildList();
}
onBlacklistChanged: {
root.rebuildList()
root.rebuildList();
}
onSourceChanged: {
root.rebuildList()
root.rebuildList();
}
onWhitelistChanged: {
root.rebuildList();
}
}

View File

@ -1,178 +1,157 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import QtQuick.Controls.impl
import Proton
SettingsView {
id: root
fillHeight: false
property bool _valuesChanged: (imapField.text * 1 !== Backend.imapPort || smtpField.text * 1 !== Backend.smtpPort)
property var notifications
property bool _valuesChanged: (
imapField.text*1 !== Backend.imapPort ||
smtpField.text*1 !== Backend.smtpPort
)
function isPortFree(field) {
const num = field.text * 1;
if (num === Backend.imapPort)
return true;
if (num === Backend.smtpPort)
return true;
if (!Backend.isPortFree(num)) {
field.error = true;
field.errorString = qsTr("Port occupied");
return false;
}
return true;
}
function setDefaultValues() {
imapField.text = Backend.imapPort;
smtpField.text = Backend.smtpPort;
imapField.error = false;
smtpField.error = false;
}
function validate(port) {
const num = port * 1;
if (!(num > 1 && num < 65536)) {
return qsTr("Invalid port number");
}
if (imapField.text === smtpField.text) {
return qsTr("Port numbers must be different");
}
}
fillHeight: false
Component.onCompleted: root.setDefaultValues()
onBack: {
root.setDefaultValues();
}
Label {
Layout.fillWidth: true
colorScheme: root.colorScheme
text: qsTr("Default ports")
type: Label.Heading
Layout.fillWidth: true
}
Label {
Layout.fillWidth: true
color: root.colorScheme.text_weak
colorScheme: root.colorScheme
text: qsTr("Changes require reconfiguration of your email client.")
type: Label.Body
color: root.colorScheme.text_weak
Layout.fillWidth: true
wrapMode: Text.WordWrap
}
RowLayout {
spacing: 16
TextField {
id: imapField
Layout.alignment: Qt.AlignTop | Qt.AlignLeft
Layout.preferredWidth: 160
colorScheme: root.colorScheme
label: qsTr("IMAP port")
Layout.preferredWidth: 160
Layout.alignment: Qt.AlignTop | Qt.AlignLeft
validator: root.validate
}
TextField {
id: smtpField
Layout.alignment: Qt.AlignTop | Qt.AlignLeft
Layout.preferredWidth: 160
colorScheme: root.colorScheme
label: qsTr("SMTP port")
Layout.preferredWidth: 160
Layout.alignment: Qt.AlignTop | Qt.AlignLeft
validator: root.validate
}
}
Rectangle {
Layout.fillWidth: true
height: 1
color: root.colorScheme.border_weak
height: 1
}
RowLayout {
spacing: 12
Button {
id: submitButton
colorScheme: root.colorScheme
text: qsTr("Save")
enabled: (!loading) && root._valuesChanged
text: qsTr("Save")
onClicked: {
// removing error here because we may have set it manually (port occupied)
imapField.error = false
smtpField.error = false
imapField.error = false;
smtpField.error = false;
// checking errors separately because we want to display "same port" error only once
imapField.validate()
imapField.validate();
if (imapField.error) {
return
return;
}
smtpField.validate()
smtpField.validate();
if (smtpField.error) {
return
return;
}
submitButton.loading = true
submitButton.loading = true;
// check both ports before returning an error
var err = false
err |= !isPortFree(imapField)
err |= !isPortFree(smtpField)
let err = false;
err |= !isPortFree(imapField);
err |= !isPortFree(smtpField);
if (err) {
submitButton.loading = false
return
submitButton.loading = false;
return;
}
// We turn off all port error notification. They well be restored if problems persist
root.notifications.imapPortStartupError.active = false
root.notifications.smtpPortStartupError.active = false
root.notifications.imapPortChangeError.active = false
root.notifications.smtpPortChangeError.active = false
Backend.setMailServerSettings(imapField.text, smtpField.text, Backend.useSSLForIMAP, Backend.useSSLForSMTP)
// We turn off all port error notification. They will be restored if problems persist
root.notifications.imapPortStartupError.active = false;
root.notifications.smtpPortStartupError.active = false;
root.notifications.imapPortChangeError.active = false;
root.notifications.smtpPortChangeError.active = false;
Backend.setMailServerSettings(imapField.text, smtpField.text, Backend.useSSLForIMAP, Backend.useSSLForSMTP);
}
}
Button {
colorScheme: root.colorScheme
text: qsTr("Cancel")
onClicked: root.back()
secondary: true
}
text: qsTr("Cancel")
onClicked: root.back()
}
Connections {
target: Backend
function onChangeMailServerSettingsFinished() {
submitButton.loading = false
submitButton.loading = false;
}
target: Backend
}
}
onBack: {
root.setDefaultValues()
}
function validate(port) {
var num = port*1
if (! (num > 1 && num < 65536) ) {
return qsTr("Invalid port number")
}
if (imapField.text == smtpField.text) {
return qsTr("Port numbers must be different")
}
return
}
function isPortFree(field) {
var num = field.text*1
if (num === Backend.imapPort) return true
if (num === Backend.smtpPort) return true
if (!Backend.isPortFree(num)) {
field.error = true
field.errorString = qsTr("Port occupied")
return false
}
return true
}
function setDefaultValues(){
imapField.text = Backend.imapPort
smtpField.text = Backend.smtpPort
imapField.error = false
smtpField.error = false
}
Component.onCompleted: root.setDefaultValues()
}

View File

@ -1,20 +1,15 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick
import QtQuick.Templates as T

View File

@ -1,20 +1,15 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQml
import QtQuick
import QtQuick.Window
@ -25,14 +20,14 @@ import QtQuick.Templates as T
T.ApplicationWindow {
id: root
property ColorScheme colorScheme
// popup priority based on types
enum PopupType {
Banner = 0,
Dialog = 1
Banner,
Dialog
}
property ColorScheme colorScheme
// contains currently visible popup
property var popupVisible: null
@ -41,85 +36,61 @@ T.ApplicationWindow {
// overriding get method to ignore any role and return directly object itself
function get(row) {
if (row < 0 || row >= count) {
return undefined
return undefined;
}
return data(index(row, 0), Qt.DisplayRole)
}
onRowsInserted: function(parent, first, last) {
for (var i = first; i <= last; i++) {
var obj = popups.get(i)
obj.onShouldShowChanged.connect( root.processPopups )
}
processPopups()
return data(index(row, 0), Qt.DisplayRole);
}
onRowsAboutToBeRemoved: function (parent, first, last) {
for (var i = first; i <= last; i++ ) {
var obj = popups.get(i)
obj.onShouldShowChanged.disconnect( root.processPopups )
for (let i = first; i <= last; i++) {
const obj = popups.get(i);
obj.onShouldShowChanged.disconnect(root.processPopups);
// if currently visible popup was removed
if (root.popupVisible === obj) {
root.popupVisible.visible = false
root.popupVisible = null
root.popupVisible.visible = false;
root.popupVisible = null;
}
}
processPopups()
processPopups();
}
onRowsInserted: function (parent, first, last) {
for (let i = first; i <= last; i++) {
const obj = popups.get(i);
obj.onShouldShowChanged.connect(root.processPopups);
}
processPopups();
}
}
function processPopups() {
if ((root.popupVisible) && (!root.popupVisible.shouldShow)) {
root.popupVisible.visible = false
root.popupVisible.visible = false;
}
var topmost = null
for (var i = 0; i < popups.count; i++) {
var obj = popups.get(i)
let topmost = null;
for (let i = 0; i < popups.count; i++) {
const obj = popups.get(i);
if (obj.shouldShow === false) {
continue
continue;
}
if (topmost && (topmost.popupType > obj.popupType)) {
continue
continue;
}
if (topmost && (topmost.popupType === obj.popupType) && (topmost.occurred > obj.occurred)) {
continue
continue;
}
topmost = obj
topmost = obj;
}
if (root.popupVisible !== topmost) {
if (root.popupVisible) {
root.popupVisible.visible = false
root.popupVisible.visible = false;
}
root.popupVisible = topmost
root.popupVisible = topmost;
}
if (!root.popupVisible) {
return
}
root.popupVisible.visible = true
}
Connections {
target: root.popupVisible
function onVisibleChanged() {
if (root.popupVisible.visible) {
return
}
root.popupVisible = null
root.processPopups()
return;
}
root.popupVisible.visible = true;
}
color: root.colorScheme.background_norm
@ -127,8 +98,19 @@ T.ApplicationWindow {
Overlay.modal: Rectangle {
color: root.colorScheme.backdrop_norm
}
Overlay.modeless: Rectangle {
color: "transparent"
}
Connections {
function onVisibleChanged() {
if (root.popupVisible.visible) {
return;
}
root.popupVisible = null;
root.processPopups();
}
target: root.popupVisible
}
}

View File

@ -1,20 +1,15 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.impl
@ -23,212 +18,169 @@ import QtQuick.Layouts
import "." as Proton
T.Button {
property ColorScheme colorScheme
property alias secondary: control.flat
readonly property bool primary: !secondary
readonly property bool isIcon: control.text === ""
readonly property bool hasTextAndIcon: (control.text !== "") && (iconImage.source.toString().length > 0)
property bool loading: false
property bool borderless: false
property int labelType: Proton.Label.LabelType.Body
property alias textVerticalAlignment: label.verticalAlignment
property alias textHorizontalAlignment: label.horizontalAlignment
id: control
implicitWidth: Math.max(
implicitBackgroundWidth + leftInset + rightInset,
implicitContentWidth + leftPadding + rightPadding
)
implicitHeight: Math.max(
implicitBackgroundHeight + topInset + bottomInset,
implicitContentHeight + topPadding + bottomPadding
)
padding: 8
horizontalPadding: 16
spacing: 10
property bool borderless: false
property ColorScheme colorScheme
readonly property bool hasTextAndIcon: (control.text !== "") && (iconImage.source.toString().length > 0)
readonly property bool isIcon: control.text === ""
property int labelType: Proton.Label.LabelType.Body
property bool loading: false
readonly property bool primary: !secondary
property alias secondary: control.flat
property alias textHorizontalAlignment: label.horizontalAlignment
property alias textVerticalAlignment: label.verticalAlignment
font: label.font
icon.width: 16
icon.height: 16
horizontalPadding: 16
icon.color: {
if (primary && !isIcon) {
return "#FFFFFF"
return "#FFFFFF";
} else {
return control.colorScheme.text_norm
return control.colorScheme.text_norm;
}
}
icon.height: 16
icon.width: 16
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, implicitContentHeight + topPadding + bottomPadding)
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, implicitContentWidth + leftPadding + rightPadding)
padding: 8
spacing: 10
background: Rectangle {
border.color: {
return control.colorScheme.border_norm;
}
border.width: secondary && !borderless ? 1 : 0
color: {
if (!isIcon) {
if (primary) {
// Primary colors
if (control.down) {
return control.colorScheme.interaction_norm_active;
}
if (control.enabled && (control.highlighted || control.hovered || control.checked || control.activeFocus)) {
return control.colorScheme.interaction_norm_hover;
}
if (control.loading) {
return control.colorScheme.interaction_norm_hover;
}
return control.colorScheme.interaction_norm;
} else {
// Secondary colors
if (control.down) {
return control.colorScheme.interaction_default_active;
}
if (control.enabled && (control.highlighted || control.hovered || control.checked || control.activeFocus)) {
return control.colorScheme.interaction_default_hover;
}
if (control.loading) {
return control.colorScheme.interaction_default_hover;
}
return control.colorScheme.interaction_default;
}
} else {
if (primary) {
// Primary icon colors
if (control.down) {
return control.colorScheme.interaction_default_active;
}
if (control.enabled && (control.highlighted || control.hovered || control.checked || control.activeFocus)) {
return control.colorScheme.interaction_default_hover;
}
if (control.loading) {
return control.colorScheme.interaction_default_hover;
}
return control.colorScheme.interaction_default;
} else {
// Secondary icon colors
if (control.down) {
return control.colorScheme.interaction_default_active;
}
if (control.enabled && (control.highlighted || control.hovered || control.checked || control.activeFocus)) {
return control.colorScheme.interaction_default_hover;
}
if (control.loading) {
return control.colorScheme.interaction_default_hover;
}
return control.colorScheme.interaction_default;
}
}
}
implicitHeight: 36
implicitWidth: 36
opacity: control.enabled || control.loading ? 1.0 : 0.5
radius: ProtonStyle.button_radius
visible: true
}
contentItem: RowLayout {
id: _contentItem
spacing: control.hasTextAndIcon ? control.spacing : 0
Proton.Label {
colorScheme: root.colorScheme
id: label
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
elide: Text.ElideRight
horizontalAlignment: Qt.AlignHCenter
visible: !control.isIcon
text: control.text
Layout.fillWidth: true
color: {
if (primary && !isIcon) {
return "#FFFFFF"
return "#FFFFFF";
} else {
return control.colorScheme.text_norm
return control.colorScheme.text_norm;
}
}
colorScheme: root.colorScheme
elide: Text.ElideRight
horizontalAlignment: Qt.AlignHCenter
opacity: control.enabled || control.loading ? 1.0 : 0.5
text: control.text
type: labelType
visible: !control.isIcon
}
ColorImage {
id: iconImage
Layout.alignment: Qt.AlignCenter
color: control.icon.color
height: {
if (control.loading) {
return width;
}
Math.min(control.icon.height, availableHeight);
}
source: control.loading ? "/qml/icons/Loader_16.svg" : control.icon.source
sourceSize.height: control.icon.height
sourceSize.width: control.icon.width
visible: control.loading || control.icon.source
width: {
// special case for loading since we want icon to be square for rotation animation
if (control.loading) {
return Math.min(control.icon.width, availableWidth, control.icon.height, availableHeight)
return Math.min(control.icon.width, availableWidth, control.icon.height, availableHeight);
}
return Math.min(control.icon.width, availableWidth)
return Math.min(control.icon.width, availableWidth);
}
height: {
if (control.loading) {
return width
}
Math.min(control.icon.height, availableHeight)
}
sourceSize.width: control.icon.width
sourceSize.height: control.icon.height
color: control.icon.color
source: control.loading ? "/qml/icons/Loader_16.svg" : control.icon.source
visible: control.loading || control.icon.source
RotationAnimation {
target: iconImage
loops: Animation.Infinite
direction: RotationAnimation.Clockwise
duration: 1000
from: 0
to: 360
direction: RotationAnimation.Clockwise
loops: Animation.Infinite
running: control.loading
target: iconImage
to: 360
}
}
}
background: Rectangle {
implicitWidth: 36
implicitHeight: 36
radius: ProtonStyle.button_radius
visible: true
color: {
if (!isIcon) {
if (primary) {
// Primary colors
if (control.down) {
return control.colorScheme.interaction_norm_active
}
if (control.enabled && (control.highlighted || control.hovered || control.checked || control.activeFocus)) {
return control.colorScheme.interaction_norm_hover
}
if (control.loading) {
return control.colorScheme.interaction_norm_hover
}
return control.colorScheme.interaction_norm
} else {
// Secondary colors
if (control.down) {
return control.colorScheme.interaction_default_active
}
if (control.enabled && (control.highlighted || control.hovered || control.checked || control.activeFocus)) {
return control.colorScheme.interaction_default_hover
}
if (control.loading) {
return control.colorScheme.interaction_default_hover
}
return control.colorScheme.interaction_default
}
} else {
if (primary) {
// Primary icon colors
if (control.down) {
return control.colorScheme.interaction_default_active
}
if (control.enabled && (control.highlighted || control.hovered || control.checked || control.activeFocus)) {
return control.colorScheme.interaction_default_hover
}
if (control.loading) {
return control.colorScheme.interaction_default_hover
}
return control.colorScheme.interaction_default
} else {
// Secondary icon colors
if (control.down) {
return control.colorScheme.interaction_default_active
}
if (control.enabled && (control.highlighted || control.hovered || control.checked || control.activeFocus)) {
return control.colorScheme.interaction_default_hover
}
if (control.loading) {
return control.colorScheme.interaction_default_hover
}
return control.colorScheme.interaction_default
}
}
}
border.color: {
return control.colorScheme.border_norm
}
border.width: secondary && !borderless ? 1 : 0
opacity: control.enabled || control.loading ? 1.0 : 0.5
}
Component.onCompleted: {
if (!control.colorScheme) {
console.trace()
var next = root
for (var i = 0; i<1000; i++) {
console.log(i, next, "colorscheme", next.colorScheme)
next = next.parent
if (!next) break
console.trace();
let next = root;
for (let i = 0; i < 1000; i++) {
console.log(i, next, "colorscheme", next.colorScheme);
next = next.parent;
if (!next)
break;
}
console.error("ColorScheme not defined")
console.error("ColorScheme not defined");
}
}
}

View File

@ -1,97 +1,96 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.impl
import QtQuick.Templates as T
T.CheckBox {
property ColorScheme colorScheme
property bool error: false
id: control
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
implicitContentWidth + leftPadding + rightPadding)
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
implicitContentHeight + topPadding + bottomPadding,
implicitIndicatorHeight + topPadding + bottomPadding)
property ColorScheme colorScheme
property bool error: false
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, implicitContentHeight + topPadding + bottomPadding, implicitIndicatorHeight + topPadding + bottomPadding)
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, implicitContentWidth + leftPadding + rightPadding)
padding: 0
spacing: 8
contentItem: CheckLabel {
color: {
if (!enabled) {
return control.colorScheme.text_disabled;
}
if (error) {
return control.colorScheme.signal_danger;
}
return control.colorScheme.text_norm;
}
font.family: ProtonStyle.font_family
font.letterSpacing: ProtonStyle.body_letter_spacing
font.pixelSize: ProtonStyle.body_font_size
font.weight: ProtonStyle.fontWeight_400
leftPadding: control.indicator && !control.mirrored ? control.indicator.width + control.spacing : 0
lineHeight: ProtonStyle.body_line_height
lineHeightMode: Text.FixedHeight
rightPadding: control.indicator && control.mirrored ? control.indicator.width + control.spacing : 0
text: control.text
}
indicator: Rectangle {
implicitWidth: 20
border.color: {
if (!control.enabled) {
return control.colorScheme.field_disabled;
}
if (control.error) {
return control.colorScheme.signal_danger;
}
if (control.hovered || control.activeFocus) {
return control.colorScheme.interaction_norm_hover;
}
return control.colorScheme.field_norm;
}
border.width: control.checked ? 0 : 1
color: {
if (!checked) {
return control.colorScheme.background_norm;
}
if (!control.enabled) {
return control.colorScheme.field_disabled;
}
if (control.error) {
return control.colorScheme.signal_danger;
}
if (control.hovered || control.activeFocus) {
return control.colorScheme.interaction_norm_hover;
}
return control.colorScheme.interaction_norm;
}
implicitHeight: 20
implicitWidth: 20
radius: ProtonStyle.checkbox_radius
x: text ? (control.mirrored ? control.width - width - control.rightPadding : control.leftPadding) : control.leftPadding + (control.availableWidth - width) / 2
y: control.topPadding + (control.availableHeight - height) / 2
color: {
if (!checked) {
return control.colorScheme.background_norm
}
if (!control.enabled) {
return control.colorScheme.field_disabled
}
if (control.error) {
return control.colorScheme.signal_danger
}
if (control.hovered || control.activeFocus) {
return control.colorScheme.interaction_norm_hover
}
return control.colorScheme.interaction_norm
}
border.width: control.checked ? 0 : 1
border.color: {
if (!control.enabled) {
return control.colorScheme.field_disabled
}
if (control.error) {
return control.colorScheme.signal_danger
}
if (control.hovered || control.activeFocus) {
return control.colorScheme.interaction_norm_hover
}
return control.colorScheme.field_norm
}
ColorImage {
color: "#FFFFFF"
height: parent.height - 4
source: "/qml/icons/ic-check.svg"
sourceSize.height: parent.height - 4
sourceSize.width: parent.width - 4
visible: control.checkState === Qt.Checked
width: parent.width - 4
x: (parent.width - width) / 2
y: (parent.height - height) / 2
width: parent.width - 4
height: parent.height - 4
sourceSize.width: parent.width - 4
sourceSize.height: parent.height - 4
color: "#FFFFFF"
source: "/qml/icons/ic-check.svg"
visible: control.checkState === Qt.Checked
}
// TODO: do we need PartiallyChecked state?
@ -105,30 +104,4 @@ T.CheckBox {
// visible: control.checkState === Qt.PartiallyChecked
//}
}
contentItem: CheckLabel {
leftPadding: control.indicator && !control.mirrored ? control.indicator.width + control.spacing : 0
rightPadding: control.indicator && control.mirrored ? control.indicator.width + control.spacing : 0
text: control.text
color: {
if (!enabled) {
return control.colorScheme.text_disabled
}
if (error) {
return control.colorScheme.signal_danger
}
return control.colorScheme.text_norm
}
font.family: ProtonStyle.font_family
font.weight: ProtonStyle.fontWeight_400
font.pixelSize: ProtonStyle.body_font_size
lineHeight: ProtonStyle.body_line_height
lineHeightMode: Text.FixedHeight
font.letterSpacing: ProtonStyle.body_letter_spacing
}
}

View File

@ -1,93 +1,88 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick
import QtQml
QtObject {
// should be a pointer to ColorScheme object
property var prominent
// Primary
property color primary_norm
// Backdrop
property color backdrop_norm
property color background_avatar
// Interaction-norm
property color interaction_norm
property color interaction_norm_hover
property color interaction_norm_active
// Text
property color text_norm
property color text_weak
property color text_hint
property color text_disabled
property color text_invert
// Field
property color field_norm
property color field_hover
property color field_disabled
// Background
property color background_norm
property color background_strong
property color background_weak
// Border
property color border_norm
property color border_weak
property color field_disabled
property color field_hover
// Background
property color background_norm
property color background_weak
property color background_strong
property color background_avatar
// Interaction-weak
property color interaction_weak
property color interaction_weak_hover
property color interaction_weak_active
// Field
property color field_norm
// Interaction-default
property color interaction_default
property color interaction_default_hover
property color interaction_default_active
property color interaction_default_hover
// Interaction-norm
property color interaction_norm
property color interaction_norm_active
property color interaction_norm_hover
// Interaction-weak
property color interaction_weak
property color interaction_weak_active
property color interaction_weak_hover
property string logo_img
// Primary
property color primary_norm
// should be a pointer to ColorScheme object
property var prominent
property color scrollbar_hover
// Scrollbar
property color scrollbar_norm
property color scrollbar_hover
// Signal
property color signal_danger
property color signal_danger_hover
property color signal_danger_active
property color signal_warning
property color signal_warning_hover
property color signal_warning_active
property color signal_success
property color signal_success_hover
property color signal_success_active
property color signal_info
property color signal_info_hover
property color signal_info_active
property color shadow_lifted
// Shadows
property color shadow_norm
property color shadow_lifted
// Backdrop
property color backdrop_norm
// Signal
property color signal_danger
property color signal_danger_active
property color signal_danger_hover
property color signal_info
property color signal_info_active
property color signal_info_hover
property color signal_success
property color signal_success_active
property color signal_success_hover
property color signal_warning
property color signal_warning_active
property color signal_warning_hover
property color text_disabled
property color text_hint
property color text_invert
// Text
property color text_norm
property color text_weak
// Images
property string welcome_img
property string logo_img
}

View File

@ -1,20 +1,15 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick
import QtQuick.Window
import QtQuick.Controls
@ -26,148 +21,124 @@ T.ComboBox {
property ColorScheme colorScheme
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
implicitContentWidth + leftPadding + rightPadding)
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
implicitContentHeight + topPadding + bottomPadding,
implicitIndicatorHeight + topPadding + bottomPadding)
bottomPadding: 5
font.family: ProtonStyle.font_family
font.letterSpacing: ProtonStyle.body_letter_spacing
font.pixelSize: ProtonStyle.body_font_size
font.weight: ProtonStyle.fontWeight_400
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, implicitContentHeight + topPadding + bottomPadding, implicitIndicatorHeight + topPadding + bottomPadding)
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, implicitContentWidth + leftPadding + rightPadding)
leftPadding: 12 + (!root.mirrored || !indicator || !indicator.visible ? 0 : indicator.width + spacing)
rightPadding: 12 + (root.mirrored || !indicator || !indicator.visible ? 0 : indicator.width + spacing)
topPadding: 5
bottomPadding: 5
spacing: 8
topPadding: 5
font.family: ProtonStyle.font_family
font.weight: ProtonStyle.fontWeight_400
font.pixelSize: ProtonStyle.body_font_size
font.letterSpacing: ProtonStyle.body_letter_spacing
background: Rectangle {
border.color: root.colorScheme.border_norm
border.width: 1
color: {
if (root.down) {
return root.colorScheme.interaction_default_active;
}
if (root.enabled && root.hovered || root.activeFocus) {
return root.colorScheme.interaction_default_hover;
}
if (!root.enabled) {
return root.colorScheme.interaction_default;
}
return root.colorScheme.background_norm;
}
implicitHeight: 36
implicitWidth: 140
radius: ProtonStyle.context_item_radius
}
contentItem: T.TextField {
padding: 5
text: root.editable ? root.editText : root.displayText
font: root.font
enabled: root.editable
autoScroll: root.editable
readOnly: root.down
color: root.enabled ? root.colorScheme.text_norm : root.colorScheme.text_disabled
enabled: root.editable
font: root.font
inputMethodHints: root.inputMethodHints
padding: 5
placeholderTextColor: root.enabled ? root.colorScheme.text_hint : root.colorScheme.text_disabled
readOnly: root.down
selectedTextColor: root.colorScheme.text_invert
selectionColor: root.colorScheme.interaction_norm
text: root.editable ? root.editText : root.displayText
validator: root.validator
verticalAlignment: TextInput.AlignVCenter
color: root.enabled ? root.colorScheme.text_norm : root.colorScheme.text_disabled
selectionColor: root.colorScheme.interaction_norm
selectedTextColor: root.colorScheme.text_invert
placeholderTextColor: root.enabled ? root.colorScheme.text_hint : root.colorScheme.text_disabled
background: Rectangle {
radius: ProtonStyle.context_item_radius
visible: root.enabled && root.editable && !root.flat
border.color: {
if (root.activeFocus) {
return root.colorScheme.interaction_norm
return root.colorScheme.interaction_norm;
}
if (root.hovered || root.activeFocus) {
return root.colorScheme.field_hover
return root.colorScheme.field_hover;
}
return root.colorScheme.field_norm
return root.colorScheme.field_norm;
}
border.width: 1
color: root.colorScheme.background_norm
radius: ProtonStyle.context_item_radius
visible: root.enabled && root.editable && !root.flat
}
}
background: Rectangle {
implicitWidth: 140
implicitHeight: 36
radius: ProtonStyle.context_item_radius
color: {
if (root.down) {
return root.colorScheme.interaction_default_active
}
if (root.enabled && root.hovered || root.activeFocus) {
return root.colorScheme.interaction_default_hover
}
if (!root.enabled) {
return root.colorScheme.interaction_default
}
return root.colorScheme.background_norm
}
border.color: root.colorScheme.border_norm
border.width: 1
}
indicator: ColorImage {
x: root.mirrored ? 12 : root.width - width - 12
y: root.topPadding + (root.availableHeight - height) / 2
color: root.enabled ? root.colorScheme.text_norm : root.colorScheme.text_disabled
source: popup.visible ? "/qml/icons/ic-chevron-up.svg" : "/qml/icons/ic-chevron-down.svg"
sourceSize.width: 16
sourceSize.height: 16
}
delegate: ItemDelegate {
width: parent.width
text: root.textRole ? (Array.isArray(root.model) ? modelData[root.textRole] : model[root.textRole]) : modelData
palette.text: {
if (!root.enabled) {
return root.colorScheme.text_disabled
}
if (selected) {
return root.colorScheme.text_invert
}
return root.colorScheme.text_norm
}
font: root.font
hoverEnabled: root.hoverEnabled
property bool selected: root.currentIndex === index
font: root.font
highlighted: root.highlightedIndex === index
hoverEnabled: root.hoverEnabled
palette.highlightedText: selected ? root.colorScheme.text_invert : root.colorScheme.text_norm
palette.text: {
if (!root.enabled) {
return root.colorScheme.text_disabled;
}
if (selected) {
return root.colorScheme.text_invert;
}
return root.colorScheme.text_norm;
}
text: root.textRole ? (Array.isArray(root.model) ? modelData[root.textRole] : model[root.textRole]) : modelData
width: parent.width
background: PaddedRectangle {
radius: ProtonStyle.context_item_radius
color: {
if (parent.down) {
return root.colorScheme.interaction_default_active
return root.colorScheme.interaction_default_active;
}
if (parent.selected) {
return root.colorScheme.interaction_norm
return root.colorScheme.interaction_norm;
}
if (parent.hovered || parent.highlighted) {
return root.colorScheme.interaction_default_hover
return root.colorScheme.interaction_default_hover;
}
return root.colorScheme.interaction_default
return root.colorScheme.interaction_default;
}
radius: ProtonStyle.context_item_radius
}
}
indicator: ColorImage {
color: root.enabled ? root.colorScheme.text_norm : root.colorScheme.text_disabled
source: popup.visible ? "/qml/icons/ic-chevron-up.svg" : "/qml/icons/ic-chevron-down.svg"
sourceSize.height: 16
sourceSize.width: 16
x: root.mirrored ? 12 : root.width - width - 12
y: root.topPadding + (root.availableHeight - height) / 2
}
popup: T.Popup {
y: root.height
width: root.width
bottomMargin: 8
height: Math.min(contentItem.implicitHeight, root.Window.height - topMargin - bottomMargin)
topMargin: 8
bottomMargin: 8
width: root.width
y: root.height
background: Rectangle {
border.color: root.colorScheme.border_weak
border.width: 1
color: root.colorScheme.background_norm
radius: ProtonStyle.dialog_radius
}
contentItem: Item {
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
@ -175,21 +146,14 @@ T.ComboBox {
ListView {
anchors.fill: parent
anchors.margins: 8
currentIndex: root.highlightedIndex
implicitHeight: contentHeight
model: root.delegateModel
currentIndex: root.highlightedIndex
spacing: 4
T.ScrollIndicator.vertical: ScrollIndicator { }
T.ScrollIndicator.vertical: ScrollIndicator {
}
}
}
background: Rectangle {
color: root.colorScheme.background_norm
radius: ProtonStyle.dialog_radius
border.color: root.colorScheme.border_weak
border.width: 1
}
}
}

View File

@ -1,20 +1,15 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQml
import QtQuick
import QtQuick.Templates as T
@ -23,58 +18,46 @@ import QtQuick.Controls.impl
T.Dialog {
id: root
property ColorScheme colorScheme
Component.onCompleted: {
if (!ApplicationWindow.window) {
return
}
if (ApplicationWindow.window.popups === undefined) {
return
}
var obj = this
ApplicationWindow.window.popups.append( { obj } )
}
readonly property int popupType: ApplicationWindow.PopupType.Dialog
property bool shouldShow: false
readonly property var occurred: shouldShow ? new Date() : undefined
function open() {
root.shouldShow = true
}
readonly property int popupType: ApplicationWindow.PopupType.Dialog
property bool shouldShow: false
function close() {
root.shouldShow = false
root.shouldShow = false;
}
function open() {
root.shouldShow = true;
}
anchors.centerIn: Overlay.overlay
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
contentWidth + leftPadding + rightPadding,
implicitHeaderWidth,
implicitFooterWidth)
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
contentHeight + topPadding + bottomPadding
+ (implicitHeaderHeight > 0 ? implicitHeaderHeight + spacing : 0)
+ (implicitFooterHeight > 0 ? implicitFooterHeight + spacing : 0))
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, contentHeight + topPadding + bottomPadding + (implicitHeaderHeight > 0 ? implicitHeaderHeight + spacing : 0) + (implicitFooterHeight > 0 ? implicitFooterHeight + spacing : 0))
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, contentWidth + leftPadding + rightPadding, implicitHeaderWidth, implicitFooterWidth)
padding: 24
// TODO: Add DropShadow here
T.Overlay.modal: Rectangle {
color: root.colorScheme.backdrop_norm
}
T.Overlay.modeless: Rectangle {
color: "transparent"
}
background: Rectangle {
color: root.colorScheme.background_norm
radius: ProtonStyle.dialog_radius
}
// TODO: Add DropShadow here
T.Overlay.modal: Rectangle {
color: root.colorScheme.backdrop_norm
}
T.Overlay.modeless: Rectangle {
color: "transparent"
Component.onCompleted: {
if (!ApplicationWindow.window) {
return;
}
if (ApplicationWindow.window.popups === undefined) {
return;
}
const obj = this;
ApplicationWindow.window.popups.append({
"obj": obj
});
}
}

View File

@ -1,32 +1,23 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.impl
import QtQuick.Templates as T
import "." as Proton
T.Label {
id: root
property ColorScheme colorScheme
enum LabelType {
// weight 700, size 28, height 36
Heading,
@ -47,96 +38,92 @@ T.Label {
// weight 700, size 12, height 16, spacing 0.4
Caption_bold
}
property ColorScheme colorScheme
property int type: Proton.Label.LabelType.Body
function link(url, text) {
return `<a href="${url}">${text}</a>`;
}
color: root.enabled ? root.colorScheme.text_norm : root.colorScheme.text_disabled
linkColor: root.colorScheme.interaction_norm
palette.link: linkColor
font.family: ProtonStyle.font_family
lineHeightMode: Text.FixedHeight
font.weight: {
switch (root.type) {
case Proton.Label.LabelType.Heading:
return ProtonStyle.fontWeight_700
case Proton.Label.LabelType.Title:
return ProtonStyle.fontWeight_700
case Proton.Label.LabelType.Lead:
return ProtonStyle.fontWeight_400
case Proton.Label.LabelType.Body:
return ProtonStyle.fontWeight_400
case Proton.Label.LabelType.Body_semibold:
return ProtonStyle.fontWeight_600
case Proton.Label.LabelType.Body_bold:
return ProtonStyle.fontWeight_700
case Proton.Label.LabelType.Caption:
return ProtonStyle.fontWeight_400
case Proton.Label.LabelType.Caption_semibold:
return ProtonStyle.fontWeight_600
case Proton.Label.LabelType.Caption_bold:
return ProtonStyle.fontWeight_700
}
}
font.pixelSize: {
switch (root.type) {
case Proton.Label.LabelType.Heading:
return ProtonStyle.heading_font_size
case Proton.Label.LabelType.Title:
return ProtonStyle.title_font_size
case Proton.Label.LabelType.Lead:
return ProtonStyle.lead_font_size
case Proton.Label.LabelType.Body:
case Proton.Label.LabelType.Body_semibold:
case Proton.Label.LabelType.Body_bold:
return ProtonStyle.body_font_size
case Proton.Label.LabelType.Caption:
case Proton.Label.LabelType.Caption_semibold:
case Proton.Label.LabelType.Caption_bold:
return ProtonStyle.caption_font_size
}
}
lineHeight: {
switch (root.type) {
case Proton.Label.LabelType.Heading:
return ProtonStyle.heading_line_height
case Proton.Label.LabelType.Title:
return ProtonStyle.title_line_height
case Proton.Label.LabelType.Lead:
return ProtonStyle.lead_line_height
case Proton.Label.LabelType.Body:
case Proton.Label.LabelType.Body_semibold:
case Proton.Label.LabelType.Body_bold:
return ProtonStyle.body_line_height
case Proton.Label.LabelType.Caption:
case Proton.Label.LabelType.Caption_semibold:
case Proton.Label.LabelType.Caption_bold:
return ProtonStyle.caption_line_height
}
}
font.letterSpacing: {
switch (root.type) {
case Proton.Label.LabelType.Heading:
case Proton.Label.LabelType.Title:
case Proton.Label.LabelType.Lead:
return 0
return 0;
case Proton.Label.LabelType.Body:
case Proton.Label.LabelType.Body_semibold:
case Proton.Label.LabelType.Body_bold:
return ProtonStyle.body_letter_spacing
return ProtonStyle.body_letter_spacing;
case Proton.Label.LabelType.Caption:
case Proton.Label.LabelType.Caption_semibold:
case Proton.Label.LabelType.Caption_bold:
return ProtonStyle.caption_letter_spacing
return ProtonStyle.caption_letter_spacing;
}
}
verticalAlignment: Text.AlignBottom
function link(url, text) {
return `<a href="${url}">${text}</a>`
font.pixelSize: {
switch (root.type) {
case Proton.Label.LabelType.Heading:
return ProtonStyle.heading_font_size;
case Proton.Label.LabelType.Title:
return ProtonStyle.title_font_size;
case Proton.Label.LabelType.Lead:
return ProtonStyle.lead_font_size;
case Proton.Label.LabelType.Body:
case Proton.Label.LabelType.Body_semibold:
case Proton.Label.LabelType.Body_bold:
return ProtonStyle.body_font_size;
case Proton.Label.LabelType.Caption:
case Proton.Label.LabelType.Caption_semibold:
case Proton.Label.LabelType.Caption_bold:
return ProtonStyle.caption_font_size;
}
}
font.weight: {
switch (root.type) {
case Proton.Label.LabelType.Heading:
return ProtonStyle.fontWeight_700;
case Proton.Label.LabelType.Title:
return ProtonStyle.fontWeight_700;
case Proton.Label.LabelType.Lead:
return ProtonStyle.fontWeight_400;
case Proton.Label.LabelType.Body:
return ProtonStyle.fontWeight_400;
case Proton.Label.LabelType.Body_semibold:
return ProtonStyle.fontWeight_600;
case Proton.Label.LabelType.Body_bold:
return ProtonStyle.fontWeight_700;
case Proton.Label.LabelType.Caption:
return ProtonStyle.fontWeight_400;
case Proton.Label.LabelType.Caption_semibold:
return ProtonStyle.fontWeight_600;
case Proton.Label.LabelType.Caption_bold:
return ProtonStyle.fontWeight_700;
}
}
lineHeight: {
switch (root.type) {
case Proton.Label.LabelType.Heading:
return ProtonStyle.heading_line_height;
case Proton.Label.LabelType.Title:
return ProtonStyle.title_line_height;
case Proton.Label.LabelType.Lead:
return ProtonStyle.lead_line_height;
case Proton.Label.LabelType.Body:
case Proton.Label.LabelType.Body_semibold:
case Proton.Label.LabelType.Body_bold:
return ProtonStyle.body_line_height;
case Proton.Label.LabelType.Caption:
case Proton.Label.LabelType.Caption_semibold:
case Proton.Label.LabelType.Caption_bold:
return ProtonStyle.caption_line_height;
}
}
lineHeightMode: Text.FixedHeight
linkColor: root.colorScheme.interaction_norm
palette.link: linkColor
verticalAlignment: Text.AlignBottom
}

View File

@ -1,20 +1,15 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.impl
@ -27,22 +22,19 @@ T.Menu {
property ColorScheme colorScheme
implicitWidth: Math.max(
implicitBackgroundWidth + leftInset + rightInset,
contentWidth + leftPadding + rightPadding
)
implicitHeight: Math.max(
implicitBackgroundHeight + topInset + bottomInset,
contentHeight + topPadding + bottomPadding
)
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, contentHeight + topPadding + bottomPadding)
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, contentWidth + leftPadding + rightPadding)
margins: 0
overlap: 1
delegate: MenuItem {
colorScheme: control.colorScheme
background: Rectangle {
border.color: colorScheme.border_weak
border.width: 1
color: colorScheme.background_norm
implicitHeight: 40
implicitWidth: 200
radius: ProtonStyle.account_row_radius
}
contentItem: Item {
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
@ -50,23 +42,17 @@ T.Menu {
ListView {
anchors.fill: parent
anchors.margins: 8
implicitHeight: contentHeight
model: control.contentModel
interactive: Window.window ? contentHeight > Window.window.height : false
clip: true
currentIndex: control.currentIndex
implicitHeight: contentHeight
interactive: Window.window ? contentHeight > Window.window.height : false
model: control.contentModel
ScrollIndicator.vertical: ScrollIndicator {}
ScrollIndicator.vertical: ScrollIndicator {
}
}
}
background: Rectangle {
implicitWidth: 200
implicitHeight: 40
color: colorScheme.background_norm
border.width: 1
border.color: colorScheme.border_weak
radius: ProtonStyle.account_row_radius
delegate: MenuItem {
colorScheme: control.colorScheme
}
}

View File

@ -1,20 +1,15 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.impl
@ -26,46 +21,39 @@ T.MenuItem {
property ColorScheme colorScheme
width: parent.width // required. Other item overflows to the right of the menu and get clipped.
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
implicitContentHeight + topPadding + bottomPadding,
implicitIndicatorHeight + topPadding + bottomPadding)
font.family: ProtonStyle.font_family
font.letterSpacing: ProtonStyle.body_letter_spacing
font.pixelSize: ProtonStyle.body_font_size
font.weight: ProtonStyle.fontWeight_400
icon.color: control.enabled ? control.colorScheme.text_norm : control.colorScheme.text_disabled
icon.height: 24
icon.width: 24
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, implicitContentHeight + topPadding + bottomPadding, implicitIndicatorHeight + topPadding + bottomPadding)
padding: 12
spacing: 6
icon.width: 24
icon.height: 24
icon.color: control.enabled ? control.colorScheme.text_norm : control.colorScheme.text_disabled
font.family: ProtonStyle.font_family
font.weight: ProtonStyle.fontWeight_400
font.pixelSize: ProtonStyle.body_font_size
font.letterSpacing: ProtonStyle.body_letter_spacing
contentItem: IconLabel {
id: iconLabel
readonly property real arrowPadding: control.subMenu && control.arrow ? control.arrow.width + control.spacing : 0
readonly property real indicatorPadding: control.checkable && control.indicator ? control.indicator.width + control.spacing : 0
leftPadding: !control.mirrored ? indicatorPadding : arrowPadding
rightPadding: control.mirrored ? indicatorPadding : arrowPadding
spacing: control.spacing
mirrored: control.mirrored
display: control.display
alignment: Qt.AlignLeft
icon: control.icon
text: control.text
font: control.font
color: control.enabled ? control.colorScheme.text_norm : control.colorScheme.text_disabled
}
width: parent.width // required. Other item overflows to the right of the menu and get clipped.
background: Rectangle {
implicitWidth: 164
implicitHeight: 36
radius: ProtonStyle.button_radius
color: control.down ? control.colorScheme.interaction_default_active : control.highlighted ? control.colorScheme.interaction_default_hover : control.colorScheme.interaction_default
implicitHeight: 36
implicitWidth: 164
radius: ProtonStyle.button_radius
}
contentItem: IconLabel {
id: iconLabel
readonly property real arrowPadding: control.subMenu && control.arrow ? control.arrow.width + control.spacing : 0
readonly property real indicatorPadding: control.checkable && control.indicator ? control.indicator.width + control.spacing : 0
alignment: Qt.AlignLeft
color: control.enabled ? control.colorScheme.text_norm : control.colorScheme.text_disabled
display: control.display
font: control.font
icon: control.icon
leftPadding: !control.mirrored ? indicatorPadding : arrowPadding
mirrored: control.mirrored
rightPadding: control.mirrored ? indicatorPadding : arrowPadding
spacing: control.spacing
text: control.text
}
}

View File

@ -1,20 +1,15 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQml
import QtQuick
import QtQuick.Controls
@ -23,45 +18,40 @@ import QtQuick.Templates as T
T.Popup {
id: root
property ColorScheme colorScheme
Component.onCompleted: {
if (!ApplicationWindow.window) {
return
}
if (ApplicationWindow.window.popups === undefined) {
return
}
var obj = this
ApplicationWindow.window.popups.append( { obj } )
}
property int popupType: ApplicationWindow.PopupType.Banner
property bool shouldShow: false
readonly property var occurred: shouldShow ? new Date() : undefined
function open() {
root.shouldShow = true
}
property int popupType: ApplicationWindow.PopupType.Banner
property bool shouldShow: false
function close() {
root.shouldShow = false
root.shouldShow = false;
}
function open() {
root.shouldShow = true;
}
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
contentWidth + leftPadding + rightPadding)
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
contentHeight + topPadding + bottomPadding)
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, contentHeight + topPadding + bottomPadding)
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, contentWidth + leftPadding + rightPadding)
// TODO: Add DropShadow here
T.Overlay.modal: Rectangle {
color: root.colorScheme.backdrop_norm
}
T.Overlay.modeless: Rectangle {
color: "transparent"
}
Component.onCompleted: {
if (!ApplicationWindow.window) {
return;
}
if (ApplicationWindow.window.popups === undefined) {
return;
}
const obj = this;
ApplicationWindow.window.popups.append({
"obj": obj
});
}
}

View File

@ -1,115 +1,91 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.impl
import QtQuick.Templates as T
T.RadioButton {
property ColorScheme colorScheme
property bool error: false
id: control
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
implicitContentWidth + leftPadding + rightPadding)
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
implicitContentHeight + topPadding + bottomPadding,
implicitIndicatorHeight + topPadding + bottomPadding)
property ColorScheme colorScheme
property bool error: false
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, implicitContentHeight + topPadding + bottomPadding, implicitIndicatorHeight + topPadding + bottomPadding)
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, implicitContentWidth + leftPadding + rightPadding)
padding: 0
spacing: 8
contentItem: CheckLabel {
color: {
if (!enabled) {
return control.colorScheme.text_disabled;
}
if (error) {
return control.colorScheme.signal_danger;
}
return control.colorScheme.text_norm;
}
font.family: ProtonStyle.font_family
font.letterSpacing: ProtonStyle.body_letter_spacing
font.pixelSize: ProtonStyle.body_font_size
font.weight: ProtonStyle.fontWeight_400
leftPadding: control.indicator && !control.mirrored ? control.indicator.width + control.spacing : 0
lineHeight: ProtonStyle.body_line_height
lineHeightMode: Text.FixedHeight
rightPadding: control.indicator && control.mirrored ? control.indicator.width + control.spacing : 0
text: control.text
}
indicator: Rectangle {
implicitWidth: 20
border.color: {
if (!control.enabled) {
return control.colorScheme.field_disabled;
}
if (control.error) {
return control.colorScheme.signal_danger;
}
if (control.hovered || control.activeFocus) {
return control.colorScheme.interaction_norm_hover;
}
return control.colorScheme.field_norm;
}
border.width: 1
color: control.colorScheme.background_norm
implicitHeight: 20
implicitWidth: 20
radius: width / 2
x: text ? (control.mirrored ? control.width - width - control.rightPadding : control.leftPadding) : control.leftPadding + (control.availableWidth - width) / 2
y: control.topPadding + (control.availableHeight - height) / 2
color: control.colorScheme.background_norm
border.width: 1
border.color: {
if (!control.enabled) {
return control.colorScheme.field_disabled
}
if (control.error) {
return control.colorScheme.signal_danger
}
if (control.hovered || control.activeFocus) {
return control.colorScheme.interaction_norm_hover
}
return control.colorScheme.field_norm
}
Rectangle {
x: (parent.width - width) / 2
y: (parent.height - height) / 2
width: 8
height: 8
radius: width / 2
color: {
if (!control.enabled) {
return control.colorScheme.field_disabled
return control.colorScheme.field_disabled;
}
if (control.error) {
return control.colorScheme.signal_danger
return control.colorScheme.signal_danger;
}
if (control.hovered || control.activeFocus) {
return control.colorScheme.interaction_norm_hover
return control.colorScheme.interaction_norm_hover;
}
return control.colorScheme.interaction_norm
return control.colorScheme.interaction_norm;
}
height: 8
radius: width / 2
visible: control.checked
width: 8
x: (parent.width - width) / 2
y: (parent.height - height) / 2
}
}
contentItem: CheckLabel {
leftPadding: control.indicator && !control.mirrored ? control.indicator.width + control.spacing : 0
rightPadding: control.indicator && control.mirrored ? control.indicator.width + control.spacing : 0
text: control.text
color: {
if (!enabled) {
return control.colorScheme.text_disabled
}
if (error) {
return control.colorScheme.signal_danger
}
return control.colorScheme.text_norm
}
font.family: ProtonStyle.font_family
font.weight: ProtonStyle.fontWeight_400
font.pixelSize: ProtonStyle.body_font_size
lineHeight: ProtonStyle.body_line_height
lineHeightMode: Text.FixedHeight
font.letterSpacing: ProtonStyle.body_letter_spacing
}
}

View File

@ -1,387 +1,188 @@
// 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/>.
pragma Singleton
import QtQml
import QtQuick
import "./"
import "."
// https://wiki.qt.io/Qml_Styling
// http://imaginativethinking.ca/make-qml-component-singleton/
QtObject {
id: root
// TODO: Once we will use Qt >=5.15 this should be refactored with inline components as follows:
// https://doc.qt.io/qt-5/qtqml-documents-definetypes.html#inline-components
// component ColorScheme: QtObject {
// property color primary_norm
// ...
// }
property ColorScheme lightStyle: ColorScheme {
id: _lightStyle
prominent: lightProminentStyle
// Primary
primary_norm: "#6D4AFF"
// Interaction-norm
interaction_norm: "#6D4AFF"
interaction_norm_hover: "#4D34B3"
interaction_norm_active: "#372580"
// Text
text_norm: "#0C0C14"
text_weak: "#706D6B"
text_hint: "#8F8D8A"
text_disabled: "#C2BFBC"
text_invert: "#FFFFFF"
// Field
field_norm: "#ADABA8"
field_hover: "#8F8D8A"
field_disabled: "#D1CFCD"
// Border
border_norm: "#D1CFCD"
border_weak: "#EAE7E4"
// Background
background_norm: "#FFFFFF"
background_weak: "#F5F4F2"
background_strong: "#EAE7E4"
background_avatar: "#C2BFBC"
// Interaction-weak
interaction_weak: "#D1CFCD"
interaction_weak_hover: "#C2BFBC"
interaction_weak_active: "#A8A6A3"
// Interaction-default
interaction_default: Qt.rgba(0,0,0,0)
interaction_default_hover: Qt.rgba(194./255., 191./255., 188./255., 0.2)
interaction_default_active: Qt.rgba(194./255., 191./255., 188./255., 0.4)
// Scrollbar
scrollbar_norm: "#D1CFCD"
scrollbar_hover: "#C2BFBC"
// Signal
signal_danger: "#DC3251"
signal_danger_hover: "#F74F6D"
signal_danger_active: "#B72346"
signal_warning: "#FF9900"
signal_warning_hover: "#FFB800"
signal_warning_active: "#FF851A"
signal_success: "#1EA885"
signal_success_hover: "#23C299"
signal_success_active: "#198F71"
signal_info: "#239ECE"
signal_info_hover: "#27B1E8"
signal_info_active: "#1F83B5"
// Shadows
shadow_norm: Qt.rgba(0,0,0, 0.1) // #000000 10% x:0 y:1 blur:4
shadow_lifted: Qt.rgba(0,0,0, 0.16) // #000000 16% x:0 y:8 blur:24
// Backdrop
backdrop_norm: Qt.rgba(12./255., 12./255., 20./255., 0.32)
// Images
welcome_img: "/qml/icons/img-welcome.png"
logo_img: "/qml/icons/product_logos.svg"
}
property ColorScheme lightProminentStyle: ColorScheme {
id: _lightProminentStyle
prominent: this
// Primary
primary_norm: "#8A6EFF"
// Interaction-norm
interaction_norm: "#6D4AFF"
interaction_norm_hover: "#7C5CFF"
interaction_norm_active: "#8A6EFF"
// Text
text_norm: "#FFFFFF"
text_weak: "#9282D4"
text_hint: "#544399"
text_disabled: "#4A398F"
text_invert: "#1B1340"
// Field
field_norm: "#9282D4"
field_hover: "#7C5CFF"
field_disabled: "#38277A"
// Border
border_norm: "#413085"
border_weak: "#3C2B80"
// Background
background_norm: "#1B1340"
background_weak: "#271C57"
background_strong: "#38277A"
background_avatar: "#6D4AFF"
// Interaction-weak
interaction_weak: "#4A398F"
interaction_weak_hover: "#6D4AFF"
interaction_weak_active: "#8A6EFF"
// Interaction-default
interaction_default: Qt.rgba(0,0,0,0)
interaction_default_hover: Qt.rgba(68./255., 78./255., 114./255., 0.2)
interaction_default_active: Qt.rgba(68./255., 78./255., 114./255., 0.3)
// Scrollbar
scrollbar_norm: "#413085"
scrollbar_hover: "#4A398F"
// Signal
signal_danger: "#F5385A"
signal_danger_hover: "#FF5473"
signal_danger_active: "#DC3251"
signal_warning: "#FF9900"
signal_warning_hover: "#FFB800"
signal_warning_active: "#FF8419"
signal_success: "#1EA885"
signal_success_hover: "#23C299"
signal_success_active: "#198F71"
signal_info: "#2C89DB"
signal_info_hover: "#3491E3"
signal_info_active: "#1F83B5"
// Shadows
shadow_norm: Qt.rgba(0,0,0, 0.32) // #000000 32% x:0 y:1 blur:4
shadow_lifted: Qt.rgba(0,0,0, 0.40) // #000000 40% x:0 y:8 blur:24
// Backdrop
backdrop_norm: Qt.rgba(0,0,0, 0.32)
// Images
welcome_img: "/qml/icons/img-welcome-dark.png"
logo_img: "/qml/icons/product_logos_dark.svg"
}
property ColorScheme darkStyle: ColorScheme {
id: _darkStyle
prominent: darkProminentStyle
// Primary
primary_norm: "#8A6EFF"
// Interaction-norm
interaction_norm: "#6D4AFF"
interaction_norm_hover: "#7C5CFF"
interaction_norm_active: "#8A6EFF"
// Text
text_norm: "#FFFFFF"
text_weak: "#A7A4B5"
text_hint: "#6D697D"
text_disabled: "#5B576B"
text_invert: "#1C1B24"
// Field
field_norm: "#5B576B"
field_hover: "#6D697D"
field_disabled: "#3F3B4C"
// Border
border_norm: "#4A4658"
border_weak: "#343140"
// Background
background_norm: "#1C1B24"
background_weak: "#292733"
background_strong: "#3F3B4C"
background_avatar: "#6D4AFF"
// Interaction-weak
interaction_weak: "#4A4658"
interaction_weak_hover: "#5B576B"
interaction_weak_active: "#6D697D"
// Interaction-default
interaction_default: "#00000000"
interaction_default_hover: Qt.rgba(91./255.,87./255.,107./255.,0.2)
interaction_default_active: Qt.rgba(91./255.,87./255.,107./255.,0.4)
// Scrollbar
scrollbar_norm: "#4A4658"
scrollbar_hover: "#5B576B"
// Signal
signal_danger: "#F5385A"
signal_danger_hover: "#FF5473"
signal_danger_active: "#DC3251"
signal_warning: "#FF9900"
signal_warning_hover: "#FFB800"
signal_warning_active: "#FF8419"
signal_success: "#1EA885"
signal_success_hover: "#23C299"
signal_success_active: "#198F71"
signal_info: "#239ECE"
signal_info_hover: "#27B1E8"
signal_info_active: "#1F83B5"
// Shadows
shadow_norm: Qt.rgba(0,0,0,0.4) // #000000 40% x+0 y+1 blur:4
shadow_lifted: Qt.rgba(0,0,0,0.48) // #000000 48% x+0 y+8 blur:24
// Backdrop
backdrop_norm: Qt.rgba(0,0,0,0.32)
// Images
welcome_img: "/qml/icons/img-welcome-dark.png"
logo_img: "/qml/icons/product_logos_dark.svg"
}
property real account_hover_radius: 12 * root.px // px
property real account_row_radius: 12 * root.px // px
property real avatar_radius: 8 * root.px // px
property real banner_radius: 12 * root.px // px
property real big_avatar_radius: 12 * root.px // px
property int body_font_size: 14
property real body_letter_spacing: 0.2 * root.px
property int body_line_height: 20
property real button_radius: 8 * root.px // px
property int caption_font_size: 12
property real caption_letter_spacing: 0.4 * root.px
property int caption_line_height: 16
property real card_radius: 12 * root.px // px
property real checkbox_radius: 4 * root.px // px
property real context_item_radius: 8 * root.px // px
property ColorScheme currentStyle: lightStyle
property ColorScheme darkProminentStyle: ColorScheme {
id: _darkProminentStyle
prominent: this
// Backdrop
backdrop_norm: Qt.rgba(0, 0, 0, 0.32)
background_avatar: "#6D4AFF"
// Primary
primary_norm: "#8A6EFF"
// Interaction-norm
interaction_norm: "#6D4AFF"
interaction_norm_hover: "#7C5CFF"
interaction_norm_active: "#8A6EFF"
// Text
text_norm: "#FFFFFF"
text_weak: "#A7A4B5"
text_hint: "#6D697D"
text_disabled: "#5B576B"
text_invert: "#1C1B24"
// Field
field_norm: "#5B576B"
field_hover: "#6D697D"
field_disabled: "#3F3B4C"
// Background
background_norm: "#16141c"
background_strong: "#3F3B4C"
background_weak: "#292733"
// Border
border_norm: "#4A4658"
border_weak: "#343140"
field_disabled: "#3F3B4C"
field_hover: "#6D697D"
// Background
background_norm: "#16141c"
background_weak: "#292733"
background_strong: "#3F3B4C"
background_avatar: "#6D4AFF"
// Interaction-weak
interaction_weak: "#4A4658"
interaction_weak_hover: "#5B576B"
interaction_weak_active: "#6D697D"
// Field
field_norm: "#5B576B"
// Interaction-default
interaction_default: "#00000000"
interaction_default_hover: Qt.rgba(91./255.,87./255.,107./255.,0.2)
interaction_default_active: Qt.rgba(91./255.,87./255.,107./255.,0.4)
interaction_default_active: Qt.rgba(91. / 255., 87. / 255., 107. / 255., 0.4)
interaction_default_hover: Qt.rgba(91. / 255., 87. / 255., 107. / 255., 0.2)
// Interaction-norm
interaction_norm: "#6D4AFF"
interaction_norm_active: "#8A6EFF"
interaction_norm_hover: "#7C5CFF"
// Interaction-weak
interaction_weak: "#4A4658"
interaction_weak_active: "#6D697D"
interaction_weak_hover: "#5B576B"
logo_img: "/qml/icons/product_logos_dark.svg"
// Primary
primary_norm: "#8A6EFF"
prominent: this
scrollbar_hover: "#5B576B"
// Scrollbar
scrollbar_norm: "#4A4658"
scrollbar_hover: "#5B576B"
shadow_lifted: Qt.rgba(0, 0, 0, 0.48) // #000000 48% x+0 y+8 blur:24
// Shadows
shadow_norm: Qt.rgba(0, 0, 0, 0.4) // #000000 40% x+0 y+1 blur:4
// Signal
signal_danger: "#F5385A"
signal_danger_hover: "#FF5473"
signal_danger_active: "#DC3251"
signal_warning: "#FF9900"
signal_warning_hover: "#FFB800"
signal_warning_active: "#FF8419"
signal_success: "#1EA885"
signal_success_hover: "#23C299"
signal_success_active: "#198F71"
signal_danger_hover: "#FF5473"
signal_info: "#239ECE"
signal_info_hover: "#27B1E8"
signal_info_active: "#1F83B5"
signal_info_hover: "#27B1E8"
signal_success: "#1EA885"
signal_success_active: "#198F71"
signal_success_hover: "#23C299"
signal_warning: "#FF9900"
signal_warning_active: "#FF8419"
signal_warning_hover: "#FFB800"
text_disabled: "#5B576B"
text_hint: "#6D697D"
text_invert: "#1C1B24"
// Shadows
shadow_norm: Qt.rgba(0,0,0,0.4) // #000000 40% x+0 y+1 blur:4
shadow_lifted: Qt.rgba(0,0,0,0.48) // #000000 48% x+0 y+8 blur:24
// Backdrop
backdrop_norm: Qt.rgba(0,0,0,0.32)
// Text
text_norm: "#FFFFFF"
text_weak: "#A7A4B5"
// Images
welcome_img: "/qml/icons/img-welcome-dark.png"
logo_img: "/qml/icons/product_logos_dark.svg"
}
property ColorScheme darkStyle: ColorScheme {
id: _darkStyle
property ColorScheme currentStyle: lightStyle
// Backdrop
backdrop_norm: Qt.rgba(0, 0, 0, 0.32)
background_avatar: "#6D4AFF"
property string font_family: {
switch (Qt.platform.os) {
case "windows":
return "Segoe UI"
case "osx":
return ".AppleSystemUIFont" // should be SF Pro for the foreseeable future. Using "SF Pro Display" directly here is not allowed by the font's license.
case "linux":
return "Ubuntu"
default:
console.error("Unknown platform")
}
// Background
background_norm: "#1C1B24"
background_strong: "#3F3B4C"
background_weak: "#292733"
// Border
border_norm: "#4A4658"
border_weak: "#343140"
field_disabled: "#3F3B4C"
field_hover: "#6D697D"
// Field
field_norm: "#5B576B"
// Interaction-default
interaction_default: "#00000000"
interaction_default_active: Qt.rgba(91. / 255., 87. / 255., 107. / 255., 0.4)
interaction_default_hover: Qt.rgba(91. / 255., 87. / 255., 107. / 255., 0.2)
// Interaction-norm
interaction_norm: "#6D4AFF"
interaction_norm_active: "#8A6EFF"
interaction_norm_hover: "#7C5CFF"
// Interaction-weak
interaction_weak: "#4A4658"
interaction_weak_active: "#6D697D"
interaction_weak_hover: "#5B576B"
logo_img: "/qml/icons/product_logos_dark.svg"
// Primary
primary_norm: "#8A6EFF"
prominent: darkProminentStyle
scrollbar_hover: "#5B576B"
// Scrollbar
scrollbar_norm: "#4A4658"
shadow_lifted: Qt.rgba(0, 0, 0, 0.48) // #000000 48% x+0 y+8 blur:24
// Shadows
shadow_norm: Qt.rgba(0, 0, 0, 0.4) // #000000 40% x+0 y+1 blur:4
// Signal
signal_danger: "#F5385A"
signal_danger_active: "#DC3251"
signal_danger_hover: "#FF5473"
signal_info: "#239ECE"
signal_info_active: "#1F83B5"
signal_info_hover: "#27B1E8"
signal_success: "#1EA885"
signal_success_active: "#198F71"
signal_success_hover: "#23C299"
signal_warning: "#FF9900"
signal_warning_active: "#FF8419"
signal_warning_hover: "#FFB800"
text_disabled: "#5B576B"
text_hint: "#6D697D"
text_invert: "#1C1B24"
// Text
text_norm: "#FFFFFF"
text_weak: "#A7A4B5"
// Images
welcome_img: "/qml/icons/img-welcome-dark.png"
}
property real px : 1.00 // px
property real input_radius : 8 * root.px // px
property real button_radius : 8 * root.px // px
property real checkbox_radius : 4 * root.px // px
property real avatar_radius : 8 * root.px // px
property real big_avatar_radius : 12 * root.px // px
property real account_hover_radius : 12 * root.px // px
property real account_row_radius : 12 * root.px // px
property real context_item_radius : 8 * root.px // px
property real banner_radius : 12 * root.px // px
property real dialog_radius : 12 * root.px // px
property real card_radius : 12 * root.px // px
property real progress_bar_radius : 3 * root.px // px
property real tooltip_radius : 8 * root.px // px
property int heading_font_size: 28
property int heading_line_height: 36
property int title_font_size: 20
property int title_line_height: 24
property int lead_font_size: 18
property int lead_line_height: 26
property int body_font_size: 14
property int body_line_height: 20
property real body_letter_spacing: 0.2 * root.px
property int caption_font_size: 12
property int caption_line_height: 16
property real caption_letter_spacing: 0.4 * root.px
property real dialog_radius: 12 * root.px // px
property int fontWeight_100: Font.Thin
property int fontWeight_200: Font.Light
property int fontWeight_300: Font.ExtraLight
@ -391,4 +192,179 @@ QtObject {
property int fontWeight_700: Font.Bold
property int fontWeight_800: Font.ExtraBold
property int fontWeight_900: Font.Black
property string font_family: {
switch (Qt.platform.os) {
case "windows":
return "Segoe UI";
case "osx":
return ".AppleSystemUIFont"; // should be SF Pro for the foreseeable future. Using "SF Pro Display" directly here is not allowed by the font's license.
case "linux":
return "Ubuntu";
default:
console.error("Unknown platform");
}
}
property int heading_font_size: 28
property int heading_line_height: 36
property real input_radius: 8 * root.px // px
property int lead_font_size: 18
property int lead_line_height: 26
property ColorScheme lightProminentStyle: ColorScheme {
id: _lightProminentStyle
// Backdrop
backdrop_norm: Qt.rgba(0, 0, 0, 0.32)
background_avatar: "#6D4AFF"
// Background
background_norm: "#1B1340"
background_strong: "#38277A"
background_weak: "#271C57"
// Border
border_norm: "#413085"
border_weak: "#3C2B80"
field_disabled: "#38277A"
field_hover: "#7C5CFF"
// Field
field_norm: "#9282D4"
// Interaction-default
interaction_default: Qt.rgba(0, 0, 0, 0)
interaction_default_active: Qt.rgba(68. / 255., 78. / 255., 114. / 255., 0.3)
interaction_default_hover: Qt.rgba(68. / 255., 78. / 255., 114. / 255., 0.2)
// Interaction-norm
interaction_norm: "#6D4AFF"
interaction_norm_active: "#8A6EFF"
interaction_norm_hover: "#7C5CFF"
// Interaction-weak
interaction_weak: "#4A398F"
interaction_weak_active: "#8A6EFF"
interaction_weak_hover: "#6D4AFF"
logo_img: "/qml/icons/product_logos_dark.svg"
// Primary
primary_norm: "#8A6EFF"
prominent: this
scrollbar_hover: "#4A398F"
// Scrollbar
scrollbar_norm: "#413085"
shadow_lifted: Qt.rgba(0, 0, 0, 0.40) // #000000 40% x:0 y:8 blur:24
// Shadows
shadow_norm: Qt.rgba(0, 0, 0, 0.32) // #000000 32% x:0 y:1 blur:4
// Signal
signal_danger: "#F5385A"
signal_danger_active: "#DC3251"
signal_danger_hover: "#FF5473"
signal_info: "#2C89DB"
signal_info_active: "#1F83B5"
signal_info_hover: "#3491E3"
signal_success: "#1EA885"
signal_success_active: "#198F71"
signal_success_hover: "#23C299"
signal_warning: "#FF9900"
signal_warning_active: "#FF8419"
signal_warning_hover: "#FFB800"
text_disabled: "#4A398F"
text_hint: "#544399"
text_invert: "#1B1340"
// Text
text_norm: "#FFFFFF"
text_weak: "#9282D4"
// Images
welcome_img: "/qml/icons/img-welcome-dark.png"
}
// TODO: Once we will use Qt >=5.15 this should be refactored with inline components as follows:
// https://doc.qt.io/qt-5/qtqml-documents-definetypes.html#inline-components
// component ColorScheme: QtObject {
// property color primary_norm
// ...
// }
property ColorScheme lightStyle: ColorScheme {
id: _lightStyle
// Backdrop
backdrop_norm: Qt.rgba(12. / 255., 12. / 255., 20. / 255., 0.32)
background_avatar: "#C2BFBC"
// Background
background_norm: "#FFFFFF"
background_strong: "#EAE7E4"
background_weak: "#F5F4F2"
// Border
border_norm: "#D1CFCD"
border_weak: "#EAE7E4"
field_disabled: "#D1CFCD"
field_hover: "#8F8D8A"
// Field
field_norm: "#ADABA8"
// Interaction-default
interaction_default: Qt.rgba(0, 0, 0, 0)
interaction_default_active: Qt.rgba(194. / 255., 191. / 255., 188. / 255., 0.4)
interaction_default_hover: Qt.rgba(194. / 255., 191. / 255., 188. / 255., 0.2)
// Interaction-norm
interaction_norm: "#6D4AFF"
interaction_norm_active: "#372580"
interaction_norm_hover: "#4D34B3"
// Interaction-weak
interaction_weak: "#D1CFCD"
interaction_weak_active: "#A8A6A3"
interaction_weak_hover: "#C2BFBC"
logo_img: "/qml/icons/product_logos.svg"
// Primary
primary_norm: "#6D4AFF"
prominent: lightProminentStyle
scrollbar_hover: "#C2BFBC"
// Scrollbar
scrollbar_norm: "#D1CFCD"
shadow_lifted: Qt.rgba(0, 0, 0, 0.16) // #000000 16% x:0 y:8 blur:24
// Shadows
shadow_norm: Qt.rgba(0, 0, 0, 0.1) // #000000 10% x:0 y:1 blur:4
// Signal
signal_danger: "#DC3251"
signal_danger_active: "#B72346"
signal_danger_hover: "#F74F6D"
signal_info: "#239ECE"
signal_info_active: "#1F83B5"
signal_info_hover: "#27B1E8"
signal_success: "#1EA885"
signal_success_active: "#198F71"
signal_success_hover: "#23C299"
signal_warning: "#FF9900"
signal_warning_active: "#FF851A"
signal_warning_hover: "#FFB800"
text_disabled: "#C2BFBC"
text_hint: "#8F8D8A"
text_invert: "#FFFFFF"
// Text
text_norm: "#0C0C14"
text_weak: "#706D6B"
// Images
welcome_img: "/qml/icons/img-welcome.png"
}
property real progress_bar_radius: 3 * root.px // px
property real px: 1.00 // px
property int title_font_size: 20
property int title_line_height: 24
property real tooltip_radius: 8 * root.px // px
}

View File

@ -1,150 +1,124 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick
import QtQuick.Templates as T
import QtQuick.Controls
import QtQuick.Controls.impl
T.Switch {
property ColorScheme colorScheme
id: control
property ColorScheme colorScheme
property bool loading: false
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, implicitContentHeight + topPadding + bottomPadding, implicitIndicatorHeight + topPadding + bottomPadding)
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, implicitContentWidth + leftPadding + rightPadding)
padding: 0
spacing: 7
contentItem: CheckLabel {
id: label
color: control.enabled || control.loading ? control.colorScheme.text_norm : control.colorScheme.text_disabled
font.family: ProtonStyle.font_family
font.letterSpacing: ProtonStyle.body_letter_spacing
font.pixelSize: ProtonStyle.body_font_size
font.weight: ProtonStyle.fontWeight_400
leftPadding: control.indicator && !control.mirrored ? control.indicator.width + control.spacing : 0
lineHeight: ProtonStyle.body_line_height
lineHeightMode: Text.FixedHeight
rightPadding: control.indicator && control.mirrored ? control.indicator.width + control.spacing : 0
text: control.text
}
indicator: Rectangle {
border.color: control.hovered ? control.colorScheme.field_hover : control.colorScheme.field_norm
border.width: control.enabled && !loading ? 1 : 0
color: control.enabled || control.loading ? control.colorScheme.background_norm : control.colorScheme.background_strong
implicitHeight: 24
implicitWidth: 40
radius: height / 2.
x: text ? (control.mirrored ? control.width - width - control.rightPadding : control.leftPadding) : control.leftPadding + (control.availableWidth - width) / 2
y: control.topPadding + (control.availableHeight - height) / 2
Rectangle {
color: {
if (!control.enabled) {
return control.colorScheme.field_disabled;
}
if (control.checked) {
if (control.hovered || control.activeFocus) {
return control.colorScheme.interaction_norm_hover;
}
return control.colorScheme.interaction_norm;
}
if (control.hovered || control.activeFocus) {
return control.colorScheme.field_hover;
}
return control.colorScheme.field_norm;
}
height: 24
radius: parent.radius
visible: !loading
width: 24
x: Math.max(0, Math.min(parent.width - width, control.visualPosition * parent.width - (width / 2)))
y: (parent.height - height) / 2
Behavior on x {
enabled: !control.down
SmoothedAnimation {
velocity: 200
}
}
ColorImage {
color: "#FFFFFF"
height: 16
source: "/qml/icons/ic-check.svg"
sourceSize.height: 16
sourceSize.width: 16
visible: control.checked
width: 16
x: (parent.width - width) / 2
y: (parent.height - height) / 2
}
}
ColorImage {
id: loadingImage
color: control.colorScheme.interaction_norm_hover
height: 18
source: "/qml/icons/Loader_16.svg"
sourceSize.height: 18
sourceSize.width: 18
visible: control.loading
width: 18
x: parent.width - width
y: (parent.height - height) / 2
RotationAnimation {
direction: RotationAnimation.Clockwise
duration: 1000
from: 0
loops: Animation.Infinite
running: control.loading
target: loadingImage
to: 360
}
}
}
// TODO: store previous enabled state and restore it?
// For now assuming that only enabled buttons could have loading state
onLoadingChanged: {
if (loading) {
enabled = false
} else {
enabled = true
}
}
id: control
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
implicitContentWidth + leftPadding + rightPadding)
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
implicitContentHeight + topPadding + bottomPadding,
implicitIndicatorHeight + topPadding + bottomPadding)
padding: 0
spacing: 7
indicator: Rectangle {
implicitWidth: 40
implicitHeight: 24
x: text ? (control.mirrored ? control.width - width - control.rightPadding : control.leftPadding) : control.leftPadding + (control.availableWidth - width) / 2
y: control.topPadding + (control.availableHeight - height) / 2
radius: height / 2.
color: control.enabled || control.loading ? control.colorScheme.background_norm : control.colorScheme.background_strong
border.width: control.enabled && !loading ? 1 : 0
border.color: control.hovered ? control.colorScheme.field_hover : control.colorScheme.field_norm
Rectangle {
x: Math.max(0, Math.min(parent.width - width, control.visualPosition * parent.width - (width / 2)))
y: (parent.height - height) / 2
width: 24
height: 24
radius: parent.radius
visible: !loading
color: {
if (!control.enabled) {
return control.colorScheme.field_disabled
}
if (control.checked) {
if (control.hovered || control.activeFocus) {
return control.colorScheme.interaction_norm_hover
}
return control.colorScheme.interaction_norm
}
if (control.hovered || control.activeFocus) {
return control.colorScheme.field_hover
}
return control.colorScheme.field_norm
}
ColorImage {
x: (parent.width - width) / 2
y: (parent.height - height) / 2
width: 16
height: 16
sourceSize.width: 16
sourceSize.height: 16
color: "#FFFFFF"
source: "/qml/icons/ic-check.svg"
visible: control.checked
}
Behavior on x {
enabled: !control.down
SmoothedAnimation { velocity: 200 }
}
}
ColorImage {
id: loadingImage
x: parent.width - width
y: (parent.height - height) / 2
width: 18
height: 18
sourceSize.width: 18
sourceSize.height: 18
color: control.colorScheme.interaction_norm_hover
source: "/qml/icons/Loader_16.svg"
visible: control.loading
RotationAnimation {
target: loadingImage
loops: Animation.Infinite
duration: 1000
from: 0
to: 360
direction: RotationAnimation.Clockwise
running: control.loading
}
}
}
contentItem: CheckLabel {
id: label
leftPadding: control.indicator && !control.mirrored ? control.indicator.width + control.spacing : 0
rightPadding: control.indicator && control.mirrored ? control.indicator.width + control.spacing : 0
text: control.text
color: control.enabled || control.loading ? control.colorScheme.text_norm : control.colorScheme.text_disabled
font.family: ProtonStyle.font_family
font.weight: ProtonStyle.fontWeight_400
font.pixelSize: ProtonStyle.body_font_size
lineHeight: ProtonStyle.body_line_height
lineHeightMode: Text.FixedHeight
font.letterSpacing: ProtonStyle.body_letter_spacing
enabled = !loading;
}
}

View File

@ -1,54 +1,37 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQml
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.impl
import QtQuick.Templates as T
import QtQuick.Layouts
import "." as Proton
FocusScope {
id: root
property ColorScheme colorScheme
property alias background: control.background
property alias bottomInset: control.bottomInset
//property alias flickable: control.flickable
property alias focusReason: control.focusReason
property alias hoverEnabled: control.hoverEnabled
property alias hovered: control.hovered
property alias implicitBackgroundHeight: control.implicitBackgroundHeight
property alias implicitBackgroundWidth: control.implicitBackgroundWidth
property alias leftInset: control.leftInset
property alias palette: control.palette
property alias placeholderText: control.placeholderText
property alias placeholderTextColor: control.placeholderTextColor
property alias rightInset: control.rightInset
property alias topInset: control.topInset
property alias activeFocusOnPress: control.activeFocusOnPress
property string assistiveText
property alias background: control.background
property alias baseUrl: control.baseUrl
property alias bottomInset: control.bottomInset
property alias bottomPadding: control.bottomPadding
property alias canPaste: control.canPaste
property alias canRedo: control.canRedo
property alias canUndo: control.canUndo
property alias color: control.color
property ColorScheme colorScheme
property alias contentHeight: control.contentHeight
property alias contentWidth: control.contentWidth
property alias cursorDelegate: control.cursorDelegate
@ -56,21 +39,36 @@ FocusScope {
property alias cursorRectangle: control.cursorRectangle
property alias cursorVisible: control.cursorVisible
property alias effectiveHorizontalAlignment: control.effectiveHorizontalAlignment
property bool error: false
property string errorString
//property alias flickable: control.flickable
property alias focusReason: control.focusReason
property alias font: control.font
property alias hint: hint.text
property alias horizontalAlignment: control.horizontalAlignment
property alias hoverEnabled: control.hoverEnabled
property alias hovered: control.hovered
property alias hoveredLink: control.hoveredLink
property alias implicitBackgroundHeight: control.implicitBackgroundHeight
property alias implicitBackgroundWidth: control.implicitBackgroundWidth
property alias inputMethodComposing: control.inputMethodComposing
property alias inputMethodHints: control.inputMethodHints
property alias label: label.text
property alias leftInset: control.leftInset
property alias leftPadding: control.leftPadding
property alias length: control.length
property alias lineCount: control.lineCount
property alias mouseSelectionMode: control.mouseSelectionMode
property alias overwriteMode: control.overwriteMode
property alias padding: control.padding
property alias palette: control.palette
property alias persistentSelection: control.persistentSelection
property alias placeholderText: control.placeholderText
property alias placeholderTextColor: control.placeholderTextColor
property alias preeditText: control.preeditText
property alias readOnly: control.readOnly
property alias renderType: control.renderType
property alias rightInset: control.rightInset
property alias rightPadding: control.rightPadding
property alias selectByKeyboard: control.selectByKeyboard
property alias selectByMouse: control.selectByMouse
@ -84,61 +82,119 @@ FocusScope {
property alias textDocument: control.textDocument
property alias textFormat: control.textFormat
property alias textMargin: control.textMargin
property alias topInset: control.topInset
property alias topPadding: control.topPadding
property bool validateOnEditingFinished: true
// We are using our own type of validators. It should be a function
// returning an error string in case of error and undefined if no error
property var validator
property alias verticalAlignment: control.verticalAlignment
property alias wrapMode: control.wrapMode
implicitWidth: children[0].implicitWidth
implicitHeight: children[0].implicitHeight
signal editingFinished
property alias label: label.text
property alias hint: hint.text
property string assistiveText
property string errorString
property bool error: false
signal editingFinished()
function append(text) { return control.append(text) }
function clear() { return control.clear() }
function copy() { return control.copy() }
function cut() { return control.cut() }
function deselect() { return control.deselect() }
function getFormattedText(start, end) { return control.getFormattedText(start, end) }
function getText(start, end) { return control.getText(start, end) }
function insert(position, text) { return control.insert(position, text) }
function isRightToLeft(start, end) { return control.isRightToLeft(start, end) }
function linkAt(x, y) { return control.linkAt(x, y) }
function moveCursorSelection(position, mode) { return control.moveCursorSelection(position, mode) }
function paste() { return control.paste() }
function positionAt(x, y) { return control.positionAt(x, y) }
function positionToRectangle(position) { return control.positionToRectangle(position) }
function redo() { return control.redo() }
function remove(start, end) { return control.remove(start, end) }
function select(start, end) { return control.select(start, end) }
function selectAll() { return control.selectAll() }
function selectWord() { return control.selectWord() }
function undo() { return control.undo() }
function append(text) {
return control.append(text);
}
function clear() {
return control.clear();
}
function copy() {
return control.copy();
}
function cut() {
return control.cut();
}
function deselect() {
return control.deselect();
}
function getFormattedText(start, end) {
return control.getFormattedText(start, end);
}
function getText(start, end) {
return control.getText(start, end);
}
// Calculates the height of the component to make exactly lineNum visible in edit area
function heightForLinesVisible(lineNum) {
var totalHeight = 0
totalHeight += headerLayout.height
totalHeight += footerLayout.height
totalHeight += control.topPadding + control.bottomPadding
totalHeight += lineNum * fontMetrics.height
return totalHeight
let totalHeight = 0;
totalHeight += headerLayout.height;
totalHeight += footerLayout.height;
totalHeight += control.topPadding + control.bottomPadding;
totalHeight += lineNum * fontMetrics.height;
return totalHeight;
}
function insert(position, text) {
return control.insert(position, text);
}
function isRightToLeft(start, end) {
return control.isRightToLeft(start, end);
}
function linkAt(x, y) {
return control.linkAt(x, y);
}
function moveCursorSelection(position, mode) {
return control.moveCursorSelection(position, mode);
}
function paste() {
return control.paste();
}
function positionAt(x, y) {
return control.positionAt(x, y);
}
function positionToRectangle(position) {
return control.positionToRectangle(position);
}
function redo() {
return control.redo();
}
function remove(start, end) {
return control.remove(start, end);
}
function select(start, end) {
return control.select(start, end);
}
function selectAll() {
return control.selectAll();
}
function selectWord() {
return control.selectWord();
}
function undo() {
return control.undo();
}
function validate() {
if (validator === undefined) {
return;
}
const error = validator(text);
if (error) {
root.error = true;
root.errorString = error;
} else {
root.error = false;
root.errorString = "";
}
}
implicitHeight: children[0].implicitHeight
implicitWidth: children[0].implicitWidth
onEditingFinished: {
if (!validateOnEditingFinished) {
return;
}
validate();
}
onTextChanged: {
root.error = false;
root.errorString = "";
}
FontMetrics {
id: fontMetrics
font: control.font
}
ColumnLayout {
anchors.fill: parent
spacing: 0
@ -149,154 +205,131 @@ FocusScope {
spacing: 0
Proton.Label {
colorScheme: root.colorScheme
id: label
Layout.fillWidth: true
color: root.enabled ? root.colorScheme.text_norm : root.colorScheme.text_disabled
colorScheme: root.colorScheme
type: Proton.Label.LabelType.Body_semibold
}
Proton.Label {
colorScheme: root.colorScheme
id: hint
Layout.fillWidth: true
color: root.enabled ? root.colorScheme.text_weak : root.colorScheme.text_disabled
colorScheme: root.colorScheme
horizontalAlignment: Text.AlignRight
type: Proton.Label.LabelType.Caption
}
}
ScrollView {
id: controlView
Layout.fillHeight: true
Layout.fillWidth: true
clip: true
T.TextArea {
id: control
implicitWidth: Math.max(
contentWidth + leftPadding + rightPadding,
implicitBackgroundWidth + leftInset + rightInset,
placeholder.implicitWidth + leftPadding + rightPadding
)
implicitHeight: Math.max(
contentHeight + topPadding + bottomPadding,
implicitBackgroundHeight + topInset + bottomInset,
placeholder.implicitHeight + topPadding + bottomPadding
)
topPadding: 8
KeyNavigation.backtab: root.KeyNavigation.backtab
KeyNavigation.down: root.KeyNavigation.down
KeyNavigation.left: root.KeyNavigation.left
KeyNavigation.priority: root.KeyNavigation.priority
KeyNavigation.right: root.KeyNavigation.right
KeyNavigation.tab: root.KeyNavigation.tab
KeyNavigation.up: root.KeyNavigation.up
bottomPadding: 8
leftPadding: 12
rightPadding: 12
font.family: ProtonStyle.font_family
font.weight: ProtonStyle.fontWeight_400
font.pixelSize: ProtonStyle.body_font_size
font.letterSpacing: ProtonStyle.body_letter_spacing
color: control.enabled ? root.colorScheme.text_norm : root.colorScheme.text_disabled
placeholderTextColor: control.enabled ? root.colorScheme.text_hint : root.colorScheme.text_disabled
selectionColor: control.palette.highlight
selectedTextColor: control.palette.highlightedText
onEditingFinished: root.editingFinished()
wrapMode: TextInput.Wrap
color: {
if (!control.enabled) {
return root.colorScheme.text_disabled
}
if (control.readOnly) {
return root.colorScheme.text_hint
}
return root.colorScheme.text_norm
}
// enforcing default focus here within component
focus: root.focus
KeyNavigation.priority: root.KeyNavigation.priority
KeyNavigation.backtab: root.KeyNavigation.backtab
KeyNavigation.tab: root.KeyNavigation.tab
KeyNavigation.up: root.KeyNavigation.up
KeyNavigation.down: root.KeyNavigation.down
KeyNavigation.left: root.KeyNavigation.left
KeyNavigation.right: root.KeyNavigation.right
font.family: ProtonStyle.font_family
font.letterSpacing: ProtonStyle.body_letter_spacing
font.pixelSize: ProtonStyle.body_font_size
font.weight: ProtonStyle.fontWeight_400
implicitHeight: Math.max(contentHeight + topPadding + bottomPadding, implicitBackgroundHeight + topInset + bottomInset, placeholder.implicitHeight + topPadding + bottomPadding)
implicitWidth: Math.max(contentWidth + leftPadding + rightPadding, implicitBackgroundWidth + leftInset + rightInset, placeholder.implicitWidth + leftPadding + rightPadding)
leftPadding: 12
placeholderTextColor: control.enabled ? root.colorScheme.text_hint : root.colorScheme.text_disabled
rightPadding: 12
selectByMouse: true
cursorDelegate: Rectangle {
id: cursor
width: 1
color: root.colorScheme.interaction_norm
visible: control.activeFocus && !control.readOnly && control.selectionStart === control.selectionEnd
Connections {
target: control
function onCursorPositionChanged() {
// keep a moving cursor visible
cursor.opacity = 1
timer.restart()
}
}
Timer {
id: timer
running: control.activeFocus && !control.readOnly
repeat: true
interval: Qt.styleHints.cursorFlashTime / 2
onTriggered: cursor.opacity = !cursor.opacity ? 1 : 0
// force the cursor visible when gaining focus
onRunningChanged: cursor.opacity = 1
}
}
PlaceholderText {
id: placeholder
x: control.leftPadding
y: control.topPadding
width: control.width - (control.leftPadding + control.rightPadding)
height: control.height - (control.topPadding + control.bottomPadding)
text: control.placeholderText
font: control.font
color: control.placeholderTextColor
verticalAlignment: control.verticalAlignment
visible: !control.length && !control.preeditText && (!control.activeFocus || control.horizontalAlignment !== Qt.AlignHCenter)
elide: Text.ElideRight
renderType: control.renderType
}
selectedTextColor: control.palette.highlightedText
selectionColor: control.palette.highlight
topPadding: 8
wrapMode: TextInput.Wrap
background: Rectangle {
anchors.fill: parent
radius: ProtonStyle.input_radius
visible: true
color: root.colorScheme.background_norm
border.color: {
if (!control.enabled) {
return root.colorScheme.field_disabled
if (!control.enabled || control.readOnly) {
return root.colorScheme.field_disabled;
}
if (control.activeFocus) {
return root.colorScheme.interaction_norm
return root.colorScheme.interaction_norm;
}
if (root.error) {
return root.colorScheme.signal_danger
return root.colorScheme.signal_danger;
}
if (control.hovered) {
return root.colorScheme.field_hover
return root.colorScheme.field_hover;
}
return root.colorScheme.field_norm
return root.colorScheme.field_norm;
}
border.width: 1
color: root.colorScheme.background_norm
radius: ProtonStyle.input_radius
visible: true
}
cursorDelegate: Rectangle {
id: cursor
color: root.colorScheme.interaction_norm
visible: control.activeFocus && !control.readOnly && control.selectionStart === control.selectionEnd
width: 1
Connections {
function onCursorPositionChanged() {
// keep a moving cursor visible
cursor.opacity = 1;
timer.restart();
}
target: control
}
Timer {
id: timer
interval: Qt.styleHints.cursorFlashTime / 2
repeat: true
running: control.activeFocus && !control.readOnly
// force the cursor visible when gaining focus
onRunningChanged: cursor.opacity = 1
onTriggered: cursor.opacity = !cursor.opacity ? 1 : 0
}
}
onEditingFinished: root.editingFinished()
PlaceholderText {
id: placeholder
color: control.placeholderTextColor
elide: Text.ElideRight
font: control.font
height: control.height - (control.topPadding + control.bottomPadding)
renderType: control.renderType
text: control.placeholderText
verticalAlignment: control.verticalAlignment
visible: !control.length && !control.preeditText && (!control.activeFocus || control.horizontalAlignment !== Qt.AlignHCenter)
width: control.width - (control.leftPadding + control.rightPadding)
x: control.leftPadding
y: control.topPadding
}
}
}
RowLayout {
id: footerLayout
Layout.fillWidth: true
@ -304,67 +337,29 @@ FocusScope {
ColorImage {
id: errorIcon
Layout.rightMargin: 4
visible: root.error && (assistiveText.text.length > 0)
source: "/qml/icons/ic-exclamation-circle-filled.svg"
color: root.colorScheme.signal_danger
height: assistiveText.height
source: "/qml/icons/ic-exclamation-circle-filled.svg"
sourceSize.height: assistiveText.height
visible: root.error && (assistiveText.text.length > 0)
}
Proton.Label {
colorScheme: root.colorScheme
id: assistiveText
Layout.fillWidth: true
text: root.error ? root.errorString : root.assistiveText
color: {
if (!root.enabled) {
return root.colorScheme.text_disabled
return root.colorScheme.text_disabled;
}
if (root.error) {
return root.colorScheme.signal_danger
return root.colorScheme.signal_danger;
}
return root.colorScheme.text_weak
return root.colorScheme.text_weak;
}
colorScheme: root.colorScheme
text: root.error ? root.errorString : root.assistiveText
type: root.error ? Proton.Label.LabelType.Caption_semibold : Proton.Label.LabelType.Caption
}
}
}
property bool validateOnEditingFinished: true
onEditingFinished: {
if (!validateOnEditingFinished) {
return
}
validate()
}
function validate() {
if (validator === undefined) {
return
}
var error = validator(text)
if (error) {
root.error = true
root.errorString = error
} else {
root.error = false
root.errorString = ""
}
}
onTextChanged: {
root.error = false
root.errorString = ""
}
}

View File

@ -1,54 +1,38 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQml
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.impl
import QtQuick.Templates as T
import QtQuick.Layouts
import "." as Proton
FocusScope {
id: root
property ColorScheme colorScheme
property alias background: control.background
property alias bottomInset: control.bottomInset
property alias focusReason: control.focusReason
property alias hoverEnabled: control.hoverEnabled
property alias hovered: control.hovered
property alias implicitBackgroundHeight: control.implicitBackgroundHeight
property alias implicitBackgroundWidth: control.implicitBackgroundWidth
property alias leftInset: control.leftInset
property alias palette: control.palette
property alias placeholderText: control.placeholderText
property alias placeholderTextColor: control.placeholderTextColor
property alias rightInset: control.rightInset
property alias topInset: control.topInset
property alias acceptableInput: control.acceptableInput
property alias activeFocusOnPress: control.activeFocusOnPress
property string assistiveText
property alias autoScroll: control.autoScroll
property alias background: control.background
property alias bottomInset: control.bottomInset
property alias bottomPadding: control.bottomPadding
property alias canPaste: control.canPaste
property alias canRedo: control.canRedo
property alias canUndo: control.canUndo
property alias color: control.color
property ColorScheme colorScheme
//property alias contentHeight: control.contentHeight
//property alias contentWidth: control.contentWidth
property alias cursorDelegate: control.cursorDelegate
@ -56,24 +40,39 @@ FocusScope {
property alias cursorRectangle: control.cursorRectangle
property alias cursorVisible: control.cursorVisible
property alias displayText: control.displayText
property int echoMode: TextInput.Normal
property alias effectiveHorizontalAlignment: control.effectiveHorizontalAlignment
property bool error: false
property string errorString
property alias focusReason: control.focusReason
property alias font: control.font
property alias hint: hint.text
property alias horizontalAlignment: control.horizontalAlignment
property alias hoverEnabled: control.hoverEnabled
property alias hovered: control.hovered
property alias implicitBackgroundHeight: control.implicitBackgroundHeight
property alias implicitBackgroundWidth: control.implicitBackgroundWidth
property alias inputMask: control.inputMask
property alias inputMethodComposing: control.inputMethodComposing
property alias inputMethodHints: control.inputMethodHints
property alias label: label.text
property alias leftInset: control.leftInset
property alias leftPadding: control.leftPadding
property alias length: control.length
property alias maximumLength: control.maximumLength
property alias mouseSelectionMode: control.mouseSelectionMode
property alias overwriteMode: control.overwriteMode
property alias padding: control.padding
property alias palette: control.palette
property alias passwordCharacter: control.passwordCharacter
property alias passwordMaskDelay: control.passwordMaskDelay
property alias persistentSelection: control.persistentSelection
property alias placeholderText: control.placeholderText
property alias placeholderTextColor: control.placeholderTextColor
property alias preeditText: control.preeditText
property alias readOnly: control.readOnly
property alias renderType: control.renderType
property alias rightInset: control.rightInset
property alias rightPadding: control.rightPadding
property alias selectByMouse: control.selectByMouse
property alias selectedText: control.selectedText
@ -82,47 +81,102 @@ FocusScope {
property alias selectionEnd: control.selectionEnd
property alias selectionStart: control.selectionStart
property alias text: control.text
property alias topInset: control.topInset
property bool validateOnEditingFinished: true
// We are using our own type of validators. It should be a function
// returning an error string in case of error and undefined if no error
property var validator
property alias verticalAlignment: control.verticalAlignment
property alias wrapMode: control.wrapMode
implicitWidth: children[0].implicitWidth
signal accepted
signal editingFinished
signal textEdited
function clear() {
control.clear();
}
function copy() {
control.copy();
}
function cut() {
control.cut();
}
function deselect() {
control.deselect();
}
function ensureVisible(position) {
control.ensureVisible(position);
}
function forceActiveFocus() {
control.forceActiveFocus();
}
function getText(start, end) {
control.getText(start, end);
}
function insert(position, text) {
control.insert(position, text);
}
function isRightToLeft(start, end) {
control.isRightToLeft(start, end);
}
function moveCursorSelection(position, mode) {
control.moveCursorSelection(position, mode);
}
function paste() {
control.paste();
}
function positionAt(x, y, position) {
control.positionAt(x, y, position);
}
function positionToRectangle(pos) {
control.positionToRectangle(pos);
}
function redo() {
control.redo();
}
function remove(start, end) {
control.remove(start, end);
}
function select(start, end) {
control.select(start, end);
}
function selectAll() {
control.selectAll();
}
function selectWord() {
control.selectWord();
}
function undo() {
control.undo();
}
function validate() {
if (validator === undefined) {
return;
}
const error = validator(text);
if (error) {
root.error = true;
root.errorString = error;
} else {
root.error = false;
root.errorString = "";
}
}
implicitHeight: children[0].implicitHeight
implicitWidth: children[0].implicitWidth
property alias label: label.text
property alias hint: hint.text
property string assistiveText
property string errorString
property int echoMode: TextInput.Normal
property bool error: false
signal accepted()
signal editingFinished()
signal textEdited()
function clear() { control.clear() }
function copy() { control.copy() }
function cut() { control.cut() }
function deselect() { control.deselect() }
function ensureVisible(position) { control.ensureVisible(position) }
function getText(start, end) { control.getText(start, end) }
function insert(position, text) { control.insert(position, text) }
function isRightToLeft(start, end) { control.isRightToLeft(start, end) }
function moveCursorSelection(position, mode) { control.moveCursorSelection(position, mode) }
function paste() { control.paste() }
function positionAt(x, y, position) { control.positionAt(x, y, position) }
function positionToRectangle(pos) { control.positionToRectangle(pos) }
function redo() { control.redo() }
function remove(start, end) { control.remove(start, end) }
function select(start, end) { control.select(start, end) }
function selectAll() { control.selectAll() }
function selectWord() { control.selectWord() }
function undo() { control.undo() }
function forceActiveFocus() { control.forceActiveFocus() }
onEditingFinished: {
if (!validateOnEditingFinished) {
return;
}
validate();
}
onTextChanged: {
root.error = false;
root.errorString = "";
}
ColumnLayout {
anchors.fill: parent
@ -133,19 +187,18 @@ FocusScope {
spacing: 0
Proton.Label {
colorScheme: root.colorScheme
id: label
Layout.fillHeight: true
Layout.fillWidth: true
colorScheme: root.colorScheme
type: Proton.Label.LabelType.Body_semibold
}
Proton.Label {
colorScheme: root.colorScheme
id: hint
Layout.fillHeight: true
Layout.fillWidth: true
color: root.enabled ? root.colorScheme.text_weak : root.colorScheme.text_disabled
colorScheme: root.colorScheme
horizontalAlignment: Text.AlignRight
type: Proton.Label.LabelType.Caption
}
@ -156,36 +209,29 @@ FocusScope {
// will be adjusted to background's width making text field and eye button overlap
Rectangle {
id: background
Layout.fillHeight: true
Layout.fillWidth: true
radius: ProtonStyle.input_radius
visible: true
color: root.colorScheme.background_norm
border.color: {
if (!control.enabled) {
return root.colorScheme.field_disabled
return root.colorScheme.field_disabled;
}
if (control.activeFocus) {
return root.colorScheme.interaction_norm
return root.colorScheme.interaction_norm;
}
if (root.error) {
return root.colorScheme.signal_danger
return root.colorScheme.signal_danger;
}
if (control.hovered) {
return root.colorScheme.field_hover
return root.colorScheme.field_hover;
}
return root.colorScheme.field_norm
return root.colorScheme.field_norm;
}
border.width: 1
implicitWidth: children[0].implicitWidth
color: root.colorScheme.background_norm
implicitHeight: children[0].implicitHeight
implicitWidth: children[0].implicitWidth
radius: ProtonStyle.input_radius
visible: true
RowLayout {
anchors.fill: parent
@ -193,190 +239,135 @@ FocusScope {
T.TextField {
id: control
KeyNavigation.backtab: root.KeyNavigation.backtab
KeyNavigation.down: root.KeyNavigation.down
KeyNavigation.left: root.KeyNavigation.left
KeyNavigation.priority: root.KeyNavigation.priority
KeyNavigation.right: root.KeyNavigation.right
KeyNavigation.tab: root.KeyNavigation.tab
KeyNavigation.up: root.KeyNavigation.up
Layout.fillHeight: true
Layout.fillWidth: true
implicitWidth: implicitBackgroundWidth + leftInset + rightInset
|| Math.max(contentWidth, placeholder.implicitWidth) + leftPadding + rightPadding
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
contentHeight + topPadding + bottomPadding,
placeholder.implicitHeight + topPadding + bottomPadding)
topPadding: 8
bottomPadding: 8
leftPadding: 12
rightPadding: 12
font.family: ProtonStyle.font_family
font.weight: ProtonStyle.fontWeight_400
font.pixelSize: ProtonStyle.body_font_size
font.letterSpacing: ProtonStyle.body_letter_spacing
color: control.enabled ? root.colorScheme.text_norm : root.colorScheme.text_disabled
placeholderTextColor: control.enabled ? root.colorScheme.text_hint : root.colorScheme.text_disabled
selectionColor: control.palette.highlight
selectedTextColor: control.palette.highlightedText
verticalAlignment: TextInput.AlignVCenter
echoMode: eyeButton.checked ? TextInput.Normal : root.echoMode
// enforcing default focus here within component
focus: true
KeyNavigation.priority: root.KeyNavigation.priority
KeyNavigation.backtab: root.KeyNavigation.backtab
KeyNavigation.tab: root.KeyNavigation.tab
KeyNavigation.up: root.KeyNavigation.up
KeyNavigation.down: root.KeyNavigation.down
KeyNavigation.left: root.KeyNavigation.left
KeyNavigation.right: root.KeyNavigation.right
font.family: ProtonStyle.font_family
font.letterSpacing: ProtonStyle.body_letter_spacing
font.pixelSize: ProtonStyle.body_font_size
font.weight: ProtonStyle.fontWeight_400
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, contentHeight + topPadding + bottomPadding, placeholder.implicitHeight + topPadding + bottomPadding)
implicitWidth: implicitBackgroundWidth + leftInset + rightInset || Math.max(contentWidth, placeholder.implicitWidth) + leftPadding + rightPadding
leftPadding: 12
placeholderTextColor: control.enabled ? root.colorScheme.text_hint : root.colorScheme.text_disabled
rightPadding: 12
selectByMouse: true
selectedTextColor: control.palette.highlightedText
selectionColor: control.palette.highlight
topPadding: 8
verticalAlignment: TextInput.AlignVCenter
background: Item {
implicitHeight: 36
implicitWidth: 80
visible: false
}
cursorDelegate: Rectangle {
id: cursor
width: 1
color: root.colorScheme.interaction_norm
visible: control.activeFocus && !control.readOnly && control.selectionStart === control.selectionEnd
width: 1
Connections {
target: control
function onCursorPositionChanged() {
// keep a moving cursor visible
cursor.opacity = 1
timer.restart()
cursor.opacity = 1;
timer.restart();
}
}
target: control
}
Timer {
id: timer
running: control.activeFocus && !control.readOnly
repeat: true
interval: Qt.styleHints.cursorFlashTime / 2
onTriggered: cursor.opacity = !cursor.opacity ? 1 : 0
repeat: true
running: control.activeFocus && !control.readOnly
// force the cursor visible when gaining focus
onRunningChanged: cursor.opacity = 1
onTriggered: cursor.opacity = !cursor.opacity ? 1 : 0
}
}
onAccepted: {
root.accepted();
}
onEditingFinished: {
root.editingFinished();
}
onTextEdited: {
root.textEdited();
}
PlaceholderText {
id: placeholder
x: control.leftPadding
y: control.topPadding
width: control.width - (control.leftPadding + control.rightPadding)
height: control.height - (control.topPadding + control.bottomPadding)
text: control.placeholderText
font: control.font
color: control.placeholderTextColor
elide: Text.ElideRight
font: control.font
height: control.height - (control.topPadding + control.bottomPadding)
renderType: control.renderType
text: control.placeholderText
verticalAlignment: control.verticalAlignment
visible: !control.length && !control.preeditText && (!control.activeFocus || control.horizontalAlignment !== Qt.AlignHCenter)
elide: Text.ElideRight
renderType: control.renderType
}
background: Item {
implicitWidth: 80
implicitHeight: 36
visible: false
}
onAccepted: {
root.accepted()
}
onEditingFinished: {
root.editingFinished()
}
onTextEdited: {
root.textEdited()
width: control.width - (control.leftPadding + control.rightPadding)
x: control.leftPadding
y: control.topPadding
}
}
Proton.Button {
colorScheme: root.colorScheme
id: eyeButton
Layout.fillHeight: true
visible: root.echoMode === TextInput.Password
icon.color: control.color
checkable: true
colorScheme: root.colorScheme
icon.color: control.color
icon.source: checked ? "../icons/ic-eye-slash.svg" : "../icons/ic-eye.svg"
visible: root.echoMode === TextInput.Password
}
}
}
RowLayout {
Layout.fillWidth: true
spacing: 0
ColorImage {
id: errorIcon
Layout.rightMargin: 4
visible: root.error && (assistiveText.text.length > 0)
source: "../icons/ic-exclamation-circle-filled.svg"
color: root.colorScheme.signal_danger
height: assistiveText.lineHeight
source: "../icons/ic-exclamation-circle-filled.svg"
sourceSize.height: assistiveText.lineHeight
visible: root.error && (assistiveText.text.length > 0)
}
Proton.Label {
colorScheme: root.colorScheme
id: assistiveText
Layout.fillHeight: true
Layout.fillWidth: true
wrapMode: Text.WordWrap
text: root.error ? root.errorString : root.assistiveText
color: {
if (!root.enabled) {
return root.colorScheme.text_disabled
return root.colorScheme.text_disabled;
}
if (root.error) {
return root.colorScheme.signal_danger
return root.colorScheme.signal_danger;
}
return root.colorScheme.text_weak
return root.colorScheme.text_weak;
}
colorScheme: root.colorScheme
text: root.error ? root.errorString : root.assistiveText
type: root.error ? Proton.Label.LabelType.Caption_semibold : Proton.Label.LabelType.Caption
wrapMode: Text.WordWrap
}
}
}
property bool validateOnEditingFinished: true
onEditingFinished: {
if (!validateOnEditingFinished) {
return
}
validate()
}
function validate() {
if (validator === undefined) {
return
}
var error = validator(text)
if (error) {
root.error = true
root.errorString = error
} else {
root.error = false
root.errorString = ""
}
}
onTextChanged: {
root.error = false
root.errorString = ""
}
}

View File

@ -1,20 +1,15 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
@ -22,92 +17,106 @@ import QtQuick.Controls.impl
Item {
id: root
property var colorScheme
property bool _disabled: !enabled
property bool checked
property var colorScheme
property bool hovered
property bool loading
signal clicked
property bool _disabled: !enabled
implicitHeight: children[0].implicitHeight
implicitWidth: children[0].implicitWidth
Rectangle {
id: indicator
implicitWidth: 40
implicitHeight: 24
radius: width/2
color: {
if (root.loading) return "transparent"
if (root._disabled) return root.colorScheme.background_strong
return root.colorScheme.background_norm
}
border {
width: 1
color: (root._disabled || root.loading) ? "transparent" : colorScheme.field_norm
if (root.loading)
return "transparent";
if (root._disabled)
return root.colorScheme.background_strong;
return root.colorScheme.background_norm;
}
implicitHeight: 24
implicitWidth: 40
radius: width / 2
border {
color: (root._disabled || root.loading) ? "transparent" : colorScheme.field_norm
width: 1
}
Rectangle {
anchors.verticalCenter: indicator.verticalCenter
anchors.left: indicator.left
anchors.leftMargin: root.checked ? 16 : 0
width: 24
height: 24
radius: width/2
anchors.verticalCenter: indicator.verticalCenter
color: {
if (root.loading) return "transparent"
if (root._disabled) return root.colorScheme.field_disabled
if (root.loading)
return "transparent";
if (root._disabled)
return root.colorScheme.field_disabled;
if (root.checked) {
if (root.hovered) return root.colorScheme.interaction_norm_hover
return root.colorScheme.interaction_norm
if (root.hovered)
return root.colorScheme.interaction_norm_hover;
return root.colorScheme.interaction_norm;
} else {
if (root.hovered) return root.colorScheme.field_hover
return root.colorScheme.field_norm
if (root.hovered)
return root.colorScheme.field_hover;
return root.colorScheme.field_norm;
}
}
height: 24
radius: width / 2
width: 24
ColorImage {
anchors.centerIn: parent
source: "/qml/icons/ic-check.svg"
color: root.colorScheme.background_norm
height: root.colorScheme.body_font_size
source: "/qml/icons/ic-check.svg"
sourceSize.height: root.colorScheme.body_font_size
visible: root.checked
}
}
ColorImage {
id: loader
anchors.centerIn: parent
source: "/qml/icons/Loader_16.svg"
color: root.colorScheme.text_norm
height: root.colorScheme.body_font_size
source: "/qml/icons/Loader_16.svg"
sourceSize.height: root.colorScheme.body_font_size
visible: root.loading
RotationAnimation {
target: loader
loops: Animation.Infinite
direction: RotationAnimation.Clockwise
duration: 1000
from: 0
to: 360
direction: RotationAnimation.Clockwise
loops: Animation.Infinite
running: root.loading
target: loader
to: 360
}
}
MouseArea {
anchors.fill: indicator
hoverEnabled: true
onEntered: {root.hovered = true }
onExited: {root.hovered = false }
onClicked: { if (root.enabled) root.clicked();}
onPressed: {root.hovered = true }
onReleased: { root.hovered = containsMouse }
onClicked: {
if (root.enabled)
root.clicked();
}
onEntered: {
root.hovered = true;
}
onExited: {
root.hovered = false;
}
onPressed: {
root.hovered = true;
}
onReleased: {
root.hovered = containsMouse;
}
}
}
}

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