Compare commits

..

353 Commits

Author SHA1 Message Date
b12873f1df Fix of speed of checking whether message is deleted 2020-10-01 13:42:16 +00:00
ec73170e9b Use label.Path instead of Name 2020-09-30 09:38:35 +02:00
51c8bceed8 Changelog and use pmmime 2020-09-24 18:19:35 +02:00
e02c7c7f06 Parsing message with empty address as '<>' 2020-09-24 15:37:21 +02:00
15c1d7bc24 fix: duplicate charset param 2020-09-24 14:26:45 +02:00
a89a3f6612 Convert to UTF-8 any message part which specifies charset 2020-09-24 13:58:15 +02:00
d956b04062 Parsing non-utf8 multipart/alternative message 2020-09-24 13:17:38 +02:00
ef1671d4ab Parsing message with empty CC 2020-09-24 12:23:31 +02:00
fe926cbd57 IE release notes and GODT-738 2020-09-23 13:50:08 +02:00
e01747e3b9 Merge branch 'release/forth' into devel 2020-09-23 13:10:11 +02:00
85220848d0 Update total even if its zero 2020-09-23 09:24:58 +02:00
70f91ae55b notes and build v1.4.0 2020-09-21 13:29:33 +02:00
a73b30ed9e Better naming 2020-09-18 10:25:14 +02:00
7337f78d4a PMAPI target - parallel upload 2020-09-18 10:25:14 +02:00
9b5da91f7c Fix: Yahoo not supporting TLS1.3 GODT-730 2020-09-18 07:53:53 +00:00
c7669b950f fix: gitignore should also ignore ie build files 2020-09-17 14:33:46 +02:00
b3ed8d51a7 fix: version check for catalina 2020-09-17 11:35:05 +00:00
60b7d980f4 Fix integration test - deleting from All Mail 2020-09-17 10:19:55 +00:00
abf2238e6f Wrap imap-id with backend caller 2020-09-17 08:59:28 +00:00
b4a358c084 User agent detected by fake IMAP extension instead of AUTH callback 2020-09-17 08:59:28 +00:00
3606a0ab9f QA build with option to change API URL by ENV variable 2020-09-17 08:30:31 +00:00
c5665d0dd7 Unsilent errors reading mbox files 2020-09-16 15:51:08 +02:00
d6464c0048 Fixes after rebase 2020-09-16 09:51:58 +00:00
5496a26f73 Finish tests for moving without MOVE support 2020-09-16 09:51:57 +00:00
ec9a799fe9 test - move like outlook - GODT-536 2020-09-16 09:51:57 +00:00
730abadfc3 Do not allow deleting messages from All Mail 2020-09-16 09:51:57 +00:00
60e1548685 log both timeouts in update send 2020-09-16 09:51:57 +00:00
7430c7f1f5 Timeout for sending IMAP update 2020-09-16 09:51:57 +00:00
6671b78799 Simplified integration tests 2020-09-16 09:51:57 +00:00
c7578cf53c \Deleted flag support finish 2020-09-16 09:51:57 +00:00
66e04dd5ed Implement deleted flag GODT-461 2020-09-16 09:51:57 +00:00
803353e300 Tests for deleted flag GODT-496 2020-09-16 09:51:57 +00:00
f3773c9d78 I/E measurements 2020-09-16 09:29:13 +00:00
41ac61bbe8 fix: less spammy go-message logs 2020-09-15 09:37:29 +00:00
0d3d6747ac fix: grammar in gui 2020-09-15 08:51:02 +00:00
eaa9a458c4 test: use actual broken eml 2020-09-15 06:31:45 +00:00
46e5cb9c83 test: use message.Parse for fakeapi import parser 2020-09-15 06:31:45 +00:00
dc5387a512 fix: bug report window title 2020-09-15 08:04:51 +02:00
4b7c234e78 feat: strip comments from addresses 2020-09-14 14:46:44 +02:00
5bca6fc3cf chore: tidy up before merge 2020-09-14 14:19:35 +02:00
97b64ebb70 fix: credits and release notes 2020-09-11 11:41:03 +02:00
9b3cc9dc34 feat: convert content type in html meta tags 2020-09-11 11:41:03 +02:00
afeed4a801 feat: use upstream go-message 2020-09-11 11:41:03 +02:00
dd70b30f76 fix: don't use full pk fingerprint, only use first 8 chars 2020-09-11 11:41:03 +02:00
3e8e3c912b fix: don't doubly apply 822 texwrapper 2020-09-11 11:41:03 +02:00
5d0e3f36b4 fix: unhandled charset in header 2020-09-11 11:41:03 +02:00
da751a38e3 fix: public key names and content types 2020-09-11 11:41:03 +02:00
f9af17dd9b fix: allow unknown encodings during initial parse 2020-09-11 11:41:03 +02:00
f622ecf678 feat: logging throughout parser 2020-09-11 11:41:03 +02:00
475e673b87 feat: add logging for encoding detection 2020-09-11 11:41:03 +02:00
3916ddc8e4 fix: allow overriding sign via contact settings if set 2020-09-11 11:41:03 +02:00
ef2ace0afe fix: always check charset before utf8 validity 2020-09-11 11:41:03 +02:00
b5d3737a7e fix: sign not overriding global 2020-09-11 11:41:03 +02:00
d872d77cf5 fix: draft mime type instead of composermode 2020-09-11 11:41:03 +02:00
1f17628399 fix: unequal number of rich/plain parts 2020-09-11 11:41:03 +02:00
4ab8f7d6b5 fix: pubkey should not be collected as attachment 2020-09-11 11:41:03 +02:00
fa5f4acdac docs: add docstring for buildBodies 2020-09-11 11:41:03 +02:00
642666fa59 docs: add docstrings for walker/visitor handlers/rules 2020-09-11 11:41:03 +02:00
a2cf5374b9 feat: more efficient regexp use in parser 2020-09-11 11:41:03 +02:00
6a7a77fc51 refactor: tidier encoding detection 2020-09-11 11:41:03 +02:00
f4dfadce52 feat: attach public key 2020-09-11 11:41:03 +02:00
9ba08e5edb refactor: remove dead code 2020-09-11 11:41:03 +02:00
9821b5bbc2 feat: recreate message with parser's writer 2020-09-11 11:41:03 +02:00
5343a6fc0f fix: fallback to detecting charset if cannot handle specified one 2020-09-11 11:41:03 +02:00
180c6699e0 fix: don't select multipart/alternative if length is 0 2020-09-11 11:41:03 +02:00
7d1b0d0a40 docs: changelog 2020-09-11 11:41:03 +02:00
caff73d06c docs: add HELP about 7bit filter 2020-09-11 11:41:03 +02:00
f4d073b4cf test: ignore weird test for now 2020-09-11 11:41:02 +02:00
65d8b382d0 fix: panic when no params available 2020-09-11 11:41:02 +02:00
0e7e13211b refactor: don't reconstruct mimeBody 2020-09-11 11:41:02 +02:00
7e1af9ff4e fix: linter issues 2020-09-11 11:41:02 +02:00
37186846db feat: wrap attachment lines as per rfc822 2020-09-11 11:41:02 +02:00
a5a61c9428 feat: set attachment headers 2020-09-11 11:41:02 +02:00
ea01c155da feat: handle foreign encodings 2020-09-11 11:41:02 +02:00
f4374a02da refactor: tidy a bit 2020-09-11 11:41:02 +02:00
0d4d95360f feat: set header 2020-09-11 11:41:02 +02:00
f88071b2ca feat: parse date 2020-09-11 11:41:02 +02:00
e01a523ae3 feat: pull out most things as attachments 2020-09-11 11:41:02 +02:00
c6b18b45b5 feat: better handling of multipart messages 2020-09-11 11:41:02 +02:00
a7da66ccbc feat: enter and exit handlers 2020-09-11 11:41:02 +02:00
8bd74c5edc feat: set mime type 2020-09-11 11:41:02 +02:00
2b36d3ab7b feat: attach public key 2020-09-11 11:41:02 +02:00
45b863f931 feat: parse most header values 2020-09-11 11:41:02 +02:00
953150cfdb feat: add part getter 2020-09-11 11:41:02 +02:00
6ea3fc1963 feat: initial parser exposing walker/writer 2020-09-11 11:41:02 +02:00
7207a5d59e docs: changelog 2020-09-11 09:08:19 +00:00
dd2264da6f fix: notify of unencrypted recipient 2020-09-11 09:08:19 +00:00
9261b6337e docs: changelog 2020-09-11 10:48:27 +02:00
4f6e8c30c7 fix: use correct package type for signed inline 2020-09-11 10:29:02 +02:00
614a00eac1 Update release date for Congo in Changelog 2020-09-09 12:23:42 +02:00
de58c7a905 Cookies for Import-Export 2020-09-09 09:09:35 +02:00
2e439e17cf Remove unused scope methods 2020-09-09 06:21:02 +00:00
f73aeec97f Update changelog 2020-09-08 08:43:05 +00:00
8a7b4bb919 Improve user agent 2020-09-08 08:43:05 +00:00
78fd73ee2a Merge branch 'release/congo' into devel 2020-09-08 09:37:05 +02:00
bfdfc81d65 release notes 2020-09-07 08:03:02 +02:00
bf6963859f rename IE app GODT-690 2020-09-04 11:55:30 +02:00
33bf64cc4e Fix hover on links in popups 2020-09-04 10:43:59 +02:00
bb1d27a5be Do not ignore errors 2020-09-03 14:36:12 +02:00
bc07896436 Sentry report after parser panic 2020-08-31 17:23:22 +02:00
1d2e584799 Convert panics from message parser to error 2020-08-31 15:57:45 +02:00
9218598140 Update routes to API v4 2020-08-31 07:42:20 +00:00
af89931f05 Hardcoded version 2020-08-27 13:59:07 +02:00
84147a2cb0 Fix flaky tests 2020-08-25 10:20:49 +02:00
2269a9edb7 Pause event loop while FETCHing to prevetn EXPUNGE 2020-08-24 08:26:31 +00:00
61867fbde7 Add hour when days don't match GODT-655 2020-08-24 10:11:51 +02:00
2d9417d501 Migrate from old credentials 2020-08-24 10:11:51 +02:00
4973e38748 Import-Export app everywhere 2020-08-24 10:11:51 +02:00
e4704cd459 Release notes 2020-08-24 10:11:51 +02:00
8592a264c0 Fix showing error msg 2020-08-24 10:11:51 +02:00
40aeb6c010 Fixing IE icon 2020-08-24 10:11:51 +02:00
71b9a3b205 Release notes for Import-Export 2020-08-24 10:11:51 +02:00
2182e573f9 Update maximal date on every DateInput dropdown toggle 2020-08-24 10:11:51 +02:00
5f02e59fa4 Fix showing table with errors 2020-08-24 10:11:51 +02:00
29ff8cf54b fix: double colon in window title again 2020-08-24 10:11:51 +02:00
f8cf4e966f fix: double colon in window title 2020-08-24 10:11:51 +02:00
df80e7eb27 Keep Import-Export credits up to date 2020-08-24 10:11:51 +02:00
658ead9fb3 Import/Export final touches 2020-08-24 10:11:51 +02:00
4f0af0fb02 Import/Export metrics 2020-08-24 10:11:51 +02:00
7e5e3d3dd4 Import/Export GUI 2020-08-24 10:11:51 +02:00
1c10cc5065 Import/Export backend 2020-08-24 10:11:51 +02:00
49316a935c Shared GUI for Bridge and Import/Export 2020-08-24 10:11:50 +02:00
b598779c0f Import/Export backend prep 2020-08-24 10:11:50 +02:00
9d65192ad7 feat: clear expired cookies from persistent storage 2020-08-24 09:08:42 +02:00
0e14155185 fix: cookie expiry needs to be set 2020-08-21 12:42:49 +02:00
56f4f3d017 fix: better error handling when message is still in send queue 2020-08-19 15:25:38 +02:00
f5617ced3f fix (GODT-597): duplicate send when draft creation takes a long time 2020-08-18 13:40:56 +00:00
35b37c7097 fix (GODT-597): duplicate send when draft creation takes a long time 2020-08-18 13:40:56 +00:00
77c6ba381e fix: mime type 2020-08-18 09:14:46 +00:00
34df24ede3 docs: changelog 2020-08-18 09:14:46 +00:00
33d705a39d test: fix test content types 2020-08-18 09:14:46 +00:00
64fbd0655f fix: sign when no contact present 2020-08-18 09:14:46 +00:00
b700a7823e fix: send to internal 2020-08-18 09:14:46 +00:00
145da7ffa5 refactor: pass mailSettings in to avoid extra call 2020-08-18 09:14:46 +00:00
61a841ced7 refactor: builder pattern for generateSendingInfo 2020-08-18 09:14:46 +00:00
29978b7014 ci: use bridge-internal image 2020-08-17 08:28:32 +00:00
dd73687555 ci: switch to using container from bridge-internal 2020-08-17 08:28:32 +00:00
5411b29d17 Merge branch 'release/v1.3.X' into devel 2020-08-17 09:58:39 +02:00
6c93f1f1ec Fix integration tests - compiting message flags 2020-08-17 09:10:03 +02:00
1dcaa200e0 fix: docstring mistakes 2020-08-13 11:54:52 +02:00
66082af40f test: add test that cookie jar loads cookies 2020-08-13 11:39:28 +02:00
209af59232 refactor: make cookie architecture less crazy 2020-08-13 11:31:11 +02:00
9f24c666b9 docs: add docstrings 2020-08-12 15:46:19 +02:00
3101fc5543 fix: add missing license 2020-08-12 15:05:14 +02:00
182bbd556f fix: failure to create cookie jar is not fatal error 2020-08-12 14:56:55 +02:00
e333ccd29e feat: persistent cookies 2020-08-12 14:55:24 +02:00
ce4a75caf5 fix: properly decide whether it is first gui start 2020-08-06 09:29:30 +02:00
01a8c9e9d7 Adding GUI troubleshoot popup GODT-554 GODT-583 2020-08-06 08:12:37 +02:00
2c910378ce feat: detect bad certificate error 2020-08-06 07:34:36 +02:00
34ef9063cb fix: better first start setting 2020-08-05 15:20:20 +02:00
f651d39820 chore: bump dependencies 2020-08-03 07:47:49 +00:00
7e6d09a247 test: generate tls cert/key in test 2020-08-03 09:24:39 +02:00
da381130a3 Check log file size more often to prevent huge log files 2020-07-31 13:24:25 +02:00
7baa4dc117 release notes 2020-07-29 07:07:50 +02:00
be07cb83c9 chore: bump linter to v1.29.0 2020-07-28 08:37:40 +00:00
dfbd86c7bc fix: add missing option to modify system keychain 2020-07-27 17:23:59 +02:00
e3ab829ad3 fix: missing command in exec call 2020-07-27 13:05:46 +02:00
b12ef1327c refactor: better confirmer result locking 2020-07-24 13:04:29 +00:00
d66bcc4b63 fix: bad ID in frontend 2020-07-24 13:04:29 +00:00
5ad307868e feat: add expiry 2020-07-24 13:04:29 +00:00
369c6ebf85 fix: clean up after setting result 2020-07-24 13:04:29 +00:00
c988d739a1 docs: add docstrings for confirmer 2020-07-24 13:04:29 +00:00
36ef9f20ae feat: use confirmer in smtp 2020-07-24 13:04:29 +00:00
c8f118a26b feat: implement confirmer 2020-07-24 13:04:29 +00:00
be20714842 feat: better way to add trusted cert in macOS 2020-07-24 14:51:30 +02:00
1711442878 Fix setting flags 2020-07-23 14:49:43 +02:00
79e6799f40 fix: panic in integration tests 2020-07-22 11:04:23 +00:00
6023162443 feat: add build tag for deterministic imap password 2020-07-22 09:32:20 +00:00
01e0fe4863 ci: use shared macos runner 2020-07-21 16:58:47 +02:00
f073301481 fix: versioning 2020-07-17 13:04:11 +02:00
1df81e4a34 chore: bump changelog 2020-07-16 11:51:56 +02:00
bf0945eaef fix: race condition in AuthRefresh that could cause user to be logged out 2020-07-16 10:19:50 +02:00
11e01ca163 chore: bump version to 1.3.0 2020-07-15 15:36:55 +02:00
a650a04a88 Bump bbolt dependency 2020-07-15 13:27:40 +02:00
bdc11c8358 ci: enable automated builds on all platforms 2020-07-14 14:57:25 +00:00
ed7a0dc9b3 fix: don't assume contact keys are armored 2020-07-14 16:43:06 +02:00
fc4e77604f fix: don't panic if not given tls connection in pin checker 2020-07-09 13:19:32 +02:00
abaeace4b3 chore: bump go-imap version to get select fix 2020-07-08 10:34:18 +02:00
457b524ba8 chore: bump go-imap to include delimiter fix 2020-07-07 11:10:37 +00:00
d89d627349 test: increase minimum listener event receive time 2020-07-06 16:11:53 +02:00
51ff880fd9 test: fix flaky test TestSyncAllMail 2020-07-03 14:43:44 +02:00
b25baa2524 test: set sent label properly 2020-07-03 07:45:16 +00:00
10e384f4df test: add tests for parsing mime message with bad 2231 filename 2020-07-03 09:19:18 +02:00
35ae2011b6 Merge branch master into devel 2020-07-02 10:16:08 +02:00
5348ae7d18 Changelog wording 2020-07-01 09:19:11 +02:00
2512d3647a chore: bump linter to v1.27.0 2020-07-01 07:06:55 +00:00
1e8cb35fcb test: add test for multiline 2020-06-30 16:33:29 +02:00
0b0991d682 fix: infinite loop when decoding invalid 2231 charset 2020-06-29 15:40:46 +02:00
813e99f399 Fix flaky integration tests 2020-06-26 09:51:56 +00:00
7301e5571c fix: return error if parsing header fails GODT-502 2020-06-26 11:35:07 +02:00
b6707749e5 chore: bump go-imap dependencies 2020-06-25 10:52:51 +02:00
7ec4309ae1 fix: correctly handle failure to unlock single key 2020-06-24 14:22:26 +02:00
ec224a962f fix: hang when reloading keys 2020-06-22 10:19:13 +02:00
012be60311 test: remove time checks 2020-06-17 15:30:41 +02:00
02804d067c fix: ensure doh connections are closed when it is disabled 2020-06-17 10:57:12 +02:00
9241a9bdbf feat: add reloadkeys method 2020-06-16 12:51:28 +02:00
f3e6af5571 feat: clear keys after unmarshaling 2020-06-16 10:23:21 +02:00
7a13b89274 test: reword scenario 2020-06-16 07:34:46 +00:00
5cb78b0a03 fix: review comments 2020-06-16 07:34:46 +00:00
c19bb0fa97 feat: migrate to gopenpgp v2 2020-06-16 07:34:46 +00:00
de16f6f2d1 Apply suggestion to internal/store/mailbox_message.go 2020-06-16 09:15:16 +02:00
7963b3c152 Apply suggestion to internal/store/mailbox_message.go 2020-06-16 07:05:35 +00:00
f82ab3189b Apply suggestion to internal/store/mailbox_message.go 2020-06-16 07:05:35 +00:00
49cc49b1e2 [GODT-354] Do not label/unlabel messsages from All Mail folder 2020-06-16 07:05:35 +00:00
9808c44714 fix: avoid listing credentials, prefer getting 2020-06-15 14:27:01 +02:00
c329711f9c fix: bad fish 2020-06-11 14:01:58 +02:00
928fa93765 fix: don't remove log dir on startup 2020-06-05 10:48:34 +02:00
45e99caa23 fix: handle double charset everywhere by using our ParseMediaType 2020-06-03 12:51:31 +00:00
80b2bfc2a5 fix: crash in message.combineParts when copying a nil slice 2020-06-03 12:41:51 +00:00
6070a3b7cc fix: crash if fail to find necessary html element 2020-06-03 14:05:20 +02:00
9e633400b0 feat: [GODT-360] detect charset embedded in html and xml 2020-06-02 09:44:50 +02:00
84d344cb0a chore: bump docker-credential-helpers version 2020-05-29 14:54:43 +02:00
e43033b42b feat: revert back to quoted-printable 2020-05-29 12:21:48 +00:00
e5d63edb62 test: add message.Parse tests 2020-05-29 12:21:48 +00:00
579e962980 check license 2020-05-29 14:01:10 +02:00
2919d1a3c0 fix: properly find parent id 2020-05-28 06:53:00 +00:00
1ba319bb69 Pimp up changelog 2020-05-28 06:18:29 +00:00
8cdebb6d05 Fix flaky store cooldown test 2020-05-27 15:20:38 +00:00
cc14b523cb fix: correct doh timeouts 2020-05-27 07:32:26 +00:00
ad877431de fix: check doh permission 2020-05-27 07:32:26 +00:00
40d8c458d2 Store factory to make store optional 2020-05-26 14:57:41 +00:00
3b0b1a457b docs: add locations of bridge files to readme 2020-05-26 08:02:15 +02:00
7ac4c9aecf fix: don't logout user if auth refresh fails because internet dropped 2020-05-25 15:21:20 +00:00
390182d247 do not complie windows twice, always pack to tgz 2020-05-25 13:46:50 +00:00
cb8a15a9fd fix: crash when removing account while messages are being returned 2020-05-25 08:29:42 +00:00
4d2baa6b85 Renamed bridge to general users and keep bridge only for bridge stuff 2020-05-25 09:02:34 +02:00
7724ca3996 release notes 2020-05-23 09:05:58 +00:00
4393d67bf2 GODT-396 reduce number of exists calls 2020-05-23 09:05:58 +00:00
d222b39793 Apply suggestion to test/features/imap/message/create.feature 2020-05-23 11:07:06 +02:00
6ae78217db Fix appending to Sent 2020-05-23 11:07:06 +02:00
b91c286332 fix gitlab dind 2020-05-23 10:55:57 +02:00
4e2ab9b389 Validate recipient emails in send before asking for their public keys 2020-05-21 07:26:34 +00:00
c6c6cfc7d7 Fix Changelog history 2020-05-21 09:27:46 +02:00
a78b1ca00f refactor: remove dead code 2020-05-20 11:33:22 +02:00
d718720b29 fix: custom message bad pgp using template 2020-05-20 10:59:00 +02:00
f64cb4b56d fix: wrong zip format 2020-05-19 16:19:51 +02:00
b2b43ac909 fix: missing ci package zip 2020-05-18 12:45:42 +00:00
f2b8d02cd2 fix: can't connect to docker 2020-05-18 14:19:19 +02:00
50ed40f205 release notes 2020-05-18 14:02:29 +02:00
3c92ff18ff feat: use zip for windows targetos 2020-05-18 12:35:27 +02:00
41f6cd3bcd feat: build windows in pipeline 2020-05-18 12:13:44 +02:00
66f23bef99 feat: cross compilation for windows 2020-05-18 10:28:06 +02:00
bbf1364e30 feat: tls report cache 2020-05-14 14:12:41 +02:00
6147c214c3 Better error message when request is canceled 2020-05-12 10:49:04 +00:00
f87ca36ffd refactor: tidy up DecodeCharset 2020-05-12 10:12:19 +00:00
37f4e46bdc feat: fallback to latin1 if charset not specified and not utf8 2020-05-12 10:12:19 +00:00
a7b9572e6b Fix reference parsing 2020-05-11 14:48:12 +00:00
4090c490b1 Apply suggestion to pkg/pmapi/messages.go 2020-05-11 14:48:12 +00:00
d33d7237bd Apply suggestion to pkg/pmapi/messages.go 2020-05-11 14:48:12 +00:00
9ed778f2b3 Apply suggestion to pkg/pmapi/messages.go 2020-05-11 14:48:12 +00:00
70fca64a36 Pop-out messageID format into constants 2020-05-11 14:48:12 +00:00
30425d5fcd Fix few typos 2020-05-11 14:48:12 +00:00
2639f7333e Simplify references parsing 2020-05-11 14:48:12 +00:00
4a8d07d54e Update auto-generated files 2020-05-07 19:14:30 +00:00
833fce8702 chore: bump linter 2020-05-07 16:24:10 +00:00
c1a57a2e12 Apply suggestion to internal/store/event_loop.go 2020-05-07 16:19:06 +02:00
cda3000a7a Apply suggestion to internal/store/store_test_exports.go 2020-05-07 16:19:06 +02:00
4b2977041a fix: missing messages after changing primary address 2020-05-07 16:19:06 +02:00
2d200f6f8c test: add test with changing address order 2020-05-07 16:19:06 +02:00
c61e8bdc71 Merge remote-tracking branch 'origin/master' into devel 2020-05-07 15:30:08 +02:00
7e8dc22837 Add build-files into .gitignore 2020-05-06 05:05:35 +00:00
e43bd231ed Final touches of go-imap v1 implementation 2020-05-05 11:47:47 +00:00
1998d92432 Updates() needs to return imapBackend.Update instead of interface with go-imap v1 2020-05-05 11:47:47 +00:00
313e803fdd Implement new SearchCriteria from latest go-imap 2020-05-05 11:47:47 +00:00
cabcb3ae2b Upgrade to latest go-imap-quota with fix for go-imap v1 2020-05-05 11:47:47 +00:00
e57a3c2a3a Notify about new mailbox 2020-05-05 11:47:47 +00:00
7a87a7ea2f fix: fixes after rebase 2020-05-05 11:47:47 +00:00
c939893131 Unseen is first sequence number of unseen message not count of messages 2020-05-05 11:47:47 +00:00
ea0f3115a3 usage of latest upstream go-imap 2020-05-05 11:47:47 +00:00
3d3b91b242 Merge commit comments 2020-05-05 11:20:39 +00:00
cd38c86b4b CI build mac 2020-05-05 11:20:39 +00:00
2379598078 fix: Build without GNU sed 2020-05-05 11:08:08 +00:00
984b28e8f9 User Agent do not contain bridge version, only client in format 2020-05-05 11:00:18 +00:00
1d49a484a8 test: fix typo 2020-05-04 12:31:51 +02:00
99aabf07b3 Apply suggestion to pkg/config/pmapi_prod.go 2020-05-04 07:53:55 +00:00
6e537db5ff Apply suggestion to pkg/pmapi/client.go 2020-05-04 07:53:55 +00:00
668fc7f039 feat: MinSpeed -> MinBytesPerSecond, check every 3 seconds 2020-05-04 07:53:55 +00:00
284a097d4f fix: lower min speed 2020-05-04 07:53:55 +00:00
e5944518ca chore: improve logging 2020-05-04 07:37:51 +00:00
df3a9ea19e test: add comment for why tests are disabled 2020-05-04 07:37:51 +00:00
2db1b113e0 fix: correct timeouts according to spec 2020-05-04 07:37:51 +00:00
68d2591c73 test: fix tls tests 2020-05-04 07:37:51 +00:00
e9735c6110 refactor: set app version when enabling remote tls issue reporting 2020-05-04 07:37:51 +00:00
0fd5ca3a24 feat: dialer refactor to support modular dialing/checking/proxying 2020-05-04 07:37:51 +00:00
8c2f88fe70 Apply suggestion to Changelog.md 2020-04-30 09:20:03 +00:00
23f492705b fix: better draft detection for parentID 2020-04-30 09:20:03 +00:00
44233e5bd3 fix 404 errors by using absolute urls 2020-04-30 09:12:26 +00:00
33770ce129 fix: crash in fakeapi if user is nil 2020-04-30 09:03:16 +00:00
faec347054 test: use the correct constants.Version in integration tests 2020-04-30 09:44:15 +02:00
9b68625522 Update BUILDS.md to list libsecret dev files
Fixes #11
2020-04-29 15:02:39 +02:00
8288a39ff4 Update issue templates
General issue template
2020-04-29 07:57:01 +02:00
b15d22c8cc Reduce number of synchronizations GODT-313
* [x] expononential cooldown of retries
* [x] do not trigger sync by counts
* [x] randomization of event poll interval
2020-04-28 14:20:37 +00:00
d42deb2ad5 fix: variable name in readme 2020-04-28 12:39:05 +00:00
bb5227c1f4 fix: app version and variable location 2020-04-28 12:39:05 +00:00
0589f329e9 refactor: dedicated constants package, no explicit bridge version 2020-04-28 12:39:05 +00:00
522cadb8b1 refactor: dedicated constants package, no explicit bridge version 2020-04-28 12:39:05 +00:00
32ca7b3903 fix: envvar conflict on fedora 2020-04-28 12:39:05 +00:00
7d30459417 test: empty auth update channel in tests 2020-04-28 12:21:54 +00:00
8f15041d8f fix: race condition when updating user auth 2020-04-28 12:21:54 +00:00
51846efed5 Merge branch 'release/v1.2.7' into devel 2020-04-27 15:54:02 +02:00
a1b01d5922 feat: add nogui build in makefile 2020-04-24 08:47:48 +00:00
76b480298a fix: better error messages for 422 2020-04-23 08:34:38 +00:00
a51841158c docs: add libglvnd as build deps 2020-04-22 10:16:22 +02:00
68d1442a8f Update copySuccess & appendSuccess messages according to RFCs. https://github.com/ProtonMail/proton-bridge/issues/3 2020-04-22 09:45:27 +02:00
1457005f86 fix: address review comments 2020-04-21 13:29:26 +02:00
febdf98349 test: attempt less flaky tests 2020-04-21 08:36:39 +00:00
d4482994ec fix: missing and incorrect comments 2020-04-21 08:36:39 +00:00
244a18ac8c feat: update changelog 2020-04-21 08:36:39 +00:00
e027aa5fae test: use clientmanager to logout fakeapi 2020-04-21 08:36:39 +00:00
99635cd56d feat: max retries of 5 for client logout 2020-04-21 08:36:39 +00:00
e95aece6d3 refactor: don't pass client directly to store syncer 2020-04-21 08:36:39 +00:00
38f0425670 refactor: make sentry report its own package 2020-04-21 08:36:39 +00:00
4809d97cb1 feat: clientmanager has checkconnection 2020-04-21 08:36:39 +00:00
bfc4069df4 feat: remove user from bridge users list if init failed 2020-04-21 08:36:39 +00:00
3f32fd95e0 feat: refresh expired access tokens in one goroutine 2020-04-21 08:36:39 +00:00
40e96b9d1e feat: retry client auth delete while api is unreachable 2020-04-21 08:36:39 +00:00
80f4e1e346 Fixing unit tests for client manager.
* [x] pmapi: refresh auth uid won't change
* [x] bridge tests:
    * update mocks
    * delete auth when FinishLogin fails
    * check for mailbox password
    * add `gomock.InOrder` for better test control
* [x] fix linter issues except TODOs
* [x] make rootScheme unexported
* [x] store tests: update mocks
2020-04-21 08:36:39 +00:00
debd374d75 fix: don't delete uid of anonymous clients 2020-04-21 08:36:39 +00:00
ed8595fa5b test: some work on integration tests (fake) 2020-04-21 08:36:39 +00:00
fec5f2d3c3 test: fix most integration tests (live) 2020-04-21 08:36:39 +00:00
bafd4e714e refactor: remove unnecessary getters 2020-04-21 08:36:39 +00:00
d787d8b223 fix: use clientsLocker mutex 2020-04-21 08:36:39 +00:00
abca7284dd refactor: make getHost and getScheme private 2020-04-21 08:36:39 +00:00
db02eb694d refactor: no more pmapifactory 2020-04-21 08:36:39 +00:00
5bf4d9c6f5 refactor: prefer anonymous clients 2020-04-21 08:36:39 +00:00
b01be382fc refactor: GetBridgeAuthChannel --> GetAuthUpdateChannel 2020-04-21 08:36:38 +00:00
042c340881 feat: make store use ClientManager 2020-04-21 08:36:38 +00:00
f269be4291 refactor: make pmapi.Client the interface 2020-04-21 08:36:38 +00:00
6e38a65bd8 feat: improve login flow 2020-04-21 08:36:38 +00:00
941e09079c feat: implement token expiration watcher 2020-04-21 08:36:38 +00:00
ce29d4d74e feat: switch to proxy when need be 2020-04-21 08:36:38 +00:00
f239e8f3bf feat: central auth channel for clients 2020-04-21 08:36:38 +00:00
0a55fac29a feat: simple client manager 2020-04-21 08:36:38 +00:00
fb263e84a9 Add license to windows build 2020-04-20 17:10:02 +00:00
366a9d6d6c Clean also All Mail and Drafts mailboxes before running integration test 2020-04-20 09:11:06 +00:00
8f8fbc745d fix: correctly install tls certs with osascript 2020-04-17 16:51:32 +02:00
b75a6f7cf8 Bump version 1.2.7 and release notes 2020-04-17 11:52:42 +00:00
9072f84646 Apply suggestion to internal/store/event_loop.go 2020-04-17 10:55:19 +00:00
c6f32192b9 refactor: return ErrNoSuchAPIID any time we get 422 2020-04-17 12:12:44 +02:00
49a64a656c refactor: remove unexported fetchMessage 2020-04-17 11:46:19 +02:00
1c83cc9754 Apply suggestion to internal/store/event_loop.go 2020-04-17 09:19:39 +00:00
341a6501e6 fix: don't return error when event data is nil 2020-04-17 09:19:39 +00:00
e1ecc11f38 feat: add ErrNoSuchMessage to pmapi 2020-04-17 09:19:39 +00:00
d1e63254f2 Apply suggestion to internal/store/event_loop.go 2020-04-17 09:19:39 +00:00
0998c67f20 Apply suggestion to internal/store/event_loop.go 2020-04-17 09:19:39 +00:00
91ec7edc06 fix: better event loop error handling 2020-04-17 09:19:39 +00:00
aea816029f Apply suggestion to internal/imap/mailbox_messages.go 2020-04-17 08:39:17 +00:00
e166748270 Added IMAP extension MOVE with UIDPLUS support 2020-04-17 08:39:17 +00:00
0c7a328165 Completely delete old draft instead moving to trash when user updates draft 2020-04-17 08:31:35 +00:00
e962434c8f feat: bump go-appdir 2020-04-17 07:44:54 +00:00
46f3721d43 More logs about event loop activity 2020-04-17 06:31:59 +00:00
0cb1ff9b16 Do not send an EXISTS reposnse after EXPUNGE or when nothing changed 2020-04-16 12:46:16 +00:00
a246a35cb7 docs: fix bad folder for integration tests 2020-04-15 19:55:48 +02:00
545 changed files with 38269 additions and 9978 deletions

30
.gitignore vendored
View File

@ -18,8 +18,30 @@ coverage.html
mem.pprof
# Auto generated frontend
frontend/qml/BridgeUI/*.qmlc
frontend/qml/ProtonUI/*.qmlc
frontend/qml/ProtonUI/fontawesome.ttf
frontend/qml/ProtonUI/images
internal/frontend/qml/BridgeUI/*.qmlc
internal/frontend/qml/ImportExportUI/*.qmlc
internal/frontend/qml/ProtonUI/*.qmlc
internal/frontend/qml/ProtonUI/fontawesome.ttf
internal/frontend/qml/ProtonUI/images
internal/frontend/qml/ImportExportUI/images
frontend/qml/*.qmlc
# Build files
bridge_darwin_*.tgz
cmd/Desktop-Bridge/deploy
cmd/Import-Export/deploy
internal/frontend/qt*/moc.cpp
internal/frontend/qt*/moc.go
internal/frontend/qt*/moc.h
internal/frontend/qt*/moc_cgo_*.go
internal/frontend/qt*/moc_moc.h
internal/frontend/qt*/rcc.cpp
internal/frontend/qt*/rcc.qrc
internal/frontend/qt*/rcc_cgo_*.go
internal/frontend/rcc.cpp
internal/frontend/rcc.qrc
internal/frontend/rcc_cgo_*.go
vendor-cache/
/main.go

View File

@ -1,4 +1,4 @@
image: gitlab.protontech.ch:4567/go/bridge/ci
image: gitlab.protontech.ch:4567/go/bridge-internal
before_script:
- eval $(ssh-agent -s)
@ -13,36 +13,15 @@ before_script:
cache:
key: go-mod
paths:
- .cache
- .cache
policy: pull
stages:
- image
- cache
- test
- build
- mirror
# Stage: IMAGE
build-ci-image:
stage: image
image: docker:stable
before_script: []
cache: {}
tags:
- heavy
only:
changes:
- ci/*
services:
- docker:dind
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker info
- docker build -t gitlab.protontech.ch:4567/go/bridge/ci:latest ci
- docker push gitlab.protontech.ch:4567/go/bridge/ci:latest
# Stage: CACHE
# This will ensure latest dependency versions and updates the cache for
@ -52,11 +31,11 @@ cache-push:
only:
- branches
script:
- echo ""
- echo ""
cache:
key: go-mod
paths:
- .cache
- .cache
# Stage: TEST
@ -96,18 +75,141 @@ dependency-updates:
# Stage: BUILD
build-linux:
.build-base:
stage: build
# Test build every time (= we want to know build is possible).
only:
- branches
script:
- make build
artifacts:
name: "bridge-linux-$CI_COMMIT_REF_NAME-$CI_COMMIT_SHORT_SHA"
expire_in: 2 week
build-linux:
extends: .build-base
artifacts:
name: "bridge-linux-$CI_COMMIT_SHORT_SHA"
paths:
- bridge_*.tgz
expire_in: 2 week
build-linux-qa:
extends: .build-base
only:
- web
script:
- BUILD_TAGS="build_qa pmapi_qa" make build
artifacts:
name: "bridge-linux-qa-$CI_COMMIT_SHORT_SHA"
paths:
- bridge_*.tgz
build-ie-linux:
extends: .build-base
script:
- make build-ie
artifacts:
name: "ie-linux-$CI_COMMIT_SHORT_SHA"
paths:
- ie_*.tgz
.build-darwin-base:
extends: .build-base
before_script:
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null
- export PATH=/usr/local/bin:$PATH
- export PATH=/usr/local/opt/git/bin:$PATH
- export PATH=/usr/local/opt/make/libexec/gnubin:$PATH
- export PATH=/usr/local/opt/go@1.13/bin:$PATH
- export PATH=/usr/local/opt/gnu-sed/libexec/gnubin:$PATH
- export GOPATH=~/go
- export PATH=$GOPATH/bin:$PATH
- export CGO_CPPFLAGS='-Wno-error -Wno-nullability-completeness -Wno-expansion-to-defined -Wno-builtin-requires-header'
cache: {}
tags:
- macOS
build-darwin:
extends: .build-darwin-base
artifacts:
name: "bridge-darwin-$CI_COMMIT_SHORT_SHA"
paths:
- bridge_*.tgz
build-darwin-qa:
extends: .build-darwin-base
only:
- web
script:
- BUILD_TAGS="build_qa pmapi_qa" make build
artifacts:
name: "bridge-darwin-qa-$CI_COMMIT_SHORT_SHA"
paths:
- bridge_*.tgz
build-ie-darwin:
extends: .build-darwin-base
script:
- make build-ie
artifacts:
name: "ie-darwin-$CI_COMMIT_SHORT_SHA"
paths:
- ie_*.tgz
.build-windows-base:
extends: .build-base
services:
- docker:dind
variables:
DOCKER_HOST: tcp://docker:2375
build-windows:
extends: .build-windows-base
script:
# We need to install docker because qtdeploy builds for windows inside a docker container.
# Docker will connect to the dockerd daemon provided by the runner service docker:dind at tcp://docker:2375.
- curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh
- apt-get update && apt-get -y install binutils-mingw-w64 tar gzip
- ln -s /usr/bin/x86_64-w64-mingw32-windres /usr/bin/windres
- go mod download
- TARGET_OS=windows make build
artifacts:
name: "bridge-windows-$CI_COMMIT_SHORT_SHA"
paths:
- bridge_*.tgz
build-windows-qa:
extends: .build-windows-base
only:
- web
script:
# We need to install docker because qtdeploy builds for windows inside a docker container.
# Docker will connect to the dockerd daemon provided by the runner service docker:dind at tcp://docker:2375.
- curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh
- apt-get update && apt-get -y install binutils-mingw-w64 tar gzip
- ln -s /usr/bin/x86_64-w64-mingw32-windres /usr/bin/windres
- go mod download
- TARGET_OS=windows BUILD_TAGS="build_qa pmapi_qa" make build
artifacts:
name: "bridge-windows-qa-$CI_COMMIT_SHORT_SHA"
paths:
- bridge_*.tgz
build-ie-windows:
extends: .build-windows-base
script:
# We need to install docker because qtdeploy builds for windows inside a docker container.
# Docker will connect to the dockerd daemon provided by the runner service docker:dind at tcp://docker:2375.
- curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh
- apt-get update && apt-get -y install binutils-mingw-w64 tar gzip
- ln -s /usr/bin/x86_64-w64-mingw32-windres /usr/bin/windres
- go mod download
- TARGET_OS=windows make build-ie
artifacts:
name: "ie-windows-$CI_COMMIT_SHORT_SHA"
paths:
- ie_*.tgz
# Stage: MIRROR
mirror-repo:
stage: mirror

View File

@ -21,6 +21,12 @@ issues:
- gochecknoinits
- gosec
linters-settings:
godox:
keywords:
- TODO
- FIXME
linters:
# setting disable-all will make only explicitly enabled linters run
disable-all: true

View File

@ -1,4 +1,4 @@
# Building ProtonMail Bridge app
# Building ProtonMail Bridge and Import-Export app
## Prerequisites
* Go 1.13
@ -6,6 +6,7 @@
* For Windows it is recommended to use MinGW 64bit shell from [MSYS2](https://www.msys2.org/)
* GCC (linux, windows) or Xcode (macOS)
* Windres (windows)
* libglvnd and libsecret development files (linux)
To enable the sending of crash reports using Sentry please set the
`main.DSNSentry` value with the client key of your sentry project before build.
@ -18,6 +19,8 @@ Otherwise, the sending of crash reports will be disabled.
export MSYSTEM=
```
### Build Bridge
* in project root run
```bash
@ -25,9 +28,22 @@ make build
```
* The result will be stored in `./cmd/Destop-Bridge/deploy/${GOOS}/`
* for `linux`, the binary will have the name of the project directory (e.g `bridge`)
* for `windows`, the binary will have the file extension `.exe` (e.g `bridge.exe`)
* for `darwin`, the application will be created with name of the project directory (e.g `bridge.app`)
* for `linux`, the binary will have the name of the project directory (e.g `proton-bridge`)
* for `windows`, the binary will have the file extension `.exe` (e.g `proton-bridge.exe`)
* for `darwin`, the application will be created with name of the project directory (e.g `proton-bridge.app`)
### Build Import-Export
* in project root run
```bash
make build-ie
```
* The result will be stored in `./cmd/Import-Export/deploy/${GOOS}/`
* for `linux`, the binary will have the name of the project directory (e.g `proton-bridge`)
* for `windows`, the binary will have the file extension `.exe` (e.g `proton-bridge.exe`)
* for `darwin`, the application will be created with name of the project directory (e.g `proton-bridge.app`)
## Useful tests, lints and checks
In order to be able to run following commands please install the development dependencies:
@ -35,5 +51,5 @@ In order to be able to run following commands please install the development dep
* `make test` will run all unit tests
* `make lint` will lint the whole project
* `make -C ./tests test` will run the integration tests
* `make -C ./test test` will run the integration tests
* `make run` will build Bridge without a GUI and start it in CLI mode

View File

@ -26,48 +26,48 @@ ProtonMail Bridge includes the following 3rd party software:
* [Qt](https://www.qt.io/) | Available under [multiple licences](https://www.qt.io/licensing)
* [Font Awesome 4.7.0](https://fontawesome.com/v4.7.0/) | Available under [multiple licenses](https://fontawesome.com/v4.7.0/license/)
* [notificator](github.com/0xAX/notificator) | Available under [license](https://github.com/0xAX/notificator/blob/master/LICENSE)
* [ishell](github.com/abiosoft/ishell) | Available under [license](https://github.com/abiosoft/ishell/blob/master/LICENSE)
* [readline](github.com/abiosoft/readline) | Available under [license](https://github.com/abiosoft/readline/blob/master/LICENSE)
* [singleinstance](github.com/allan-simon/go-singleinstance) | Available under [license](https://github.com/allan-simon/go-singleinstance/blob/master/LICENSE)
* [cascadia](github.com/andybalholm/cascadia) | Available under [license](https://github.com/andybalholm/cascadia/blob/master/LICENSE)
* [gocertifi](github.com/certifi/gocertifi) | Available under [license](https://github.com/certifi/gocertifi/blob/master/LICENSE)
* [logex](github.com/chzyer/logex) | Available under [license](https://github.com/chzyer/logex/blob/master/LICENSE)
* [test](github.com/chzyer/test) | Available under [license](https://github.com/chzyer/test/blob/master/LICENSE)
* [godog](github.com/cucumber/godog) | Available under [license](https://github.com/cucumber/godog/blob/master/LICENSE)
* [wincred](github.com/danieljoos/wincred) | Available under [license](https://github.com/danieljoos/wincred/blob/master/LICENSE)
* [credential-helpers](github.com/docker/docker-credential-helpers) | Available under [license](https://github.com/docker/docker-credential-helpers/blob/master/LICENSE)
* [imap](github.com/emersion/go-imap) | Available under [license](https://github.com/emersion/go-imap/blob/master/LICENSE)
* [imap-appendlimit](github.com/emersion/go-imap-appendlimit) | Available under [license](https://github.com/emersion/go-imap-appendlimit/blob/master/LICENSE)
* [imap-idle](github.com/emersion/go-imap-idle) | Available under [license](https://github.com/emersion/go-imap-idle/blob/master/LICENSE)
* [imap-quota](github.com/emersion/go-imap-quota) | Available under [license](https://github.com/emersion/go-imap-quota/blob/master/LICENSE)
* [imap-specialuse](github.com/emersion/go-imap-specialuse) | Available under [license](https://github.com/emersion/go-imap-specialuse/blob/master/LICENSE)
* [sasl](github.com/emersion/go-sasl) | Available under [license](https://github.com/emersion/go-sasl/blob/master/LICENSE)
* [smtp](github.com/emersion/go-smtp) | Available under [license](https://github.com/emersion/go-smtp/blob/master/LICENSE)
* [textwrapper](github.com/emersion/go-textwrapper) | Available under [license](https://github.com/emersion/go-textwrapper/blob/master/LICENSE)
* [vcard](github.com/emersion/go-vcard) | Available under [license](https://github.com/emersion/go-vcard/blob/master/LICENSE)
* [color](github.com/fatih/color) | Available under [license](https://github.com/fatih/color/blob/master/LICENSE)
* [shlex](github.com/flynn-archive/go-shlex) | Available under [license](https://github.com/flynn-archive/go-shlex/blob/master/LICENSE)
* [raven](github.com/getsentry/raven-go) | Available under [license](https://github.com/getsentry/raven-go/blob/master/LICENSE)
* [resty](github.com/go-resty/resty/v2) | Available under [license](https://github.com/go-resty/resty/v2/blob/master/LICENSE)
* [mock](github.com/golang/mock) | Available under [license](https://github.com/golang/mock/blob/master/LICENSE)
* [cmp](github.com/google/go-cmp) | Available under [license](https://github.com/google/go-cmp/blob/master/LICENSE)
* [gopherjs](github.com/gopherjs/gopherjs) | Available under [license](https://github.com/gopherjs/gopherjs/blob/master/LICENSE)
* [multierror](github.com/hashicorp/go-multierror) | Available under [license](https://github.com/hashicorp/go-multierror/blob/master/LICENSE)
* [bcrypt](github.com/jameskeane/bcrypt) | Available under [license](https://github.com/jameskeane/bcrypt/blob/master/LICENSE)
* [html2text](github.com/jaytaylor/html2text) | Available under [license](https://github.com/jaytaylor/html2text/blob/master/LICENSE)
* [enmime](github.com/jhillyerd/enmime) | Available under [license](https://github.com/jhillyerd/enmime/blob/master/LICENSE)
* [osext](github.com/kardianos/osext) | Available under [license](https://github.com/kardianos/osext/blob/master/LICENSE)
* [keychain](github.com/keybase/go-keychain) | Available under [license](https://github.com/keybase/go-keychain/blob/master/LICENSE)
* [aurora](github.com/logrusorgru/aurora) | Available under [license](https://github.com/logrusorgru/aurora/blob/master/LICENSE)
* [dns](github.com/miekg/dns) | Available under [license](https://github.com/miekg/dns/blob/master/LICENSE)
* [uuid](github.com/myesui/uuid) | Available under [license](https://github.com/myesui/uuid/blob/master/LICENSE)
* [jsondiff](github.com/nsf/jsondiff) | Available under [license](https://github.com/nsf/jsondiff/blob/master/LICENSE)
* [logrus](github.com/sirupsen/logrus) | Available under [license](https://github.com/sirupsen/logrus/blob/master/LICENSE)
* [golang](github.com/skratchdot/open-golang) | Available under [license](https://github.com/skratchdot/open-golang/blob/master/LICENSE)
* [testify](github.com/stretchr/testify) | Available under [license](https://github.com/stretchr/testify/blob/master/LICENSE)
* [uuid](github.com/twinj/uuid) | Available under [license](https://github.com/twinj/uuid/blob/master/LICENSE)
* [cli](github.com/urfave/cli) | Available under [license](https://github.com/urfave/cli/blob/master/LICENSE)
* [notificator](https://github.com/0xAX/notificator) | Available under [license](https://github.com/0xAX/notificator/blob/master/LICENSE)
* [ishell](https://github.com/abiosoft/ishell) | Available under [license](https://github.com/abiosoft/ishell/blob/master/LICENSE)
* [readline](https://github.com/abiosoft/readline) | Available under [license](https://github.com/abiosoft/readline/blob/master/LICENSE)
* [singleinstance](https://github.com/allan-simon/go-singleinstance) | Available under [license](https://github.com/allan-simon/go-singleinstance/blob/master/LICENSE)
* [cascadia](https://github.com/andybalholm/cascadia) | Available under [license](https://github.com/andybalholm/cascadia/blob/master/LICENSE)
* [gocertifi](https://github.com/certifi/gocertifi) | Available under [license](https://github.com/certifi/gocertifi/blob/master/LICENSE)
* [logex](https://github.com/chzyer/logex) | Available under [license](https://github.com/chzyer/logex/blob/master/LICENSE)
* [test](https://github.com/chzyer/test) | Available under [license](https://github.com/chzyer/test/blob/master/LICENSE)
* [godog](https://github.com/cucumber/godog) | Available under [license](https://github.com/cucumber/godog/blob/master/LICENSE)
* [wincred](https://github.com/danieljoos/wincred) | Available under [license](https://github.com/danieljoos/wincred/blob/master/LICENSE)
* [credential-helpers](https://github.com/docker/docker-credential-helpers) | Available under [license](https://github.com/docker/docker-credential-helpers/blob/master/LICENSE)
* [imap](https://github.com/emersion/go-imap) | Available under [license](https://github.com/emersion/go-imap/blob/master/LICENSE)
* [imap-appendlimit](https://github.com/emersion/go-imap-appendlimit) | Available under [license](https://github.com/emersion/go-imap-appendlimit/blob/master/LICENSE)
* [imap-idle](https://github.com/emersion/go-imap-idle) | Available under [license](https://github.com/emersion/go-imap-idle/blob/master/LICENSE)
* [imap-quota](https://github.com/emersion/go-imap-quota) | Available under [license](https://github.com/emersion/go-imap-quota/blob/master/LICENSE)
* [imap-specialuse](https://github.com/emersion/go-imap-specialuse) | Available under [license](https://github.com/emersion/go-imap-specialuse/blob/master/LICENSE)
* [sasl](https://github.com/emersion/go-sasl) | Available under [license](https://github.com/emersion/go-sasl/blob/master/LICENSE)
* [smtp](https://github.com/emersion/go-smtp) | Available under [license](https://github.com/emersion/go-smtp/blob/master/LICENSE)
* [textwrapper](https://github.com/emersion/go-textwrapper) | Available under [license](https://github.com/emersion/go-textwrapper/blob/master/LICENSE)
* [vcard](https://github.com/emersion/go-vcard) | Available under [license](https://github.com/emersion/go-vcard/blob/master/LICENSE)
* [color](https://github.com/fatih/color) | Available under [license](https://github.com/fatih/color/blob/master/LICENSE.md)
* [shlex](https://github.com/flynn-archive/go-shlex) | Available under [license](https://github.com/flynn-archive/go-shlex/blob/master/COPYING)
* [raven](https://github.com/getsentry/raven-go) | Available under [license](https://github.com/getsentry/raven-go/blob/master/LICENSE)
* [resty](https://github.com/go-resty/resty) | Available under [license](https://github.com/go-resty/resty/blob/master/LICENSE)
* [mock](https://github.com/golang/mock) | Available under [license](https://github.com/golang/mock/blob/master/LICENSE)
* [cmp](https://github.com/google/go-cmp) | Available under [license](https://github.com/google/go-cmp/blob/master/LICENSE)
* [gopherjs](https://github.com/gopherjs/gopherjs) | Available under [license](https://github.com/gopherjs/gopherjs/blob/master/LICENSE)
* [multierror](https://github.com/hashicorp/go-multierror) | Available under [license](https://github.com/hashicorp/go-multierror/blob/master/LICENSE)
* [bcrypt](https://github.com/jameskeane/bcrypt) | Available under [license](https://github.com/jameskeane/bcrypt/blob/master/LICENSE)
* [html2text](https://github.com/jaytaylor/html2text) | Available under [license](https://github.com/jaytaylor/html2text/blob/master/LICENSE)
* [enmime](https://github.com/jhillyerd/enmime) | Available under [license](https://github.com/jhillyerd/enmime/blob/master/LICENSE)
* [osext](https://github.com/kardianos/osext) | Available under [license](https://github.com/kardianos/osext/blob/master/LICENSE)
* [keychain](https://github.com/keybase/go-keychain) | Available under [license](https://github.com/keybase/go-keychain/blob/master/LICENSE)
* [aurora](https://github.com/logrusorgru/aurora) | Available under [license](https://github.com/logrusorgru/aurora/blob/master/LICENSE)
* [dns](https://github.com/miekg/dns) | Available under [license](https://github.com/miekg/dns/blob/master/LICENSE)
* [uuid](https://github.com/myesui/uuid) | Available under [license](https://github.com/myesui/uuid/blob/master/LICENSE)
* [jsondiff](https://github.com/nsf/jsondiff) | Available under [license](https://github.com/nsf/jsondiff/blob/master/LICENSE)
* [logrus](https://github.com/sirupsen/logrus) | Available under [license](https://github.com/sirupsen/logrus/blob/master/LICENSE)
* [golang](https://github.com/skratchdot/open-golang) | Available under [license](https://github.com/skratchdot/open-golang/blob/master/LICENSE)
* [testify](https://github.com/stretchr/testify) | Available under [license](https://github.com/stretchr/testify/blob/master/LICENSE)
* [uuid](https://github.com/twinj/uuid) | Available under [license](https://github.com/twinj/uuid/blob/master/LICENSE)
* [cli](https://github.com/urfave/cli) | Available under [license](https://github.com/urfave/cli/blob/master/LICENSE)
* [BBolt](https://pkg.go.dev/go.etcd.io/bbolt/?tab=doc) | Available under [license](https://pkg.go.dev/go.etcd.io/bbolt?tab=licenses#LICENSE)
* [testify.v1](https://gopkg.in/stretchr/testify.v1) | Available under [license](https://github.com/stretchr/testify/blob/master/LICENSE)

File diff suppressed because it is too large Load Diff

178
Makefile
View File

@ -1,17 +1,36 @@
export GO111MODULE=on
# By default, the target OS is the same as the host OS,
# but this can be overridden by setting TARGET_OS to "windows"/"darwin"/"linux".
GOOS:=$(shell go env GOOS)
TARGET_CMD?=Desktop-Bridge
TARGET_OS?=${GOOS}
## Build
.PHONY: build check-has-go
.PHONY: build build-ie build-nogui build-ie-nogui check-has-go
VERSION?=1.2.6-git
# Keep version hardcoded so app build works also without Git repository.
BRIDGE_APP_VERSION?=1.4.0-git
IE_APP_VERSION?=1.1.0-git
APP_VERSION:=${BRIDGE_APP_VERSION}
SRC_ICO:=logo.ico
SRC_ICNS:=Bridge.icns
SRC_SVG:=logo.svg
TGT_ICNS:=Bridge.icns
ifeq "${TARGET_CMD}" "Import-Export"
APP_VERSION:=${IE_APP_VERSION}
SRC_ICO:=ie.ico
SRC_ICNS:=ie.icns
SRC_SVG:=ie.svg
TGT_ICNS:=ImportExport.icns
endif
REVISION:=$(shell git rev-parse --short=10 HEAD)
BUILD_TIME:=$(shell date +%FT%T%z)
BUILD_TAGS?=pmapi_prod
BUILD_FLAGS:=-tags='${BUILD_TAGS}'
BUILD_FLAGS_NOGUI:=-tags='${BUILD_TAGS} nogui'
GO_LDFLAGS:=$(addprefix -X main.,Version=${VERSION} Revision=${REVISION} BuildTime=${BUILD_TIME})
GO_LDFLAGS:=$(addprefix -X github.com/ProtonMail/proton-bridge/pkg/constants.,Version=${APP_VERSION} Revision=${REVISION} BuildTime=${BUILD_TIME})
ifneq "${BUILD_LDFLAGS}" ""
GO_LDFLAGS+= ${BUILD_LDFLAGS}
endif
@ -19,74 +38,95 @@ GO_LDFLAGS:=-ldflags '${GO_LDFLAGS}'
BUILD_FLAGS+= ${GO_LDFLAGS}
BUILD_FLAGS_NOGUI+= ${GO_LDFLAGS}
DEPLOY_DIR:=cmd/Desktop-Bridge/deploy
DEPLOY_DIR:=cmd/${TARGET_CMD}/deploy
ICO_FILES:=
EXE:=$(shell basename ${CURDIR})
ifeq "${GOOS}" "windows"
EXE+=.exe
ICO_FILES:=logo.ico icon.rc icon_windows.syso
ifeq "${TARGET_OS}" "windows"
EXE:=${EXE}.exe
ICO_FILES:=${SRC_ICO} icon.rc icon_windows.syso
endif
ifeq "${GOOS}" "darwin"
ifeq "${TARGET_OS}" "darwin"
DARWINAPP_CONTENTS:=${DEPLOY_DIR}/darwin/${EXE}.app/Contents
EXE:=${EXE}.app/Contents/MacOS/${EXE}
endif
EXE_TARGET:=${DEPLOY_DIR}/${GOOS}/${EXE}
TGZ_TARGET:=bridge_${GOOS}_${REVISION}.tgz
EXE_TARGET:=${DEPLOY_DIR}/${TARGET_OS}/${EXE}
TGZ_TARGET:=bridge_${TARGET_OS}_${REVISION}.tgz
ifeq "${TARGET_CMD}" "Import-Export"
TGZ_TARGET:=ie_${TARGET_OS}_${REVISION}.tgz
endif
build: ${TGZ_TARGET}
build-ie:
TARGET_CMD=Import-Export $(MAKE) build
${TGZ_TARGET}: ${DEPLOY_DIR}/${GOOS}
build-nogui:
go build ${BUILD_FLAGS_NOGUI} -o ${TARGET_CMD} cmd/${TARGET_CMD}/main.go
build-ie-nogui:
TARGET_CMD=Import-Export $(MAKE) build-nogui
${TGZ_TARGET}: ${DEPLOY_DIR}/${TARGET_OS}
rm -f $@
cd ${DEPLOY_DIR} && tar czf ../../../$@ ${GOOS}
cd ${DEPLOY_DIR} && tar czf ../../../$@ ${TARGET_OS}
${DEPLOY_DIR}/linux: ${EXE_TARGET}
cp -pf ./internal/frontend/share/icons/logo.svg ${DEPLOY_DIR}/linux/
cp -pf ./internal/frontend/share/icons/${SRC_SVG} ${DEPLOY_DIR}/linux/logo.svg
cp -pf ./LICENSE ${DEPLOY_DIR}/linux/
cp -pf ./Changelog.md ${DEPLOY_DIR}/linux/
${DEPLOY_DIR}/darwin: ${EXE_TARGET}
cp ./internal/frontend/share/icons/Bridge.icns ${DARWINAPP_CONTENTS}/Resources/
cp -r "utils/addcert.scpt" ${DARWINAPP_CONTENTS}/Resources/
cp ./internal/frontend/share/icons/${SRC_ICNS} ${DARWINAPP_CONTENTS}/Resources/${TGT_ICNS}
cp LICENSE ${DARWINAPP_CONTENTS}/Resources/
rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebEngine.framework"
rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebView.framework"
rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebEngineCore.framework"
./utils/remove_non_relative_links_darwin.sh "${EXE_TARGET}"
${DEPLOY_DIR}/windows: ${EXE_TARGET}
cp ./internal/frontend/share/icons/logo.ico ${DEPLOY_DIR}/windows/
cp ./internal/frontend/share/icons/${SRC_ICO} ${DEPLOY_DIR}/windows/logo.ico
cp LICENSE ${DEPLOY_DIR}/windows/
QT_BUILD_TARGET:=build desktop
ifneq "${GOOS}" "${TARGET_OS}"
ifeq "${TARGET_OS}" "windows"
QT_BUILD_TARGET:=-docker build windows_64_shared
endif
endif
${EXE_TARGET}: check-has-go gofiles ${ICO_FILES} update-vendor
rm -rf deploy ${GOOS} ${DEPLOY_DIR}
cp cmd/Desktop-Bridge/main.go .
qtdeploy ${BUILD_FLAGS} build desktop
mv deploy cmd/Desktop-Bridge
rm -rf ${GOOS} main.go
rm -rf deploy ${TARGET_OS} ${DEPLOY_DIR}
cp cmd/${TARGET_CMD}/main.go .
qtdeploy ${BUILD_FLAGS} ${QT_BUILD_TARGET}
mv deploy cmd/${TARGET_CMD}
rm -rf ${TARGET_OS} main.go
logo.ico: ./internal/frontend/share/icons/logo.ico
cp $^ .
logo.ico ie.ico: ./internal/frontend/share/icons/${SRC_ICO}
cp $^ $@
icon.rc: ./internal/frontend/share/icon.rc
cp $^ .
./internal/frontend/qt/icon_windows.syso: ./internal/frontend/share/icon.rc logo.ico
windres $< $@
icon_windows.syso: ./internal/frontend/qt/icon_windows.syso
cp $^ .
icon_windows.syso: icon.rc logo.ico
windres --target=pe-x86-64 -o $@ $<
## Rules for therecipe/qt
.PHONY: prepare-vendor update-vendor
THERECIPE_QTVER:=$(shell grep "github.com/therecipe/qt " go.mod | sed -r 's;.* v[0-9\.]+-[0-9]+-([a-f0-9]*).*;\1;')
THERECIPE_ENV:=github.com/therecipe/env_${GOOS}_amd64_513
THERECIPE_ENV:=github.com/therecipe/env_${TARGET_OS}_amd64_513
# vendor folder will be deleted by gomod hence we cache the big repo
# therecipe/env in order to download it only once
vendor-cache/${THERECIPE_ENV}:
git clone https://${THERECIPE_ENV}.git vendor-cache/${THERECIPE_ENV}
# The command used to make symlinks is different on windows.
# So if the GOOS is windows and we aren't crossbuilding (in which case the host os would still be *nix)
# we need to change the LINKCMD to something windowsy.
LINKCMD:=ln -sf ${CURDIR}/vendor-cache/${THERECIPE_ENV} vendor/${THERECIPE_ENV}
ifeq "${GOOS}" "windows"
WINDIR:=$(subst /c/,c:\\,${CURDIR})/vendor-cache/${THERECIPE_ENV}
LINKCMD:=cmd //c 'mklink $(subst /,\,vendor\${THERECIPE_ENV} ${WINDIR})'
WINDIR:=$(subst /c/,c:\\,${CURDIR})/vendor-cache/${THERECIPE_ENV}
LINKCMD:=cmd //c 'mklink $(subst /,\,vendor\${THERECIPE_ENV} ${WINDIR})'
endif
prepare-vendor:
@ -100,7 +140,7 @@ update-vendor: vendor-cache/${THERECIPE_ENV} prepare-vendor
## Dev dependencies
.PHONY: install-devel-tools install-linter install-go-mod-outdated
LINTVER:="v1.23.6"
LINTVER:="v1.29.0"
LINTSRC:="https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh"
install-dev-dependencies: install-devel-tools install-linter install-go-mod-outdated
@ -118,12 +158,15 @@ install-go-mod-outdated:
## Checks, mocks and docs
.PHONY: check-has-go check-license test bench coverage mocks lint updates doc
.PHONY: check-has-go add-license change-copyright-year test bench coverage mocks lint-license lint-golang lint updates doc
check-has-go:
@which go || (echo "Install Go-lang!" && exit 1)
check-license:
find . -not -path "./vendor/*" -not -name "*mock*.go" -regextype posix-egrep -regex ".*\.go|.*\.qml" -exec grep -L "Copyright (c) 2020 Proton Technologies AG" {} \;
add-license:
./utils/missing_license.sh add
change-copyright-year:
./utils/missing_license.sh change-year
test: gofiles
@# Listing packages manually to not run Qt folder (which needs to run qtsetup first) and integration tests.
@ -134,9 +177,14 @@ test: gofiles
./internal/frontend/autoconfig/... \
./internal/frontend/cli/... \
./internal/imap/... \
./internal/metrics/... \
./internal/importexport/... \
./internal/preferences/... \
./internal/smtp/... \
./internal/store/... \
./internal/transfer/... \
./internal/updates/... \
./internal/users/... \
./pkg/...
bench:
@ -148,11 +196,18 @@ coverage: test
go tool cover -html=/tmp/coverage.out -o=coverage.html
mocks:
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/bridge Configer,PreferenceProvider,PanicHandler,PMAPIProvider,CredentialsStorer > internal/bridge/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/store PanicHandler,BridgeUser > internal/store/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/users Configer,PanicHandler,ClientManager,CredentialsStorer,StoreMaker > internal/users/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/transfer PanicHandler,ClientManager > internal/transfer/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/store PanicHandler,ClientManager,BridgeUser > internal/store/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/listener Listener > internal/store/mocks/utils_mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/pmapi Client > pkg/pmapi/mocks/mocks.go
lint:
lint: lint-golang lint-license
lint-license:
./utils/missing_license.sh check
lint-golang:
which golangci-lint || $(MAKE) install-linter
golangci-lint run ./...
@ -166,15 +221,20 @@ doc:
.PHONY: gofiles
# Following files are for the whole app so it makes sense to have them in bridge package.
# (Options like cmd or internal were considered and bridge package is the best place for them.)
gofiles: ./internal/bridge/credits.go ./internal/bridge/release_notes.go
gofiles: ./internal/bridge/credits.go ./internal/bridge/release_notes.go ./internal/importexport/credits.go ./internal/importexport/release_notes.go
./internal/bridge/credits.go: ./utils/credits.sh go.mod
cd ./utils/ && ./credits.sh
./internal/bridge/release_notes.go: ./utils/release-notes.sh ./release-notes/notes.txt ./release-notes/bugs.txt
cd ./utils/ && ./release-notes.sh
cd ./utils/ && ./credits.sh bridge
./internal/bridge/release_notes.go: ./utils/release-notes.sh ./release-notes/notes-bridge.txt ./release-notes/bugs-bridge.txt
cd ./utils/ && ./release-notes.sh bridge
./internal/importexport/credits.go: ./utils/credits.sh go.mod
cd ./utils/ && ./credits.sh importexport
./internal/importexport/release_notes.go: ./utils/release-notes.sh ./release-notes/notes-importexport.txt ./release-notes/bugs-importexport.txt
cd ./utils/ && ./release-notes.sh importexport
## Run and debug
.PHONY: run run-qt run-qt-cli run-nogui run-nogui-cli run-debug qmlpreview qt-fronted-clean clean
.PHONY: run run-qt run-qt-cli run-nogui run-nogui-cli run-debug run-qml-preview run-ie-qml-preview run-ie run-ie-qt run-ie-qt-cli run-ie-nogui run-ie-nogui-cli clean-vendor clean-frontend-qt clean-frontend-qt-ie clean-frontend-qt-common clean
VERBOSITY?=debug-client
RUN_FLAGS:=-m -l=${VERBOSITY}
@ -186,23 +246,39 @@ run-qt-cli: ${EXE_TARGET}
PROTONMAIL_ENV=dev ./$< ${RUN_FLAGS} -c
run-nogui: clean-vendor gofiles
PROTONMAIL_ENV=dev go run ${BUILD_FLAGS_NOGUI} cmd/Desktop-Bridge/main.go ${RUN_FLAGS} | tee last.log
PROTONMAIL_ENV=dev go run ${BUILD_FLAGS_NOGUI} cmd/${TARGET_CMD}/main.go ${RUN_FLAGS} | tee last.log
run-nogui-cli: clean-vendor gofiles
PROTONMAIL_ENV=dev go run ${BUILD_FLAGS_NOGUI} cmd/Desktop-Bridge/main.go ${RUN_FLAGS} -c
PROTONMAIL_ENV=dev go run ${BUILD_FLAGS_NOGUI} cmd/${TARGET_CMD}/main.go ${RUN_FLAGS} -c
run-debug:
PROTONMAIL_ENV=dev dlv debug --build-flags "${BUILD_FLAGS_NOGUI}" cmd/Desktop-Bridge/main.go -- ${RUN_FLAGS}
PROTONMAIL_ENV=dev dlv debug --build-flags "${BUILD_FLAGS_NOGUI}" cmd/${TARGET_CMD}/main.go -- ${RUN_FLAGS}
run-qml-preview:
make -C internal/frontend/qt -f Makefile.local qmlpreview
$(MAKE) -C internal/frontend/qt -f Makefile.local qmlpreview
run-ie-qml-preview:
$(MAKE) -C internal/frontend/qt-ie -f Makefile.local qmlpreview
run-ie:
TARGET_CMD=Import-Export $(MAKE) run
run-ie-qt:
TARGET_CMD=Import-Export $(MAKE) run-qt
run-ie-nogui:
TARGET_CMD=Import-Export $(MAKE) run-nogui
clean-frontend-qt:
make -C internal/frontend/qt -f Makefile.local clean
$(MAKE) -C internal/frontend/qt -f Makefile.local clean
clean-frontend-qt-ie:
$(MAKE) -C internal/frontend/qt-ie -f Makefile.local clean
clean-frontend-qt-common:
$(MAKE) -C internal/frontend/qt-common -f Makefile.local clean
clean-vendor: clean-frontend-qt
clean-vendor: clean-frontend-qt clean-frontend-qt-ie clean-frontend-qt-common
rm -rf ./vendor
clean: clean-frontend-qt
clean: clean-vendor
rm -rf vendor-cache
rm -rf cmd/Desktop-Bridge/deploy
rm -f build last.log mem.pprof
rm -rf cmd/Import-Export/deploy
rm -f build last.log mem.pprof main.go
rm -rf logo.ico icon.rc icon_windows.syso internal/frontend/qt/icon_windows.syso

View File

@ -1,4 +1,4 @@
# ProtonMail Bridge
# ProtonMail Bridge and Import Export app
Copyright (c) 2020 Proton Technologies AG
This repository holds the ProtonMail Bridge application.
@ -6,7 +6,8 @@ For a detailed build information see [BUILDS](./BUILDS.md).
For licensing information see [COPYING](./COPYING.md).
For contribution policy see [CONTRIBUTING](./CONTRIBUTING.md).
## Description
## Description Bridge
ProtonMail Bridge for e-mail clients.
When launched, Bridge will initialize local IMAP/SMTP servers and render
@ -23,6 +24,16 @@ background.
More details [on the public website](https://protonmail.com/bridge).
## Description Import-Export app
ProtonMail Import-Export app for importing and exporting messages.
To transfer messages, firstly log in using your ProtonMail credentials.
For import, expand your account, and pick the address to which to import
messages from IMAP server or local EML or MBOX files. For export, pick
the whole account or only a specific address. Then, in both cases,
configure transfer rules (match source and target mailboxes, set time
range limits and so on) and hit start. Once the transfer is complete,
check the results.
## Keychain
You need to have a keychain in order to run the ProtonMail Bridge. On Mac or
@ -31,22 +42,53 @@ Windows, Bridge uses native credential managers. On Linux, use
or
[pass](https://www.passwordstore.org/).
## Environment Variables
### Bridge application
- `BRIDGESTRICTMODE`: tells bridge to turn on `bbolt`'s "strict mode" which checks the database after every `Commit`. Set to `1` to enable.
### Dev build or run
- `APP_VERSION`: set the bridge app version used during testing or building
- `PROTONMAIL_ENV`: when set to `dev` it is not using Sentry to report crashes
- `VERBOSITY`: set log level used during test time and by the makefile.
- `VERSION`: set the bridge app version used during testing or building.
- `VERBOSITY`: set log level used during test time and by the makefile
### Integration testing
- `TEST_ENV`: set which env to use (fake or live)
- `TEST_APP`: set which app to test (bridge or ie)
- `TEST_ACCOUNTS`: set JSON file with configured accounts
- `TAGS`: set build tags for tests
- `FEATURES`: set feature dir, file or scenario to test
## Files
### Database
The database stores metadata necessary for presenting messages and mailboxes to an email client:
- Linux: `~/.cache/protonmail/bridge/<cacheVersion>/mailbox-<userID>.db` (unless `XDG_CACHE_HOME` is set, in which case that is used as your `~`)
- macOS: `~/Library/Caches/protonmail/bridge/<cacheVersion>/mailbox-<userID>.db`
- Windows: `%LOCALAPPDATA%\protonmail\bridge\<cacheVersion>\mailbox-<userID>.db`
### Preferences
User preferences are stored in json at the following location:
- Linux: `~/.cache/protonmail/bridge/<cacheVersion>/prefs.json` (unless `XDG_CACHE_HOME` is set, in which case that is used as your `~`)
- macOS: `~/Library/Caches/protonmail/bridge/<cacheVersion>/prefs.json`
- Windows: `%LOCALAPPDATA%\protonmail\bridge\<cacheVersion>\prefs.json`
### IMAP Cache
The currently subscribed mailboxes are held in a json file:
- Linux: `~/.cache/protonmail/bridge/<cacheVersion>/user_info.json` (unless `XDG_CACHE_HOME` is set, in which case that is used as your `~`)
- macOS: `~/Library/Caches/protonmail/bridge/<cacheVersion>/user_info.json`
- Windows: `%LOCALAPPDATA%\protonmail\bridge\<cacheVersion>\user_info.json`
### Lock file
Bridge utilises an on-disk lock to ensure only one instance is run at once. The lock file is here:
- Linux: `~/.cache/protonmail/bridge/<cacheVersion>/bridge.lock` (unless `XDG_CACHE_HOME` is set, in which case that is used as your `~`)
- macOS: `~/Library/Caches/protonmail/bridge/<cacheVersion>/bridge.lock`
- Windows: `%LOCALAPPDATA%\protonmail\bridge\<cacheVersion>\bridge.lock`
### TLS Certificate and Key
When bridge first starts, it generates a unique TLS certificate and key file at the following locations:
- Linux: `~/.config/protonmail/bridge/{cert,key}.pem` (unless `XDG_CONFIG_HOME` is set, in which case that is used as your `~/.config`)
- macOS: `~/Library/ApplicationSupport/protonmail/bridge/{cert,key}.pem`
- Windows: `%APPDATA%\protonmail\bridge\{cert,key}.pem`

View File

@ -1,4 +0,0 @@
FROM gitlab.protontech.ch:4567/protonmail/ci-containers/go
RUN apt-get -y update
RUN apt-get -y install openssh-client libsecret-1-dev libgl1-mesa-dev time connect-proxy

View File

@ -35,138 +35,56 @@ package main
*/
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"runtime"
"runtime/pprof"
"strconv"
"strings"
"github.com/ProtonMail/proton-bridge/internal/api"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/bridge/credentials"
"github.com/ProtonMail/proton-bridge/internal/cmd"
"github.com/ProtonMail/proton-bridge/internal/cookies"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/frontend"
"github.com/ProtonMail/proton-bridge/internal/imap"
"github.com/ProtonMail/proton-bridge/internal/pmapifactory"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/internal/smtp"
"github.com/ProtonMail/proton-bridge/pkg/args"
"github.com/ProtonMail/proton-bridge/internal/updates"
"github.com/ProtonMail/proton-bridge/internal/users/credentials"
"github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/pkg/constants"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/updates"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/allan-simon/go-singleinstance"
"github.com/getsentry/raven-go"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
// cacheVersion is used for cache files such as lock, events, preferences, user_info, db files.
// Different number will drop old files and create new ones.
const cacheVersion = "c11"
const (
// cacheVersion is used for cache files such as lock, events, preferences, user_info, db files.
// Different number will drop old files and create new ones.
cacheVersion = "c11"
// Following variables are set via ldflags during build.
var (
// Version of the build.
Version = "" //nolint[gochecknoglobals]
// Revision is current hash of the build.
Revision = "" //nolint[gochecknoglobals]
// BuildTime stamp of the build.
BuildTime = "" //nolint[gochecknoglobals]
// AppShortName to make setup
AppShortName = "bridge" //nolint[gochecknoglobals]
// DSNSentry client keys to be able to report crashes to Sentry
DSNSentry = "" //nolint[gochecknoglobals]
appName = "bridge"
)
var (
longVersion = Version + " (" + Revision + ")" //nolint[gochecknoglobals]
buildVersion = longVersion + " " + BuildTime //nolint[gochecknoglobals]
log = config.GetLogEntry("main") //nolint[gochecknoglobals]
// How many crashes in a row.
numberOfCrashes = 0 //nolint[gochecknoglobals]
// After how many crashes bridge gives up starting.
maxAllowedCrashes = 10 //nolint[gochecknoglobals]
log = logrus.WithField("pkg", "main") //nolint[gochecknoglobals]
)
func main() {
if err := raven.SetDSN(DSNSentry); err != nil {
log.WithError(err).Errorln("Can not setup sentry DSN")
}
raven.SetRelease(Revision)
bridge.UpdateCurrentUserAgent(Version, runtime.GOOS, "", "")
args.FilterProcessSerialNumberFromArgs()
filterRestartNumberFromArgs()
app := cli.NewApp()
app.Name = "Protonmail Bridge"
app.Version = buildVersion
app.Flags = []cli.Flag{
cli.StringFlag{
Name: "log-level, l",
Usage: "Set the log level (one of panic, fatal, error, warn, info, debug, debug-client, debug-server)"},
cli.BoolFlag{
Name: "no-window",
Usage: "Don't show window after start"},
cli.BoolFlag{
Name: "cli, c",
Usage: "Use command line interface"},
cli.BoolFlag{
Name: "noninteractive",
Usage: "Start Bridge entirely noninteractively"},
cli.StringFlag{
Name: "version-json, g",
Usage: "Generate json version file"},
cli.BoolFlag{
Name: "mem-prof, m",
Usage: "Generate memory profile"},
cli.BoolFlag{
Name: "cpu-prof, p",
Usage: "Generate CPU profile"},
}
app.Usage = "ProtonMail IMAP and SMTP Bridge"
app.Action = run
// Always log the basic info about current bridge.
logrus.SetLevel(logrus.InfoLevel)
log.WithField("version", Version).
WithField("revision", Revision).
WithField("runtime", runtime.GOOS).
WithField("build", BuildTime).
WithField("args", os.Args).
WithField("appLong", app.Name).
WithField("appShort", AppShortName).
Info("Run app")
if err := app.Run(os.Args); err != nil {
log.Error("Program exited with error: ", err)
}
}
type panicHandler struct {
cfg *config.Config
err *error // Pointer to error of cli action.
}
func (ph *panicHandler) HandlePanic() {
r := recover()
if r == nil {
return
}
config.HandlePanic(ph.cfg, fmt.Sprintf("Recover: %v", r))
frontend.HandlePanic()
*ph.err = cli.NewExitError("Panic and restart", 666)
numberOfCrashes++
log.Error("Restarting after panic")
restartApp()
os.Exit(666)
cmd.Main(
"ProtonMail Bridge",
"ProtonMail IMAP and SMTP Bridge",
[]cli.Flag{
cli.BoolFlag{
Name: "no-window",
Usage: "Don't show window after start"},
cli.BoolFlag{
Name: "noninteractive",
Usage: "Start Bridge entirely noninteractively"},
},
run,
)
}
// run initializes and starts everything in a precise order.
@ -174,13 +92,17 @@ func (ph *panicHandler) HandlePanic() {
// IMPORTANT: ***Read the comments before CHANGING the order ***
func run(context *cli.Context) (contextError error) { // nolint[funlen]
// We need to have config instance to setup a logs, panic handler, etc ...
cfg := config.New(AppShortName, Version, Revision, cacheVersion)
cfg := config.New(appName, constants.Version, constants.Revision, cacheVersion)
// We want to know about any problem. Our PanicHandler calls sentry which is
// not dependent on anything else. If that fails, it tries to create crash
// report which will not be possible if no folder can be created. That's the
// only problem we will not be notified about in any way.
panicHandler := &panicHandler{cfg, &contextError}
panicHandler := &cmd.PanicHandler{
AppName: "ProtonMail Bridge",
Config: cfg,
Err: &contextError,
}
defer panicHandler.HandlePanic()
// First we need config and create necessary folder; it's dependency for everything.
@ -192,13 +114,6 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen]
logLevel := context.GlobalString("log-level")
debugClient, debugServer := config.SetupLog(cfg, logLevel)
// Should be called after logs are configured but before preferences are created.
migratePreferencesFromC10(cfg)
if err := cfg.ClearOldData(); err != nil {
log.Error("Cannot clear old data: ", err)
}
// Doesn't make sense to continue when Bridge was invoked with wrong arguments.
// We should tell that to the user before we do anything else.
if context.Args().First() != "" {
@ -208,12 +123,16 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen]
// It's safe to get version JSON file even when other instance is running.
// (thus we put it before check of presence of other Bridge instance).
updates := updates.New(AppShortName, Version, Revision, BuildTime, bridge.ReleaseNotes, bridge.ReleaseFixedBugs, cfg.GetUpdateDir())
updates := updates.NewBridge(cfg.GetUpdateDir())
if dir := context.GlobalString("version-json"); dir != "" {
generateVersionFiles(updates, dir)
cmd.GenerateVersionFiles(updates, dir)
return nil
}
// Should be called after logs are configured but before preferences are created.
migratePreferencesFromC10(cfg)
// ClearOldData before starting new bridge to do a proper setup.
//
// IMPORTANT: If you the change position of this you will need to wait
@ -240,7 +159,7 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen]
if err != nil {
log.Warn("Bridge is already running")
if err := api.CheckOtherInstanceAndFocus(pref.GetInt(preferences.APIPortKey), tls); err != nil {
numberOfCrashes = maxAllowedCrashes
cmd.DisableRestart()
log.Error("Second instance: ", err)
}
return cli.NewExitError("Bridge is already running.", 3)
@ -249,18 +168,12 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen]
// In case user wants to do CPU or memory profiles...
if doCPUProfile := context.GlobalBool("cpu-prof"); doCPUProfile {
f, err := os.Create("cpu.pprof")
if err != nil {
log.Fatal("Could not create CPU profile: ", err)
}
if err := pprof.StartCPUProfile(f); err != nil {
log.Fatal("Could not start CPU profile: ", err)
}
cmd.StartCPUProfile()
defer pprof.StopCPUProfile()
}
if doMemoryProfile := context.GlobalBool("mem-prof"); doMemoryProfile {
defer makeMemoryProfile()
defer cmd.MakeMemoryProfile()
}
// Now we initialize all Bridge parts.
@ -268,14 +181,27 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen]
eventListener := listener.New()
events.SetupEvents(eventListener)
credentialsStore, credentialsError := credentials.NewStore()
credentialsStore, credentialsError := credentials.NewStore(appName)
if credentialsError != nil {
log.Error("Could not get credentials store: ", credentialsError)
}
pmapiClientFactory := pmapifactory.New(cfg, eventListener)
cm := pmapi.NewClientManager(cfg.GetAPIConfig())
bridgeInstance := bridge.New(cfg, pref, panicHandler, eventListener, Version, pmapiClientFactory, credentialsStore)
// Different build types have different roundtrippers (e.g. we want to enable
// TLS fingerprint checks in production builds). GetRoundTripper has a different
// implementation depending on whether build flag pmapi_prod is used or not.
cm.SetRoundTripper(cfg.GetRoundTripper(cm, eventListener))
// Cookies must be persisted across restarts.
jar, err := cookies.NewCookieJar(pref)
if err != nil {
logrus.WithError(err).Warn("Could not create cookie jar")
} else {
cm.SetCookieJar(jar)
}
bridgeInstance := bridge.New(cfg, pref, panicHandler, eventListener, cm, credentialsStore)
imapBackend := imap.NewIMAPBackend(panicHandler, eventListener, cfg, bridgeInstance)
smtpBackend := smtp.NewSMTPBackend(panicHandler, eventListener, pref, bridgeInstance)
@ -321,7 +247,7 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen]
}
showWindowOnStart := !context.GlobalBool("no-window")
frontend := frontend.New(Version, buildVersion, frontendMode, showWindowOnStart, panicHandler, cfg, pref, eventListener, updates, bridgeInstance, smtpBackend)
frontend := frontend.New(constants.Version, constants.BuildVersion, frontendMode, showWindowOnStart, panicHandler, cfg, pref, eventListener, updates, bridgeInstance, smtpBackend)
// Last part is to start everything.
log.Debug("Starting frontend...")
@ -331,7 +257,7 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen]
}
if frontend.IsAppRestarting() {
restartApp()
cmd.RestartApp()
}
return nil
@ -341,7 +267,7 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen]
// It will happen only when c10/prefs.json exists and c11/prefs.json not.
// No configuration changed between c10 and c11 versions.
func migratePreferencesFromC10(cfg *config.Config) {
pref10Path := config.New(AppShortName, Version, Revision, "c10").GetPreferencesPath()
pref10Path := config.New(appName, constants.Version, constants.Revision, "c10").GetPreferencesPath()
if _, err := os.Stat(pref10Path); os.IsNotExist(err) {
log.WithField("path", pref10Path).Trace("Old preferences does not exist, migration skipped")
return
@ -359,7 +285,7 @@ func migratePreferencesFromC10(cfg *config.Config) {
return
}
err = ioutil.WriteFile(pref11Path, data, 0644)
err = ioutil.WriteFile(pref11Path, data, 0600)
if err != nil {
log.WithError(err).Error("Problem to migrate preferences")
return
@ -367,68 +293,3 @@ func migratePreferencesFromC10(cfg *config.Config) {
log.Info("Preferences migrated")
}
// generateVersionFiles writes a JSON file with details about current build.
// Those files are used for upgrading the app.
func generateVersionFiles(updates *updates.Updates, dir string) {
log.Info("Generating version files")
for _, goos := range []string{"windows", "darwin", "linux"} {
log.Debug("Generating JSON for ", goos)
if err := updates.CreateJSONAndSign(dir, goos); err != nil {
log.Error(err)
}
}
}
func makeMemoryProfile() {
name := "./mem.pprof"
f, err := os.Create(name)
if err != nil {
log.Error("Could not create memory profile: ", err)
}
if abs, err := filepath.Abs(name); err == nil {
name = abs
}
log.Info("Writing memory profile to ", name)
runtime.GC() // get up-to-date statistics
if err := pprof.WriteHeapProfile(f); err != nil {
log.Error("Could not write memory profile: ", err)
}
_ = f.Close()
}
// filterRestartNumberFromArgs removes flag with a number how many restart we already did.
// See restartApp how that number is used.
func filterRestartNumberFromArgs() {
tmp := os.Args[:0]
for i, arg := range os.Args {
if !strings.HasPrefix(arg, "--restart_") {
tmp = append(tmp, arg)
continue
}
var err error
numberOfCrashes, err = strconv.Atoi(os.Args[i][10:])
if err != nil {
numberOfCrashes = maxAllowedCrashes
}
}
os.Args = tmp
}
// restartApp starts a new instance in background.
func restartApp() {
if numberOfCrashes >= maxAllowedCrashes {
log.Error("Too many crashes")
return
}
if exeFile, err := os.Executable(); err == nil {
arguments := append(os.Args[1:], fmt.Sprintf("--restart_%d", numberOfCrashes))
cmd := exec.Command(exeFile, arguments...) //nolint[gosec]
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
if err := cmd.Start(); err != nil {
log.Error("Restart failed: ", err)
}
}
}

177
cmd/Import-Export/main.go Normal file
View File

@ -0,0 +1,177 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package main
import (
"runtime/pprof"
"github.com/ProtonMail/proton-bridge/internal/cmd"
"github.com/ProtonMail/proton-bridge/internal/cookies"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/frontend"
"github.com/ProtonMail/proton-bridge/internal/importexport"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/internal/updates"
"github.com/ProtonMail/proton-bridge/internal/users/credentials"
"github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/pkg/constants"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/allan-simon/go-singleinstance"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
const (
// cacheVersion is used for cache files such as lock, or preferences.
// Different number will drop old files and create new ones.
cacheVersion = "c11"
appName = "importExport"
appNameDash = "import-export-app"
)
var (
log = logrus.WithField("pkg", "main") //nolint[gochecknoglobals]
)
func main() {
cmd.Main(
"ProtonMail Import-Export",
"ProtonMail Import-Export app",
nil,
run,
)
}
// run initializes and starts everything in a precise order.
//
// IMPORTANT: ***Read the comments before CHANGING the order ***
func run(context *cli.Context) (contextError error) { // nolint[funlen]
// We need to have config instance to setup a logs, panic handler, etc ...
cfg := config.New(appName, constants.Version, constants.Revision, cacheVersion)
// We want to know about any problem. Our PanicHandler calls sentry which is
// not dependent on anything else. If that fails, it tries to create crash
// report which will not be possible if no folder can be created. That's the
// only problem we will not be notified about in any way.
panicHandler := &cmd.PanicHandler{
AppName: "ProtonMail Import-Export app",
Config: cfg,
Err: &contextError,
}
defer panicHandler.HandlePanic()
// First we need config and create necessary folder; it's dependency for everything.
if err := cfg.CreateDirs(); err != nil {
log.Fatal("Cannot create necessary folders: ", err)
}
// Setup of logs should be as soon as possible to ensure we record every wanted report in the log.
logLevel := context.GlobalString("log-level")
_, _ = config.SetupLog(cfg, logLevel)
// Doesn't make sense to continue when Import-Export was invoked with wrong arguments.
// We should tell that to the user before we do anything else.
if context.Args().First() != "" {
_ = cli.ShowAppHelp(context)
return cli.NewExitError("Unknown argument", 4)
}
// It's safe to get version JSON file even when other instance is running.
// (thus we put it before check of presence of other Import-Export instance).
updates := updates.NewImportExport(cfg.GetUpdateDir())
if dir := context.GlobalString("version-json"); dir != "" {
cmd.GenerateVersionFiles(updates, dir)
return nil
}
// Now we can try to proceed with starting the Import-Export. First we need to ensure
// this is the only instance. If not, we will end and focus the existing one.
lock, err := singleinstance.CreateLockFile(cfg.GetLockPath())
if err != nil {
log.Warn("Import-Export app is already running")
return cli.NewExitError("Import-Export app is already running.", 3)
}
defer lock.Close() //nolint[errcheck]
// In case user wants to do CPU or memory profiles...
if doCPUProfile := context.GlobalBool("cpu-prof"); doCPUProfile {
cmd.StartCPUProfile()
defer pprof.StopCPUProfile()
}
if doMemoryProfile := context.GlobalBool("mem-prof"); doMemoryProfile {
defer cmd.MakeMemoryProfile()
}
// Now we initialize all Import-Export parts.
log.Debug("Initializing import-export...")
eventListener := listener.New()
events.SetupEvents(eventListener)
credentialsStore, credentialsError := credentials.NewStore(appNameDash)
if credentialsError != nil {
log.Error("Could not get credentials store: ", credentialsError)
}
cm := pmapi.NewClientManager(cfg.GetAPIConfig())
// Different build types have different roundtrippers (e.g. we want to enable
// TLS fingerprint checks in production builds). GetRoundTripper has a different
// implementation depending on whether build flag pmapi_prod is used or not.
cm.SetRoundTripper(cfg.GetRoundTripper(cm, eventListener))
pref := preferences.New(cfg)
// Cookies must be persisted across restarts.
jar, err := cookies.NewCookieJar(pref)
if err != nil {
logrus.WithError(err).Warn("Could not create cookie jar")
} else {
cm.SetCookieJar(jar)
}
importexportInstance := importexport.New(cfg, panicHandler, eventListener, cm, credentialsStore)
// Decide about frontend mode before initializing rest of import-export.
var frontendMode string
switch {
case context.GlobalBool("cli"):
frontendMode = "cli"
default:
frontendMode = "qt"
}
log.WithField("mode", frontendMode).Debug("Determined frontend mode to use")
frontend := frontend.NewImportExport(constants.Version, constants.BuildVersion, frontendMode, panicHandler, cfg, eventListener, updates, importexportInstance)
// Last part is to start everything.
log.Debug("Starting frontend...")
if err := frontend.Loop(credentialsError); err != nil {
log.Error("Frontend failed with error: ", err)
return cli.NewExitError("Frontend error", 2)
}
if frontend.IsAppRestarting() {
cmd.RestartApp()
}
return nil
}

135
doc/importexport.md Normal file
View File

@ -0,0 +1,135 @@
# Import-Export app
## Main blocks
This is basic overview of the main Import-Export blocks.
```mermaid
graph LR
S[ProtonMail server]
U[User]
subgraph "Import-Export app"
Users
Frontend["Qt / CLI"]
ImportExport
Transfer
Frontend --> ImportExport
Frontend --> Transfer
ImportExport --> Users
ImportExport --> Transfer
end
EML --> Transfer
MBOX --> Transfer
IMAP --> Transfer
S --> Transfer
Transfer --> EML
Transfer --> MBOX
Transfer --> S
U --> Frontend
```
## Code structure
More detailed graph of main types used in Import-Export app and connection between them.
```mermaid
graph TD
PM[ProtonMail Server]
EML[EML]
MBOX[MBOX]
IMAP[IMAP]
subgraph "Import-Export app"
subgraph "pkg users"
subgraph "pkg credentials"
CredStore[Store]
Creds[Credentials]
CredStore --> Creds
end
US[Users]
U[User]
US --> U
end
subgraph "pkg frontend"
CLI
Qt
end
subgraph "pkg importExport"
IE[ImportExport]
end
subgraph "pkg transfer"
Transfer
Rules
Progress
Provider
LocalProvider
EMLProvider
MBOXProvider
IMAPProvider
PMAPIProvider
Mailbox
Message
Transfer --> |source|Provider
Transfer --> |target|Provider
Transfer --> Rules
Transfer --> Progress
Provider --> LocalProvider
Provider --> EMLProvider
Provider --> MBOXProvider
Provider --> IMAPProvider
Provider --> PMAPIProvider
LocalProvider --> EMLProvider
LocalProvider --> MBOXProvider
Provider --> Mailbox
Provider --> Message
end
subgraph PMAPI
APIM[ClientManager]
APIC[Client]
APIM --> APIC
end
end
CLI --> IE
CLI --> Transfer
CLI --> Progress
Qt --> IE
Qt --> Transfer
Qt --> Progress
U --> CredStore
U --> Creds
US --> APIM
U --> APIM
PMAPIProvider --> APIM
EMLProvider --> EML
MBOXProvider --> MBOX
IMAPProvider --> IMAP
IE --> US
IE --> Transfer
APIC --> PM
```

View File

@ -2,8 +2,13 @@
Documentation pages in order to read for a novice:
* [Development cycle](development.md)
## Bridge
* [Bridge code](bridge.md)
* [Internal Bridge database](database.md)
* [Communication between Bridge, Client and Server](communication.md)
* [Encryption](encryption.md)
## Import-Export app
* [Import-Export code](importexport.md)

78
go.mod
View File

@ -5,74 +5,78 @@ go 1.13
// These dependencies are `replace`d below, so the version numbers should be ignored.
// They are in a separate require block to highlight this.
require (
github.com/docker/docker-credential-helpers v0.0.0-00010101000000-000000000000
github.com/emersion/go-imap v0.0.0-20171113213225-939ec3994dbe
github.com/emersion/go-imap-quota v0.0.0-20171113212021-e883a2bc54d6
github.com/docker/docker-credential-helpers v0.6.3
github.com/emersion/go-smtp v0.0.0-20180712174835-db5eec195e67
github.com/jameskeane/bcrypt v0.0.0-20170924085257-7509ea014998
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
)
require (
github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1
github.com/ProtonMail/go-appdir v1.0.0
github.com/Masterminds/semver/v3 v3.1.0
github.com/ProtonMail/go-appdir v1.1.0
github.com/ProtonMail/go-apple-mobileconfig v0.0.0-20160701194735-7ea9927a11f6
github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a
github.com/ProtonMail/go-imap-id v0.0.0-20171219160728-ed0baee567ee
github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5
github.com/ProtonMail/gopenpgp v1.0.1-0.20190912180537-d398098113ed
github.com/ProtonMail/gopenpgp/v2 v2.0.1
github.com/PuerkitoBio/goquery v1.5.1
github.com/abiosoft/ishell v2.0.0+incompatible
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect
github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc
github.com/andybalholm/cascadia v1.1.0
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 // indirect
github.com/chzyer/logex v1.1.10 // indirect
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
github.com/cucumber/godog v0.8.1
github.com/danieljoos/wincred v1.0.2 // indirect
github.com/emersion/go-imap-appendlimit v0.0.0-20160923165328-beeb382f2a42
github.com/emersion/go-imap-idle v0.0.0-20161227184850-e03ba1e0ed89
github.com/emersion/go-imap-specialuse v0.0.0-20161227184202-ba031ced6a62
github.com/emersion/go-imap-unselect v0.0.0-20161227183655-1e6dc73ac8fe
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b
github.com/emersion/go-imap v1.0.6-0.20200708083111-011063d6c9df
github.com/emersion/go-imap-appendlimit v0.0.0-20190308131241-25671c986a6a
github.com/emersion/go-imap-idle v0.0.0-20200601154248-f05f54664cc4
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342
github.com/emersion/go-imap-quota v0.0.0-20200423100218-dcfd1b7d2b41
github.com/emersion/go-imap-specialuse v0.0.0-20200722111535-598ff00e4075
github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26
github.com/emersion/go-mbox v1.0.0
github.com/emersion/go-message v0.12.1-0.20200903165315-e1abe21f389a
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe
github.com/emersion/go-vcard v0.0.0-20190105225839-8856043f13c5 // indirect
github.com/fatih/color v1.9.0
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
github.com/getsentry/raven-go v0.2.0
github.com/go-resty/resty/v2 v2.2.0
github.com/golang/mock v1.4.3
github.com/google/go-cmp v0.4.0
github.com/go-resty/resty/v2 v2.3.0
github.com/golang/mock v1.4.4
github.com/google/go-cmp v0.5.1
github.com/google/uuid v1.1.1
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c // indirect
github.com/hashicorp/go-multierror v1.0.0
github.com/jaytaylor/html2text v0.0.0-20200220170450-61d9dc4d7195
github.com/jhillyerd/enmime v0.8.0
github.com/hashicorp/go-multierror v1.1.0
github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
github.com/keybase/go-keychain v0.0.0-20200218013740-86d4642e4ce2
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381
github.com/miekg/dns v1.1.29
github.com/keybase/go-keychain v0.0.0-20200502122510-cda31fe0c86d
github.com/logrusorgru/aurora v2.0.3+incompatible
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/miekg/dns v1.1.30
github.com/myesui/uuid v1.0.0 // indirect
github.com/nsf/jsondiff v0.0.0-20190712045011-8443391ee9b6
github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce
github.com/olekukonko/tablewriter v0.0.4 // indirect
github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.4.2
github.com/sirupsen/logrus v1.6.0
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
github.com/stretchr/testify v1.5.1
github.com/therecipe/qt v0.0.0-20200126204426-5074eb6d8c41
github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200126204426-5074eb6d8c41 // indirect
github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200126204426-5074eb6d8c41 // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/stretchr/testify v1.6.1
github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e
github.com/twinj/uuid v1.0.0 // indirect
github.com/urfave/cli v1.22.3
go.etcd.io/bbolt v1.3.3
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550
golang.org/x/net v0.0.0-20200301022130-244492dfa37a
golang.org/x/text v0.3.2
github.com/urfave/cli v1.22.4
go.etcd.io/bbolt v1.3.5
golang.org/x/net v0.0.0-20200707034311-ab3426394381
golang.org/x/text v0.3.3
gopkg.in/stretchr/testify.v1 v1.2.2 // indirect
)
replace (
github.com/docker/docker-credential-helpers => github.com/ProtonMail/docker-credential-helpers v1.0.0
github.com/emersion/go-imap => github.com/ProtonMail/go-imap v0.0.0-20190327080220-0e686f0e855f
github.com/emersion/go-imap-quota => github.com/ProtonMail/go-imap-quota v0.0.0-20171219161528-20f0ba8904de
github.com/docker/docker-credential-helpers => github.com/ProtonMail/docker-credential-helpers v1.1.0
github.com/emersion/go-imap => github.com/ProtonMail/go-imap v0.0.0-20200828124548-d04b0dc1f399
github.com/emersion/go-mbox => github.com/ProtonMail/mbox v0.0.0-20200918064939-909a18c9af45
github.com/emersion/go-smtp => github.com/ProtonMail/go-smtp v0.0.0-20181206232543-8261df20d309
github.com/jameskeane/bcrypt => github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998
golang.org/x/crypto => github.com/ProtonMail/crypto v0.0.0-20190604143603-d3d8a14a4d4f
golang.org/x/crypto => github.com/ProtonMail/crypto v0.0.0-20200416114516-1fa7f403fb9c
)

207
go.sum
View File

@ -1,32 +1,37 @@
github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1 h1:j9HaafapDbPbGRDku6e/HRs6KBMcKHiWcm1/9Sbxnl4=
github.com/0xAX/notificator v0.0.0-20191016112426-3962a5ea8da1/go.mod h1:NtXa9WwQsukMHZpjNakTTz0LArxvGYdPA9CjIcUSZ6s=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver/v3 v3.1.0 h1:Y2lUDsFKVRSYGojLJ1yLxSXdMmMYTYls0rCvoqmMUQk=
github.com/Masterminds/semver/v3 v3.1.0/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998 h1:YT2uVwQiRQZxCaaahwfcgTq2j3j66w00n/27gb/zubs=
github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
github.com/ProtonMail/crypto v0.0.0-20190604143603-d3d8a14a4d4f h1:cFhATQTJGK2iZ0dc+jRhr75mh6bsc5Ug6NliaBya8Kw=
github.com/ProtonMail/crypto v0.0.0-20190604143603-d3d8a14a4d4f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
github.com/ProtonMail/docker-credential-helpers v1.0.0 h1:0DQXbZNvUszWgXUuP7TzvQdwnkK1D5Zf/glBgCFJFCk=
github.com/ProtonMail/docker-credential-helpers v1.0.0/go.mod h1:R1gQindzdYFcWJuuGXteYHDJzUCVtyU+EpEqp9aWcFs=
github.com/ProtonMail/go-appdir v1.0.0 h1:PZXQ0HkveuEugga3LeDycxWtybrXQfKR0ThxURd6ojw=
github.com/ProtonMail/go-appdir v1.0.0/go.mod h1:3d8Y9F5mbEUjrYbcJ3rcDxcWbqbttF+011nVZmdRdzc=
github.com/ProtonMail/crypto v0.0.0-20200416114516-1fa7f403fb9c h1:DAvlgde2Stu18slmjwikiMPs/CKPV35wSvmJS34z0FU=
github.com/ProtonMail/crypto v0.0.0-20200416114516-1fa7f403fb9c/go.mod h1:Pxr7w4gA2ikI4sWyYwEffm+oew1WAJHzG1SiDpQMkrI=
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/go-appdir v1.1.0 h1:9hdNDlU9kTqRKVNzmoqah8qqrj5QZyLByQdwQNlFWig=
github.com/ProtonMail/go-appdir v1.1.0/go.mod h1:3d8Y9F5mbEUjrYbcJ3rcDxcWbqbttF+011nVZmdRdzc=
github.com/ProtonMail/go-apple-mobileconfig v0.0.0-20160701194735-7ea9927a11f6 h1:YsSJ/mvZFYydQm/hRrt8R8UtgETixN2y3LK98f5LT60=
github.com/ProtonMail/go-apple-mobileconfig v0.0.0-20160701194735-7ea9927a11f6/go.mod h1:EtDfBMIDWmVe4viZCuBTEfe3OIIo0ghbpOaAZVO+hVg=
github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a h1:fXK2KsfnkBV9Nh+9SKzHchYjuE9s0vI20JG1mbtEAcc=
github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
github.com/ProtonMail/go-imap v0.0.0-20190327080220-0e686f0e855f h1:QkLm4yfhBQuBxrC46Vhy2sonOWVrwIJo5bgKpA82+TY=
github.com/ProtonMail/go-imap v0.0.0-20190327080220-0e686f0e855f/go.mod h1:+m2uLXghuYktgE/vc5AkmCxx1qhu33ZKHFWg1cGZPD0=
github.com/ProtonMail/go-imap-id v0.0.0-20171219160728-ed0baee567ee h1:Q/nK7A9xzUimAZsQDa/yaw3xW9PkTTnJnkT5wAkXrmI=
github.com/ProtonMail/go-imap-id v0.0.0-20171219160728-ed0baee567ee/go.mod h1:795VPXcRUIQ9JyMNHP4el582VokQfippgjkQP3Gk0r0=
github.com/ProtonMail/go-imap-quota v0.0.0-20171219161528-20f0ba8904de h1:+LA9teDYUwGkBvg0kqZPZetmxIv1r7s9/npBP1yzKs0=
github.com/ProtonMail/go-imap-quota v0.0.0-20171219161528-20f0ba8904de/go.mod h1:85zbnYVWIY7//iScX9fnB/kKOGH9B86YPqtpr7f1i7A=
github.com/ProtonMail/go-mime v0.0.0-20190521135552-09454e3dbe72 h1:hGCc4Oc2fD3I5mNnZ1VlREncVc9EXJF8dxW3sw16gWM=
github.com/ProtonMail/go-mime v0.0.0-20190521135552-09454e3dbe72/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4=
github.com/ProtonMail/go-imap v0.0.0-20200828124548-d04b0dc1f399 h1:wBo/Xgb/Dn2loU47D+PJaOoIZ67i3AqYp51gLn8YE5U=
github.com/ProtonMail/go-imap v0.0.0-20200828124548-d04b0dc1f399/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU=
github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde h1:5koQozTDELymYOyFbQ/VSubexAEXzDR8qGM5mO8GRdw=
github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde/go.mod h1:795VPXcRUIQ9JyMNHP4el582VokQfippgjkQP3Gk0r0=
github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a h1:W6RrgN/sTxg1msqzFFb+G80MFmpjMw61IU+slm+wln4=
github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4=
github.com/ProtonMail/go-smtp v0.0.0-20181206232543-8261df20d309 h1:2pzfKjhBjSnw3BgmfTYRFQr1rFGxhfhUY0KKkg+RYxE=
github.com/ProtonMail/go-smtp v0.0.0-20181206232543-8261df20d309/go.mod h1:6UoBvDAMA/cTBwS3Y7tGpKnY5RH1F1uYHschT6eqAkI=
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5 h1:Uga1DHFN4GUxuDQr0F71tpi8I9HqPIlZodZAI1lR6VQ=
github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5/go.mod h1:oeP9CMN+ajWp5jKp1kue5daJNwMMxLF+ujPaUIoJWlA=
github.com/ProtonMail/gopenpgp v1.0.1-0.20190912180537-d398098113ed h1:3gib6hGF61VfRu7cqqkODyRUgES5uF/fkLQanPPJiO8=
github.com/ProtonMail/gopenpgp v1.0.1-0.20190912180537-d398098113ed/go.mod h1:NstNbZx1OIoyq+2qHAFLwDFpHbMk8L2i2Vr+LioJ3/g=
github.com/ProtonMail/gopenpgp/v2 v2.0.1 h1:x0uvDhry5WzoHeJO4J3dgMLhG4Z9PeBJ2O+sDOY0LcU=
github.com/ProtonMail/gopenpgp/v2 v2.0.1/go.mod h1:wQQCJo7DURO6S9VwH+kSDEYs/B63yZnAEfGlOg8YNBY=
github.com/ProtonMail/mbox v0.0.0-20200918064939-909a18c9af45 h1:GDh55hDI2sNiirDqEWV8b6EB729u78Qxu3nKF970n6g=
github.com/ProtonMail/mbox v0.0.0-20200918064939-909a18c9af45/go.mod h1:Yp9IVuuOYLEuMv4yjgDHvhb5mHOcYH6x92Oas3QqEZI=
github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE=
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw=
github.com/abiosoft/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzgHf/aS6+zVXRj14cVk9qg=
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8=
@ -35,9 +40,6 @@ github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc h1:m
github.com/allan-simon/go-singleinstance v0.0.0-20160830203053-79edcfdc2dfc/go.mod h1:qqsTQiwdyqxU05iDCsi0oN3P4nrVxAmn8xCtODDSf/U=
github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/aslakhellesoy/gox v1.0.100/go.mod h1:AJl542QsKKG96COVsv0N74HHzVQgDIQPceVUh1aeU2M=
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI=
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8=
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 h1:JLaf/iINcLyjwbtTsCJjc6rtlASgHeIJPrB6QmwURnA=
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
@ -47,34 +49,33 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cucumber/gherkin-go/v11 v11.0.0 h1:cwVwN1Qn2VRSfHZNLEh5x00tPBmZcjATBWDpxsR5Xug=
github.com/cucumber/gherkin-go/v11 v11.0.0/go.mod h1:CX33k2XU2qog4e+TFjOValoq6mIUq0DmVccZs238R9w=
github.com/cucumber/godog v0.8.1 h1:lVb+X41I4YDreE+ibZ50bdXmySxgRviYFgKY6Aw4XE8=
github.com/cucumber/godog v0.8.1/go.mod h1:vSh3r/lM+psC1BPXvdkSEuNjmXfpVqrMGYAElF6hxnA=
github.com/cucumber/godog v0.9.0 h1:QOb8wyC7f+FVFXzY3RdgowwJUb4WeJfqbnQqaH4jp+A=
github.com/cucumber/godog v0.9.0/go.mod h1:roWCHkpeK6UTOyIRRl7IR+fgfBeZ4vZR7OSq2J/NbM4=
github.com/cucumber/messages-go/v10 v10.0.1/go.mod h1:kA5T38CBlBbYLU12TIrJ4fk4wSkVVOgyh7Enyy8WnSg=
github.com/cucumber/messages-go/v10 v10.0.3 h1:m/9SD/K/A15WP7i1aemIv7cwvUw+viS51Ui5HBw1cdE=
github.com/cucumber/messages-go/v10 v10.0.3/go.mod h1:9jMZ2Y8ZxjLY6TG2+x344nt5rXstVVDYSdS5ySfI1WY=
github.com/danieljoos/wincred v1.0.2 h1:zf4bhty2iLuwgjgpraD2E9UbvO+fe54XXGJbOwe23fU=
github.com/danieljoos/wincred v1.0.2/go.mod h1:SnuYRW9lp1oJrZX/dXJqr0cPK5gYXqx3EJbmjhLdK9U=
github.com/danieljoos/wincred v1.1.0 h1:3RNcEpBg4IhIChZdFRSdlQt1QjCp1sMAPIrOnm7Yf8g=
github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emersion/go-imap-appendlimit v0.0.0-20160923165328-beeb382f2a42 h1:3TeZ5gy3We/LVL0sqmGhM8dFDTSM7Hyj7PMIdl6OTs4=
github.com/emersion/go-imap-appendlimit v0.0.0-20160923165328-beeb382f2a42/go.mod h1:ikgISoP7pRAolqsVP64yMteJa2FIpS6ju88eBT6K1yQ=
github.com/emersion/go-imap-idle v0.0.0-20161227184850-e03ba1e0ed89 h1:AzbVhcrxgJO5MfSvzG5q4IfrYVm0Jw4AHNPz47+DiR0=
github.com/emersion/go-imap-idle v0.0.0-20161227184850-e03ba1e0ed89/go.mod h1:o14zPKCmEH5WC1vU5SdPoZGgNvQx7zzKSnxPQlobo78=
github.com/emersion/go-imap-specialuse v0.0.0-20161227184202-ba031ced6a62 h1:4ZAfwfc8aDlj26kkEap1UDSwwDnJp9Ie8Uj1MSXAkPk=
github.com/emersion/go-imap-specialuse v0.0.0-20161227184202-ba031ced6a62/go.mod h1:/nybxhI8kXom8Tw6BrHMl42usALvka6meORflnnYwe4=
github.com/emersion/go-imap-unselect v0.0.0-20161227183655-1e6dc73ac8fe h1:2R2XpJkmbyy7PcSjnCPOnNfu+GuRzgWR9U2+j/d1O+0=
github.com/emersion/go-imap-unselect v0.0.0-20161227183655-1e6dc73ac8fe/go.mod h1:+gnnZx3Mg3MnCzZrv0eZdp5puxXQUgGT/6N6L7ShKfM=
github.com/emersion/go-imap-unselect v0.0.0-20161227193600-1e6dc73ac8fe h1:WeXweyFnbM2DQx0wxHkJKXYXwXpApopIeAjDTipW5Z4=
github.com/emersion/go-imap-unselect v0.0.0-20161227193600-1e6dc73ac8fe/go.mod h1:+gnnZx3Mg3MnCzZrv0eZdp5puxXQUgGT/6N6L7ShKfM=
github.com/emersion/go-imap-appendlimit v0.0.0-20190308131241-25671c986a6a h1:bMdSPm6sssuOFpIaveu3XGAijMS3Tq2S3EqFZmZxidc=
github.com/emersion/go-imap-appendlimit v0.0.0-20190308131241-25671c986a6a/go.mod h1:ikgISoP7pRAolqsVP64yMteJa2FIpS6ju88eBT6K1yQ=
github.com/emersion/go-imap-idle v0.0.0-20200601154248-f05f54664cc4 h1:/JIALzmCduf5o8TWJSiOBzTb9+R0SChwElUrJLlp2po=
github.com/emersion/go-imap-idle v0.0.0-20200601154248-f05f54664cc4/go.mod h1:o14zPKCmEH5WC1vU5SdPoZGgNvQx7zzKSnxPQlobo78=
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342 h1:5p1t3e1PomYgLWwEwhwEU5kVBwcyAcVrOpexv8AeZx0=
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342/go.mod h1:QuMaZcKFDVI0yCrnAbPLfbwllz1wtOrZH8/vZ5yzp4w=
github.com/emersion/go-imap-quota v0.0.0-20200423100218-dcfd1b7d2b41 h1:z5lDGnSURauBEDdNLj3o0+HogVYKQCGeY3Anl/xyRfU=
github.com/emersion/go-imap-quota v0.0.0-20200423100218-dcfd1b7d2b41/go.mod h1:iApyhIQBiU4XFyr+3kdJyyGqle82TbQyuP2o+OZHrV0=
github.com/emersion/go-imap-specialuse v0.0.0-20200722111535-598ff00e4075 h1:z8TiDE4yqtzNeA1yb6ZRcktd+BHlXQbKGugvmDuc488=
github.com/emersion/go-imap-specialuse v0.0.0-20200722111535-598ff00e4075/go.mod h1:/nybxhI8kXom8Tw6BrHMl42usALvka6meORflnnYwe4=
github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26 h1:FiSb8+XBQQSkcX3ubr+1tAtlRJBYaFmRZqOAweZ9Wy8=
github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26/go.mod h1:+gnnZx3Mg3MnCzZrv0eZdp5puxXQUgGT/6N6L7ShKfM=
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b h1:uhWtEWBHgop1rqEk2klKaxPAkVDCXexai6hSuRQ7Nvs=
github.com/emersion/go-mbox v1.0.0 h1:HN6aKbyqmgIfK9fS/gen+NRr2wXLSxZXWfdAIAnzQPc=
github.com/emersion/go-mbox v1.0.0/go.mod h1:Yp9IVuuOYLEuMv4yjgDHvhb5mHOcYH6x92Oas3QqEZI=
github.com/emersion/go-message v0.11.1/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY=
github.com/emersion/go-message v0.12.1-0.20200903165315-e1abe21f389a h1:3C6qIGgPr1qAT0ikRD5NbyKpME/iHCDeXhpv/JJsFsE=
github.com/emersion/go-message v0.12.1-0.20200903165315-e1abe21f389a/go.mod h1:kYIioST9GDHte9/BRWgi93rpqbDuFftMjKSMaXS8ABo=
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe h1:40SWqY0zE3qCi6ZrtTf5OUdNm5lDnGnjRSq9GgmeTrg=
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/emersion/go-vcard v0.0.0-20190105225839-8856043f13c5 h1:n9qx98xiS5V4x2WIpPC2rr9mUM5ri9r/YhCEKbhCHro=
@ -85,67 +86,60 @@ github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BMXYYRWT
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:rZfgFAXFS/z/lEd6LJmf9HVZ1LkgYiHx5pHhV5DR16M=
github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs=
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
github.com/go-resty/resty/v2 v2.2.0 h1:vgZ1cdblp8Aw4jZj3ZsKh6yKAlMg3CHMrqFSFFd+jgY=
github.com/go-resty/resty/v2 v2.2.0/go.mod h1:nYW/8rxqQCmI3bPz9Fsmjbr2FBjGuR2Mzt6kDh3zZ7w=
github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw=
github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561 h1:aBzukfDxQlCTVS0NBUjI5YA3iVeaZ9Tb5PxNrrIP1xs=
github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/go-resty/resty/v2 v2.3.0 h1:JOOeAvjSlapTT92p8xiS19Zxev1neGikoHsXJeOq8So=
github.com/go-resty/resty/v2 v2.3.0/go.mod h1:UpN9CgLZNsv4e9XG50UU8xdI0F43UQ4HmxLBDwaroHU=
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20190411002643-bd77b112433e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4=
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-version v1.0.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/jaytaylor/html2text v0.0.0-20190408195923-01ec452cbe43 h1:jTkyeF7NZ5oIr0ESmcrpiDgAfoidCBF4F5kJhjtaRwE=
github.com/jaytaylor/html2text v0.0.0-20190408195923-01ec452cbe43/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
github.com/jaytaylor/html2text v0.0.0-20200220170450-61d9dc4d7195 h1:j0UEFmS7wSjAwKEIkgKBn8PRDfjcuggzr93R9wk53nQ=
github.com/jaytaylor/html2text v0.0.0-20200220170450-61d9dc4d7195/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
github.com/jhillyerd/enmime v0.8.0 h1:PHc/2LXtnDmCDm0V4+5NlBx+MoubmufhuNXwpKSV2o8=
github.com/jhillyerd/enmime v0.8.0/go.mod h1:MBHs3ugk03NGjMM6PuRynlKf+HA5eSillZ+TRCm73AE=
github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI=
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
github.com/jameshoulahan/go-imap v0.0.0-20200728140727-d57327f48843 h1:suxlO4AC4E4bjueAsL0m+qp8kmkxRWMGj+5bBU/KJ8g=
github.com/jameshoulahan/go-imap v0.0.0-20200728140727-d57327f48843/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU=
github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7 h1:g0fAGBisHaEQ0TRq1iBvemFRf+8AEWEmBESSiWB3Vsc=
github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/keybase/go-keychain v0.0.0-20200218013740-86d4642e4ce2 h1:1XZArHAPddaXKbg51etNbCjkNUkKgSa0s8dSz2LYB2g=
github.com/keybase/go-keychain v0.0.0-20200218013740-86d4642e4ce2/go.mod h1:JJNrCn9otv/2QP4D7SMJBgaleKpOf66PnW6F5WGNRIc=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/keybase/go-keychain v0.0.0-20200502122510-cda31fe0c86d h1:gVjhBCfVGl32RIBooOANzfw+0UqX8HU+yPlMv8vypcg=
github.com/keybase/go-keychain v0.0.0-20200502122510-cda31fe0c86d/go.mod h1:W6EbaYmb4RldPn0N3gvVHjY1wmU59kbymhW9NATWhwY=
github.com/keybase/go.dbus v0.0.0-20200324223359-a94be52c0b03/go.mod h1:a8clEhrrGV/d76/f9r2I41BwANMihfZYV9C223vaxqE=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
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/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 h1:bqDmpDG49ZRnB5PcgP0RXtQvnMSgIF14M7CBd2shtXs=
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/martinlindhe/base36 v1.0.0 h1:eYsumTah144C0A8P1T/AVSUk5ZoLnhfYFM3OGQxB52A=
github.com/martinlindhe/base36 v1.0.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/miekg/dns v1.1.29 h1:xHBEhR+t5RzcFJjBLJlax2daXOrTYtr9z4WdKEfWFzg=
github.com/miekg/dns v1.1.29/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/miekg/dns v1.1.30 h1:Qww6FseFn8PRfw07jueqIXqodm0JKiiKuK0DeXSqfyo=
github.com/miekg/dns v1.1.30/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/myesui/uuid v1.0.0 h1:xCBmH4l5KuvLYc5L7AS7SZg9/jKdIFubM7OVoLqaQUI=
github.com/myesui/uuid v1.0.0/go.mod h1:2CDfNgU0LR8mIdO8vdWd8i9gWWxLlcoIGGpSNgafq84=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nsf/jsondiff v0.0.0-20190712045011-8443391ee9b6 h1:qsqscDgSJy+HqgMTR+3NwjYJBbp1+honwDsszLoS+pA=
github.com/nsf/jsondiff v0.0.0-20190712045011-8443391ee9b6/go.mod h1:uFMI8w+ref4v2r9jz+c9i1IfIttS/OkmLfrk1jne5hs=
github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88=
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce h1:RPclfga2SEJmgMmz2k+Mg7cowZ8yv4Trqw9UsJby758=
github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce/go.mod h1:uFMI8w+ref4v2r9jz+c9i1IfIttS/OkmLfrk1jne5hs=
github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8=
github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -153,13 +147,12 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca h1:NugYot0LIVPxTvN8n+Kvkn6TrbMyxQiuvKdEwFdR9vI=
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
@ -171,31 +164,28 @@ github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoH
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/therecipe/qt v0.0.0-20200126204426-5074eb6d8c41 h1:yBVcrpbaQYJBdKT2pxTdlL4hBE/eM4UPcyj9YpyvSok=
github.com/therecipe/qt v0.0.0-20200126204426-5074eb6d8c41/go.mod h1:SUUR2j3aE1z6/g76SdD6NwACEpvCxb3fvG82eKbD6us=
github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200126204426-5074eb6d8c41/go.mod h1:7m8PDYDEtEVqfjoUQc2UrFqhG0CDmoVJjRlQxexndFc=
github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200126204426-5074eb6d8c41/go.mod h1:mH55Ek7AZcdns5KPp99O0bg+78el64YCYWHiQKrOdt4=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e h1:G0DQ/TRQyrEZjtLlLwevFjaRiG8eeCMlq9WXQ2OO2bk=
github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e/go.mod h1:SUUR2j3aE1z6/g76SdD6NwACEpvCxb3fvG82eKbD6us=
github.com/twinj/uuid v1.0.0 h1:fzz7COZnDrXGTAOHGuUGYd6sG+JMq+AoE7+Jlu0przk=
github.com/twinj/uuid v1.0.0/go.mod h1:mMgcE1RHFUFqe5AfiwlINXisXfDGro23fWdPUfOMjRY=
github.com/urfave/cli v1.22.3 h1:FpNT6zq26xNpHZy08emi755QwzLPs6Pukqjlc7RfOMU=
github.com/urfave/cli v1.22.3/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
github.com/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA=
github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190420063019-afa5a82059c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0 h1:MsuvTghUPjX762sGLnGsxC3HM0B5r83wEtYcYR8/vRs=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -203,34 +193,29 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe h1:6fAMxZRR6sl1Uq8U61gxU+kPTs2tR8uOySCbBP7BN/M=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425 h1:VvQyQJN0tSuecqgcIxMWnnfG5kSmgy9KZR9sW3W5QeA=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/stretchr/testify.v1 v1.2.2 h1:yhQC6Uy5CqibAIlk1wlusa/MJ3iAN49/BsR/dCCKz3M=
gopkg.in/stretchr/testify.v1 v1.2.2/go.mod h1:QI5V/q6UbPmuhtm10CaFZxED9NreB8PnFYN9JcR6TxU=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -32,10 +32,11 @@ import (
"github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/ports"
"github.com/sirupsen/logrus"
)
var (
log = config.GetLogEntry("api") //nolint[gochecknoglobals]
log = logrus.WithField("pkg", "api") //nolint[gochecknoglobals]
)
type apiServer struct {

View File

@ -15,55 +15,31 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Package bridge provides core business logic providing API over credentials store and PM API.
// Package bridge provides core functionality of Bridge app.
package bridge
import (
"errors"
"strconv"
"strings"
"sync"
"time"
"github.com/ProtonMail/proton-bridge/internal/events"
m "github.com/ProtonMail/proton-bridge/internal/metrics"
"github.com/ProtonMail/proton-bridge/internal/metrics"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/internal/store"
"github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/internal/users"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/hashicorp/go-multierror"
"github.com/ProtonMail/proton-bridge/pkg/listener"
logrus "github.com/sirupsen/logrus"
)
var (
log = config.GetLogEntry("bridge") //nolint[gochecknoglobals]
isApplicationOutdated = false //nolint[gochecknoglobals]
log = logrus.WithField("pkg", "bridge") //nolint[gochecknoglobals]
)
// Bridge is a struct handling users.
type Bridge struct {
config Configer
pref PreferenceProvider
panicHandler PanicHandler
events listener.Listener
version string
pmapiClientFactory PMAPIProviderFactory
credStorer CredentialsStorer
storeCache *store.Cache
*users.Users
// users is a list of accounts that have been added to bridge.
// They are stored sorted in the credentials store in the order
// that they were added to bridge chronologically.
// People are used to that and so we preserve that ordering here.
users []*User
// idleUpdates is a channel which the imap backend listens to and which it uses
// to send idle updates to the mail client (eg thunderbird).
// The user stores should send idle updates on this channel.
idleUpdates chan interface{}
lock sync.RWMutex
pref PreferenceProvider
clientManager users.ClientManager
userAgentClientName string
userAgentClientVersion string
@ -73,46 +49,29 @@ type Bridge struct {
func New(
config Configer,
pref PreferenceProvider,
panicHandler PanicHandler,
panicHandler users.PanicHandler,
eventListener listener.Listener,
version string,
pmapiClientFactory PMAPIProviderFactory,
credStorer CredentialsStorer,
clientManager users.ClientManager,
credStorer users.CredentialsStorer,
) *Bridge {
log.Trace("Creating new bridge")
b := &Bridge{
config: config,
pref: pref,
panicHandler: panicHandler,
events: eventListener,
version: version,
pmapiClientFactory: pmapiClientFactory,
credStorer: credStorer,
storeCache: store.NewCache(config.GetIMAPCachePath()),
idleUpdates: make(chan interface{}),
lock: sync.RWMutex{},
}
// Allow DoH before starting bridge if the user has previously set this setting.
// Allow DoH before starting the app if the user has previously set this setting.
// This allows us to start even if protonmail is blocked.
if pref.GetBool(preferences.AllowProxyKey) {
AllowDoH()
clientManager.AllowProxy()
}
go func() {
defer panicHandler.HandlePanic()
b.watchBridgeOutdated()
}()
storeFactory := newStoreFactory(config, panicHandler, clientManager, eventListener)
u := users.New(config, panicHandler, eventListener, clientManager, credStorer, storeFactory, true)
b := &Bridge{
Users: u,
if b.credStorer == nil {
log.Error("Bridge has no credentials store")
} else if err := b.loadUsersFromCredentialsStore(); err != nil {
log.WithError(err).Error("Could not load all users from credentials store")
pref: pref,
clientManager: clientManager,
}
if pref.GetBool(preferences.FirstStartKey) {
b.SendMetric(m.New(m.Setup, m.FirstStart, m.Label(version)))
b.SendMetric(metrics.New(metrics.Setup, metrics.FirstStart, metrics.Label(config.GetVersion())))
pref.SetBool(preferences.FirstStartKey, false)
}
go b.heartbeat()
@ -122,325 +81,22 @@ func New(
// heartbeat sends a heartbeat signal once a day.
func (b *Bridge) heartbeat() {
for range time.NewTicker(1 * time.Hour).C {
ticker := time.NewTicker(1 * time.Minute)
for range ticker.C {
next, err := strconv.ParseInt(b.pref.Get(preferences.NextHeartbeatKey), 10, 64)
if err != nil {
continue
}
nextTime := time.Unix(next, 0)
if time.Now().After(nextTime) {
b.SendMetric(m.New(m.Heartbeat, m.Daily, m.NoLabel))
b.SendMetric(metrics.New(metrics.Heartbeat, metrics.Daily, metrics.NoLabel))
nextTime = nextTime.Add(24 * time.Hour)
b.pref.Set(preferences.NextHeartbeatKey, strconv.FormatInt(nextTime.Unix(), 10))
}
}
}
func (b *Bridge) loadUsersFromCredentialsStore() (err error) {
b.lock.Lock()
defer b.lock.Unlock()
userIDs, err := b.credStorer.List()
if err != nil {
return
}
for _, userID := range userIDs {
l := log.WithField("user", userID)
apiClient := b.pmapiClientFactory(userID)
user, newUserErr := newUser(b.panicHandler, userID, b.events, b.credStorer, apiClient, b.storeCache, b.config.GetDBDir())
if newUserErr != nil {
l.WithField("user", userID).WithError(newUserErr).Warn("Could not load user, skipping")
continue
}
b.users = append(b.users, user)
if initUserErr := user.init(b.idleUpdates, apiClient); initUserErr != nil {
l.WithField("user", userID).WithError(initUserErr).Warn("Could not initialise user")
}
}
return err
}
func (b *Bridge) watchBridgeOutdated() {
ch := make(chan string)
b.events.Add(events.UpgradeApplicationEvent, ch)
for range ch {
isApplicationOutdated = true
b.closeAllConnections()
}
}
func (b *Bridge) closeAllConnections() {
for _, user := range b.users {
user.closeAllConnections()
}
}
// Login authenticates a user.
// The login flow:
// * Authenticate user:
// client, auth, err := bridge.Authenticate(username, password)
//
// * In case user `auth.HasTwoFactor()`, ask for it and fully authenticate the user.
// auth2FA, err := client.Auth2FA(twoFactorCode)
//
// * In case user `auth.HasMailboxPassword()`, ask for it, otherwise use `password`
// and then finish the login procedure.
// user, err := bridge.FinishLogin(client, auth, mailboxPassword)
func (b *Bridge) Login(username, password string) (loginClient PMAPIProvider, auth *pmapi.Auth, err error) {
log.WithField("username", username).Trace("Logging in to bridge")
b.crashBandicoot(username)
// We need to use "login" client because we need userID to properly
// assign access tokens into token manager.
loginClient = b.pmapiClientFactory("login")
authInfo, err := loginClient.AuthInfo(username)
if err != nil {
log.WithField("username", username).WithError(err).Error("Could not get auth info for user")
return nil, nil, err
}
if auth, err = loginClient.Auth(username, password, authInfo); err != nil {
log.WithField("username", username).WithError(err).Error("Could not get auth for user")
return loginClient, auth, err
}
return loginClient, auth, nil
}
// FinishLogin finishes the login procedure and adds the user into the credentials store.
// See `Login` for more details of the login flow.
func (b *Bridge) FinishLogin(loginClient PMAPIProvider, auth *pmapi.Auth, mbPassword string) (user *User, err error) { //nolint[funlen]
log.Trace("Finishing bridge login")
defer func() {
if err == pmapi.ErrUpgradeApplication {
b.events.Emit(events.UpgradeApplicationEvent, "")
}
}()
b.lock.Lock()
defer b.lock.Unlock()
mbPassword, err = pmapi.HashMailboxPassword(mbPassword, auth.KeySalt)
if err != nil {
log.WithError(err).Error("Could not hash mailbox password")
if logoutErr := loginClient.Logout(); logoutErr != nil {
log.WithError(logoutErr).Error("Clean login session after hash password failed.")
}
return
}
if _, err = loginClient.Unlock(mbPassword); err != nil {
log.WithError(err).Error("Could not decrypt keyring")
if logoutErr := loginClient.Logout(); logoutErr != nil {
log.WithError(logoutErr).Error("Clean login session after unlock failed.")
}
return
}
apiUser, err := loginClient.CurrentUser()
if err != nil {
log.WithError(err).Error("Could not get login API user")
if logoutErr := loginClient.Logout(); logoutErr != nil {
log.WithError(logoutErr).Error("Clean login session after get current user failed.")
}
return
}
user, hasUser := b.hasUser(apiUser.ID)
// If the user exists and is logged in, we don't want to do anything.
if hasUser && user.IsConnected() {
err = errors.New("user is already logged in")
log.WithError(err).Warn("User is already logged in")
if logoutErr := loginClient.Logout(); logoutErr != nil {
log.WithError(logoutErr).Warn("Could not discard auth generated during second login")
}
return
}
apiToken := auth.UID() + ":" + auth.RefreshToken
apiClient := b.pmapiClientFactory(apiUser.ID)
auth, err = apiClient.AuthRefresh(apiToken)
if err != nil {
log.WithError(err).Error("Could refresh token in new client")
if logoutErr := loginClient.Logout(); logoutErr != nil {
log.WithError(logoutErr).Warn("Could not discard auth generated after auth refresh")
}
return
}
// We load the current user again because it should now have addresses loaded.
apiUser, err = apiClient.CurrentUser()
if err != nil {
log.WithError(err).Error("Could not get current API user")
if logoutErr := loginClient.Logout(); logoutErr != nil {
log.WithError(logoutErr).Error("Clean login session after get current user failed.")
}
return
}
apiToken = auth.UID() + ":" + auth.RefreshToken
activeEmails := apiClient.Addresses().ActiveEmails()
if _, err = b.credStorer.Add(apiUser.ID, apiUser.Name, apiToken, mbPassword, activeEmails); err != nil {
log.WithError(err).Error("Could not add user to credentials store")
return
}
// If it's a new user, generate the user object.
if !hasUser {
user, err = newUser(b.panicHandler, apiUser.ID, b.events, b.credStorer, apiClient, b.storeCache, b.config.GetDBDir())
if err != nil {
log.WithField("user", apiUser.ID).WithError(err).Error("Could not create user")
return
}
}
// Set up the user auth and store (which we do for both new and existing users).
if err = user.init(b.idleUpdates, apiClient); err != nil {
log.WithField("user", user.userID).WithError(err).Error("Could not initialise user")
return
}
if !hasUser {
b.users = append(b.users, user)
b.SendMetric(m.New(m.Setup, m.NewUser, m.NoLabel))
}
b.events.Emit(events.UserRefreshEvent, apiUser.ID)
return user, err
}
// GetUsers returns all added users into keychain (even logged out users).
func (b *Bridge) GetUsers() []*User {
b.lock.RLock()
defer b.lock.RUnlock()
return b.users
}
// GetUser returns a user by `query` which is compared to users' ID, username
// or any attached e-mail address.
func (b *Bridge) GetUser(query string) (*User, error) {
b.crashBandicoot(query)
b.lock.RLock()
defer b.lock.RUnlock()
for _, user := range b.users {
if strings.EqualFold(user.ID(), query) || strings.EqualFold(user.Username(), query) {
return user, nil
}
for _, address := range user.GetAddresses() {
if strings.EqualFold(address, query) {
return user, nil
}
}
}
return nil, errors.New("user " + query + " not found")
}
// ClearData closes all connections (to release db files and so on) and clears all data.
func (b *Bridge) ClearData() error {
var result *multierror.Error
for _, user := range b.users {
if err := user.Logout(); err != nil {
result = multierror.Append(result, err)
}
if err := user.closeStore(); err != nil {
result = multierror.Append(result, err)
}
}
if err := b.config.ClearData(); err != nil {
result = multierror.Append(result, err)
}
return result.ErrorOrNil()
}
// DeleteUser deletes user completely; it logs user out from the API, stops any
// active connection, deletes from credentials store and removes from the Bridge struct.
func (b *Bridge) DeleteUser(userID string, clearStore bool) error {
b.lock.Lock()
defer b.lock.Unlock()
log := log.WithField("user", userID)
for idx, user := range b.users {
if user.ID() == userID {
if err := user.Logout(); err != nil {
log.WithError(err).Error("Cannot logout user")
// We can try to continue to remove the user.
// Token will still be valid, but will expire eventually.
}
if err := user.closeStore(); err != nil {
log.WithError(err).Error("Failed to close user store")
}
if clearStore {
// Clear cache after closing connections (done in logout).
if err := user.clearStore(); err != nil {
log.WithError(err).Error("Failed to clear user")
}
}
if err := b.credStorer.Delete(userID); err != nil {
log.WithError(err).Error("Cannot remove user")
return err
}
b.users = append(b.users[:idx], b.users[idx+1:]...)
return nil
}
}
return errors.New("user " + userID + " not found")
}
// ReportBug reports a new bug from the user.
func (b *Bridge) ReportBug(osType, osVersion, description, accountName, address, emailClient string) error {
apiClient := b.pmapiClientFactory("bug_reporter")
title := "[Bridge] Bug"
err := apiClient.ReportBugWithEmailClient(
osType,
osVersion,
title,
description,
accountName,
address,
emailClient,
)
if err != nil {
log.Error("Reporting bug failed: ", err)
return err
}
log.Info("Bug successfully reported")
return nil
}
// SendMetric sends a metric. We don't want to return any errors, only log them.
func (b *Bridge) SendMetric(m m.Metric) {
apiClient := b.pmapiClientFactory("metric_reporter")
cat, act, lab := m.Get()
err := apiClient.SendSimpleMetric(string(cat), string(act), string(lab))
if err != nil {
log.Error("Sending metric failed: ", err)
}
log.WithFields(logrus.Fields{
"cat": cat,
"act": act,
"lab": lab,
}).Debug("Metric successfully sent")
}
// GetCurrentClient returns currently connected client (e.g. Thunderbird).
func (b *Bridge) GetCurrentClient() string {
res := b.userAgentClientName
@ -455,56 +111,42 @@ func (b *Bridge) GetCurrentClient() string {
func (b *Bridge) SetCurrentClient(clientName, clientVersion string) {
b.userAgentClientName = clientName
b.userAgentClientVersion = clientVersion
b.updateCurrentUserAgent()
b.updateUserAgent()
}
// SetCurrentOS updates OS and sets the user agent on pmapi. By default we use
// `runtime.GOOS`, but this can be overridden in case of better detection.
func (b *Bridge) SetCurrentOS(os string) {
b.userAgentOS = os
b.updateCurrentUserAgent()
b.updateUserAgent()
}
// GetIMAPUpdatesChannel sets the channel on which idle events should be sent.
func (b *Bridge) GetIMAPUpdatesChannel() chan interface{} {
if b.idleUpdates == nil {
log.Warn("Bridge updates channel is nil")
func (b *Bridge) updateUserAgent() {
b.clientManager.SetUserAgent(b.userAgentClientName, b.userAgentClientVersion, b.userAgentOS)
}
// ReportBug reports a new bug from the user.
func (b *Bridge) ReportBug(osType, osVersion, description, accountName, address, emailClient string) error {
c := b.clientManager.GetAnonymousClient()
defer c.Logout()
title := "[Bridge] Bug"
report := pmapi.ReportReq{
OS: osType,
OSVersion: osVersion,
Browser: emailClient,
Title: title,
Description: description,
Username: accountName,
Email: address,
}
return b.idleUpdates
}
// AllowDoH instructs bridge to use DoH to access an API proxy if necessary.
// It also needs to work before bridge is initialised (because we may need to use the proxy at startup).
func AllowDoH() {
pmapi.GlobalAllowDoH()
}
// DisallowDoH instructs bridge to not use DoH to access an API proxy if necessary.
// It also needs to work before bridge is initialised (because we may need to use the proxy at startup).
func DisallowDoH() {
pmapi.GlobalDisallowDoH()
}
func (b *Bridge) updateCurrentUserAgent() {
UpdateCurrentUserAgent(b.version, b.userAgentOS, b.userAgentClientName, b.userAgentClientVersion)
}
// hasUser returns whether the bridge currently has a user with ID `id`.
func (b *Bridge) hasUser(id string) (user *User, ok bool) {
for _, u := range b.users {
if u.ID() == id {
user, ok = u, true
return
}
if err := c.Report(report); err != nil {
log.Error("Reporting bug failed: ", err)
return err
}
return
}
log.Info("Bug successfully reported")
// "Easter egg" for testing purposes.
func (b *Bridge) crashBandicoot(username string) {
if username == "crash@bandicoot" {
panic("Your wish is my command… I crash!")
}
return nil
}

View File

@ -1,233 +0,0 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge
import (
"errors"
"testing"
"github.com/ProtonMail/proton-bridge/internal/bridge/credentials"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/metrics"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
gomock "github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
)
func TestBridgeFinishLoginBadPassword(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
// Init bridge with no user from keychain.
m.credentialsStore.EXPECT().List().Return([]string{}, nil)
// Set up mocks for FinishLogin.
err := errors.New("bad password")
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, err)
m.pmapiClient.EXPECT().Logout().Return(nil)
checkBridgeFinishLogin(t, m, testAuth, testCredentials.MailboxPassword, "", err)
}
func TestBridgeFinishLoginUpgradeApplication(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
// Init bridge with no user from keychain.
m.credentialsStore.EXPECT().List().Return([]string{}, nil)
// Set up mocks for FinishLogin.
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, pmapi.ErrUpgradeApplication)
m.eventListener.EXPECT().Emit(events.UpgradeApplicationEvent, "")
err := errors.New("Cannot logout when upgrade needed")
m.pmapiClient.EXPECT().Logout().Return(err)
checkBridgeFinishLogin(t, m, testAuth, testCredentials.MailboxPassword, "", pmapi.ErrUpgradeApplication)
}
func refreshWithToken(token string) *pmapi.Auth {
return &pmapi.Auth{
RefreshToken: token,
KeySalt: "", // No salting in tests.
}
}
func credentialsWithToken(token string) *credentials.Credentials {
tmp := &credentials.Credentials{}
*tmp = *testCredentials
tmp.APIToken = token
return tmp
}
func TestBridgeFinishLoginNewUser(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
// Bridge finds no users in the keychain.
m.credentialsStore.EXPECT().List().Return([]string{}, nil)
// Get user to be able to setup new client with proper userID.
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil)
m.pmapiClient.EXPECT().CurrentUser().Return(testPMAPIUser, nil)
// Setup of new client.
m.pmapiClient.EXPECT().AuthRefresh(":tok").Return(refreshWithToken("afterLogin"), nil)
m.pmapiClient.EXPECT().CurrentUser().Return(testPMAPIUser, nil)
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
// Set up mocks for authorising the new user (in user.init).
m.credentialsStore.EXPECT().Add("user", "username", ":afterLogin", testCredentials.MailboxPassword, []string{testPMAPIAddress.Email})
m.credentialsStore.EXPECT().Get("user").Return(credentialsWithToken(":afterLogin"), nil).Times(2)
m.pmapiClient.EXPECT().AuthRefresh(":afterLogin").Return(refreshWithToken("afterCredentials"), nil)
m.credentialsStore.EXPECT().Get("user").Return(credentialsWithToken("afterCredentials"), nil)
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil)
m.pmapiClient.EXPECT().UnlockAddresses([]byte(testCredentials.MailboxPassword)).Return(nil)
m.credentialsStore.EXPECT().UpdateToken("user", ":afterCredentials").Return(nil)
// Set up mocks for creating the user's store (in store.New).
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil)
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil)
// Emit event for new user and send metrics.
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
m.pmapiClient.EXPECT().SendSimpleMetric(string(metrics.Setup), string(metrics.NewUser), string(metrics.NoLabel))
// Set up mocks for starting the store's event loop (in store.New).
// The event loop runs in another goroutine so this might happen at any time.
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil)
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil)
// Set up mocks for performing the initial store sync.
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil)
checkBridgeFinishLogin(t, m, testAuth, testCredentials.MailboxPassword, "user", nil)
}
func TestBridgeFinishLoginExistingUser(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
loggedOutCreds := *testCredentials
loggedOutCreds.APIToken = ""
loggedOutCreds.MailboxPassword = ""
// Bridge finds one logged out user in the keychain.
m.credentialsStore.EXPECT().List().Return([]string{"user"}, nil)
// New user
m.credentialsStore.EXPECT().Get("user").Return(&loggedOutCreds, nil)
// Init user
m.credentialsStore.EXPECT().Get("user").Return(&loggedOutCreds, nil)
m.pmapiClient.EXPECT().ListLabels().Return(nil, pmapi.ErrInvalidToken)
m.pmapiClient.EXPECT().Addresses().Return(nil)
// Get user to be able to setup new client with proper userID.
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil)
m.pmapiClient.EXPECT().CurrentUser().Return(testPMAPIUser, nil)
// Setup of new client.
m.pmapiClient.EXPECT().AuthRefresh(":tok").Return(refreshWithToken("afterLogin"), nil)
m.pmapiClient.EXPECT().CurrentUser().Return(testPMAPIUser, nil)
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
// Set up mocks for authorising the new user (in user.init).
m.credentialsStore.EXPECT().Add("user", "username", ":afterLogin", testCredentials.MailboxPassword, []string{testPMAPIAddress.Email})
m.credentialsStore.EXPECT().Get("user").Return(credentialsWithToken(":afterLogin"), nil)
m.pmapiClient.EXPECT().AuthRefresh(":afterLogin").Return(refreshWithToken("afterCredentials"), nil)
m.credentialsStore.EXPECT().Get("user").Return(credentialsWithToken("afterCredentials"), nil)
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil)
m.pmapiClient.EXPECT().UnlockAddresses([]byte(testCredentials.MailboxPassword)).Return(nil)
m.credentialsStore.EXPECT().UpdateToken("user", ":afterCredentials").Return(nil)
// Set up mocks for creating the user's store (in store.New).
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil)
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil)
// Reload account list in GUI.
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
// Set up mocks for starting the store's event loop (in store.New)
// The event loop runs in another goroutine so this might happen at any time.
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil)
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil)
// Set up mocks for performing the initial store sync.
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil)
checkBridgeFinishLogin(t, m, testAuth, testCredentials.MailboxPassword, "user", nil)
}
func TestBridgeDoubleLogin(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
// Firstly, start bridge with existing user...
m.credentialsStore.EXPECT().List().Return([]string{"user"}, nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.credentialsStore.EXPECT().UpdateToken("user", ":reftok").Return(nil)
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil)
m.pmapiClient.EXPECT().UnlockAddresses([]byte(testCredentials.MailboxPassword)).Return(nil)
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil)
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil)
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil)
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil)
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil)
bridge := testNewBridge(t, m)
defer cleanUpBridgeUserData(bridge)
// Then, try to log in again...
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil)
m.pmapiClient.EXPECT().CurrentUser().Return(testPMAPIUser, nil)
m.pmapiClient.EXPECT().Logout()
_, err := bridge.FinishLogin(m.pmapiClient, testAuth, testCredentials.MailboxPassword)
assert.Equal(t, "user is already logged in", err.Error())
}
func checkBridgeFinishLogin(t *testing.T, m mocks, auth *pmapi.Auth, mailboxPassword string, expectedUserID string, expectedErr error) {
bridge := testNewBridge(t, m)
defer cleanUpBridgeUserData(bridge)
user, err := bridge.FinishLogin(m.pmapiClient, auth, mailboxPassword)
waitForEvents()
assert.Equal(t, expectedErr, err)
if expectedUserID != "" {
assert.Equal(t, expectedUserID, user.ID())
assert.Equal(t, 1, len(bridge.users))
assert.Equal(t, expectedUserID, bridge.users[0].ID())
} else {
assert.Equal(t, (*User)(nil), user)
assert.Equal(t, 0, len(bridge.users))
}
}

View File

@ -1,162 +0,0 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge
import (
"errors"
"testing"
credentials "github.com/ProtonMail/proton-bridge/internal/bridge/credentials"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/metrics"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
gomock "github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
)
func TestNewBridgeNoKeychain(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
m.credentialsStore.EXPECT().List().Return([]string{}, errors.New("no keychain"))
checkBridgeNew(t, m, []*credentials.Credentials{})
}
func TestNewBridgeWithoutUsersInCredentialsStore(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
m.credentialsStore.EXPECT().List().Return([]string{}, nil)
checkBridgeNew(t, m, []*credentials.Credentials{})
}
func TestNewBridgeWithDisconnectedUser(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
m.credentialsStore.EXPECT().List().Return([]string{"user"}, nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil).Times(2)
m.pmapiClient.EXPECT().ListLabels().Return(nil, errors.New("ErrUnauthorized"))
m.pmapiClient.EXPECT().Addresses().Return(nil)
checkBridgeNew(t, m, []*credentials.Credentials{testCredentialsDisconnected})
}
func TestNewBridgeWithConnectedUserWithBadToken(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
m.credentialsStore.EXPECT().List().Return([]string{"user"}, nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
m.credentialsStore.EXPECT().Logout("user").Return(nil)
m.pmapiClient.EXPECT().AuthRefresh("token").Return(nil, errors.New("bad token"))
m.eventListener.EXPECT().Emit(events.LogoutEvent, "user")
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
m.pmapiClient.EXPECT().Logout().Return(nil)
m.pmapiClient.EXPECT().SetAuths(nil)
m.credentialsStore.EXPECT().Logout("user").Return(nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil)
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
checkBridgeNew(t, m, []*credentials.Credentials{testCredentialsDisconnected})
}
func TestNewBridgeWithConnectedUser(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil)
m.pmapiClient.EXPECT().UnlockAddresses([]byte(testCredentials.MailboxPassword)).Return(nil)
// Set up mocks for store initialisation for the authorized user.
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil)
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil)
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil)
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil).AnyTimes()
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil)
m.credentialsStore.EXPECT().List().Return([]string{"user"}, nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
m.credentialsStore.EXPECT().UpdateToken("user", ":reftok").Return(nil)
checkBridgeNew(t, m, []*credentials.Credentials{testCredentials})
}
// Tests two users with different states and checks also the order from
// credentials store is kept also in array of Bridge users.
func TestNewBridgeWithUsers(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil)
m.pmapiClient.EXPECT().Unlock(testCredentials.MailboxPassword).Return(nil, nil)
m.pmapiClient.EXPECT().UnlockAddresses([]byte(testCredentials.MailboxPassword)).Return(nil)
m.credentialsStore.EXPECT().List().Return([]string{"user", "user"}, nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil).Times(2)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
m.credentialsStore.EXPECT().UpdateToken("user", ":reftok").Return(nil)
// Set up mocks for store initialisation for the unauth user.
m.pmapiClient.EXPECT().ListLabels().Return(nil, errors.New("ErrUnauthorized"))
m.pmapiClient.EXPECT().Addresses().Return(nil)
// Set up mocks for store initialisation for the authorized user.
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil)
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil)
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil)
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil).AnyTimes()
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil)
checkBridgeNew(t, m, []*credentials.Credentials{testCredentialsDisconnected, testCredentials})
}
func TestNewBridgeFirstStart(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
m.prefProvider.EXPECT().GetBool(preferences.FirstStartKey).Return(true)
m.credentialsStore.EXPECT().List().Return([]string{}, nil)
m.pmapiClient.EXPECT().SendSimpleMetric(string(metrics.Setup), string(metrics.FirstStart), gomock.Any())
testNewBridge(t, m)
}
func checkBridgeNew(t *testing.T, m mocks, expectedCredentials []*credentials.Credentials) {
bridge := testNewBridge(t, m)
defer cleanUpBridgeUserData(bridge)
assert.Equal(m.t, len(expectedCredentials), len(bridge.GetUsers()))
credentials := []*credentials.Credentials{}
for _, user := range bridge.users {
credentials = append(credentials, user.creds)
}
assert.Equal(m.t, expectedCredentials, credentials)
}

View File

@ -1,121 +0,0 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge
import (
"errors"
"testing"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/stretchr/testify/assert"
)
func TestGetNoUser(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
checkBridgeGetUser(t, m, "nouser", -1, "user nouser not found")
}
func TestGetUserByID(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
checkBridgeGetUser(t, m, "user", 0, "")
checkBridgeGetUser(t, m, "users", 1, "")
}
func TestGetUserByName(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
checkBridgeGetUser(t, m, "username", 0, "")
checkBridgeGetUser(t, m, "usersname", 1, "")
}
func TestGetUserByEmail(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
checkBridgeGetUser(t, m, "user@pm.me", 0, "")
checkBridgeGetUser(t, m, "users@pm.me", 1, "")
checkBridgeGetUser(t, m, "anotheruser@pm.me", 1, "")
checkBridgeGetUser(t, m, "alsouser@pm.me", 1, "")
}
func TestDeleteUser(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
bridge := testNewBridgeWithUsers(t, m)
defer cleanUpBridgeUserData(bridge)
m.pmapiClient.EXPECT().Logout().Return(nil)
m.credentialsStore.EXPECT().Logout("user").Return(nil)
m.credentialsStore.EXPECT().Delete("user").Return(nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
err := bridge.DeleteUser("user", true)
assert.NoError(t, err)
assert.Equal(t, 1, len(bridge.users))
}
// Even when logout fails, delete is done.
func TestDeleteUserWithFailingLogout(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
bridge := testNewBridgeWithUsers(t, m)
defer cleanUpBridgeUserData(bridge)
m.pmapiClient.EXPECT().Logout().Return(nil)
m.credentialsStore.EXPECT().Logout("user").Return(errors.New("logout failed"))
m.credentialsStore.EXPECT().Delete("user").Return(nil).Times(2)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
err := bridge.DeleteUser("user", true)
assert.NoError(t, err)
assert.Equal(t, 1, len(bridge.users))
}
func checkBridgeGetUser(t *testing.T, m mocks, query string, index int, expectedError string) {
bridge := testNewBridgeWithUsers(t, m)
defer cleanUpBridgeUserData(bridge)
user, err := bridge.GetUser(query)
waitForEvents()
if expectedError != "" {
assert.Equal(m.t, expectedError, err.Error())
} else {
assert.NoError(m.t, err)
}
var expectedUser *User
if index >= 0 {
expectedUser = bridge.users[index]
}
assert.Equal(m.t, expectedUser, user)
}

View File

@ -15,8 +15,8 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Code generated by ./credits.sh at Thu Apr 9 13:39:29 CEST 2020. DO NOT EDIT.
// Code generated by ./credits.sh at Tue Sep 29 14:56:25 CEST 2020. DO NOT EDIT.
package bridge
const Credits = "github.com/0xAX/notificator;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/go-imap-quota;github.com/ProtonMail/go-smtp;github.com/ProtonMail/go-vcard;github.com/ProtonMail/gopenpgp;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/andybalholm/cascadia;github.com/certifi/gocertifi;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/danieljoos/wincred;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-specialuse;github.com/emersion/go-imap-unselect;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/raven-go;github.com/go-resty/resty/v2;github.com/golang/mock;github.com/google/go-cmp;github.com/gopherjs/gopherjs;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/jhillyerd/enmime;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/pkg/errors;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"
const Credits = "github.com/0xAX/notificator;github.com/Masterminds/semver/v3;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/go-smtp;github.com/ProtonMail/go-vcard;github.com/ProtonMail/gopenpgp/v2;github.com/ProtonMail/mbox;github.com/PuerkitoBio/goquery;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/certifi/gocertifi;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-specialuse;github.com/emersion/go-imap-unselect;github.com/emersion/go-mbox;github.com/emersion/go-message;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/raven-go;github.com/go-resty/resty/v2;github.com/golang/mock;github.com/google/go-cmp;github.com/google/uuid;github.com/gopherjs/gopherjs;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/mattn/go-runewidth;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/olekukonko/tablewriter;github.com/pkg/errors;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/ssor/bom;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;"

View File

@ -1,923 +0,0 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/ProtonMail/proton-bridge/internal/bridge (interfaces: Configer,PreferenceProvider,PanicHandler,PMAPIProvider,CredentialsStorer)
// Package mocks is a generated GoMock package.
package mocks
import (
credentials "github.com/ProtonMail/proton-bridge/internal/bridge/credentials"
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi"
crypto "github.com/ProtonMail/gopenpgp/crypto"
gomock "github.com/golang/mock/gomock"
io "io"
reflect "reflect"
)
// MockConfiger is a mock of Configer interface
type MockConfiger struct {
ctrl *gomock.Controller
recorder *MockConfigerMockRecorder
}
// MockConfigerMockRecorder is the mock recorder for MockConfiger
type MockConfigerMockRecorder struct {
mock *MockConfiger
}
// NewMockConfiger creates a new mock instance
func NewMockConfiger(ctrl *gomock.Controller) *MockConfiger {
mock := &MockConfiger{ctrl: ctrl}
mock.recorder = &MockConfigerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockConfiger) EXPECT() *MockConfigerMockRecorder {
return m.recorder
}
// ClearData mocks base method
func (m *MockConfiger) ClearData() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ClearData")
ret0, _ := ret[0].(error)
return ret0
}
// ClearData indicates an expected call of ClearData
func (mr *MockConfigerMockRecorder) ClearData() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClearData", reflect.TypeOf((*MockConfiger)(nil).ClearData))
}
// GetAPIConfig mocks base method
func (m *MockConfiger) GetAPIConfig() *pmapi.ClientConfig {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAPIConfig")
ret0, _ := ret[0].(*pmapi.ClientConfig)
return ret0
}
// GetAPIConfig indicates an expected call of GetAPIConfig
func (mr *MockConfigerMockRecorder) GetAPIConfig() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAPIConfig", reflect.TypeOf((*MockConfiger)(nil).GetAPIConfig))
}
// GetDBDir mocks base method
func (m *MockConfiger) GetDBDir() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetDBDir")
ret0, _ := ret[0].(string)
return ret0
}
// GetDBDir indicates an expected call of GetDBDir
func (mr *MockConfigerMockRecorder) GetDBDir() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDBDir", reflect.TypeOf((*MockConfiger)(nil).GetDBDir))
}
// GetIMAPCachePath mocks base method
func (m *MockConfiger) GetIMAPCachePath() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetIMAPCachePath")
ret0, _ := ret[0].(string)
return ret0
}
// GetIMAPCachePath indicates an expected call of GetIMAPCachePath
func (mr *MockConfigerMockRecorder) GetIMAPCachePath() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetIMAPCachePath", reflect.TypeOf((*MockConfiger)(nil).GetIMAPCachePath))
}
// MockPreferenceProvider is a mock of PreferenceProvider interface
type MockPreferenceProvider struct {
ctrl *gomock.Controller
recorder *MockPreferenceProviderMockRecorder
}
// MockPreferenceProviderMockRecorder is the mock recorder for MockPreferenceProvider
type MockPreferenceProviderMockRecorder struct {
mock *MockPreferenceProvider
}
// NewMockPreferenceProvider creates a new mock instance
func NewMockPreferenceProvider(ctrl *gomock.Controller) *MockPreferenceProvider {
mock := &MockPreferenceProvider{ctrl: ctrl}
mock.recorder = &MockPreferenceProviderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockPreferenceProvider) EXPECT() *MockPreferenceProviderMockRecorder {
return m.recorder
}
// Get mocks base method
func (m *MockPreferenceProvider) Get(arg0 string) string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get", arg0)
ret0, _ := ret[0].(string)
return ret0
}
// Get indicates an expected call of Get
func (mr *MockPreferenceProviderMockRecorder) Get(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockPreferenceProvider)(nil).Get), arg0)
}
// GetBool mocks base method
func (m *MockPreferenceProvider) GetBool(arg0 string) bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetBool", arg0)
ret0, _ := ret[0].(bool)
return ret0
}
// GetBool indicates an expected call of GetBool
func (mr *MockPreferenceProviderMockRecorder) GetBool(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBool", reflect.TypeOf((*MockPreferenceProvider)(nil).GetBool), arg0)
}
// GetInt mocks base method
func (m *MockPreferenceProvider) GetInt(arg0 string) int {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetInt", arg0)
ret0, _ := ret[0].(int)
return ret0
}
// GetInt indicates an expected call of GetInt
func (mr *MockPreferenceProviderMockRecorder) GetInt(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInt", reflect.TypeOf((*MockPreferenceProvider)(nil).GetInt), arg0)
}
// Set mocks base method
func (m *MockPreferenceProvider) Set(arg0, arg1 string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Set", arg0, arg1)
}
// Set indicates an expected call of Set
func (mr *MockPreferenceProviderMockRecorder) Set(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Set", reflect.TypeOf((*MockPreferenceProvider)(nil).Set), arg0, arg1)
}
// MockPanicHandler is a mock of PanicHandler interface
type MockPanicHandler struct {
ctrl *gomock.Controller
recorder *MockPanicHandlerMockRecorder
}
// MockPanicHandlerMockRecorder is the mock recorder for MockPanicHandler
type MockPanicHandlerMockRecorder struct {
mock *MockPanicHandler
}
// NewMockPanicHandler creates a new mock instance
func NewMockPanicHandler(ctrl *gomock.Controller) *MockPanicHandler {
mock := &MockPanicHandler{ctrl: ctrl}
mock.recorder = &MockPanicHandlerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockPanicHandler) EXPECT() *MockPanicHandlerMockRecorder {
return m.recorder
}
// HandlePanic mocks base method
func (m *MockPanicHandler) HandlePanic() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "HandlePanic")
}
// HandlePanic indicates an expected call of HandlePanic
func (mr *MockPanicHandlerMockRecorder) HandlePanic() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandlePanic", reflect.TypeOf((*MockPanicHandler)(nil).HandlePanic))
}
// MockPMAPIProvider is a mock of PMAPIProvider interface
type MockPMAPIProvider struct {
ctrl *gomock.Controller
recorder *MockPMAPIProviderMockRecorder
}
// MockPMAPIProviderMockRecorder is the mock recorder for MockPMAPIProvider
type MockPMAPIProviderMockRecorder struct {
mock *MockPMAPIProvider
}
// NewMockPMAPIProvider creates a new mock instance
func NewMockPMAPIProvider(ctrl *gomock.Controller) *MockPMAPIProvider {
mock := &MockPMAPIProvider{ctrl: ctrl}
mock.recorder = &MockPMAPIProviderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockPMAPIProvider) EXPECT() *MockPMAPIProviderMockRecorder {
return m.recorder
}
// Addresses mocks base method
func (m *MockPMAPIProvider) Addresses() pmapi.AddressList {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Addresses")
ret0, _ := ret[0].(pmapi.AddressList)
return ret0
}
// Addresses indicates an expected call of Addresses
func (mr *MockPMAPIProviderMockRecorder) Addresses() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Addresses", reflect.TypeOf((*MockPMAPIProvider)(nil).Addresses))
}
// Auth mocks base method
func (m *MockPMAPIProvider) Auth(arg0, arg1 string, arg2 *pmapi.AuthInfo) (*pmapi.Auth, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Auth", arg0, arg1, arg2)
ret0, _ := ret[0].(*pmapi.Auth)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Auth indicates an expected call of Auth
func (mr *MockPMAPIProviderMockRecorder) Auth(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Auth", reflect.TypeOf((*MockPMAPIProvider)(nil).Auth), arg0, arg1, arg2)
}
// Auth2FA mocks base method
func (m *MockPMAPIProvider) Auth2FA(arg0 string, arg1 *pmapi.Auth) (*pmapi.Auth2FA, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Auth2FA", arg0, arg1)
ret0, _ := ret[0].(*pmapi.Auth2FA)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Auth2FA indicates an expected call of Auth2FA
func (mr *MockPMAPIProviderMockRecorder) Auth2FA(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Auth2FA", reflect.TypeOf((*MockPMAPIProvider)(nil).Auth2FA), arg0, arg1)
}
// AuthInfo mocks base method
func (m *MockPMAPIProvider) AuthInfo(arg0 string) (*pmapi.AuthInfo, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AuthInfo", arg0)
ret0, _ := ret[0].(*pmapi.AuthInfo)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// AuthInfo indicates an expected call of AuthInfo
func (mr *MockPMAPIProviderMockRecorder) AuthInfo(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthInfo", reflect.TypeOf((*MockPMAPIProvider)(nil).AuthInfo), arg0)
}
// AuthRefresh mocks base method
func (m *MockPMAPIProvider) AuthRefresh(arg0 string) (*pmapi.Auth, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AuthRefresh", arg0)
ret0, _ := ret[0].(*pmapi.Auth)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// AuthRefresh indicates an expected call of AuthRefresh
func (mr *MockPMAPIProviderMockRecorder) AuthRefresh(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthRefresh", reflect.TypeOf((*MockPMAPIProvider)(nil).AuthRefresh), arg0)
}
// CountMessages mocks base method
func (m *MockPMAPIProvider) CountMessages(arg0 string) ([]*pmapi.MessagesCount, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CountMessages", arg0)
ret0, _ := ret[0].([]*pmapi.MessagesCount)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CountMessages indicates an expected call of CountMessages
func (mr *MockPMAPIProviderMockRecorder) CountMessages(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountMessages", reflect.TypeOf((*MockPMAPIProvider)(nil).CountMessages), arg0)
}
// CreateAttachment mocks base method
func (m *MockPMAPIProvider) CreateAttachment(arg0 *pmapi.Attachment, arg1, arg2 io.Reader) (*pmapi.Attachment, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateAttachment", arg0, arg1, arg2)
ret0, _ := ret[0].(*pmapi.Attachment)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateAttachment indicates an expected call of CreateAttachment
func (mr *MockPMAPIProviderMockRecorder) CreateAttachment(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateAttachment", reflect.TypeOf((*MockPMAPIProvider)(nil).CreateAttachment), arg0, arg1, arg2)
}
// CreateDraft mocks base method
func (m *MockPMAPIProvider) CreateDraft(arg0 *pmapi.Message, arg1 string, arg2 int) (*pmapi.Message, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateDraft", arg0, arg1, arg2)
ret0, _ := ret[0].(*pmapi.Message)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateDraft indicates an expected call of CreateDraft
func (mr *MockPMAPIProviderMockRecorder) CreateDraft(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateDraft", reflect.TypeOf((*MockPMAPIProvider)(nil).CreateDraft), arg0, arg1, arg2)
}
// CreateLabel mocks base method
func (m *MockPMAPIProvider) CreateLabel(arg0 *pmapi.Label) (*pmapi.Label, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateLabel", arg0)
ret0, _ := ret[0].(*pmapi.Label)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateLabel indicates an expected call of CreateLabel
func (mr *MockPMAPIProviderMockRecorder) CreateLabel(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateLabel", reflect.TypeOf((*MockPMAPIProvider)(nil).CreateLabel), arg0)
}
// CurrentUser mocks base method
func (m *MockPMAPIProvider) CurrentUser() (*pmapi.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CurrentUser")
ret0, _ := ret[0].(*pmapi.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CurrentUser indicates an expected call of CurrentUser
func (mr *MockPMAPIProviderMockRecorder) CurrentUser() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CurrentUser", reflect.TypeOf((*MockPMAPIProvider)(nil).CurrentUser))
}
// DecryptAndVerifyCards mocks base method
func (m *MockPMAPIProvider) DecryptAndVerifyCards(arg0 []pmapi.Card) ([]pmapi.Card, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DecryptAndVerifyCards", arg0)
ret0, _ := ret[0].([]pmapi.Card)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DecryptAndVerifyCards indicates an expected call of DecryptAndVerifyCards
func (mr *MockPMAPIProviderMockRecorder) DecryptAndVerifyCards(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DecryptAndVerifyCards", reflect.TypeOf((*MockPMAPIProvider)(nil).DecryptAndVerifyCards), arg0)
}
// DeleteLabel mocks base method
func (m *MockPMAPIProvider) DeleteLabel(arg0 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteLabel", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteLabel indicates an expected call of DeleteLabel
func (mr *MockPMAPIProviderMockRecorder) DeleteLabel(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteLabel", reflect.TypeOf((*MockPMAPIProvider)(nil).DeleteLabel), arg0)
}
// DeleteMessages mocks base method
func (m *MockPMAPIProvider) DeleteMessages(arg0 []string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteMessages", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteMessages indicates an expected call of DeleteMessages
func (mr *MockPMAPIProviderMockRecorder) DeleteMessages(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteMessages", reflect.TypeOf((*MockPMAPIProvider)(nil).DeleteMessages), arg0)
}
// EmptyFolder mocks base method
func (m *MockPMAPIProvider) EmptyFolder(arg0, arg1 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "EmptyFolder", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// EmptyFolder indicates an expected call of EmptyFolder
func (mr *MockPMAPIProviderMockRecorder) EmptyFolder(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EmptyFolder", reflect.TypeOf((*MockPMAPIProvider)(nil).EmptyFolder), arg0, arg1)
}
// GetAttachment mocks base method
func (m *MockPMAPIProvider) GetAttachment(arg0 string) (io.ReadCloser, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAttachment", arg0)
ret0, _ := ret[0].(io.ReadCloser)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetAttachment indicates an expected call of GetAttachment
func (mr *MockPMAPIProviderMockRecorder) GetAttachment(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAttachment", reflect.TypeOf((*MockPMAPIProvider)(nil).GetAttachment), arg0)
}
// GetContactByID mocks base method
func (m *MockPMAPIProvider) GetContactByID(arg0 string) (pmapi.Contact, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetContactByID", arg0)
ret0, _ := ret[0].(pmapi.Contact)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetContactByID indicates an expected call of GetContactByID
func (mr *MockPMAPIProviderMockRecorder) GetContactByID(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetContactByID", reflect.TypeOf((*MockPMAPIProvider)(nil).GetContactByID), arg0)
}
// GetContactEmailByEmail mocks base method
func (m *MockPMAPIProvider) GetContactEmailByEmail(arg0 string, arg1, arg2 int) ([]pmapi.ContactEmail, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetContactEmailByEmail", arg0, arg1, arg2)
ret0, _ := ret[0].([]pmapi.ContactEmail)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetContactEmailByEmail indicates an expected call of GetContactEmailByEmail
func (mr *MockPMAPIProviderMockRecorder) GetContactEmailByEmail(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetContactEmailByEmail", reflect.TypeOf((*MockPMAPIProvider)(nil).GetContactEmailByEmail), arg0, arg1, arg2)
}
// GetEvent mocks base method
func (m *MockPMAPIProvider) GetEvent(arg0 string) (*pmapi.Event, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetEvent", arg0)
ret0, _ := ret[0].(*pmapi.Event)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetEvent indicates an expected call of GetEvent
func (mr *MockPMAPIProviderMockRecorder) GetEvent(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEvent", reflect.TypeOf((*MockPMAPIProvider)(nil).GetEvent), arg0)
}
// GetMailSettings mocks base method
func (m *MockPMAPIProvider) GetMailSettings() (pmapi.MailSettings, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetMailSettings")
ret0, _ := ret[0].(pmapi.MailSettings)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetMailSettings indicates an expected call of GetMailSettings
func (mr *MockPMAPIProviderMockRecorder) GetMailSettings() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMailSettings", reflect.TypeOf((*MockPMAPIProvider)(nil).GetMailSettings))
}
// GetMessage mocks base method
func (m *MockPMAPIProvider) GetMessage(arg0 string) (*pmapi.Message, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetMessage", arg0)
ret0, _ := ret[0].(*pmapi.Message)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetMessage indicates an expected call of GetMessage
func (mr *MockPMAPIProviderMockRecorder) GetMessage(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMessage", reflect.TypeOf((*MockPMAPIProvider)(nil).GetMessage), arg0)
}
// GetPublicKeysForEmail mocks base method
func (m *MockPMAPIProvider) GetPublicKeysForEmail(arg0 string) ([]pmapi.PublicKey, bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetPublicKeysForEmail", arg0)
ret0, _ := ret[0].([]pmapi.PublicKey)
ret1, _ := ret[1].(bool)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// GetPublicKeysForEmail indicates an expected call of GetPublicKeysForEmail
func (mr *MockPMAPIProviderMockRecorder) GetPublicKeysForEmail(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPublicKeysForEmail", reflect.TypeOf((*MockPMAPIProvider)(nil).GetPublicKeysForEmail), arg0)
}
// Import mocks base method
func (m *MockPMAPIProvider) Import(arg0 []*pmapi.ImportMsgReq) ([]*pmapi.ImportMsgRes, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Import", arg0)
ret0, _ := ret[0].([]*pmapi.ImportMsgRes)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Import indicates an expected call of Import
func (mr *MockPMAPIProviderMockRecorder) Import(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Import", reflect.TypeOf((*MockPMAPIProvider)(nil).Import), arg0)
}
// KeyRingForAddressID mocks base method
func (m *MockPMAPIProvider) KeyRingForAddressID(arg0 string) *crypto.KeyRing {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "KeyRingForAddressID", arg0)
ret0, _ := ret[0].(*crypto.KeyRing)
return ret0
}
// KeyRingForAddressID indicates an expected call of KeyRingForAddressID
func (mr *MockPMAPIProviderMockRecorder) KeyRingForAddressID(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KeyRingForAddressID", reflect.TypeOf((*MockPMAPIProvider)(nil).KeyRingForAddressID), arg0)
}
// LabelMessages mocks base method
func (m *MockPMAPIProvider) LabelMessages(arg0 []string, arg1 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LabelMessages", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// LabelMessages indicates an expected call of LabelMessages
func (mr *MockPMAPIProviderMockRecorder) LabelMessages(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LabelMessages", reflect.TypeOf((*MockPMAPIProvider)(nil).LabelMessages), arg0, arg1)
}
// ListLabels mocks base method
func (m *MockPMAPIProvider) ListLabels() ([]*pmapi.Label, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListLabels")
ret0, _ := ret[0].([]*pmapi.Label)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListLabels indicates an expected call of ListLabels
func (mr *MockPMAPIProviderMockRecorder) ListLabels() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListLabels", reflect.TypeOf((*MockPMAPIProvider)(nil).ListLabels))
}
// ListMessages mocks base method
func (m *MockPMAPIProvider) ListMessages(arg0 *pmapi.MessagesFilter) ([]*pmapi.Message, int, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListMessages", arg0)
ret0, _ := ret[0].([]*pmapi.Message)
ret1, _ := ret[1].(int)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// ListMessages indicates an expected call of ListMessages
func (mr *MockPMAPIProviderMockRecorder) ListMessages(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListMessages", reflect.TypeOf((*MockPMAPIProvider)(nil).ListMessages), arg0)
}
// Logout mocks base method
func (m *MockPMAPIProvider) Logout() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Logout")
ret0, _ := ret[0].(error)
return ret0
}
// Logout indicates an expected call of Logout
func (mr *MockPMAPIProviderMockRecorder) Logout() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logout", reflect.TypeOf((*MockPMAPIProvider)(nil).Logout))
}
// MarkMessagesRead mocks base method
func (m *MockPMAPIProvider) MarkMessagesRead(arg0 []string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "MarkMessagesRead", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// MarkMessagesRead indicates an expected call of MarkMessagesRead
func (mr *MockPMAPIProviderMockRecorder) MarkMessagesRead(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MarkMessagesRead", reflect.TypeOf((*MockPMAPIProvider)(nil).MarkMessagesRead), arg0)
}
// MarkMessagesUnread mocks base method
func (m *MockPMAPIProvider) MarkMessagesUnread(arg0 []string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "MarkMessagesUnread", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// MarkMessagesUnread indicates an expected call of MarkMessagesUnread
func (mr *MockPMAPIProviderMockRecorder) MarkMessagesUnread(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MarkMessagesUnread", reflect.TypeOf((*MockPMAPIProvider)(nil).MarkMessagesUnread), arg0)
}
// ReportBugWithEmailClient mocks base method
func (m *MockPMAPIProvider) ReportBugWithEmailClient(arg0, arg1, arg2, arg3, arg4, arg5, arg6 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReportBugWithEmailClient", arg0, arg1, arg2, arg3, arg4, arg5, arg6)
ret0, _ := ret[0].(error)
return ret0
}
// ReportBugWithEmailClient indicates an expected call of ReportBugWithEmailClient
func (mr *MockPMAPIProviderMockRecorder) ReportBugWithEmailClient(arg0, arg1, arg2, arg3, arg4, arg5, arg6 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportBugWithEmailClient", reflect.TypeOf((*MockPMAPIProvider)(nil).ReportBugWithEmailClient), arg0, arg1, arg2, arg3, arg4, arg5, arg6)
}
// SendMessage mocks base method
func (m *MockPMAPIProvider) SendMessage(arg0 string, arg1 *pmapi.SendMessageReq) (*pmapi.Message, *pmapi.Message, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SendMessage", arg0, arg1)
ret0, _ := ret[0].(*pmapi.Message)
ret1, _ := ret[1].(*pmapi.Message)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// SendMessage indicates an expected call of SendMessage
func (mr *MockPMAPIProviderMockRecorder) SendMessage(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendMessage", reflect.TypeOf((*MockPMAPIProvider)(nil).SendMessage), arg0, arg1)
}
// SendSimpleMetric mocks base method
func (m *MockPMAPIProvider) SendSimpleMetric(arg0, arg1, arg2 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SendSimpleMetric", arg0, arg1, arg2)
ret0, _ := ret[0].(error)
return ret0
}
// SendSimpleMetric indicates an expected call of SendSimpleMetric
func (mr *MockPMAPIProviderMockRecorder) SendSimpleMetric(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendSimpleMetric", reflect.TypeOf((*MockPMAPIProvider)(nil).SendSimpleMetric), arg0, arg1, arg2)
}
// SetAuths mocks base method
func (m *MockPMAPIProvider) SetAuths(arg0 chan<- *pmapi.Auth) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "SetAuths", arg0)
}
// SetAuths indicates an expected call of SetAuths
func (mr *MockPMAPIProviderMockRecorder) SetAuths(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetAuths", reflect.TypeOf((*MockPMAPIProvider)(nil).SetAuths), arg0)
}
// UnlabelMessages mocks base method
func (m *MockPMAPIProvider) UnlabelMessages(arg0 []string, arg1 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UnlabelMessages", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// UnlabelMessages indicates an expected call of UnlabelMessages
func (mr *MockPMAPIProviderMockRecorder) UnlabelMessages(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnlabelMessages", reflect.TypeOf((*MockPMAPIProvider)(nil).UnlabelMessages), arg0, arg1)
}
// Unlock mocks base method
func (m *MockPMAPIProvider) Unlock(arg0 string) (*crypto.KeyRing, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Unlock", arg0)
ret0, _ := ret[0].(*crypto.KeyRing)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Unlock indicates an expected call of Unlock
func (mr *MockPMAPIProviderMockRecorder) Unlock(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Unlock", reflect.TypeOf((*MockPMAPIProvider)(nil).Unlock), arg0)
}
// UnlockAddresses mocks base method
func (m *MockPMAPIProvider) UnlockAddresses(arg0 []byte) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UnlockAddresses", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// UnlockAddresses indicates an expected call of UnlockAddresses
func (mr *MockPMAPIProviderMockRecorder) UnlockAddresses(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnlockAddresses", reflect.TypeOf((*MockPMAPIProvider)(nil).UnlockAddresses), arg0)
}
// UpdateLabel mocks base method
func (m *MockPMAPIProvider) UpdateLabel(arg0 *pmapi.Label) (*pmapi.Label, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateLabel", arg0)
ret0, _ := ret[0].(*pmapi.Label)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateLabel indicates an expected call of UpdateLabel
func (mr *MockPMAPIProviderMockRecorder) UpdateLabel(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateLabel", reflect.TypeOf((*MockPMAPIProvider)(nil).UpdateLabel), arg0)
}
// UpdateUser mocks base method
func (m *MockPMAPIProvider) UpdateUser() (*pmapi.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateUser")
ret0, _ := ret[0].(*pmapi.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateUser indicates an expected call of UpdateUser
func (mr *MockPMAPIProviderMockRecorder) UpdateUser() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUser", reflect.TypeOf((*MockPMAPIProvider)(nil).UpdateUser))
}
// MockCredentialsStorer is a mock of CredentialsStorer interface
type MockCredentialsStorer struct {
ctrl *gomock.Controller
recorder *MockCredentialsStorerMockRecorder
}
// MockCredentialsStorerMockRecorder is the mock recorder for MockCredentialsStorer
type MockCredentialsStorerMockRecorder struct {
mock *MockCredentialsStorer
}
// NewMockCredentialsStorer creates a new mock instance
func NewMockCredentialsStorer(ctrl *gomock.Controller) *MockCredentialsStorer {
mock := &MockCredentialsStorer{ctrl: ctrl}
mock.recorder = &MockCredentialsStorerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockCredentialsStorer) EXPECT() *MockCredentialsStorerMockRecorder {
return m.recorder
}
// Add mocks base method
func (m *MockCredentialsStorer) Add(arg0, arg1, arg2, arg3 string, arg4 []string) (*credentials.Credentials, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Add", arg0, arg1, arg2, arg3, arg4)
ret0, _ := ret[0].(*credentials.Credentials)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Add indicates an expected call of Add
func (mr *MockCredentialsStorerMockRecorder) Add(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Add", reflect.TypeOf((*MockCredentialsStorer)(nil).Add), arg0, arg1, arg2, arg3, arg4)
}
// Delete mocks base method
func (m *MockCredentialsStorer) Delete(arg0 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete
func (mr *MockCredentialsStorerMockRecorder) Delete(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockCredentialsStorer)(nil).Delete), arg0)
}
// Get mocks base method
func (m *MockCredentialsStorer) Get(arg0 string) (*credentials.Credentials, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get", arg0)
ret0, _ := ret[0].(*credentials.Credentials)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Get indicates an expected call of Get
func (mr *MockCredentialsStorerMockRecorder) Get(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockCredentialsStorer)(nil).Get), arg0)
}
// List mocks base method
func (m *MockCredentialsStorer) List() ([]string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List")
ret0, _ := ret[0].([]string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List
func (mr *MockCredentialsStorerMockRecorder) List() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockCredentialsStorer)(nil).List))
}
// Logout mocks base method
func (m *MockCredentialsStorer) Logout(arg0 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Logout", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// Logout indicates an expected call of Logout
func (mr *MockCredentialsStorerMockRecorder) Logout(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logout", reflect.TypeOf((*MockCredentialsStorer)(nil).Logout), arg0)
}
// SwitchAddressMode mocks base method
func (m *MockCredentialsStorer) SwitchAddressMode(arg0 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SwitchAddressMode", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// SwitchAddressMode indicates an expected call of SwitchAddressMode
func (mr *MockCredentialsStorerMockRecorder) SwitchAddressMode(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SwitchAddressMode", reflect.TypeOf((*MockCredentialsStorer)(nil).SwitchAddressMode), arg0)
}
// UpdateEmails mocks base method
func (m *MockCredentialsStorer) UpdateEmails(arg0 string, arg1 []string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateEmails", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateEmails indicates an expected call of UpdateEmails
func (mr *MockCredentialsStorerMockRecorder) UpdateEmails(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateEmails", reflect.TypeOf((*MockCredentialsStorer)(nil).UpdateEmails), arg0, arg1)
}
// UpdateToken mocks base method
func (m *MockCredentialsStorer) UpdateToken(arg0, arg1 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateToken", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateToken indicates an expected call of UpdateToken
func (mr *MockCredentialsStorerMockRecorder) UpdateToken(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateToken", reflect.TypeOf((*MockCredentialsStorer)(nil).UpdateToken), arg0, arg1)
}

View File

@ -15,20 +15,21 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Code generated by ./release-notes.sh at Mon Apr 6 08:14:14 CEST 2020. DO NOT EDIT.
// Code generated by ./release-notes.sh at 'Mon Sep 21 01:29:10 PM CEST 2020'. DO NOT EDIT.
package bridge
const ReleaseNotes = `NOTE: We recommend to reconfigure your email client after upgrading to ensure the best results with the new draft folder support
Faster and more resilient mail synchronization process, especially for large mailboxes
• Added "Alternate Routing" feature to mitigate blocking of Proton Servers
Added synchronization of draft folder
• Improved event handling when there are frequent changes
Security improvements for loading dependent libraries
Minor UI & API communication tweaks
const ReleaseNotes = `• Bulletproofing against any potential data loss and/or duplication
• Performance improvements for handling attachments and non-standard formatting
Better stability of the message parser
• Additional foreign encoding support for outgoing messages
Complete refactor of the way messages are parsed to simplify code maintenance
• Improved User-Agent detection
Added MacOS Big Sur compatibility
Added persistent anonymous API cookies
`
const ReleaseFixedBugs = `• Fixed rare case of sending the same message multiple times in Outlook
Fixed bug in macOS update process; available from next update
const ReleaseFixedBugs = `• Fixed rare mail loss when moving from Spam folder
Limited log size
• Fixed Linux font issues (mouse hover).
`

View File

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

View File

@ -17,89 +17,22 @@
package bridge
import (
"io"
pmcrypto "github.com/ProtonMail/gopenpgp/crypto"
"github.com/ProtonMail/proton-bridge/internal/bridge/credentials"
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi" // mockgen needs this to be given an explicit import name
)
import "github.com/ProtonMail/proton-bridge/internal/users"
type Configer interface {
ClearData() error
users.Configer
StoreFactoryConfiger
}
type StoreFactoryConfiger interface {
GetDBDir() string
GetIMAPCachePath() string
GetAPIConfig() *pmapi.ClientConfig
}
type PreferenceProvider interface {
Get(key string) string
GetBool(key string) bool
SetBool(key string, val bool)
GetInt(key string) int
Set(key string, value string)
}
type PanicHandler interface {
HandlePanic()
}
type PMAPIProviderFactory func(string) PMAPIProvider
type PMAPIProvider interface {
SetAuths(auths chan<- *pmapi.Auth)
Auth(username, password string, info *pmapi.AuthInfo) (*pmapi.Auth, error)
AuthInfo(username string) (*pmapi.AuthInfo, error)
AuthRefresh(token string) (*pmapi.Auth, error)
Unlock(mailboxPassword string) (kr *pmcrypto.KeyRing, err error)
UnlockAddresses(passphrase []byte) error
CurrentUser() (*pmapi.User, error)
UpdateUser() (*pmapi.User, error)
Addresses() pmapi.AddressList
Logout() error
GetEvent(eventID string) (*pmapi.Event, error)
CountMessages(addressID string) ([]*pmapi.MessagesCount, error)
ListMessages(filter *pmapi.MessagesFilter) ([]*pmapi.Message, int, error)
GetMessage(apiID string) (*pmapi.Message, error)
Import([]*pmapi.ImportMsgReq) ([]*pmapi.ImportMsgRes, error)
DeleteMessages(apiIDs []string) error
LabelMessages(apiIDs []string, labelID string) error
UnlabelMessages(apiIDs []string, labelID string) error
MarkMessagesRead(apiIDs []string) error
MarkMessagesUnread(apiIDs []string) error
ListLabels() ([]*pmapi.Label, error)
CreateLabel(label *pmapi.Label) (*pmapi.Label, error)
UpdateLabel(label *pmapi.Label) (*pmapi.Label, error)
DeleteLabel(labelID string) error
EmptyFolder(labelID string, addressID string) error
ReportBugWithEmailClient(os, osVersion, title, description, username, email, emailClient string) error
SendSimpleMetric(category, action, label string) error
Auth2FA(twoFactorCode string, auth *pmapi.Auth) (*pmapi.Auth2FA, error)
GetMailSettings() (pmapi.MailSettings, error)
GetContactEmailByEmail(string, int, int) ([]pmapi.ContactEmail, error)
GetContactByID(string) (pmapi.Contact, error)
DecryptAndVerifyCards([]pmapi.Card) ([]pmapi.Card, error)
GetPublicKeysForEmail(string) ([]pmapi.PublicKey, bool, error)
SendMessage(string, *pmapi.SendMessageReq) (sent, parent *pmapi.Message, err error)
CreateDraft(m *pmapi.Message, parent string, action int) (created *pmapi.Message, err error)
CreateAttachment(att *pmapi.Attachment, r io.Reader, sig io.Reader) (created *pmapi.Attachment, err error)
KeyRingForAddressID(string) (kr *pmcrypto.KeyRing)
GetAttachment(id string) (att io.ReadCloser, err error)
}
type CredentialsStorer interface {
List() (userIDs []string, err error)
Add(userID, userName, apiToken, mailboxPassword string, emails []string) (*credentials.Credentials, error)
Get(userID string) (*credentials.Credentials, error)
SwitchAddressMode(userID string) error
UpdateEmails(userID string, emails []string) error
UpdateToken(userID, apiToken string) error
Logout(userID string) error
Delete(userID string) error
}

View File

@ -1,188 +0,0 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge
import (
"errors"
"testing"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
gomock "github.com/golang/mock/gomock"
a "github.com/stretchr/testify/assert"
)
func TestNewUserNoCredentialsStore(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
m.credentialsStore.EXPECT().Get("user").Return(nil, errors.New("fail"))
_, err := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.pmapiClient, m.storeCache, "/tmp")
a.Error(t, err)
}
func TestNewUserBridgeOutdated(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
m.credentialsStore.EXPECT().Logout("user").Return(nil).AnyTimes()
m.pmapiClient.EXPECT().AuthRefresh("token").Return(nil, pmapi.ErrUpgradeApplication).AnyTimes()
m.pmapiClient.EXPECT().SetAuths(gomock.Any())
m.eventListener.EXPECT().Emit(events.UpgradeApplicationEvent, "").AnyTimes()
m.pmapiClient.EXPECT().ListLabels().Return(nil, pmapi.ErrUpgradeApplication)
m.pmapiClient.EXPECT().Addresses().Return(nil)
checkNewUser(m)
}
func TestNewUserNoInternetConnection(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
m.pmapiClient.EXPECT().AuthRefresh("token").Return(nil, pmapi.ErrAPINotReachable).AnyTimes()
m.pmapiClient.EXPECT().SetAuths(gomock.Any())
m.eventListener.EXPECT().Emit(events.InternetOffEvent, "").AnyTimes()
m.pmapiClient.EXPECT().Addresses().Return(nil)
m.pmapiClient.EXPECT().ListLabels().Return(nil, pmapi.ErrAPINotReachable)
m.pmapiClient.EXPECT().GetEvent("").Return(nil, pmapi.ErrAPINotReachable).AnyTimes()
checkNewUser(m)
}
func TestNewUserAuthRefreshFails(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
m.credentialsStore.EXPECT().Logout("user").Return(nil)
m.pmapiClient.EXPECT().AuthRefresh("token").Return(nil, errors.New("bad token")).AnyTimes()
m.pmapiClient.EXPECT().SetAuths(gomock.Any())
m.eventListener.EXPECT().Emit(events.LogoutEvent, "user")
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
m.pmapiClient.EXPECT().Logout().Return(nil)
m.pmapiClient.EXPECT().SetAuths(nil)
m.credentialsStore.EXPECT().Logout("user").Return(nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil)
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
checkNewUserDisconnected(m)
}
func TestNewUserUnlockFails(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
m.credentialsStore.EXPECT().UpdateToken("user", ":reftok").Return(nil)
m.credentialsStore.EXPECT().Logout("user").Return(nil)
m.pmapiClient.EXPECT().SetAuths(gomock.Any())
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, errors.New("bad password"))
m.eventListener.EXPECT().Emit(events.LogoutEvent, "user")
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
m.pmapiClient.EXPECT().Logout().Return(nil)
m.pmapiClient.EXPECT().SetAuths(nil)
m.credentialsStore.EXPECT().Logout("user").Return(nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil)
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
checkNewUserDisconnected(m)
}
func TestNewUserUnlockAddressesFails(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
m.credentialsStore.EXPECT().UpdateToken("user", ":reftok").Return(nil)
m.credentialsStore.EXPECT().Logout("user").Return(nil)
m.pmapiClient.EXPECT().SetAuths(gomock.Any())
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil)
m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(errors.New("bad password"))
m.eventListener.EXPECT().Emit(events.LogoutEvent, "user")
m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user")
m.pmapiClient.EXPECT().Logout().Return(nil)
m.pmapiClient.EXPECT().SetAuths(nil)
m.credentialsStore.EXPECT().Logout("user").Return(nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil)
m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")
checkNewUserDisconnected(m)
}
func TestNewUser(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
m.credentialsStore.EXPECT().UpdateToken("user", ":reftok").Return(nil)
m.pmapiClient.EXPECT().SetAuths(gomock.Any())
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil)
m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(nil)
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil)
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil)
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil)
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil)
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil)
checkNewUser(m)
}
func checkNewUser(m mocks) {
user, _ := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.pmapiClient, m.storeCache, "/tmp")
defer cleanUpUserData(user)
_ = user.init(nil, m.pmapiClient)
waitForEvents()
a.Equal(m.t, testCredentials, user.creds)
}
func checkNewUserDisconnected(m mocks) {
user, _ := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.pmapiClient, m.storeCache, "/tmp")
defer cleanUpUserData(user)
_ = user.init(nil, m.pmapiClient)
waitForEvents()
a.Equal(m.t, testCredentialsDisconnected, user.creds)
}
func _TestUserEventRefreshUpdatesAddresses(t *testing.T) { // nolint[funlen]
a.Fail(t, "not implemented")
}

View File

@ -1,113 +0,0 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge
import (
"testing"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
gomock "github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func testNewUser(m mocks) *User {
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
m.credentialsStore.EXPECT().UpdateToken("user", ":reftok").Return(nil)
m.pmapiClient.EXPECT().SetAuths(gomock.Any())
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil)
m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(nil)
// Expectations for initial sync (when loading existing user from credentials store).
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil)
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress})
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil)
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil).AnyTimes()
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil)
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil).AnyTimes()
user, err := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.pmapiClient, m.storeCache, "/tmp")
assert.NoError(m.t, err)
err = user.init(nil, m.pmapiClient)
assert.NoError(m.t, err)
return user
}
func testNewUserForLogout(m mocks) *User {
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil).Times(2)
m.credentialsStore.EXPECT().UpdateToken("user", ":reftok").Return(nil)
m.pmapiClient.EXPECT().SetAuths(gomock.Any())
m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil)
m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil)
m.pmapiClient.EXPECT().Unlock("pass").Return(nil, nil)
m.pmapiClient.EXPECT().UnlockAddresses([]byte("pass")).Return(nil)
// These may or may not be hit depending on how fast the log out happens.
m.pmapiClient.EXPECT().SetAuths(nil).AnyTimes()
m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil).AnyTimes()
m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress}).AnyTimes()
m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil)
m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil).AnyTimes()
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil).AnyTimes()
m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil).AnyTimes()
user, err := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.pmapiClient, m.storeCache, "/tmp")
assert.NoError(m.t, err)
err = user.init(nil, m.pmapiClient)
assert.NoError(m.t, err)
return user
}
func cleanUpUserData(u *User) {
_ = u.clearStore()
}
func _TestNeverLongStorePath(t *testing.T) { // nolint[unused]
assert.Fail(t, "not implemented")
}
func TestClearStoreWithStore(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
user := testNewUserForLogout(m)
defer cleanUpUserData(user)
require.Nil(t, user.store.Close())
user.store = nil
assert.Nil(t, user.clearStore())
}
func TestClearStoreWithoutStore(t *testing.T) {
m := initMocks(t)
defer m.ctrl.Finish()
user := testNewUserForLogout(m)
defer cleanUpUserData(user)
assert.NotNil(t, user.store)
assert.Nil(t, user.clearStore())
}

View File

@ -15,16 +15,16 @@
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package args
package cmd
import (
"os"
"strings"
)
// FilterProcessSerialNumberFromArgs removes additional flag from MacOS. More info ProcessSerialNumber
// filterProcessSerialNumberFromArgs removes additional flag from MacOS. More info ProcessSerialNumber
// http://mirror.informatimago.com/next/developer.apple.com/documentation/Carbon/Reference/Process_Manager/prmref_main/data_type_5.html#//apple_ref/doc/uid/TP30000208/C001951
func FilterProcessSerialNumberFromArgs() {
func filterProcessSerialNumberFromArgs() {
tmp := os.Args[:0]
for _, arg := range os.Args {
if !strings.Contains(arg, "-psn_") {

86
internal/cmd/main.go Normal file
View File

@ -0,0 +1,86 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package cmd
import (
"os"
"runtime"
"github.com/ProtonMail/proton-bridge/pkg/constants"
"github.com/getsentry/raven-go"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
var (
log = logrus.WithField("pkg", "cmd") //nolint[gochecknoglobals]
baseFlags = []cli.Flag{ //nolint[gochecknoglobals]
cli.StringFlag{
Name: "log-level, l",
Usage: "Set the log level (one of panic, fatal, error, warn, info, debug, debug-client, debug-server)"},
cli.BoolFlag{
Name: "cli, c",
Usage: "Use command line interface"},
cli.StringFlag{
Name: "version-json, g",
Usage: "Generate json version file"},
cli.BoolFlag{
Name: "mem-prof, m",
Usage: "Generate memory profile"},
cli.BoolFlag{
Name: "cpu-prof, p",
Usage: "Generate CPU profile"},
}
)
// Main sets up Sentry, filters out unwanted args, creates app and runs it.
func Main(appName, usage string, extraFlags []cli.Flag, run func(*cli.Context) error) {
if err := raven.SetDSN(constants.DSNSentry); err != nil {
log.WithError(err).Errorln("Can not setup sentry DSN")
}
raven.SetRelease(constants.Revision)
filterProcessSerialNumberFromArgs()
filterRestartNumberFromArgs()
app := newApp(appName, usage, extraFlags, run)
logrus.SetLevel(logrus.InfoLevel)
log.WithField("version", constants.Version).
WithField("revision", constants.Revision).
WithField("build", constants.BuildTime).
WithField("runtime", runtime.GOOS).
WithField("args", os.Args).
WithField("appName", app.Name).
Info("Run app")
if err := app.Run(os.Args); err != nil {
log.Error("Program exited with error: ", err)
}
}
func newApp(appName, usage string, extraFlags []cli.Flag, run func(*cli.Context) error) *cli.App {
app := cli.NewApp()
app.Name = appName
app.Usage = usage
app.Version = constants.BuildVersion
app.Flags = append(baseFlags, extraFlags...) //nolint[gocritic]
app.Action = run
return app
}

54
internal/cmd/profiles.go Normal file
View File

@ -0,0 +1,54 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package cmd
import (
"os"
"path/filepath"
"runtime"
"runtime/pprof"
)
// StartCPUProfile starts CPU pprof.
func StartCPUProfile() {
f, err := os.Create("./cpu.pprof")
if err != nil {
log.Fatal("Could not create CPU profile: ", err)
}
if err := pprof.StartCPUProfile(f); err != nil {
log.Fatal("Could not start CPU profile: ", err)
}
}
// MakeMemoryProfile generates memory pprof.
func MakeMemoryProfile() {
name := "./mem.pprof"
f, err := os.Create(name)
if err != nil {
log.Fatal("Could not create memory profile: ", err)
}
if abs, err := filepath.Abs(name); err == nil {
name = abs
}
log.Info("Writing memory profile to ", name)
runtime.GC() // get up-to-date statistics
if err := pprof.WriteHeapProfile(f); err != nil {
log.Fatal("Could not write memory profile: ", err)
}
_ = f.Close()
}

108
internal/cmd/restart.go Normal file
View File

@ -0,0 +1,108 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package cmd
import (
"fmt"
"os"
"os/exec"
"strconv"
"strings"
"github.com/ProtonMail/proton-bridge/internal/frontend"
"github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/urfave/cli"
)
const (
// After how many crashes app gives up starting.
maxAllowedCrashes = 10
)
var (
// How many crashes happened so far in a row.
// It will be filled from args by `filterRestartNumberFromArgs`.
// Every call of `HandlePanic` will increase this number.
// Then it will be passed as argument to the next try by `RestartApp`.
numberOfCrashes = 0 //nolint[gochecknoglobals]
)
// filterRestartNumberFromArgs removes flag with a number how many restart we already did.
// See restartApp how that number is used.
func filterRestartNumberFromArgs() {
tmp := os.Args[:0]
for i, arg := range os.Args {
if !strings.HasPrefix(arg, "--restart_") {
tmp = append(tmp, arg)
continue
}
var err error
numberOfCrashes, err = strconv.Atoi(os.Args[i][10:])
if err != nil {
numberOfCrashes = maxAllowedCrashes
}
}
os.Args = tmp
}
// DisableRestart disables restart once `RestartApp` is called.
func DisableRestart() {
numberOfCrashes = maxAllowedCrashes
}
// RestartApp starts a new instance in background.
func RestartApp() {
if numberOfCrashes >= maxAllowedCrashes {
log.Error("Too many crashes")
return
}
if exeFile, err := os.Executable(); err == nil {
arguments := append(os.Args[1:], fmt.Sprintf("--restart_%d", numberOfCrashes))
cmd := exec.Command(exeFile, arguments...) //nolint[gosec]
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
if err := cmd.Start(); err != nil {
log.Error("Restart failed: ", err)
}
}
}
// PanicHandler defines HandlePanic which can be used anywhere in defer.
type PanicHandler struct {
AppName string
Config *config.Config
Err *error // Pointer to error of cli action.
}
// HandlePanic should be called in defer to ensure restart of app after error.
func (ph *PanicHandler) HandlePanic() {
r := recover()
if r == nil {
return
}
config.HandlePanic(ph.Config, fmt.Sprintf("Recover: %v", r))
frontend.HandlePanic(ph.AppName)
*ph.Err = cli.NewExitError("Panic and restart", 255)
numberOfCrashes++
log.Error("Restarting after panic")
RestartApp()
os.Exit(255)
}

View File

@ -0,0 +1,32 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package cmd
import "github.com/ProtonMail/proton-bridge/internal/updates"
// GenerateVersionFiles writes a JSON file with details about current build.
// Those files are used for upgrading the app.
func GenerateVersionFiles(updates *updates.Updates, dir string) {
log.Info("Generating version files")
for _, goos := range []string{"windows", "darwin", "linux"} {
log.Debug("Generating JSON for ", goos)
if err := updates.CreateJSONAndSign(dir, goos); err != nil {
log.Error(err)
}
}
}

92
internal/cookies/jar.go Normal file
View File

@ -0,0 +1,92 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Package cookies implements a persistent cookie jar which satisfies the http.CookieJar interface.
package cookies
import (
"net/http"
"net/http/cookiejar"
"net/url"
"sync"
"github.com/sirupsen/logrus"
)
// Jar implements http.CookieJar by wrapping the standard library's cookiejar.Jar.
// The jar uses a pantry to load cookies at startup and save cookies when set.
type Jar struct {
jar *cookiejar.Jar
pantry *pantry
locker sync.Locker
}
type GetterSetter interface {
Get(string) string
Set(string, string)
}
func NewCookieJar(gs GetterSetter) (*Jar, error) {
pantry := &pantry{gs: gs}
if err := pantry.discardExpiredCookies(); err != nil {
return nil, err
}
cookies, err := pantry.loadFromJSON()
if err != nil {
return nil, err
}
jar, err := cookiejar.New(nil)
if err != nil {
return nil, err
}
for rawURL, cookies := range cookies {
url, err := url.Parse(rawURL)
if err != nil {
continue
}
jar.SetCookies(url, cookies)
}
return &Jar{
jar: jar,
pantry: pantry,
locker: &sync.Mutex{},
}, nil
}
func (j *Jar) SetCookies(u *url.URL, cookies []*http.Cookie) {
j.locker.Lock()
defer j.locker.Unlock()
j.jar.SetCookies(u, cookies)
if err := j.pantry.persistCookies(u.Scheme+"://"+u.Host, cookies); err != nil {
logrus.WithError(err).Warn("Failed to persist cookie")
}
}
func (j *Jar) Cookies(u *url.URL) []*http.Cookie {
j.locker.Lock()
defer j.locker.Unlock()
return j.jar.Cookies(u)
}

View File

@ -0,0 +1,168 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package cookies
import (
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestJarGetSet(t *testing.T) {
ts := getTestServer(t, []testCookie{
{"TestName1", "TestValue1", 3600},
{"TestName2", "TestValue2", 3600},
{"TestName3", "TestValue3", 3600},
})
defer ts.Close()
client := getClientWithJar(t, make(testGetterSetter))
// Hit a server that sets some cookies.
setRes, err := client.Get(ts.URL + "/set")
if err != nil {
t.FailNow()
}
require.NoError(t, setRes.Body.Close())
// Hit a server that checks the cookies are there.
getRes, err := client.Get(ts.URL + "/get")
if err != nil {
t.FailNow()
}
require.NoError(t, getRes.Body.Close())
}
func TestJarLoad(t *testing.T) {
ts := getTestServer(t, []testCookie{
{"TestName1", "TestValue1", 3600},
{"TestName2", "TestValue2", 3600},
{"TestName3", "TestValue3", 3600},
})
defer ts.Close()
// This will be our "persistent storage" from which the cookie jar should load cookies.
gs := make(testGetterSetter)
// This client saves cookies to persistent storage.
oldClient := getClientWithJar(t, gs)
// Hit a server that sets some cookies.
setRes, err := oldClient.Get(ts.URL + "/set")
if err != nil {
t.FailNow()
}
require.NoError(t, setRes.Body.Close())
// This client loads cookies from persistent storage.
newClient := getClientWithJar(t, gs)
// Hit a server that checks the cookies are there.
getRes, err := newClient.Get(ts.URL + "/get")
if err != nil {
t.FailNow()
}
require.NoError(t, getRes.Body.Close())
}
func TestJarExpiry(t *testing.T) {
ts := getTestServer(t, []testCookie{
{"TestName1", "TestValue1", 3600},
{"TestName2", "TestValue2", 1},
{"TestName3", "TestValue3", 3600},
})
defer ts.Close()
// This will be our "persistent storage" from which the cookie jar should load cookies.
gs := make(testGetterSetter)
// This client saves cookies to persistent storage.
oldClient := getClientWithJar(t, gs)
// Hit a server that sets some cookies.
setRes, err := oldClient.Get(ts.URL + "/set")
if err != nil {
t.FailNow()
}
require.NoError(t, setRes.Body.Close())
// Wait until the second cookie expires.
time.Sleep(2 * time.Second)
// Load a client, which will clear out expired cookies.
_ = getClientWithJar(t, gs)
assert.Contains(t, gs["cookies"], "TestName1")
assert.NotContains(t, gs["cookies"], "TestName2")
assert.Contains(t, gs["cookies"], "TestName3")
}
type testCookie struct {
name, value string
maxAge int
}
func getClientWithJar(t *testing.T, gs GetterSetter) *http.Client {
jar, err := NewCookieJar(gs)
require.NoError(t, err)
return &http.Client{Jar: jar}
}
func getTestServer(t *testing.T, wantCookies []testCookie) *httptest.Server {
mux := http.NewServeMux()
mux.HandleFunc("/set", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
for _, cookie := range wantCookies {
http.SetCookie(w, &http.Cookie{
Name: cookie.name,
Value: cookie.value,
MaxAge: cookie.maxAge,
})
}
w.WriteHeader(http.StatusOK)
}))
mux.HandleFunc("/get", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Len(t, r.Cookies(), len(wantCookies))
for k, v := range r.Cookies() {
assert.Equal(t, wantCookies[k].name, v.Name)
assert.Equal(t, wantCookies[k].value, v.Value)
}
w.WriteHeader(http.StatusOK)
}))
return httptest.NewServer(mux)
}
type testGetterSetter map[string]string
func (p testGetterSetter) Set(key, value string) {
p[key] = value
}
func (p testGetterSetter) Get(key string) string {
return p[key]
}

100
internal/cookies/pantry.go Normal file
View File

@ -0,0 +1,100 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package cookies
import (
"encoding/json"
"net/http"
"time"
"github.com/ProtonMail/proton-bridge/internal/preferences"
)
// pantry persists and loads cookies to some persistent storage location.
type pantry struct {
gs GetterSetter
}
func (p *pantry) persistCookies(host string, cookies []*http.Cookie) error {
for _, cookie := range cookies {
if cookie.MaxAge > 0 {
cookie.Expires = time.Now().Add(time.Duration(cookie.MaxAge) * time.Second)
}
}
cookiesByHost, err := p.loadFromJSON()
if err != nil {
return err
}
cookiesByHost[host] = cookies
return p.saveToJSON(cookiesByHost)
}
func (p *pantry) discardExpiredCookies() error {
cookiesByHost, err := p.loadFromJSON()
if err != nil {
return err
}
for host, cookies := range cookiesByHost {
cookiesByHost[host] = discardExpiredCookies(cookies)
}
return p.saveToJSON(cookiesByHost)
}
type cookiesByHost map[string][]*http.Cookie
func (p *pantry) loadFromJSON() (cookiesByHost, error) {
b := p.gs.Get(preferences.CookiesKey)
if b == "" {
return make(cookiesByHost), nil
}
var cookies cookiesByHost
if err := json.Unmarshal([]byte(b), &cookies); err != nil {
return nil, err
}
return cookies, nil
}
func (p *pantry) saveToJSON(cookies cookiesByHost) error {
b, err := json.Marshal(cookies)
if err != nil {
return err
}
p.gs.Set(preferences.CookiesKey, string(b))
return nil
}
func discardExpiredCookies(cookies []*http.Cookie) (validCookies []*http.Cookie) {
for _, cookie := range cookies {
if cookie.Expires.After(time.Now()) {
validCookies = append(validCookies, cookie)
}
}
return
}

View File

@ -40,6 +40,7 @@ const (
NoActiveKeyForRecipientEvent = "noActiveKeyForRecipient"
UpgradeApplicationEvent = "upgradeApplication"
TLSCertIssue = "tlsCertPinningIssue"
IMAPTLSBadCert = "imapTLSBadCert"
// LogoutEventTimeout is the minimum time to permit between logout events being sent.
LogoutEventTimeout = 3 * time.Minute

View File

@ -28,9 +28,9 @@ import (
"strings"
"time"
mobileconfig "github.com/ProtonMail/go-apple-mobileconfig"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
mobileconfig "github.com/ProtonMail/go-apple-mobileconfig"
)
func init() { //nolint[gochecknoinit]
@ -43,7 +43,7 @@ func (c *appleMail) Name() string {
return "Apple Mail"
}
func (c *appleMail) Configure(imapPort, smtpPort int, imapSSL, smtpSSL bool, user types.BridgeUser, addressIndex int) error { //nolint[funlen]
func (c *appleMail) Configure(imapPort, smtpPort int, imapSSL, smtpSSL bool, user types.User, addressIndex int) error { //nolint[funlen]
var addresses string
var displayName string

View File

@ -23,7 +23,7 @@ import "github.com/ProtonMail/proton-bridge/internal/frontend/types"
type AutoConfig interface {
Name() string
Configure(imapPort int, smtpPort int, imapSSl, smtpSSL bool, user types.BridgeUser, addressIndex int) error
Configure(imapPort int, smtpPort int, imapSSl, smtpSSL bool, user types.User, addressIndex int) error
}
var available []AutoConfig //nolint[gochecknoglobals]

View File

@ -0,0 +1,100 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package cliie
import (
"fmt"
"strconv"
"strings"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/abiosoft/ishell"
)
// completeUsernames is a helper to complete usernames as the user types.
func (f *frontendCLI) completeUsernames(args []string) (usernames []string) {
if len(args) > 1 {
return
}
arg := ""
if len(args) == 1 {
arg = args[0]
}
for _, user := range f.ie.GetUsers() {
if strings.HasPrefix(strings.ToLower(user.Username()), strings.ToLower(arg)) {
usernames = append(usernames, user.Username())
}
}
return
}
// noAccountWrapper is a decorator for functions which need any account to be properly functional.
func (f *frontendCLI) noAccountWrapper(callback func(*ishell.Context)) func(*ishell.Context) {
return func(c *ishell.Context) {
users := f.ie.GetUsers()
if len(users) == 0 {
f.Println("No active accounts. Please add account to continue.")
} else {
callback(c)
}
}
}
func (f *frontendCLI) askUserByIndexOrName(c *ishell.Context) types.User {
user := f.getUserByIndexOrName("")
if user != nil {
return user
}
numberOfAccounts := len(f.ie.GetUsers())
indexRange := fmt.Sprintf("number between 0 and %d", numberOfAccounts-1)
if len(c.Args) == 0 {
f.Printf("Please choose %s or username.\n", indexRange)
return nil
}
arg := c.Args[0]
user = f.getUserByIndexOrName(arg)
if user == nil {
f.Printf("Wrong input '%s'. Choose %s or username.\n", bold(arg), indexRange)
return nil
}
return user
}
func (f *frontendCLI) getUserByIndexOrName(arg string) types.User {
users := f.ie.GetUsers()
numberOfAccounts := len(users)
if numberOfAccounts == 0 {
return nil
}
if numberOfAccounts == 1 {
return users[0]
}
if index, err := strconv.Atoi(arg); err == nil {
if index < 0 || index >= numberOfAccounts {
return nil
}
return users[index]
}
for _, user := range users {
if user.Username() == arg {
return user
}
}
return nil
}

View File

@ -0,0 +1,153 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package cliie
import (
"strings"
"github.com/abiosoft/ishell"
)
func (f *frontendCLI) listAccounts(c *ishell.Context) {
spacing := "%-2d: %-20s (%-15s, %-15s)\n"
f.Printf(bold(strings.Replace(spacing, "d", "s", -1)), "#", "account", "status", "address mode")
for idx, user := range f.ie.GetUsers() {
connected := "disconnected"
if user.IsConnected() {
connected = "connected"
}
mode := "split"
if user.IsCombinedAddressMode() {
mode = "combined"
}
f.Printf(spacing, idx, user.Username(), connected, mode)
}
f.Println()
}
func (f *frontendCLI) loginAccount(c *ishell.Context) { // nolint[funlen]
f.ShowPrompt(false)
defer f.ShowPrompt(true)
loginName := ""
if len(c.Args) > 0 {
user := f.getUserByIndexOrName(c.Args[0])
if user != nil {
loginName = user.GetPrimaryAddress()
}
}
if loginName == "" {
loginName = f.readStringInAttempts("Username", c.ReadLine, isNotEmpty)
if loginName == "" {
return
}
} else {
f.Println("Username:", loginName)
}
password := f.readStringInAttempts("Password", c.ReadPassword, isNotEmpty)
if password == "" {
return
}
f.Println("Authenticating ... ")
client, auth, err := f.ie.Login(loginName, password)
if err != nil {
f.processAPIError(err)
return
}
if auth.HasTwoFactor() {
twoFactor := f.readStringInAttempts("Two factor code", c.ReadLine, isNotEmpty)
if twoFactor == "" {
return
}
err = client.Auth2FA(twoFactor, auth)
if err != nil {
f.processAPIError(err)
return
}
}
mailboxPassword := password
if auth.HasMailboxPassword() {
mailboxPassword = f.readStringInAttempts("Mailbox password", c.ReadPassword, isNotEmpty)
}
if mailboxPassword == "" {
return
}
f.Println("Adding account ...")
user, err := f.ie.FinishLogin(client, auth, mailboxPassword)
if err != nil {
log.WithField("username", loginName).WithError(err).Error("Login was unsuccessful")
f.Println("Adding account was unsuccessful:", err)
return
}
f.Printf("Account %s was added successfully.\n", bold(user.Username()))
}
func (f *frontendCLI) logoutAccount(c *ishell.Context) {
f.ShowPrompt(false)
defer f.ShowPrompt(true)
user := f.askUserByIndexOrName(c)
if user == nil {
return
}
if f.yesNoQuestion("Are you sure you want to logout account " + bold(user.Username())) {
if err := user.Logout(); err != nil {
f.printAndLogError("Logging out failed: ", err)
}
}
}
func (f *frontendCLI) deleteAccount(c *ishell.Context) {
f.ShowPrompt(false)
defer f.ShowPrompt(true)
user := f.askUserByIndexOrName(c)
if user == nil {
return
}
if f.yesNoQuestion("Are you sure you want to " + bold("remove account "+user.Username())) {
clearCache := f.yesNoQuestion("Do you want to remove cache for this account")
if err := f.ie.DeleteUser(user.ID(), clearCache); err != nil {
f.printAndLogError("Cannot delete account: ", err)
return
}
}
}
func (f *frontendCLI) deleteAccounts(c *ishell.Context) {
f.ShowPrompt(false)
defer f.ShowPrompt(true)
if !f.yesNoQuestion("Do you really want remove all accounts") {
return
}
for _, user := range f.ie.GetUsers() {
if err := f.ie.DeleteUser(user.ID(), false); err != nil {
f.printAndLogError("Cannot delete account ", user.Username(), ": ", err)
}
}
c.Println("Keychain cleared")
}

View File

@ -0,0 +1,237 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Package cliie provides CLI interface of the Import-Export app.
package cliie
import (
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/abiosoft/ishell"
"github.com/sirupsen/logrus"
)
var (
log = logrus.WithField("pkg", "frontend/cli-ie") //nolint[gochecknoglobals]
)
type frontendCLI struct {
*ishell.Shell
config *config.Config
eventListener listener.Listener
updates types.Updater
ie types.ImportExporter
appRestart bool
}
// New returns a new CLI frontend configured with the given options.
func New( //nolint[funlen]
panicHandler types.PanicHandler,
config *config.Config,
eventListener listener.Listener,
updates types.Updater,
ie types.ImportExporter,
) *frontendCLI { //nolint[golint]
fe := &frontendCLI{
Shell: ishell.New(),
config: config,
eventListener: eventListener,
updates: updates,
ie: ie,
appRestart: false,
}
// Clear commands.
clearCmd := &ishell.Cmd{Name: "clear",
Help: "remove stored accounts and preferences. (alias: cl)",
Aliases: []string{"cl"},
}
clearCmd.AddCmd(&ishell.Cmd{Name: "accounts",
Help: "remove all accounts from keychain. (aliases: a, k, keychain)",
Aliases: []string{"a", "k", "keychain"},
Func: fe.deleteAccounts,
})
fe.AddCmd(clearCmd)
// Check commands.
checkCmd := &ishell.Cmd{Name: "check", Help: "check internet connection or new version."}
checkCmd.AddCmd(&ishell.Cmd{Name: "updates",
Help: "check for Import-Export updates. (aliases: u, v, version)",
Aliases: []string{"u", "version", "v"},
Func: fe.checkUpdates,
})
checkCmd.AddCmd(&ishell.Cmd{Name: "internet",
Help: "check internet connection. (aliases: i, conn, connection)",
Aliases: []string{"i", "con", "connection"},
Func: fe.checkInternetConnection,
})
fe.AddCmd(checkCmd)
// Print info commands.
fe.AddCmd(&ishell.Cmd{Name: "log-dir",
Help: "print path to directory with logs. (aliases: log, logs)",
Aliases: []string{"log", "logs"},
Func: fe.printLogDir,
})
fe.AddCmd(&ishell.Cmd{Name: "manual",
Help: "print URL with instructions. (alias: man)",
Aliases: []string{"man"},
Func: fe.printManual,
})
fe.AddCmd(&ishell.Cmd{Name: "release-notes",
Help: "print release notes. (aliases: notes, fixed-bugs, bugs, ver, version)",
Aliases: []string{"notes", "fixed-bugs", "bugs", "ver", "version"},
Func: fe.printLocalReleaseNotes,
})
fe.AddCmd(&ishell.Cmd{Name: "credits",
Help: "print used resources.",
Func: fe.printCredits,
})
// Account commands.
fe.AddCmd(&ishell.Cmd{Name: "list",
Help: "print the list of accounts. (aliases: l, ls)",
Func: fe.noAccountWrapper(fe.listAccounts),
Aliases: []string{"l", "ls"},
})
fe.AddCmd(&ishell.Cmd{Name: "login",
Help: "login procedure to add or connect account. Optionally use index or account as parameter. (aliases: a, add, con, connect)",
Func: fe.loginAccount,
Aliases: []string{"add", "a", "con", "connect"},
Completer: fe.completeUsernames,
})
fe.AddCmd(&ishell.Cmd{Name: "logout",
Help: "disconnect the account. Use index or account name as parameter. (aliases: d, disconnect)",
Func: fe.noAccountWrapper(fe.logoutAccount),
Aliases: []string{"d", "disconnect"},
Completer: fe.completeUsernames,
})
fe.AddCmd(&ishell.Cmd{Name: "delete",
Help: "remove the account from keychain. Use index or account name as parameter. (aliases: del, rm, remove)",
Func: fe.noAccountWrapper(fe.deleteAccount),
Aliases: []string{"del", "rm", "remove"},
Completer: fe.completeUsernames,
})
// Import-Export commands.
importCmd := &ishell.Cmd{Name: "import",
Help: "import messages. (alias: imp)",
Aliases: []string{"imp"},
}
importCmd.AddCmd(&ishell.Cmd{Name: "local",
Help: "import local messages. (aliases: loc)",
Func: fe.noAccountWrapper(fe.importLocalMessages),
Aliases: []string{"loc"},
})
importCmd.AddCmd(&ishell.Cmd{Name: "remote",
Help: "import remote messages. (aliases: rem)",
Func: fe.noAccountWrapper(fe.importRemoteMessages),
Aliases: []string{"rem"},
})
fe.AddCmd(importCmd)
exportCmd := &ishell.Cmd{Name: "export",
Help: "export messages. (alias: exp)",
Aliases: []string{"exp"},
}
exportCmd.AddCmd(&ishell.Cmd{Name: "eml",
Help: "export messages to eml files.",
Func: fe.noAccountWrapper(fe.exportMessagesToEML),
})
exportCmd.AddCmd(&ishell.Cmd{Name: "mbox",
Help: "export messages to mbox files.",
Func: fe.noAccountWrapper(fe.exportMessagesToMBOX),
})
fe.AddCmd(exportCmd)
// System commands.
fe.AddCmd(&ishell.Cmd{Name: "restart",
Help: "restart the Import-Export app.",
Func: fe.restart,
})
go func() {
defer panicHandler.HandlePanic()
fe.watchEvents()
}()
fe.eventListener.RetryEmit(events.TLSCertIssue)
fe.eventListener.RetryEmit(events.ErrorEvent)
return fe
}
func (f *frontendCLI) watchEvents() {
errorCh := f.getEventChannel(events.ErrorEvent)
internetOffCh := f.getEventChannel(events.InternetOffEvent)
internetOnCh := f.getEventChannel(events.InternetOnEvent)
addressChangedLogoutCh := f.getEventChannel(events.AddressChangedLogoutEvent)
logoutCh := f.getEventChannel(events.LogoutEvent)
certIssue := f.getEventChannel(events.TLSCertIssue)
for {
select {
case errorDetails := <-errorCh:
f.Println("Import-Export failed:", errorDetails)
case <-internetOffCh:
f.notifyInternetOff()
case <-internetOnCh:
f.notifyInternetOn()
case address := <-addressChangedLogoutCh:
f.notifyLogout(address)
case userID := <-logoutCh:
user, err := f.ie.GetUser(userID)
if err != nil {
return
}
f.notifyLogout(user.Username())
case <-certIssue:
f.notifyCertIssue()
}
}
}
func (f *frontendCLI) getEventChannel(event string) <-chan string {
ch := make(chan string)
f.eventListener.Add(event, ch)
return ch
}
// IsAppRestarting returns whether the app is currently set to restart.
func (f *frontendCLI) IsAppRestarting() bool {
return f.appRestart
}
// Loop starts the frontend loop with an interactive shell.
func (f *frontendCLI) Loop(credentialsError error) error {
if credentialsError != nil {
f.notifyCredentialsError()
return credentialsError
}
f.Print(`
Welcome to ProtonMail Import-Export app interactive shell
WARNING: The CLI is an experimental feature and does not yet cover all functionality.
`)
f.Run()
return nil
}

View File

@ -0,0 +1,224 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package cliie
import (
"fmt"
"os"
"strings"
"time"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/internal/transfer"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/abiosoft/ishell"
)
func (f *frontendCLI) importLocalMessages(c *ishell.Context) {
f.ShowPrompt(false)
defer f.ShowPrompt(true)
user, path := f.getUserAndPath(c, false)
if user == nil || path == "" {
return
}
t, err := f.ie.GetLocalImporter(user.GetPrimaryAddress(), path)
f.transfer(t, err, false, true)
}
func (f *frontendCLI) importRemoteMessages(c *ishell.Context) {
f.ShowPrompt(false)
defer f.ShowPrompt(true)
user := f.askUserByIndexOrName(c)
if user == nil {
return
}
username := f.readStringInAttempts("IMAP username", c.ReadLine, isNotEmpty)
if username == "" {
return
}
password := f.readStringInAttempts("IMAP password", c.ReadPassword, isNotEmpty)
if password == "" {
return
}
host := f.readStringInAttempts("IMAP host", c.ReadLine, isNotEmpty)
if host == "" {
return
}
port := f.readStringInAttempts("IMAP port", c.ReadLine, isNotEmpty)
if port == "" {
return
}
t, err := f.ie.GetRemoteImporter(user.GetPrimaryAddress(), username, password, host, port)
f.transfer(t, err, false, true)
}
func (f *frontendCLI) exportMessagesToEML(c *ishell.Context) {
f.ShowPrompt(false)
defer f.ShowPrompt(true)
user, path := f.getUserAndPath(c, true)
if user == nil || path == "" {
return
}
t, err := f.ie.GetEMLExporter(user.GetPrimaryAddress(), path)
f.transfer(t, err, true, false)
}
func (f *frontendCLI) exportMessagesToMBOX(c *ishell.Context) {
f.ShowPrompt(false)
defer f.ShowPrompt(true)
user, path := f.getUserAndPath(c, true)
if user == nil || path == "" {
return
}
t, err := f.ie.GetMBOXExporter(user.GetPrimaryAddress(), path)
f.transfer(t, err, true, false)
}
func (f *frontendCLI) getUserAndPath(c *ishell.Context, createPath bool) (types.User, string) {
user := f.askUserByIndexOrName(c)
if user == nil {
return nil, ""
}
path := f.readStringInAttempts("Path of EML and MBOX files", c.ReadLine, isNotEmpty)
if path == "" {
return nil, ""
}
if createPath {
_ = os.Mkdir(path, os.ModePerm)
}
return user, path
}
func (f *frontendCLI) transfer(t *transfer.Transfer, err error, askSkipEncrypted bool, askGlobalMailbox bool) {
if err != nil {
f.printAndLogError("Failed to init transferrer: ", err)
return
}
if askSkipEncrypted {
skipEncryptedMessages := f.yesNoQuestion("Skip encrypted messages")
t.SetSkipEncryptedMessages(skipEncryptedMessages)
}
if !f.setTransferRules(t) {
return
}
if askGlobalMailbox {
if err := f.setTransferGlobalMailbox(t); err != nil {
f.printAndLogError("Failed to create global mailbox: ", err)
return
}
}
progress := t.Start()
for range progress.GetUpdateChannel() {
f.printTransferProgress(progress)
}
f.printTransferResult(progress)
}
func (f *frontendCLI) setTransferGlobalMailbox(t *transfer.Transfer) error {
labelName := fmt.Sprintf("Imported %s", time.Now().Format("Jan-02-2006 15:04"))
useGlobalLabel := f.yesNoQuestion("Use global label " + labelName)
if !useGlobalLabel {
return nil
}
globalMailbox, err := t.CreateTargetMailbox(transfer.Mailbox{
Name: labelName,
Color: pmapi.LabelColors[0],
IsExclusive: false,
})
if err != nil {
return err
}
t.SetGlobalMailbox(&globalMailbox)
return nil
}
func (f *frontendCLI) setTransferRules(t *transfer.Transfer) bool {
f.Println("Rules:")
for _, rule := range t.GetRules() {
if !rule.Active {
continue
}
targets := strings.Join(rule.TargetMailboxNames(), ", ")
if rule.HasTimeLimit() {
f.Printf(" %-30s → %s (%s - %s)\n", rule.SourceMailbox.Name, targets, rule.FromDate(), rule.ToDate())
} else {
f.Printf(" %-30s → %s\n", rule.SourceMailbox.Name, targets)
}
}
return f.yesNoQuestion("Proceed")
}
func (f *frontendCLI) printTransferProgress(progress *transfer.Progress) {
failed, imported, exported, added, total := progress.GetCounts()
if total != 0 {
f.Println(fmt.Sprintf("Progress update: %d (%d / %d) / %d, failed: %d", imported, exported, added, total, failed))
}
if progress.IsPaused() {
f.Printf("Transfer is paused bacause %s", progress.PauseReason())
if !f.yesNoQuestion("Continue (y) or stop (n)") {
progress.Stop()
}
}
}
func (f *frontendCLI) printTransferResult(progress *transfer.Progress) {
err := progress.GetFatalError()
if err != nil {
f.Println("Transfer failed: " + err.Error())
return
}
statuses := progress.GetFailedMessages()
if len(statuses) == 0 {
f.Println("Transfer finished!")
return
}
f.Println("Transfer finished with errors:")
for _, messageStatus := range statuses {
f.Printf(
" %-17s | %-30s | %-30s\n %s: %s\n",
messageStatus.Time.Format("Jan 02 2006 15:04"),
messageStatus.From,
messageStatus.Subject,
messageStatus.SourceID,
messageStatus.GetErrorMessage(),
)
}
}

View File

@ -0,0 +1,46 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package cliie
import (
"github.com/abiosoft/ishell"
)
func (f *frontendCLI) restart(c *ishell.Context) {
if f.yesNoQuestion("Are you sure you want to restart the Import-Export app") {
f.Println("Restarting the Import-Export app...")
f.appRestart = true
f.Stop()
}
}
func (f *frontendCLI) checkInternetConnection(c *ishell.Context) {
if f.ie.CheckConnection() == nil {
f.Println("Internet connection is available.")
} else {
f.Println("Can not contact the server, please check your internet connection.")
}
}
func (f *frontendCLI) printLogDir(c *ishell.Context) {
f.Println("Log files are stored in\n\n ", f.config.GetLogDir())
}
func (f *frontendCLI) printManual(c *ishell.Context) {
f.Println("More instructions about the Import-Export app can be found at\n\n https://protonmail.com/support/categories/import-export/")
}

View File

@ -0,0 +1,65 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package cliie
import (
"strings"
"github.com/ProtonMail/proton-bridge/internal/importexport"
"github.com/ProtonMail/proton-bridge/internal/updates"
"github.com/abiosoft/ishell"
)
func (f *frontendCLI) checkUpdates(c *ishell.Context) {
isUpToDate, latestVersionInfo, err := f.updates.CheckIsUpToDate()
if err != nil {
f.printAndLogError("Cannot retrieve version info: ", err)
f.checkInternetConnection(c)
return
}
if isUpToDate {
f.Println("Your version is up to date.")
} else {
f.notifyNeedUpgrade()
f.Println("")
f.printReleaseNotes(latestVersionInfo)
}
}
func (f *frontendCLI) printLocalReleaseNotes(c *ishell.Context) {
localVersion := f.updates.GetLocalVersion()
f.printReleaseNotes(localVersion)
}
func (f *frontendCLI) printReleaseNotes(versionInfo updates.VersionInfo) {
f.Println(bold("ProtonMail Import-Export app "+versionInfo.Version), "\n")
if versionInfo.ReleaseNotes != "" {
f.Println(bold("Release Notes"))
f.Println(versionInfo.ReleaseNotes)
}
if versionInfo.ReleaseFixedBugs != "" {
f.Println(bold("Fixed bugs"))
f.Println(versionInfo.ReleaseFixedBugs)
}
}
func (f *frontendCLI) printCredits(c *ishell.Context) {
for _, pkg := range strings.Split(importexport.Credits, ";") {
f.Println(pkg)
}
}

View File

@ -0,0 +1,123 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package cliie
import (
"strings"
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/fatih/color"
)
const (
maxInputRepeat = 2
)
var (
bold = color.New(color.Bold).SprintFunc() //nolint[gochecknoglobals]
)
func isNotEmpty(val string) bool {
return val != ""
}
func (f *frontendCLI) yesNoQuestion(question string) bool {
f.Print(question, "? yes/"+bold("no")+": ")
yes := "yes"
answer := strings.ToLower(f.ReadLine())
for i := 0; i < len(answer); i++ {
if i >= len(yes) || answer[i] != yes[i] {
return false // Everything else is false.
}
}
return len(answer) > 0 // Empty is false.
}
func (f *frontendCLI) readStringInAttempts(title string, readFunc func() string, isOK func(string) bool) (value string) {
f.Printf("%s: ", title)
value = readFunc()
title = strings.ToLower(string(title[0])) + title[1:]
for i := 0; !isOK(value); i++ {
if i >= maxInputRepeat {
f.Println("Too many attempts")
return ""
}
f.Printf("Please fill %s: ", title)
value = readFunc()
}
return
}
func (f *frontendCLI) printAndLogError(args ...interface{}) {
log.Error(args...)
f.Println(args...)
}
func (f *frontendCLI) processAPIError(err error) {
log.Warn("API error: ", err)
switch err {
case pmapi.ErrAPINotReachable:
f.notifyInternetOff()
case pmapi.ErrUpgradeApplication:
f.notifyNeedUpgrade()
default:
f.Println("Server error:", err.Error())
}
}
func (f *frontendCLI) notifyInternetOff() {
f.Println("Internet connection is not available.")
}
func (f *frontendCLI) notifyInternetOn() {
f.Println("Internet connection is available again.")
}
func (f *frontendCLI) notifyLogout(address string) {
f.Printf("Account %s is disconnected. Login to continue using this account with email client.", address)
}
func (f *frontendCLI) notifyNeedUpgrade() {
f.Println("Please download and install the newest version of application from", f.updates.GetDownloadLink())
}
func (f *frontendCLI) notifyCredentialsError() {
// Print in 80-column width.
f.Println("ProtonMail Import-Export app is not able to detect a supported password manager")
f.Println("(pass, gnome-keyring). Please install and set up a supported password manager")
f.Println("and restart the application.")
}
func (f *frontendCLI) notifyCertIssue() {
// Print in 80-column width.
f.Println(`Connection security error: Your network connection to Proton services may
be insecure.
Description:
ProtonMail Import-Export was not able to establish a secure connection to Proton
servers due to a TLS certificate error. This means your connection may
potentially be insecure and susceptible to monitoring by third parties.
Recommendation:
* If you trust your network operator, you can continue to use ProtonMail
as usual.
* If you don't trust your network operator, reconnect to ProtonMail over a VPN
(such as ProtonVPN) which encrypts your Internet connection, or use
a different network to access ProtonMail.
`)
}

View File

@ -55,7 +55,7 @@ func (f *frontendCLI) noAccountWrapper(callback func(*ishell.Context)) func(*ish
}
}
func (f *frontendCLI) askUserByIndexOrName(c *ishell.Context) types.BridgeUser {
func (f *frontendCLI) askUserByIndexOrName(c *ishell.Context) types.User {
user := f.getUserByIndexOrName("")
if user != nil {
return user
@ -76,7 +76,7 @@ func (f *frontendCLI) askUserByIndexOrName(c *ishell.Context) types.BridgeUser {
return user
}
func (f *frontendCLI) getUserByIndexOrName(arg string) types.BridgeUser {
func (f *frontendCLI) getUserByIndexOrName(arg string) types.User {
users := f.bridge.GetUsers()
numberOfAccounts := len(users)
if numberOfAccounts == 0 {

View File

@ -63,7 +63,7 @@ func (f *frontendCLI) showAccountInfo(c *ishell.Context) {
}
}
func (f *frontendCLI) showAccountAddressInfo(user types.BridgeUser, address string) {
func (f *frontendCLI) showAccountAddressInfo(user types.User, address string) {
smtpSecurity := "STARTTLS"
if f.preferences.GetBool(preferences.SMTPSSLKey) {
smtpSecurity = "SSL"
@ -126,7 +126,7 @@ func (f *frontendCLI) loginAccount(c *ishell.Context) { // nolint[funlen]
return
}
_, err = client.Auth2FA(twoFactor, auth)
err = client.Auth2FA(twoFactor, auth)
if err != nil {
f.processAPIError(err)
return

View File

@ -21,15 +21,15 @@ package cli
import (
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/abiosoft/ishell"
"github.com/sirupsen/logrus"
)
var (
log = config.GetLogEntry("frontend/cli") //nolint[gochecknoglobals]
log = logrus.WithField("pkg", "frontend/cli") //nolint[gochecknoglobals]
)
type frontendCLI struct {
@ -76,7 +76,7 @@ func New( //nolint[funlen]
Func: fe.deleteCache,
})
clearCmd.AddCmd(&ishell.Cmd{Name: "accounts",
Help: "remove all accounts from keychain. (aliases: k, keychain)",
Help: "remove all accounts from keychain. (aliases: a, k, keychain)",
Aliases: []string{"a", "k", "keychain"},
Func: fe.deleteAccounts,
})
@ -240,8 +240,6 @@ func (f *frontendCLI) Loop(credentialsError error) error {
return credentialsError
}
f.preferences.SetBool(preferences.FirstStartKey, false)
f.Print(`
Welcome to ProtonMail Bridge interactive shell
___....___

View File

@ -22,9 +22,7 @@ import (
"strconv"
"strings"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/pkg/connection"
"github.com/ProtonMail/proton-bridge/pkg/ports"
"github.com/abiosoft/ishell"
)
@ -42,10 +40,10 @@ func (f *frontendCLI) restart(c *ishell.Context) {
}
func (f *frontendCLI) checkInternetConnection(c *ishell.Context) {
if connection.CheckInternetConnection() == nil {
if f.bridge.CheckConnection() == nil {
f.Println("Internet connection is available.")
} else {
f.Println("Can not contact server please check you internet connection.")
f.Println("Can not contact the server, please check your internet connection.")
}
}
@ -135,13 +133,13 @@ func (f *frontendCLI) toggleAllowProxy(c *ishell.Context) {
f.Println("Bridge is currently set to use alternative routing to connect to Proton if it is being blocked.")
if f.yesNoQuestion("Are you sure you want to stop bridge from doing this") {
f.preferences.SetBool(preferences.AllowProxyKey, false)
bridge.DisallowDoH()
f.bridge.DisallowProxy()
}
} else {
f.Println("Bridge is currently set to NOT use alternative routing to connect to Proton if it is being blocked.")
if f.yesNoQuestion("Are you sure you want to allow bridge to do this") {
f.preferences.SetBool(preferences.AllowProxyKey, true)
bridge.AllowDoH()
f.bridge.AllowProxy()
}
}
}

View File

@ -21,12 +21,12 @@ import (
"strings"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/pkg/updates"
"github.com/ProtonMail/proton-bridge/internal/updates"
"github.com/abiosoft/ishell"
)
func (f *frontendCLI) checkUpdates(c *ishell.Context) {
isUpToDate, latestVersionInfo, err := f.updates.CheckIsBridgeUpToDate()
isUpToDate, latestVersionInfo, err := f.updates.CheckIsUpToDate()
if err != nil {
f.printAndLogError("Cannot retrieve version info: ", err)
f.checkInternetConnection(c)

View File

@ -22,14 +22,18 @@ import (
"github.com/0xAX/notificator"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/frontend/cli"
cliie "github.com/ProtonMail/proton-bridge/internal/frontend/cli-ie"
"github.com/ProtonMail/proton-bridge/internal/frontend/qt"
qtie "github.com/ProtonMail/proton-bridge/internal/frontend/qt-ie"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/internal/importexport"
"github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/sirupsen/logrus"
)
var (
log = config.GetLogEntry("frontend") // nolint[unused]
log = logrus.WithField("pkg", "frontend") // nolint[unused]
)
// Frontend is an interface to be implemented by each frontend type (cli, gui, html).
@ -39,12 +43,12 @@ type Frontend interface {
}
// HandlePanic handles panics which occur for users with GUI.
func HandlePanic() {
func HandlePanic(appName string) {
notify := notificator.New(notificator.Options{
DefaultIcon: "../frontend/ui/icon/icon.png",
AppName: "ProtonMail Bridge",
AppName: appName,
})
_ = notify.Push("Fatal Error", "The ProtonMail Bridge has encountered a fatal error. ", "/frontend/icon/icon.png", notificator.UR_CRITICAL)
_ = notify.Push("Fatal Error", "The "+appName+" has encountered a fatal error. ", "/frontend/icon/icon.png", notificator.UR_CRITICAL)
}
// New returns initialized frontend based on `frontendType`, which can be `cli` or `qt`.
@ -85,3 +89,36 @@ func new(
return qt.New(version, buildVersion, showWindowOnStart, panicHandler, config, preferences, eventListener, updates, bridge, noEncConfirmator)
}
}
// NewImportExport returns initialized frontend based on `frontendType`, which can be `cli` or `qt`.
func NewImportExport(
version,
buildVersion,
frontendType string,
panicHandler types.PanicHandler,
config *config.Config,
eventListener listener.Listener,
updates types.Updater,
ie *importexport.ImportExport,
) Frontend {
ieWrap := types.NewImportExportWrap(ie)
return newImportExport(version, buildVersion, frontendType, panicHandler, config, eventListener, updates, ieWrap)
}
func newImportExport(
version,
buildVersion,
frontendType string,
panicHandler types.PanicHandler,
config *config.Config,
eventListener listener.Listener,
updates types.Updater,
ie types.ImportExporter,
) Frontend {
switch frontendType {
case "cli":
return cliie.New(panicHandler, config, eventListener, updates, ie)
default:
return qtie.New(version, buildVersion, panicHandler, config, eventListener, updates, ie)
}
}

View File

@ -354,7 +354,7 @@ Window {
} else {
return qsTr('A new version of Bridge is available.<br>
Check <a href="%1">release notes</a> to learn what is new in %2.<br>
You can continue with the update or download and install new version manually from<br><br>
You can continue with the update or download and install the new version manually from<br><br>
<a href="%3">%3</a>',
"Message for update in Win/Mac").arg("releaseNotes").arg(go.newversion).arg(go.landingPage)
}

View File

@ -84,7 +84,7 @@ Window {
anchors.horizontalCenter: parent.horizontalCenter
ButtonRounded {
id: cancel
id: sendAnyway
onClicked : root.hide(true)
height: Style.main.fontSize*2
//width: Style.dialog.widthButton*1.3
@ -93,7 +93,7 @@ Window {
}
ButtonRounded {
id: sendAnyway
id: cancel
onClicked : root.hide(false)
height: Style.main.fontSize*2
//width: Style.dialog.widthButton*1.3

View File

@ -237,6 +237,14 @@ Item {
winMain.tlsBarState="notOK"
}
onShowIMAPCertTroubleshoot : {
go.notifyBubble(1, qsTr(
"Bridge was unable to establish a connection with your Email client. <br> <a href=\"https://protonmail.com/support/knowledge-base/bridge-ssl-connection-issue\">Learn more</a> <br>",
"notification message"
))
}
}
Timer {

View File

@ -0,0 +1,418 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// This is main qml file
import QtQuick 2.8
import ImportExportUI 1.0
import ProtonUI 1.0
// All imports from dynamic must be loaded before
import QtQuick.Window 2.2
import QtQuick.Controls 2.1
import QtQuick.Layouts 1.3
Item {
id: gui
property alias winMain: winMain
property bool isFirstWindow: true
property int warningFlags: 0
property var locale : Qt.locale("en_US")
property date netBday : new Date("1989-03-13T00:00:00")
property var allYears : getYearList(1970,(new Date()).getFullYear())
property var allMonths : getMonthList(1,12)
property var allDays : getDayList(1,31)
property var enums : JSON.parse('{"pathOK":1,"pathEmptyPath":2,"pathWrongPath":4,"pathNotADir":8,"pathWrongPermissions":16,"pathDirEmpty":32,"errUnknownError":0,"errEventAPILogout":1,"errUpdateAPI":2,"errUpdateJSON":3,"errUserAuth":4,"errQApplication":18,"errEmailExportFailed":6,"errEmailExportMissing":7,"errNothingToImport":8,"errEmailImportFailed":12,"errDraftImportFailed":13,"errDraftLabelFailed":14,"errEncryptMessageAttachment":15,"errEncryptMessage":16,"errNoInternetWhileImport":17,"errUnlockUser":5,"errSourceMessageNotSelected":19,"errCannotParseMail":5000,"errWrongLoginOrPassword":5001,"errWrongServerPathOrPort":5002,"errWrongAuthMethod":5003,"errIMAPFetchFailed":5004,"errLocalSourceLoadFailed":1000,"errPMLoadFailed":1001,"errRemoteSourceLoadFailed":1002,"errLoadAccountList":1005,"errExit":1006,"errRetry":1007,"errAsk":1008,"errImportFailed":1009,"errCreateLabelFailed":1010,"errCreateFolderFailed":1011,"errUpdateLabelFailed":1012,"errUpdateFolderFailed":1013,"errFillFolderName":1014,"errSelectFolderColor":1015,"errNoInternet":1016,"folderTypeSystem":"system","folderTypeLabel":"label","folderTypeFolder":"folder","folderTypeExternal":"external","progressInit":"init","progressLooping":"looping","statusNoInternet":"noInternet","statusCheckingInternet":"internetCheck","statusNewVersionAvailable":"oldVersion","statusUpToDate":"upToDate","statusForceUpdate":"forceupdate"}')
IEStyle{}
MainWindow {
id: winMain
visible : true
Component.onCompleted: {
winMain.showAndRise()
}
}
BugReportWindow {
id:bugreportWin
clientVersion.visible: false
onPrefill : {
userAddress.text=""
if (accountsModel.count>0) {
var addressList = accountsModel.get(0).aliases.split(";")
if (addressList.length>0) {
userAddress.text = addressList[0]
}
}
}
}
// Signals from Go
Connections {
target: go
onProcessFinished : {
winMain.dialogAddUser.hide()
winMain.dialogGlobal.hide()
}
onOpenManual : Qt.openUrlExternally("https://protonmail.com/support/categories/import-export/")
onNotifyBubble : {
//go.highlightSystray()
winMain.bubleNote.text = message
winMain.bubleNote.place(tabIndex)
winMain.bubleNote.show()
winMain.showAndRise()
}
onBubbleClosed : {
if (winMain.updateState=="uptodate") {
//go.normalSystray()
}
}
onSetConnectionStatus: {
go.isConnectionOK = isAvailable
if (go.isConnectionOK) {
if( winMain.updateState==gui.enums.statusNoInternet) {
go.setUpdateState(gui.enums.statusUpToDate)
}
} else {
go.setUpdateState(gui.enums.statusNoInternet)
}
}
onRunCheckVersion : {
go.setUpdateState(gui.enums.statusUpToDate)
winMain.dialogGlobal.state=gui.enums.statusCheckingInternet
winMain.dialogGlobal.show()
go.isNewVersionAvailable(showMessage)
}
onSetUpdateState : {
// once app is outdated prevent from state change
if (winMain.updateState != gui.enums.statusForceUpdate) {
winMain.updateState = updateState
}
}
onSetAddAccountWarning : winMain.dialogAddUser.setWarning(message, 0)
onNotifyVersionIsTheLatest : {
winMain.popupMessage.show(
qsTr("You have the latest version!", "todo")
)
}
onNotifyError : {
var sep = go.errorDescription.indexOf("\n") < 0 ? go.errorDescription.length : go.errorDescription.indexOf("\n")
var name = go.errorDescription.slice(0, sep)
var errorMessage = go.errorDescription.slice(sep)
switch (errCode) {
case gui.enums.errPMLoadFailed :
winMain.popupMessage.show ( qsTr ( "Loading ProtonMail folders and labels was not successful." , "Error message" ) )
winMain.dialogExport.hide()
break
case gui.enums.errLocalSourceLoadFailed :
winMain.popupMessage.show(qsTr(
"Loading local folder structure was not successful. "+
"Folder does not contain valid MBOX or EML file.",
"Error message when can not find correct files in folder."
))
winMain.dialogImport.hide()
break
case gui.enums.errRemoteSourceLoadFailed :
winMain.popupMessage.show ( qsTr ( "Loading remote source structure was not successful." , "Error message" ) )
winMain.dialogImport.hide()
break
case gui.enums.errWrongServerPathOrPort :
winMain.popupMessage.show ( qsTr ( "Cannot contact server - incorrect server address and port." , "Error message" ) )
winMain.dialogImport.decrementCurrentIndex()
break
case gui.enums.errWrongLoginOrPassword :
winMain.popupMessage.show ( qsTr ( "Cannot authenticate - Incorrect email or password." , "Error message" ) )
winMain.dialogImport.decrementCurrentIndex()
break ;
case gui.enums.errWrongAuthMethod :
winMain.popupMessage.show ( qsTr ( "Cannot authenticate - Please use secured authentication method." , "Error message" ) )
winMain.dialogImport.decrementCurrentIndex()
break ;
case gui.enums.errFillFolderName:
winMain.popupMessage.show(qsTr (
"Please fill the name.",
"Error message when user did not fill the name of folder or label"
))
break
case gui.enums.errCreateLabelFailed:
winMain.popupMessage.show(qsTr(
"Cannot create label with name \"%1\"\n%2",
"Error message when it is not possible to create new label, arg1 folder name, arg2 error reason"
).arg(name).arg(errorMessage))
break
case gui.enums.errCreateFolderFailed:
winMain.popupMessage.show(qsTr(
"Cannot create folder with name \"%1\"\n%2",
"Error message when it is not possible to create new folder, arg1 folder name, arg2 error reason"
).arg(name).arg(errorMessage))
break
case gui.enums.errNothingToImport:
winMain.popupMessage.show ( qsTr ( "No emails left to import after date range applied. Please, change the date range to continue." , "Error message" ) )
winMain.dialogImport.decrementCurrentIndex()
break
case gui.enums.errNoInternetWhileImport:
case gui.enums.errNoInternet:
go.setConnectionStatus(false)
winMain.popupMessage.show ( go.canNotReachAPI )
break
case gui.enums.errPMAPIMessageTooLarge:
case gui.enums.errIMAPFetchFailed:
case gui.enums.errEmailImportFailed :
case gui.enums.errDraftImportFailed :
case gui.enums.errDraftLabelFailed :
case gui.enums.errEncryptMessageAttachment:
case gui.enums.errEncryptMessage:
//winMain.dialogImport.ask_retry_skip_cancel(name, errorMessage)
console.log("Import error", errCode, go.errorDescription)
winMain.popupMessage.show(qsTr("Error during import: \n%1\n please see log files for more details.", "message of generic error").arg(go.errorDescription))
winMain.dialogImport.hide()
break;
case gui.enums.errUnknownError : default:
console.log("Unknown Error", errCode, go.errorDescription)
winMain.popupMessage.show(qsTr("The program encounter an unknown error \n%1\n please see log files for more details.", "message of generic error").arg(go.errorDescription))
winMain.dialogExport.hide()
winMain.dialogImport.hide()
winMain.dialogAddUser.hide()
winMain.dialogGlobal.hide()
}
}
onNotifyUpdate : {
go.setUpdateState("forceUpdate")
if (!winMain.dialogUpdate.visible) {
gui.openMainWindow(true)
go.runCheckVersion(false)
winMain.dialogUpdate.show()
}
}
onNotifyLogout : {
go.notifyBubble(0, qsTr("Account %1 has been disconnected. Please log in to continue to use the Import-Export app with this account.").arg(accname) )
}
onNotifyAddressChanged : {
go.notifyBubble(0, qsTr("The address list has been changed for account %1. You may need to reconfigure the settings in your email client.").arg(accname) )
}
onNotifyAddressChangedLogout : {
go.notifyBubble(0, qsTr("The address list has been changed for account %1. You have to reconfigure the settings in your email client.").arg(accname) )
}
onNotifyKeychainRebuild : {
go.notifyBubble(1, qsTr(
"Your MacOS keychain is probably corrupted. Please consult the instructions in our <a href=\"https://protonmail.com/bridge/faq#c15\">FAQ</a>.",
"notification message"
))
}
onNotifyHasNoKeychain : {
gui.winMain.dialogGlobal.state="noKeychain"
gui.winMain.dialogGlobal.show()
}
onExportStructureLoadFinished: {
if (okay) winMain.dialogExport.okay()
else winMain.dialogExport.cancel()
}
onImportStructuresLoadFinished: {
if (okay) winMain.dialogImport.okay()
else winMain.dialogImport.cancel()
}
onSimpleErrorHappen: {
if (winMain.dialogImport.visible == true) {
winMain.dialogImport.hide()
}
if (winMain.dialogExport.visible == true) {
winMain.dialogExport.hide()
}
}
}
function folderIcon(folderName, folderType) { // translations
switch (folderName.toLowerCase()) {
case "inbox" : return Style.fa.inbox
case "sent" : return Style.fa.send
case "spam" :
case "junk" : return Style.fa.ban
case "draft" : return Style.fa.file_o
case "starred" : return Style.fa.star_o
case "trash" : return Style.fa.trash_o
case "archive" : return Style.fa.archive
default: return folderType == gui.enums.folderTypeLabel ? Style.fa.tag : Style.fa.folder_open
}
return Style.fa.sticky_note_o
}
function folderTypeTitle(folderType) { // translations
if (folderType==gui.enums.folderTypeSystem ) return ""
if (folderType==gui.enums.folderTypeLabel ) return qsTr("Labels" , "todo")
if (folderType==gui.enums.folderTypeFolder ) return qsTr("Folders" , "todo")
return "Undef"
}
function isFolderEmpty() {
return "true"
}
function getUnixTime(dateString) {
var d = new Date(dateString)
var n = d.getTime()
if (n != n) return -1
return n
}
function getYearList(minY,maxY) {
var years = new Array()
for (var i=0; i<=maxY-minY;i++) {
years[i] = (maxY-i).toString()
}
//console.log("getYearList:", years)
return years
}
function getMonthList(minM,maxM) {
var months = new Array()
for (var i=0; i<=maxM-minM;i++) {
var iMonth = new Date(1989,(i+minM-1),13)
months[i] = iMonth.toLocaleString(gui.locale, "MMM")
}
//console.log("getMonthList:", months[0], months)
return months
}
function getDayList(minD,maxD) {
var days = new Array()
for (var i=0; i<=maxD-minD;i++) {
days[i] = gui.prependZeros(i+minD,2)
}
return days
}
function prependZeros(num,desiredLength) {
var s = num+""
while (s.length < desiredLength) s="0"+s
return s
}
function daysInMonth(year,month) {
if (typeof(year) !== 'number') {
year = parseInt(year)
}
if (typeof(month) !== 'number') {
month = Date.fromLocaleDateString( gui.locale, "1970-"+month+"-10", "yyyy-MMM-dd").getMonth()+1
}
var maxDays = (new Date(year,month,0)).getDate()
if (isNaN(maxDays)) maxDays = 0
//console.log(" daysInMonth", year, month, maxDays)
return maxDays
}
function niceDateTime() {
var stamp = new Date()
var nice = getMonthList(stamp.getMonth()+1, stamp.getMonth()+1)[0]
nice += "-" + getDayList(stamp.getDate(), stamp.getDate())[0]
nice += "-" + getYearList(stamp.getFullYear(), stamp.getFullYear())[0]
nice += " " + gui.prependZeros(stamp.getHours(),2)
nice += ":" + gui.prependZeros(stamp.getMinutes(),2)
return nice
}
/*
// Debug
Connections {
target: structureExternal
onDataChanged: {
console.log("external data changed")
}
}
// Debug
Connections {
target: structurePM
onSelectedLabelsChanged: console.log("PM sel labels:", structurePM.selectedLabels)
onSelectedFoldersChanged: console.log("PM sel folders:", structurePM.selectedFolders)
onDataChanged: {
console.log("PM data changed")
}
}
*/
Timer {
id: checkVersionTimer
repeat : true
triggeredOnStart: false
interval : Style.main.verCheckRepeatTime
onTriggered : go.runCheckVersion(false)
}
property string areYouSureYouWantToQuit : qsTr("There are incomplete processes - some items are not yet transferred. Do you really want to stop and quit?")
// On start
Component.onCompleted : {
// set spell messages
go.wrongCredentials = qsTr("Incorrect username or password." , "notification", -1)
go.wrongMailboxPassword = qsTr("Incorrect mailbox password." , "notification", -1)
go.canNotReachAPI = qsTr("Cannot contact server, please check your internet connection." , "notification", -1)
go.versionCheckFailed = qsTr("Version check was unsuccessful. Please try again later." , "notification", -1)
go.credentialsNotRemoved = qsTr("Credentials could not be removed." , "notification", -1)
go.bugNotSent = qsTr("Unable to submit bug report." , "notification", -1)
go.bugReportSent = qsTr("Bug report successfully sent." , "notification", -1)
go.runCheckVersion(false)
checkVersionTimer.start()
gui.allMonths = getMonthList(1,12)
gui.allMonthsChanged()
}
}

View File

@ -0,0 +1,432 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick 2.8
import ProtonUI 1.0
import ImportExportUI 1.0
// NOTE: Keep the Column so the height and width is inherited from content
Column {
id: root
state: status
anchors.left: parent.left
property real row_width: 50 * Style.px
property int row_height: Style.accounts.heightAccount
property var listalias : aliases.split(";")
property int iAccount: index
property real spacingLastButtons: (row_width - exportAccount.anchors.leftMargin -Style.main.rightMargin - exportAccount.width - logoutAccount.width - deleteAccount.width)/2
Accessible.role: go.goos=="windows" ? Accessible.Grouping : Accessible.Row
Accessible.name: qsTr("Account %1, status %2", "Accessible text describing account row with arguments: account name and status (connected/disconnected), resp.").arg(account).arg(statusMark.text)
Accessible.description: Accessible.name
Accessible.ignored: !enabled || !visible
// Main row
Rectangle {
id: mainaccRow
anchors.left: parent.left
width : row_width
height : row_height
state: { return isExpanded ? "expanded" : "collapsed" }
color: Style.main.background
property string actionName : (
isExpanded ?
qsTr("Collapse row for account %2", "Accessible text of button showing additional configuration of account") :
qsTr("Expand row for account %2", "Accessible text of button hiding additional configuration of account")
). arg(account)
// override by other buttons
MouseArea {
id: mouseArea
anchors.fill: parent
onClicked : {
if (root.state=="connected") {
mainaccRow.toggle_accountSettings()
}
}
cursorShape : root.state == "connected" ? Qt.PointingHandCursor : Qt.ArrowCursor
hoverEnabled: true
onEntered: {
if (mainaccRow.state=="collapsed") {
mainaccRow.color = Qt.lighter(Style.main.background,1.1)
}
}
onExited: {
if (mainaccRow.state=="collapsed") {
mainaccRow.color = Style.main.background
}
}
}
// toggle down/up icon
Text {
id: toggleIcon
anchors {
left : parent.left
verticalCenter : parent.verticalCenter
leftMargin : Style.main.leftMargin
}
color: Style.main.text
font {
pointSize : Style.accounts.sizeChevron * Style.pt
family : Style.fontawesome.name
}
text: Style.fa.chevron_down
MouseArea {
anchors.fill: parent
onClicked : {
if (root.state=="connected") {
mainaccRow.toggle_accountSettings()
}
}
cursorShape : root.state == "connected" ? Qt.PointingHandCursor : Qt.ArrowCursor
Accessible.role: Accessible.Button
Accessible.name: mainaccRow.actionName
Accessible.description: mainaccRow.actionName
Accessible.onPressAction : {
if (root.state=="connected") {
mainaccRow.toggle_accountSettings()
}
}
Accessible.ignored: root.state!="connected" || !root.enabled
}
}
// account name
TextMetrics {
id: accountMetrics
font : accountName.font
elide: Qt.ElideMiddle
elideWidth: (
statusMark.anchors.leftMargin
- toggleIcon.anchors.leftMargin
)
text: account
}
Text {
id: accountName
anchors {
verticalCenter : parent.verticalCenter
left : toggleIcon.left
leftMargin : Style.main.leftMargin
}
color: Style.main.text
font {
pointSize : (Style.main.fontSize+2*Style.px) * Style.pt
}
text: accountMetrics.elidedText
}
// status
ClickIconText {
id: statusMark
anchors {
verticalCenter : parent.verticalCenter
left : parent.left
leftMargin : row_width/2
}
text : qsTr("connected", "status of a listed logged-in account")
iconText : Style.fa.circle_o
textColor : Style.main.textGreen
enabled : false
Accessible.ignored: true
}
// export
ClickIconText {
id: exportAccount
anchors {
verticalCenter : parent.verticalCenter
left : parent.left
leftMargin : 5.5*row_width/8
}
text : qsTr("Export All", "todo")
iconText : Style.fa.floppy_o
textBold : true
textColor : Style.main.textBlue
onClicked: {
dialogExport.currentIndex = 0
dialogExport.address = account
dialogExport.show()
}
}
// logout
ClickIconText {
id: logoutAccount
anchors {
verticalCenter : parent.verticalCenter
left : exportAccount.right
leftMargin : root.spacingLastButtons
}
text : qsTr("Log out", "action to log out a connected account")
iconText : Style.fa.power_off
textBold : true
textColor : Style.main.textBlue
}
// remove
ClickIconText {
id: deleteAccount
anchors {
verticalCenter : parent.verticalCenter
left : logoutAccount.right
leftMargin : root.spacingLastButtons
}
text : qsTr("Remove", "deletes an account from the account settings page")
iconText : Style.fa.trash_o
textColor : Style.main.text
onClicked : {
dialogGlobal.input=iAccount
dialogGlobal.state="deleteUser"
dialogGlobal.show()
}
}
// functions
function toggle_accountSettings() {
if (root.state=="connected") {
if (mainaccRow.state=="collapsed" ) {
mainaccRow.state="expanded"
} else {
mainaccRow.state="collapsed"
}
}
}
states: [
State {
name: "collapsed"
PropertyChanges { target : toggleIcon ; text : root.state=="connected" ? Style.fa.chevron_down : " " }
PropertyChanges { target : accountName ; font.bold : false }
PropertyChanges { target : mainaccRow ; color : Style.main.background }
PropertyChanges { target : addressList ; visible : false }
},
State {
name: "expanded"
PropertyChanges { target : toggleIcon ; text : Style.fa.chevron_up }
PropertyChanges { target : accountName ; font.bold : true }
PropertyChanges { target : mainaccRow ; color : Style.accounts.backgroundExpanded }
PropertyChanges { target : addressList ; visible : true }
}
]
}
// List of adresses
Column {
id: addressList
anchors.left : parent.left
width: row_width
visible: false
property alias model : repeaterAddresses.model
Repeater {
id: repeaterAddresses
model: ["one", "two"]
Rectangle {
id: addressRow
anchors {
left : parent.left
right : parent.right
}
height: Style.accounts.heightAddrRow
color: Style.accounts.backgroundExpanded
// iconText level down
Text {
id: levelDown
anchors {
left : parent.left
leftMargin : Style.accounts.leftMarginAddr
verticalCenter : wrapAddr.verticalCenter
}
text : Style.fa.level_up
font.family : Style.fontawesome.name
color : Style.main.textDisabled
rotation : 90
}
Rectangle {
id: wrapAddr
anchors {
top : parent.top
left : levelDown.right
right : parent.right
leftMargin : Style.main.leftMargin
rightMargin : Style.main.rightMargin
}
height: Style.accounts.heightAddr
border {
width : Style.main.border
color : Style.main.line
}
color: Style.accounts.backgroundAddrRow
TextMetrics {
id: addressMetrics
font: address.font
elideWidth: (
wrapAddr.width
- address.anchors.leftMargin
- 2*exportAlias.width
- 3*exportAlias.anchors.rightMargin
)
elide: Qt.ElideMiddle
text: modelData
}
Text {
id: address
anchors {
verticalCenter : parent.verticalCenter
left: parent.left
leftMargin: Style.main.leftMargin
}
font.pointSize : Style.main.fontSize * Style.pt
color: Style.main.text
text: addressMetrics.elidedText
}
// export
ClickIconText {
id: exportAlias
anchors {
verticalCenter: parent.verticalCenter
right: importAlias.left
rightMargin: Style.main.rightMargin
}
text: qsTr("Export", "todo")
iconText: Style.fa.floppy_o
textBold: true
textColor: Style.main.textBlue
onClicked: {
dialogExport.address = listalias[index]
dialogExport.show()
}
}
// import
ClickIconText {
id: importAlias
anchors {
verticalCenter: parent.verticalCenter
right: parent.right
rightMargin: Style.main.rightMargin
}
text: qsTr("Import", "todo")
iconText: Style.fa.upload
textBold: true
textColor: enabled ? Style.main.textBlue : Style.main.textDisabled
onClicked: {
dialogImport.address = listalias[index]
dialogImport.show()
}
}
}
}
}
}
// line
Rectangle {
id: line
color: Style.accounts.line
height: Style.accounts.heightLine
width: root.row_width
}
states: [
State {
name: "connected"
PropertyChanges {
target : addressList
model : listalias
}
PropertyChanges {
target : toggleIcon
color : Style.main.text
}
PropertyChanges {
target : accountName
color : Style.main.text
}
PropertyChanges {
target : statusMark
textColor : Style.main.textGreen
text : qsTr("connected", "status of a listed logged-in account")
iconText : Style.fa.circle
}
PropertyChanges {
target: exportAccount
visible: true
}
PropertyChanges {
target : logoutAccount
text : qsTr("Log out", "action to log out a connected account")
onClicked : {
mainaccRow.state="collapsed"
dialogGlobal.state = "logout"
dialogGlobal.input = root.iAccount
dialogGlobal.show()
dialogGlobal.confirmed()
}
}
},
State {
name: "disconnected"
PropertyChanges {
target : addressList
model : 0
}
PropertyChanges {
target : toggleIcon
color : Style.main.textDisabled
}
PropertyChanges {
target : accountName
color : Style.main.textDisabled
}
PropertyChanges {
target : statusMark
textColor : Style.main.textDisabled
text : qsTr("disconnected", "status of a listed logged-out account")
iconText : Style.fa.circle_o
}
PropertyChanges {
target : logoutAccount
text : qsTr("Log in", "action to log in a disconnected account")
onClicked : {
dialogAddUser.username = root.listalias[0]
dialogAddUser.show()
dialogAddUser.inputPassword.focusInput = true
}
}
PropertyChanges {
target: exportAccount
visible: false
}
}
]
}

View File

@ -0,0 +1,51 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// credits
import QtQuick 2.8
import ProtonUI 1.0
import ImportExportUI 1.0
Item {
id: root
Rectangle {
anchors.centerIn: parent
width: Style.main.width
height: root.parent.height - 6*Style.dialog.titleSize
color: "transparent"
ListView {
anchors.fill: parent
clip: true
model: go.credits.split(";")
delegate: AccessibleText {
anchors.horizontalCenter: parent.horizontalCenter
text: modelData
color: Style.main.text
font.pointSize: Style.main.fontSize * Style.pt
}
footer: ButtonRounded {
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("Close", "close window")
onClicked: dialogCredits.hide()
}
}
}
}

View File

@ -0,0 +1,220 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// input for year / month / day
import QtQuick 2.8
import QtQuick.Controls 2.2
import QtQml.Models 2.2
import ProtonUI 1.0
import ImportExportUI 1.0
ComboBox {
id: root
property string placeholderText : "none"
property var dropDownStyle : Style.dropDownLight
property real radius : Style.dialog.radiusButton
property bool below : true
onDownChanged : {
root.below = popup.y>0
}
font.pointSize : Style.main.fontSize * Style.pt
spacing : Style.dialog.spacing
height : Style.dialog.heightInput
width : 10*Style.px
function updateWidth() {
// make the width according to localization ( especially for Months)
var max = 10*Style.px
if (root.model === undefined) return
for (var i=-1; i<root.model.length; ++i){
metrics.text = i<0 ? root.placeholderText : root.model[i]+"MM" // "M" for extra space
max = Math.max(max, metrics.width)
}
root.width = root.spacing + max + root.spacing + indicatorIcon.width + root.spacing
//console.log("width updated", root.placeholderText, root.width)
}
TextMetrics {
id: metrics
font: root.font
text: placeholderText
}
indicator: Text {
id: indicatorIcon
color: root.enabled ? dropDownStyle.highlight : dropDownStyle.inactive
text: root.down ? Style.fa.chevron_up : Style.fa.chevron_down
font.family: Style.fontawesome.name
anchors {
right: parent.right
rightMargin: root.spacing
verticalCenter: parent.verticalCenter
}
}
contentItem: Text {
id: boxItem
leftPadding: root.spacing
rightPadding: root.spacing
text : enabled && root.currentIndex>=0 ? root.displayText : placeholderText
font : root.font
color : root.enabled ? dropDownStyle.text : dropDownStyle.inactive
verticalAlignment : Text.AlignVCenter
elide : Text.ElideRight
}
background: Rectangle {
color: Style.transparent
MouseArea {
anchors.fill: parent
onClicked: root.down ? root.popup.close() : root.popup.open()
}
}
DelegateModel { // FIXME QML DelegateModel: Error creating delegate
id: filteredData
model: root.model
filterOnGroup: "filtered"
groups: DelegateModelGroup {
id: filtered
name: "filtered"
includeByDefault: true
}
delegate: root.delegate
}
function filterItems(minIndex,maxIndex) {
// filter
var rowCount = filteredData.items.count
if (rowCount<=0) return
//console.log(" filter", root.placeholderText, rowCount, minIndex, maxIndex)
for (var iItem = 0; iItem < rowCount; iItem++) {
var entry = filteredData.items.get(iItem);
entry.inFiltered = ( iItem >= minIndex && iItem <= maxIndex )
//console.log(" inserted ", iItem, rowCount, entry.model.modelData, entry.inFiltered )
}
}
delegate: ItemDelegate {
id: thisItem
width : view.width
height : Style.dialog.heightInput
leftPadding : root.spacing
rightPadding : root.spacing
topPadding : 0
bottomPadding : 0
property int index : {
//console.log( "index: ", thisItem.DelegateModel.itemsIndex )
return thisItem.DelegateModel.itemsIndex
}
onClicked : {
//console.log("thisItem click", thisItem.index)
root.currentIndex = thisItem.index
root.activated(thisItem.index)
root.popup.close()
}
contentItem: Text {
text: modelData
color: dropDownStyle.text
font: root.font
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
color: thisItem.hovered ? dropDownStyle.highlight : dropDownStyle.background
Text {
anchors{
right: parent.right
rightMargin: root.spacing
verticalCenter: parent.verticalCenter
}
font {
family: Style.fontawesome.name
}
text: root.currentIndex == thisItem.index ? Style.fa.check : ""
color: thisItem.hovered ? dropDownStyle.text : dropDownStyle.highlight
}
Rectangle {
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
}
height: Style.dialog.borderInput
color: dropDownStyle.separator
}
}
}
popup: Popup {
y: root.height
x: -background.strokeWidth
width: root.width + 2*background.strokeWidth
modal: true
closePolicy: Popup.CloseOnPressOutside | Popup.CloseOnEscape
topPadding: background.radiusTopLeft + 2*background.strokeWidth
bottomPadding: background.radiusBottomLeft + 2*background.strokeWidth
leftPadding: 2*background.strokeWidth
rightPadding: 2*background.strokeWidth
contentItem: ListView {
id: view
clip: true
implicitHeight: winMain.height/3
model: filteredData // if you want to slide down to position: popup.visible ? root.delegateModel : null
currentIndex: root.currentIndex
ScrollIndicator.vertical: ScrollIndicator { }
}
background: RoundedRectangle {
radiusTopLeft : root.below ? 0 : root.radius
radiusBottomLeft : !root.below ? 0 : root.radius
radiusTopRight : radiusTopLeft
radiusBottomRight : radiusBottomLeft
fillColor : dropDownStyle.background
}
}
Component.onCompleted: {
//console.log(" box ", label)
root.updateWidth()
root.filterItems(0,model.length-1)
}
onModelChanged :{
//console.log("model changed", root.placeholderText)
root.updateWidth()
root.filterItems(0,model.length-1)
}
}

View File

@ -0,0 +1,264 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// input for date
import QtQuick 2.8
import QtQuick.Controls 2.2
import ProtonUI 1.0
import ImportExportUI 1.0
Rectangle {
id: root
width : row.width + (root.label == "" ? 0 : textlabel.width)
height : row.height
color : Style.transparent
property alias label : textlabel.text
property string metricsLabel : root.label
property var dropDownStyle : Style.dropDownLight
// dates
property date currentDate : new Date() // default now
property date minDate : new Date(0) // default epoch start
property date maxDate : new Date() // default now
property bool isMaxDateToday : false
property int unix : Math.floor(currentDate.getTime()/1000)
onMinDateChanged: {
if (isNaN(minDate.getTime()) || minDate.getTime() > maxDate.getTime()) {
minDate = new Date(0)
}
//console.log(" minDate changed:", root.label, minDate.toDateString())
updateRange()
}
onMaxDateChanged: {
if (isNaN(maxDate.getTime()) || minDate.getTime() > maxDate.getTime()) {
maxDate = new Date()
}
//console.log(" maxDate changed:", root.label, maxDate.toDateString())
updateRange()
}
RoundedRectangle {
id: background
anchors.fill : row
strokeColor : dropDownStyle.line
strokeWidth : Style.dialog.borderInput
fillColor : dropDownStyle.background
radiusTopLeft : row.children[0].down && !row.children[0].below ? 0 : Style.dialog.radiusButton
radiusBottomLeft : row.children[0].down && row.children[0].below ? 0 : Style.dialog.radiusButton
radiusTopRight : row.children[row.children.length-1].down && !row.children[row.children.length-1].below ? 0 : Style.dialog.radiusButton
radiusBottomRight : row.children[row.children.length-1].down && row.children[row.children.length-1].below ? 0 : Style.dialog.radiusButton
}
TextMetrics {
id: textMetrics
text: root.metricsLabel+"M"
font: textlabel.font
}
Text {
id: textlabel
anchors {
left : root.left
verticalCenter : root.verticalCenter
}
font {
pointSize: Style.dialog.fontSize * Style.pt
bold: dropDownStyle.labelBold
}
color: dropDownStyle.text
width: textMetrics.width
verticalAlignment: Text.AlignVCenter
}
Row {
id: row
anchors {
left : root.label=="" ? root.left : textlabel.right
bottom : root.bottom
}
padding : Style.dialog.borderInput
DateBox {
id: monthInput
placeholderText: qsTr("Month")
enabled: !allDates
model: gui.allMonths
onActivated: updateRange()
anchors.verticalCenter: parent.verticalCenter
dropDownStyle: root.dropDownStyle
onDownChanged: {
if (root.isMaxDateToday){
root.maxDate = new Date()
}
}
}
Rectangle {
width: Style.dialog.borderInput
height: monthInput.height
color: dropDownStyle.line
anchors.verticalCenter: parent.verticalCenter
}
DateBox {
id: dayInput
placeholderText: qsTr("Day")
enabled: !allDates
model: gui.allDays
onActivated: updateRange()
anchors.verticalCenter: parent.verticalCenter
dropDownStyle: root.dropDownStyle
onDownChanged: {
if (root.isMaxDateToday){
root.maxDate = new Date()
}
}
}
Rectangle {
width: Style.dialog.borderInput
height: monthInput.height
color: dropDownStyle.line
}
DateBox {
id: yearInput
placeholderText: qsTr("Year")
enabled: !allDates
model: gui.allYears
onActivated: updateRange()
anchors.verticalCenter: parent.verticalCenter
dropDownStyle: root.dropDownStyle
onDownChanged: {
if (root.isMaxDateToday){
root.maxDate = new Date()
}
}
}
}
function setDate(d) {
//console.trace()
//console.log( " setDate ", label, d)
if (isNaN(d = parseInt(d))) return
var newUnix = Math.min(maxDate.getTime(), d*1000) // seconds to ms
newUnix = Math.max(minDate.getTime(), newUnix)
root.updateRange(new Date(newUnix))
//console.log( " set ", currentDate.getTime())
}
function updateRange(curr) {
if (curr === undefined || isNaN(curr.getTime())) curr = root.getCurrentDate()
//console.log( " update", label, curr, curr.getTime())
//console.trace()
if (isNaN(curr.getTime())) return // shouldn't happen
// full system date range
var firstYear = parseInt(gui.allYears[0])
var firstDay = parseInt(gui.allDays[0])
if ( isNaN(firstYear) || isNaN(firstDay) ) return
// get minimal and maximal available year, month, day
// NOTE: The order is important!!!
var minYear = minDate.getFullYear()
var maxYear = maxDate.getFullYear()
var minMonth = (curr.getFullYear() == minYear ? minDate.getMonth() : 0 )
var maxMonth = (curr.getFullYear() == maxYear ? maxDate.getMonth() : 11 )
var minDay = (
curr.getFullYear() == minYear &&
curr.getMonth() == minMonth ?
minDate.getDate() : firstDay
)
var maxDay = (
curr.getFullYear() == maxYear &&
curr.getMonth() == maxMonth ?
maxDate.getDate() : gui.daysInMonth(curr.getFullYear(), curr.getMonth()+1)
)
//console.log("update ranges: ", root.label, minYear, maxYear, minMonth+1, maxMonth+1, minDay, maxDay)
//console.log("update indexes: ", root.label, firstYear-minYear, firstYear-maxYear, minMonth, maxMonth, minDay-firstDay, maxDay-firstDay)
yearInput.filterItems(firstYear-maxYear, firstYear-minYear)
monthInput.filterItems(minMonth,maxMonth) // getMonth() is index not a month (i.e. Jan==0)
dayInput.filterItems(minDay-1,maxDay-1)
// keep ordering from model not from filter
yearInput .currentIndex = firstYear - curr.getFullYear()
monthInput .currentIndex = curr.getMonth() // getMonth() is index not a month (i.e. Jan==0)
dayInput .currentIndex = curr.getDate()-firstDay
/*
console.log(
"update current indexes: ", root.label,
curr.getFullYear() , '->' , yearInput.currentIndex ,
gui.allMonths[curr.getMonth()] , '->' , monthInput.currentIndex ,
curr.getDate() , '->' , dayInput.currentIndex
)
*/
// test if current date changed
if (
yearInput.currentText == root.currentDate.getFullYear() &&
monthInput.currentText == root.currentDate.toLocaleString(gui.locale, "MMM") &&
dayInput.currentText == gui.prependZeros(root.currentDate.getDate(),2)
) {
//console.log(" currentDate NOT changed", label, root.currentDate.toDateString())
return
}
root.currentDate = root.getCurrentDate()
// console.log(" currentDate changed", label, root.currentDate.toDateString())
}
// get current date from selected
function getCurrentDate() {
if (isNaN(root.currentDate.getTime())) { // wrong current ?
console.log("!WARNING! Wrong current date format", root.currentDate)
root.currentDate = new Date(0)
}
var currentString = ""
var currentUnix = root.currentDate.getTime()
if (
yearInput.currentText != "" &&
yearInput.currentText != yearInput.placeholderText &&
monthInput.currentText != "" &&
monthInput.currentText != monthInput.placeholderText
) {
var day = gui.daysInMonth(yearInput.currentText, monthInput.currentText)
if (!isNaN(parseInt(dayInput.currentText))) {
day = Math.min(day, parseInt(dayInput.currentText))
}
var month = gui.allMonths.indexOf(monthInput.currentText)
var year = parseInt(yearInput.currentText)
var pickedDate = new Date(year, month, day)
// Compensate automatic DST in windows
if (pickedDate.getDate() != day) {
pickedDate.setTime(pickedDate.getTime() + 60*60*1000) // add hour
}
currentUnix = pickedDate.getTime()
}
return new Date(Math.max(
minDate.getTime(),
Math.min(maxDate.getTime(), currentUnix)
))
}
}

View File

@ -0,0 +1,123 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// input for date range
import QtQuick 2.8
import QtQuick.Controls 2.2
import ProtonUI 1.0
import ImportExportUI 1.0
Column {
id: dateRange
property var structure : transferRules
property string sourceID : "-1"
property alias allDates : allDatesBox.checked
property alias inputDateFrom : inputDateFrom
property alias inputDateTo : inputDateTo
function getRange() {common.getRange()}
function setRangeFromTo(from, to) {common.setRangeFromTo(from, to)}
function applyRange() {common.applyRange()}
property var dropDownStyle : Style.dropDownLight
property var isDark : dropDownStyle.background == Style.dropDownDark.background
spacing: Style.dialog.spacing
DateRangeFunctions {id:common}
DateInput {
id: inputDateFrom
label: qsTr("From:")
currentDate: gui.netBday
maxDate: inputDateTo.currentDate
dropDownStyle: dateRange.dropDownStyle
}
Rectangle {
width: inputDateTo.width
height: Style.dialog.borderInput / 2
color: isDark ? dropDownStyle.separator : Style.transparent
}
DateInput {
id: inputDateTo
label: qsTr("To:")
metricsLabel: inputDateFrom.label
currentDate: new Date() // now
minDate: inputDateFrom.currentDate
isMaxDateToday: true
dropDownStyle: dateRange.dropDownStyle
}
Rectangle {
width: inputDateTo.width
height: Style.dialog.borderInput
color: isDark ? dropDownStyle.separator : Style.transparent
}
CheckBoxLabel {
id: allDatesBox
text : qsTr("All dates")
anchors.right : inputDateTo.right
checkedSymbol : Style.fa.toggle_on
uncheckedSymbol : Style.fa.toggle_off
uncheckedColor : Style.main.textDisabled
textColor : dropDownStyle.text
symbolPointSize : Style.dialog.iconSize * Style.pt * 1.1
spacing : Style.dialog.spacing*2
TextMetrics {
id: metrics
text: allDatesBox.checkedSymbol
font {
family: Style.fontawesome.name
pointSize: allDatesBox.symbolPointSize
}
}
Rectangle {
color: allDatesBox.checked ? dotBackground.color : Style.exporting.sliderBackground
width: metrics.width*0.9
height: metrics.height*0.6
radius: height/2
z: -1
anchors {
left: allDatesBox.left
verticalCenter: allDatesBox.verticalCenter
leftMargin: 0.05 * metrics.width
}
Rectangle {
id: dotBackground
color : Style.exporting.background
height : parent.height
width : height
radius : height/2
anchors {
left : parent.left
verticalCenter : parent.verticalCenter
}
}
}
}
}

View File

@ -0,0 +1,81 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// input for date range
import QtQuick 2.8
import QtQuick.Controls 2.2
import ProtonUI 1.0
import ImportExportUI 1.0
Item {
id: root
/*
NOTE: need to be in obejct with
id: dateRange
property var structure : structureExternal
property string sourceID : structureExternal.getID ( -1 )
property alias allDates : allDatesBox.checked
property alias inputDateFrom : inputDateFrom
property alias inputDateTo : inputDateTo
function getRange() {common.getRange()}
function applyRange() {common.applyRange()}
*/
function resetRange() {
inputDateFrom.setDate(gui.netBday.getTime())
inputDateTo.setDate((new Date()).getTime())
}
function setRangeFromTo(folderFrom, folderTo){ // unix time in seconds
if ( folderFrom == 0 && folderTo ==0 ) {
dateRange.allDates = true
} else {
dateRange.allDates = false
inputDateFrom.setDate(folderFrom)
inputDateTo.setDate(folderTo)
}
}
function getRange(){ // unix time in seconds
//console.log(" ==== GET RANGE === ")
//console.trace()
var folderFrom = dateRange.structure.globalFromDate
var folderTo = dateRange.structure.globalToDate
root.setRangeFromTo(folderFrom, folderTo)
}
function applyRange(){ // unix time is seconds
if (dateRange.allDates) structure.setFromToDate(dateRange.sourceID, 0, 0)
else {
var endOfDay = new Date(inputDateTo.unix*1000)
endOfDay.setHours(23,59,59,999)
var endOfDayUnix = parseInt(endOfDay.getTime()/1000)
structure.setFromToDate(dateRange.sourceID, inputDateFrom.unix, endOfDayUnix)
}
}
Component.onCompleted: {
inputDateFrom.updateRange(gui.netBday)
inputDateTo.updateRange(new Date())
//getRange()
}
}

View File

@ -0,0 +1,163 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// List of import folder and their target
import QtQuick 2.8
import QtQuick.Controls 2.2
import ProtonUI 1.0
import ImportExportUI 1.0
Rectangle {
id:root
width : icon.width + indicator.width + 3*padding
height : icon.height + 3*padding
property real padding : Style.dialog.spacing
property bool down : popup.visible
property var structure : transferRules
property string sourceID : ""
property int sourceFromDate : 0
property int sourceToDate : 0
color: Style.transparent
RoundedRectangle {
anchors.fill: parent
radiusTopLeft: root.down ? 0 : Style.dialog.radiusButton
fillColor: root.down ? Style.main.textBlue : Style.transparent
}
Text {
id: icon
text: Style.fa.calendar_o
anchors {
left : parent.left
leftMargin : root.padding
verticalCenter : parent.verticalCenter
}
color: root.enabled ? (
root.down ? Style.main.background : Style.main.text
) : Style.main.textDisabled
font.family : Style.fontawesome.name
Text {
anchors {
verticalCenter: parent.bottom
horizontalCenter: parent.right
}
color : !root.down && root.enabled ? Style.main.textRed : icon.color
text : Style.fa.exclamation_circle
visible : !dateRangeInput.allDates
font.pointSize : root.padding * Style.pt * 1.5
font.family : Style.fontawesome.name
}
}
Text {
id: indicator
anchors {
right : parent.right
rightMargin : root.padding
verticalCenter : parent.verticalCenter
}
text : root.down ? Style.fa.chevron_up : Style.fa.chevron_down
color : !root.down && root.enabled ? Style.main.textBlue : icon.color
font.family : Style.fontawesome.name
}
MouseArea {
anchors.fill: root
onClicked: {
popup.open()
}
}
Popup {
id: popup
x : -width
modal : true
clip : true
topPadding : 0
background: RoundedRectangle {
fillColor : Style.bubble.paneBackground
strokeColor : fillColor
radiusTopRight: 0
RoundedRectangle {
anchors {
left: parent.left
right: parent.right
top: parent.top
}
height: Style.dialog.heightInput
fillColor: Style.dropDownDark.highlight
strokeColor: fillColor
radiusTopRight: 0
radiusBottomLeft: 0
radiusBottomRight: 0
}
}
contentItem : Column {
spacing: Style.dialog.spacing
Text {
anchors {
left: parent.left
}
text : qsTr("Import date range")
font.bold : Style.dropDownDark.labelBold
color : Style.dropDownDark.text
height : Style.dialog.heightInput
verticalAlignment : Text.AlignVCenter
}
DateRange {
id: dateRangeInput
allDates: true
structure: root.structure
sourceID: root.sourceID
dropDownStyle: Style.dropDownDark
}
}
onAboutToShow : updateRange()
onAboutToHide : dateRangeInput.applyRange()
}
function updateRange() {
dateRangeInput.setRangeFromTo(root.sourceFromDate, root.sourceToDate)
}
Connections {
target:root
onSourceFromDateChanged: root.updateRange()
onSourceToDateChanged: root.updateRange()
}
}

View File

@ -0,0 +1,457 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Export dialog
import QtQuick 2.8
import QtQuick.Controls 2.2
import ProtonUI 1.0
import ImportExportUI 1.0
// TODO
// - make ErrorDialog module
// - map decision to error code : ask (default), skip ()
// - what happens when import fails ? heuristic to find mail where to start from
Dialog {
id: root
enum Page {
LoadingStructure = 0, Options, Progress
}
title : set_title()
property string address
property alias finish: finish
property string msgClearUnfished: qsTr ("Remove already exported files.")
isDialogBusy : true // currentIndex == 0 || currentIndex == 3
signal cancel()
signal okay()
Rectangle { // 0
id: dialogLoading
width: root.width
height: root.height
color: Style.transparent
Text {
anchors.centerIn : dialogLoading
font.pointSize: Style.dialog.titleSize * Style.pt
color: Style.dialog.text
horizontalAlignment: Text.AlignHCenter
text: qsTr("Loading folders and labels for", "todo") +"\n" + address
}
}
Rectangle { // 1
id: dialogInput
width: root.width
height: root.height
color: Style.transparent
Row {
id: inputRow
anchors {
topMargin : root.titleHeight
top : parent.top
horizontalCenter : parent.horizontalCenter
}
spacing: 3*Style.main.leftMargin
property real columnWidth : (root.width - Style.main.leftMargin - inputRow.spacing - Style.main.rightMargin) / 2
property real columnHeight : root.height - root.titleHeight - Style.main.leftMargin
ExportStructure {
id: sourceFoldersInput
width : inputRow.columnWidth
height : inputRow.columnHeight
title : qsTr("From: %1", "todo").arg(address)
}
Column {
spacing: (inputRow.columnHeight - dateRangeInput.height - outputFormatInput.height - outputPathInput.height - buttonRow.height - infotipEncrypted.height) / 4
DateRange{
id: dateRangeInput
}
OutputFormat {
id: outputFormatInput
}
Row {
spacing: Style.dialog.spacing
CheckBoxLabel {
id: exportEncrypted
text: qsTr("Export emails that cannot be decrypted as ciphertext")
anchors {
bottom: parent.bottom
bottomMargin: Style.dialog.fontSize/1.8
}
}
InfoToolTip {
id: infotipEncrypted
anchors {
verticalCenter: exportEncrypted.verticalCenter
}
info: qsTr("Checking this option will export all emails that cannot be decrypted in ciphertext. If this option is not checked, these emails will not be exported", "todo")
}
}
FileAndFolderSelect {
id: outputPathInput
title: qsTr("Select location of export:", "todo")
width : inputRow.columnWidth // stretch folder input
}
Row {
id: buttonRow
anchors.right : parent.right
spacing : Style.dialog.rightMargin
ButtonRounded {
id:buttonCancel
fa_icon: Style.fa.times
text: qsTr("Cancel")
color_main: Style.main.textBlue
onClicked : root.cancel()
}
ButtonRounded {
id: buttonNext
fa_icon: Style.fa.check
text: qsTr("Export","todo")
enabled: transferRules != 0
color_main: Style.dialog.background
color_minor: enabled ? Style.dialog.textBlue : Style.main.textDisabled
isOpaque: true
onClicked : root.okay()
}
}
}
}
}
Rectangle { // 2
id: progressStatus
width: root.width
height: root.height
color: "transparent"
Row {
anchors {
bottom: progressbarExport.top
bottomMargin: Style.dialog.heightSeparator
left: progressbarExport.left
}
spacing: Style.main.rightMargin
AccessibleText {
id: statusLabel
text : qsTr("Status:")
font.pointSize: Style.main.iconSize * Style.pt
color : Style.main.text
}
AccessibleText {
anchors.baseline: statusLabel.baseline
text : {
if (progressbarExport.isFinished) return qsTr("finished")
if (go.progressDescription == "") return qsTr("exporting")
return go.progressDescription
}
elide: Text.ElideMiddle
width: progressbarExport.width - parent.spacing - statusLabel.width
font.pointSize: Style.dialog.textSize * Style.pt
color : Style.main.textDisabled
}
}
ProgressBar {
id: progressbarExport
implicitWidth : 2*progressStatus.width/3
implicitHeight : Style.exporting.rowHeight
value: go.progress
property int current: go.total * go.progress
property bool isFinished: finishedPartBar.width == progressbarExport.width
anchors {
centerIn: parent
}
background: Rectangle {
radius : Style.exporting.boxRadius
color : Style.exporting.progressBackground
}
contentItem: Item {
Rectangle {
id: finishedPartBar
width : parent.width * progressbarExport.visualPosition
height : parent.height
radius : Style.exporting.boxRadius
gradient : Gradient {
GradientStop { position: 0.00; color: Qt.lighter(Style.exporting.progressStatus,1.1) }
GradientStop { position: 0.66; color: Style.exporting.progressStatus }
GradientStop { position: 1.00; color: Qt.darker(Style.exporting.progressStatus,1.1) }
}
Behavior on width {
NumberAnimation { duration:800; easing.type: Easing.InOutQuad }
}
}
Text {
anchors.centerIn: parent
text: {
if (progressbarExport.isFinished) return qsTr("Export finished","todo")
if (
go.progressDescription == gui.enums.progressInit ||
(go.progress==0 && go.description=="")
) {
if (go.total>1) return qsTr("Estimating the total number of messages (%1)","todo").arg(go.total)
else return qsTr("Estimating the total number of messages","todo")
}
var msg = qsTr("Exporting message %1 of %2 (%3%)","todo")
if (pauseButton.paused) msg = qsTr("Exporting paused at message %1 of %2 (%3%)","todo")
return msg.arg(progressbarExport.current).arg(go.total).arg(Math.floor(go.progress*100))
}
color: Style.main.background
font {
pointSize: Style.dialog.fontSize * Style.pt
}
}
}
}
Row {
anchors {
top: progressbarExport.bottom
topMargin : Style.dialog.heightSeparator
horizontalCenter: parent.horizontalCenter
}
spacing: Style.dialog.rightMargin
ButtonRounded {
id: pauseButton
property bool paused : false
fa_icon : paused ? Style.fa.play : Style.fa.pause
text : paused ? qsTr("Resume") : qsTr("Pause")
color_main : Style.dialog.textBlue
onClicked : {
if (paused) {
if (winMain.updateState == gui.enums.statusNoInternet) {
go.notifyError(gui.enums.errNoInternet)
return
}
go.resumeProcess()
} else {
go.pauseProcess()
}
paused = !paused
pauseButton.focus=false
}
visible : !progressbarExport.isFinished
}
ButtonRounded {
fa_icon : Style.fa.times
text : qsTr("Cancel")
color_main : Style.dialog.textBlue
visible : !progressbarExport.isFinished
onClicked : root.ask_cancel_progress()
}
ButtonRounded {
id: finish
fa_icon : Style.fa.check
text : qsTr("Okay","todo")
color_main : Style.dialog.background
color_minor : Style.dialog.textBlue
isOpaque : true
visible : progressbarExport.isFinished
onClicked : root.okay()
}
}
ClickIconText {
id: buttonHelp
anchors {
right : parent.right
bottom : parent.bottom
rightMargin : Style.main.rightMargin
bottomMargin : Style.main.rightMargin
}
textColor : Style.main.textDisabled
iconText : Style.fa.question_circle
text : qsTr("Help", "directs the user to the online user guide")
textBold : true
onClicked : Qt.openUrlExternally("https://protonmail.com/support/categories/import-export/")
}
}
PopupMessage {
id: errorPopup
width: root.width
height: root.height
}
function check_inputs() {
if (currentIndex == 1) {
// at least one email to export
if (transferRules.rowCount() == 0){
errorPopup.show(qsTr("No emails found to export. Please try another address.", "todo"))
return false
}
// at least one source selected
/*
if (!transferRules.atLeastOneSelected) {
errorPopup.show(qsTr("Please select at least one item to export.", "todo"))
return false
}
*/
// check path
var folderCheck = go.checkPathStatus(outputPathInput.path)
switch (folderCheck) {
case gui.enums.pathEmptyPath:
errorPopup.show(qsTr("Missing export path. Please select an output folder."))
break;
case gui.enums.pathWrongPath:
errorPopup.show(qsTr("Folder '%1' not found. Please select an output folder.").arg(outputPathInput.path))
break;
case gui.enums.pathOK | gui.enums.pathNotADir:
errorPopup.show(qsTr("File '%1' is not a folder. Please select an output folder.").arg(outputPathInput.path))
break;
case gui.enums.pathWrongPermissions:
errorPopup.show(qsTr("Cannot access folder '%1'. Please check folder permissions.").arg(outputPathInput.path))
break;
}
if (
(folderCheck&gui.enums.pathOK)==0 ||
(folderCheck&gui.enums.pathNotADir)==gui.enums.pathNotADir
) return false
if (winMain.updateState == gui.enums.statusNoInternet) {
errorPopup.show(qsTr("Please check your internet connection."))
return false
}
}
return true
}
function set_title() {
switch(root.currentIndex){
case 1 : return qsTr("Select what you'd like to export:")
default: return ""
}
}
function clear_status() {
go.progress=0.0
go.total=0.0
go.progressDescription=gui.enums.progressInit
}
function ask_cancel_progress(){
errorPopup.buttonYes.visible = true
errorPopup.buttonNo.visible = true
errorPopup.buttonOkay.visible = false
errorPopup.show ("Are you sure you want to cancel this export?")
}
onCancel : {
switch (root.currentIndex) {
case 0 :
case 1 : root.hide(); break;
case 2 : // progress bar
go.cancelProcess();
// no break
default:
root.clear_status()
root.currentIndex=1
}
}
onOkay : {
var isOK = check_inputs()
if (!isOK) return
timer.interval= currentIndex==1 ? 1 : 300
switch (root.currentIndex) {
case 2: // progress
root.clear_status()
root.hide()
break
case 0: // loading structure
dateRangeInput.getRange()
//no break
default:
incrementCurrentIndex()
timer.start()
}
}
onShow: {
if (winMain.updateState==gui.enums.statusNoInternet) {
go.checkInternet()
if (winMain.updateState==gui.enums.statusNoInternet) {
go.notifyError(gui.enums.errNoInternet)
root.hide()
return
}
}
root.clear_status()
root.currentIndex=0
timer.interval = 300
timer.start()
dateRangeInput.allDates = true
}
Connections {
target: timer
onTriggered : {
switch (currentIndex) {
case 0:
go.loadStructureForExport(root.address)
sourceFoldersInput.hasItems = (transferRules.rowCount() > 0)
break
case 2:
dateRangeInput.applyRange()
go.startExport(
outputPathInput.path,
root.address,
outputFormatInput.checkedText,
exportEncrypted.checked
)
break
}
}
}
Connections {
target: errorPopup
onClickedOkay : errorPopup.hide()
onClickedYes : {
root.cancel()
errorPopup.hide()
}
onClickedNo : {
go.resumeProcess()
errorPopup.hide()
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,354 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Dialog with Yes/No buttons
import QtQuick 2.8
import QtQuick.Controls 2.2
import ProtonUI 1.0
import ImportExportUI 1.0
Dialog {
id: root
title : ""
property string input
property alias question : msg.text
property alias note : noteText.text
property alias answer : answ.text
property alias buttonYes : buttonYes
property alias buttonNo : buttonNo
isDialogBusy: currentIndex==1
signal confirmed()
Column {
id: dialogMessage
property int heightInputs : msg.height+
middleSep.height+
buttonRow.height +
(checkboxSep.visible ? checkboxSep.height : 0 ) +
(noteSep.visible ? noteSep.height : 0 ) +
(checkBoxWrapper.visible ? checkBoxWrapper.height : 0 ) +
(root.note!="" ? noteText.height : 0 )
Rectangle { color : "transparent"; width : Style.main.dummy; height : (root.height-dialogMessage.heightInputs)/2 }
AccessibleText {
id:noteText
anchors.horizontalCenter: parent.horizontalCenter
color: Style.dialog.text
font {
pointSize: Style.dialog.fontSize * Style.pt
bold: false
}
width: 2*root.width/3
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
}
Rectangle { id: noteSep; visible: note!=""; color : "transparent"; width : Style.main.dummy; height : Style.dialog.heightSeparator}
AccessibleText {
id: msg
anchors.horizontalCenter: parent.horizontalCenter
color: Style.dialog.text
font {
pointSize: Style.dialog.fontSize * Style.pt
bold: true
}
width: 2*parent.width/3
text : ""
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
}
Rectangle { id: checkboxSep; visible: checkBoxWrapper.visible; color : "transparent"; width : Style.main.dummy; height : Style.dialog.heightSeparator}
Row {
id: checkBoxWrapper
property bool isChecked : false
visible: root.state=="deleteUser"
anchors.horizontalCenter: parent.horizontalCenter
spacing: Style.dialog.spacing
function toggle() {
checkBoxWrapper.isChecked = !checkBoxWrapper.isChecked
}
Text {
id: checkbox
font {
pointSize : Style.dialog.iconSize * Style.pt
family : Style.fontawesome.name
}
anchors.verticalCenter : parent.verticalCenter
text: checkBoxWrapper.isChecked ? Style.fa.check_square_o : Style.fa.square_o
color: checkBoxWrapper.isChecked ? Style.main.textBlue : Style.main.text
MouseArea {
anchors.fill: parent
onPressed: checkBoxWrapper.toggle()
cursorShape: Qt.PointingHandCursor
}
}
Text {
id: checkBoxNote
anchors.verticalCenter : parent.verticalCenter
text: qsTr("Additionally delete all stored preferences and data", "when removing an account, this extra preference additionally deletes all cached data")
color: Style.main.text
font.pointSize: Style.dialog.fontSize * Style.pt
MouseArea {
anchors.fill: parent
onPressed: checkBoxWrapper.toggle()
cursorShape: Qt.PointingHandCursor
Accessible.role: Accessible.CheckBox
Accessible.checked: checkBoxWrapper.isChecked
Accessible.name: checkBoxNote.text
Accessible.description: checkBoxNote.text
Accessible.ignored: checkBoxNote.text == ""
Accessible.onToggleAction: checkBoxWrapper.toggle()
Accessible.onPressAction: checkBoxWrapper.toggle()
}
}
}
Rectangle { id: middleSep; color : "transparent"; width : Style.main.dummy; height : 2*Style.dialog.heightSeparator }
Row {
id: buttonRow
anchors.horizontalCenter: parent.horizontalCenter
spacing: Style.dialog.spacing
ButtonRounded {
id:buttonNo
color_main : Style.dialog.textBlue
fa_icon : Style.fa.times
text : qsTr("No")
onClicked : root.hide()
}
ButtonRounded {
id: buttonYes
color_main : Style.dialog.background
color_minor : Style.dialog.textBlue
isOpaque : true
fa_icon : Style.fa.check
text : qsTr("Yes")
onClicked : {
currentIndex=1
root.confirmed()
}
}
}
}
Column {
Rectangle { color : "transparent"; width : Style.main.dummy; height : (root.height-answ.height)/2 }
AccessibleText {
id: answ
anchors.horizontalCenter: parent.horizontalCenter
color: Style.dialog.text
font {
pointSize : Style.dialog.fontSize * Style.pt
bold : true
}
width: 3*parent.width/4
horizontalAlignment: Text.AlignHCenter
text : qsTr("Waiting...", "in general this displays between screens when processing data takes a long time")
wrapMode: Text.Wrap
}
}
states : [
State {
name: "quit"
PropertyChanges {
target: root
currentIndex : 0
title : qsTr("Close ImportExport", "quits the application")
question : qsTr("Are you sure you want to close the ImportExport?", "asked when user tries to quit the application")
note : ""
answer : qsTr("Closing ImportExport...", "displayed when user is quitting application")
}
},
State {
name: "logout"
PropertyChanges {
target: root
currentIndex : 1
title : qsTr("Logout", "title of page that displays during account logout")
question : ""
note : ""
answer : qsTr("Logging out...", "displays during account logout")
}
},
State {
name: "deleteUser"
PropertyChanges {
target: root
currentIndex : 0
title : qsTr("Delete account", "title of page that displays during account deletion")
question : qsTr("Are you sure you want to remove this account?", "displays during account deletion")
note : ""
answer : qsTr("Deleting ...", "displays during account deletion")
}
},
State {
name: "clearChain"
PropertyChanges {
target : root
currentIndex : 0
title : qsTr("Clear keychain", "title of page that displays during keychain clearing")
question : qsTr("Are you sure you want to clear your keychain?", "displays during keychain clearing")
note : qsTr("This will remove all accounts that you have added to the Import-Export app.", "displays during keychain clearing")
answer : qsTr("Clearing the keychain ...", "displays during keychain clearing")
}
},
State {
name: "clearCache"
PropertyChanges {
target: root
currentIndex : 0
title : qsTr("Clear cache", "title of page that displays during cache clearing")
question : qsTr("Are you sure you want to clear your local cache?", "displays during cache clearing")
note : qsTr("This will delete all of your stored preferences.", "displays during cache clearing")
answer : qsTr("Clearing the cache ...", "displays during cache clearing")
}
},
State {
name: "checkUpdates"
PropertyChanges {
target: root
currentIndex : 1
title : ""
question : ""
note : ""
answer : qsTr("Checking for updates ...", "displays if user clicks the Check for Updates button in the Help tab")
}
},
State {
name: "internetCheck"
PropertyChanges {
target: root
currentIndex : 1
title : ""
question : ""
note : ""
answer : qsTr("Contacting server...", "displays if user clicks the Check for Updates button in the Help tab")
}
},
State {
name: "addressmode"
PropertyChanges {
target: root
currentIndex : 0
title : ""
question : qsTr("Do you want to continue?", "asked when the user changes between split and combined address mode")
note : qsTr("Changing between split and combined address mode will require you to delete your account(s) from your email client and begin the setup process from scratch.", "displayed when the user changes between split and combined address mode")
answer : qsTr("Configuring address mode for ", "displayed when the user changes between split and combined address mode") + root.input
}
},
State {
name: "toggleAutoStart"
PropertyChanges {
target: root
currentIndex : 1
question : ""
note : ""
title : ""
answer : {
var msgTurnOn = qsTr("Turning on automatic start of ImportExport...", "when the automatic start feature is selected")
var msgTurnOff = qsTr("Turning off automatic start of ImportExport...", "when the automatic start feature is deselected")
return go.isAutoStart==0 ? msgTurnOff : msgTurnOn
}
}
},
State {
name: "undef";
PropertyChanges {
target: root
currentIndex : 1
question : ""
note : ""
title : ""
answer : ""
}
}
]
Shortcut {
sequence: StandardKey.Cancel
onActivated: root.hide()
}
Shortcut {
sequence: "Enter"
onActivated: root.confirmed()
}
onHide: {
checkBoxWrapper.isChecked = false
state = "undef"
}
onShow: {
// hide all other dialogs
winMain.dialogAddUser .visible = false
winMain.dialogCredits .visible = false
//winMain.dialogVersionInfo .visible = false
// dialogFirstStart should reappear again after closing global
root.visible = true
}
onConfirmed : {
if (state == "quit" || state == "instance exists" ) {
timer.interval = 1000
} else {
timer.interval = 300
}
answ.forceActiveFocus()
timer.start()
}
Connections {
target: timer
onTriggered: {
if ( state == "addressmode" ) { go.switchAddressMode (input) }
if ( state == "clearChain" ) { go.clearKeychain () }
if ( state == "clearCache" ) { go.clearCache () }
if ( state == "deleteUser" ) { go.deleteAccount (input, checkBoxWrapper.isChecked) }
if ( state == "logout" ) { go.logoutAccount (input) }
if ( state == "toggleAutoStart" ) { go.toggleAutoStart () }
if ( state == "quit" ) { Qt.quit () }
if ( state == "instance exists" ) { Qt.quit () }
if ( state == "checkUpdates" ) { go.runCheckVersion (true) }
}
}
Keys.onPressed: {
if (event.key == Qt.Key_Enter) {
root.confirmed()
}
}
}

View File

@ -0,0 +1,149 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// List of export folders / labels
import QtQuick 2.8
import QtQuick.Controls 2.2
import ProtonUI 1.0
import ImportExportUI 1.0
Rectangle {
id: root
color : Style.exporting.background
radius : Style.exporting.boxRadius
border {
color : Style.exporting.line
width : Style.dialog.borderInput
}
property bool hasItems: true
Text { // placeholder
visible: !root.hasItems
anchors.centerIn: parent
color: Style.main.textDisabled
font {
pointSize: Style.dialog.fontSize * Style.pt
}
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: qsTr("No emails found for this address.","todo")
}
property string title : ""
TextMetrics {
id: titleMetrics
text: root.title
elide: Qt.ElideMiddle
elideWidth: root.width - 4*Style.exporting.leftMargin
font {
pointSize: Style.dialog.fontSize * Style.pt
bold: true
}
}
Rectangle {
id: header
anchors {
top: root.top
left: root.left
}
width : root.width
height : Style.dialog.fontSize*3
color : Style.transparent
Rectangle {
anchors.bottom: parent.bottom
color : Style.exporting.line
height : Style.dialog.borderInput
width : parent.width
}
Text {
anchors {
left : parent.left
leftMargin : 2*Style.exporting.leftMargin
verticalCenter : parent.verticalCenter
}
color: Style.dialog.text
font: titleMetrics.font
text: titleMetrics.elidedText
}
}
ListView {
id: listview
clip : true
orientation : ListView.Vertical
boundsBehavior : Flickable.StopAtBounds
model : transferRules
cacheBuffer : 10000
anchors {
left : root.left
right : root.right
bottom : root.bottom
top : header.bottom
margins : Style.dialog.borderInput
}
ScrollBar.vertical: ScrollBar {
/*
policy: ScrollBar.AsNeeded
background : Rectangle {
color : Style.exporting.sliderBackground
radius : Style.exporting.boxRadius
}
contentItem : Rectangle {
color : Style.exporting.sliderForeground
radius : Style.exporting.boxRadius
implicitWidth : Style.main.rightMargin / 3
}
*/
anchors {
right: parent.right
rightMargin: Style.main.rightMargin/4
}
width: Style.main.rightMargin/3
Accessible.ignored: true
}
delegate: FolderRowButton {
property variant modelData: model
width : root.width - 5*root.border.width
type : modelData.type
folderIconColor : modelData.iconColor
title : modelData.name
isSelected : modelData.isActive
onClicked : {
//console.log("Clicked", folderId, isSelected)
transferRules.setIsRuleActive(modelData.mboxID,!model.isActive)
}
}
section.property: "type"
section.delegate: FolderRowButton {
isSection : true
width : root.width - 5*root.border.width
title : gui.folderTypeTitle(section)
isSelected : section == gui.enums.folderTypeLabel ? transferRules.isLabelGroupSelected : transferRules.isFolderGroupSelected
onClicked : transferRules.setIsGroupActive(section,!isSelected)
}
}
}

View File

@ -0,0 +1,55 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Filter only selected folders or labels
import QtQuick 2.8
import QtQml.Models 2.2
DelegateModel {
id: root
model : structurePM
//filterOnGroup : root.folderType
//delegate : root.delegate
groups : [
DelegateModelGroup {name: gui.enums.folderTypeFolder ; includeByDefault: false},
DelegateModelGroup {name: gui.enums.folderTypeLabel ; includeByDefault: false}
]
function updateFilter() {
//console.log("FilterModelDelegate::UpdateFilter")
// filter
var rowCount = root.items.count;
for (var iItem = 0; iItem < rowCount; iItem++) {
var entry = root.items.get(iItem);
entry.inLabel = (
root.filterOnGroup == gui.enums.folderTypeLabel &&
entry.model.folderType == gui.enums.folderTypeLabel
)
entry.inFolder = (
root.filterOnGroup == gui.enums.folderTypeFolder &&
entry.model.folderType != gui.enums.folderTypeLabel
)
/*
if (entry.inFolder && entry.model.folderId == selectedIDs) {
view.currentIndex = iItem
}
*/
//console.log("::::update filter:::::", iItem, entry.model.folderName, entry.inFolder, entry.inLabel)
}
}
}

View File

@ -0,0 +1,99 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Checkbox row for folder selection
import QtQuick 2.8
import QtQuick.Controls 2.2
import ProtonUI 1.0
import ImportExportUI 1.0
AccessibleButton {
id: root
property bool isSection : false
property bool isSelected : false
property string title : "N/A"
property string type : ""
property string folderIconColor : Style.main.textBlue
height : Style.exporting.rowHeight
padding : 0.0
anchors {
horizontalCenter: parent.horizontalCenter
}
background: Rectangle {
color: isSection ? Style.exporting.background : Style.exporting.rowBackground
Rectangle { // line
anchors.bottom : parent.bottom
height : Style.dialog.borderInput
width : parent.width
color : Style.exporting.background
}
}
contentItem: Rectangle {
color: "transparent"
id: content
Text {
id: checkbox
anchors {
verticalCenter : parent.verticalCenter
left : content.left
leftMargin : Style.exporting.leftMargin * (root.type == gui.enums.folderTypeSystem ? 1.0 : 2.0)
}
font {
family : Style.fontawesome.name
pointSize : Style.dialog.fontSize * Style.pt
}
color : isSelected ? Style.main.text : Style.main.textInactive
text : (isSelected ? Style.fa.check_square_o : Style.fa.square_o )
}
Text { // icon
id: folderIcon
visible: !isSection
anchors {
verticalCenter : parent.verticalCenter
left : checkbox.left
leftMargin : Style.dialog.fontSize + Style.exporting.leftMargin
}
color : root.type=="" ? Style.main.textBlue : root.folderIconColor
font {
family : Style.fontawesome.name
pointSize : Style.dialog.fontSize * Style.pt
}
text : {
return gui.folderIcon(root.title.toLowerCase(), root.type)
}
}
Text {
text: root.title
anchors {
verticalCenter : parent.verticalCenter
left : isSection ? checkbox.left : folderIcon.left
leftMargin : Style.dialog.fontSize + Style.exporting.leftMargin
}
font {
pointSize : Style.dialog.fontSize * Style.pt
bold: isSection
}
color: Style.exporting.text
}
}
}

View File

@ -0,0 +1,129 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// List the settings
import QtQuick 2.8
import ProtonUI 1.0
import ImportExportUI 1.0
Item {
id: root
// must have wrapper
Rectangle {
id: wrapper
anchors.centerIn: parent
width: parent.width
height: parent.height
color: Style.main.background
// content
Column {
anchors.horizontalCenter : parent.horizontalCenter
ButtonIconText {
id: manual
anchors.left: parent.left
text: qsTr("Setup Guide")
leftIcon.text : Style.fa.book
rightIcon.text : Style.fa.chevron_circle_right
rightIcon.font.pointSize : Style.settings.toggleSize * Style.pt
onClicked: go.openManual()
}
ButtonIconText {
id: updates
anchors.left: parent.left
text: qsTr("Check for Updates")
leftIcon.text : Style.fa.refresh
rightIcon.text : Style.fa.chevron_circle_right
rightIcon.font.pointSize : Style.settings.toggleSize * Style.pt
onClicked: {
dialogGlobal.state="checkUpdates"
dialogGlobal.show()
dialogGlobal.confirmed()
}
}
Rectangle {
anchors.horizontalCenter : parent.horizontalCenter
height: Math.max (
aboutText.height +
Style.main.fontSize,
wrapper.height - (
2*manual.height +
creditsLink.height +
Style.main.fontSize
)
)
width: wrapper.width
color : Style.transparent
Text {
id: aboutText
anchors {
bottom: parent.bottom
horizontalCenter: parent.horizontalCenter
}
color: Style.main.textDisabled
horizontalAlignment: Qt.AlignHCenter
font.family : Style.fontawesome.name
text: "ProtonMail Import-Export app Version "+go.getBackendVersion()+"\n"+Style.fa.copyright + " 2020 Proton Technologies AG"
}
}
Row {
anchors.horizontalCenter : parent.horizontalCenter
spacing : Style.main.dummy
Text {
id: creditsLink
text : qsTr("Credits", "link to click on to view list of credited libraries")
color : Style.main.textDisabled
font.pointSize: Style.main.fontSize * Style.pt
font.underline: true
MouseArea {
anchors.fill: parent
onClicked : {
winMain.dialogCredits.show()
}
cursorShape: Qt.PointingHandCursor
}
}
Text {
id: releaseNotes
text : qsTr("Release notes", "link to click on to view release notes for this version of the app")
color : Style.main.textDisabled
font.pointSize: Style.main.fontSize * Style.pt
font.underline: true
MouseArea {
anchors.fill: parent
onClicked : {
go.getLocalVersionInfo()
winMain.dialogVersionInfo.show()
}
cursorShape: Qt.PointingHandCursor
}
}
}
}
}
}

View File

@ -0,0 +1,106 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Adjust Bridge Style
import QtQuick 2.8
import ImportExportUI 1.0
import ProtonUI 1.0
Item {
Component.onCompleted : {
//Style.refdpi = go.goos == "darwin" ? 86.0 : 96.0
Style.pt = go.goos == "darwin" ? 93/Style.dpi : 80/Style.dpi
Style.main.background = "#fff"
Style.main.text = "#505061"
Style.main.textInactive = "#686876"
Style.main.line = "#dddddd"
Style.main.width = 884 * Style.px
Style.main.height = 422 * Style.px
Style.main.leftMargin = 25 * Style.px
Style.main.rightMargin = 25 * Style.px
Style.title.background = Style.main.text
Style.title.text = Style.main.background
Style.tabbar.background = "#3D3A47"
Style.tabbar.rightButton = "add account"
Style.tabbar.spacingButton = 45*Style.px
Style.accounts.backgroundExpanded = "#fafafa"
Style.accounts.backgroundAddrRow = "#fff"
Style.accounts.leftMargin2 = Style.main.width/2
Style.accounts.leftMargin3 = 5.5*Style.main.width/8
Style.dialog.background = "#fff"
Style.dialog.text = Style.main.text
Style.dialog.line = "#e2e2e2"
Style.dialog.fontSize = 12 * Style.px
Style.dialog.heightInput = 2.2*Style.dialog.fontSize
Style.dialog.heightButton = Style.dialog.heightInput
Style.dialog.borderInput = 1 * Style.px
Style.bubble.background = "#595966"
Style.bubble.paneBackground = "#454553"
Style.bubble.text = "#fff"
Style.bubble.width = 310 * Style.px
Style.bubble.widthPane = 36 * Style.px
Style.bubble.iconSize = 14 * Style.px
// colors:
// text: #515061
// tick: #686876
// blue icon: #9396cc
// row bck: #f8f8f9
// line: #ddddde or #e2e2e2
//
// slider bg: #e6e6e6
// slider fg: #515061
// info icon: #c3c3c8
// input border: #ebebeb
//
// bubble color: #595966
// bubble pane: #454553
// bubble text: #fff
//
// indent folder
//
// Dimensions:
// full width: 882px
// leftMargin: 25px
// rightMargin: 25px
// rightMargin: 25px
// middleSeparator: 69px
// width folders: 416px or (width - separators) /2
// width output: 346px or (width - separators) /2
//
// height from top to input begin: 78px
// heightSeparator: 27px
// height folder input: 26px
//
// buble width: 309px
// buble left pane icon: 14px
// buble left pane width: 36px or (2.5 icon width)
// buble height: 46px
// buble arrow height: 12px
// buble arrow width: 14px
// buble radius: 3-4px
}
}

View File

@ -0,0 +1,154 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// List of import folder and their target
import QtQuick 2.8
import QtQuick.Controls 2.2
import ProtonUI 1.0
import ImportExportUI 1.0
Rectangle {
id: root
color: Style.importing.rowBackground
height: 40
width: 300
property real leftMargin1 : folderIcon.x - root.x
property real leftMargin2 : selectFolder.x - root.x
property real nameWidth : {
var available = root.width
available -= rowPlacement.children.length * rowPlacement.spacing // spacing between places
available -= 3*rowPlacement.leftPadding // left, and 2x right
available -= folderIcon.width
available -= arrowIcon.width
available -= dateRangeMenu.width
return available/3.3 // source folder label, target folder menu, target labels menu, and 0.3x label list
}
property real iconWidth : nameWidth*0.3
property bool isSourceSelected: isActive
property string lastTargetFolder: "6" // Archive
property string lastTargetLabels: "" // no flag by default
property string sourceID : mboxID
property string sourceName : name
Rectangle {
id: line
anchors {
left : parent.left
right : parent.right
bottom : parent.bottom
}
height : Style.main.border * 2
color : Style.importing.rowLine
}
Row {
id: rowPlacement
spacing: Style.dialog.spacing
leftPadding: Style.dialog.spacing*2
anchors.verticalCenter : parent.verticalCenter
CheckBoxLabel {
id: checkBox
anchors.verticalCenter : parent.verticalCenter
checked: root.isSourceSelected
onClicked: root.toggleImport()
}
Text {
id: folderIcon
text : gui.folderIcon(root.sourceName, gui.enums.folderTypeFolder)
anchors.verticalCenter : parent.verticalCenter
color: root.isSourceSelected ? Style.main.text : Style.main.textDisabled
font {
family : Style.fontawesome.name
pointSize : Style.dialog.fontSize * Style.pt
}
}
Text {
text : root.sourceName
width: nameWidth
elide: Text.ElideRight
anchors.verticalCenter : parent.verticalCenter
color: folderIcon.color
font.pointSize : Style.dialog.fontSize * Style.pt
}
Text {
id: arrowIcon
text : Style.fa.arrow_right
anchors.verticalCenter : parent.verticalCenter
color: Style.main.text
font {
family : Style.fontawesome.name
pointSize : Style.dialog.fontSize * Style.pt
}
}
SelectFolderMenu {
id: selectFolder
sourceID: root.sourceID
targets: transferRules.targetFolders(root.sourceID)
width: nameWidth
anchors.verticalCenter : parent.verticalCenter
enabled: root.isSourceSelected
onDoNotImport: root.toggleImport()
onImportToFolder: root.importToFolder(newTargetID)
}
SelectLabelsMenu {
sourceID: root.sourceID
targets: transferRules.targetLabels(root.sourceID)
width: nameWidth
anchors.verticalCenter : parent.verticalCenter
enabled: root.isSourceSelected
onAddTargetLabel: { transferRules.addTargetID(sourceID, newTargetID) }
onRemoveTargetLabel: { transferRules.removeTargetID(sourceID, newTargetID) }
}
LabelIconList {
colorList: labelColors=="" ? [] : labelColors.split(";")
width: iconWidth
anchors.verticalCenter : parent.verticalCenter
enabled: root.isSourceSelected
}
DateRangeMenu {
id: dateRangeMenu
sourceID: root.sourceID
sourceFromDate: fromDate
sourceToDate: toDate
enabled: root.isSourceSelected
anchors.verticalCenter : parent.verticalCenter
Component.onCompleted : dateRangeMenu.updateRange()
}
}
function importToFolder(newTargetID) {
transferRules.addTargetID(root.sourceID,newTargetID)
}
function toggleImport() {
transferRules.setIsRuleActive(root.sourceID, !root.isSourceSelected)
}
}

View File

@ -0,0 +1,216 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Import report modal
import QtQuick 2.11
import QtQuick.Controls 2.4
import ProtonUI 1.0
import ImportExportUI 1.0
Rectangle {
id: root
color: "#aa101021"
visible: false
MouseArea { // disable bellow
anchors.fill: root
hoverEnabled: true
}
Rectangle {
id:background
color: Style.main.background
anchors {
fill : root
topMargin : Style.main.rightMargin
leftMargin : 2*Style.main.rightMargin
rightMargin : 2*Style.main.rightMargin
bottomMargin : 2.5*Style.main.rightMargin
}
ClickIconText {
anchors {
top : parent.top
right : parent.right
margins : .5* Style.main.rightMargin
}
iconText : Style.fa.times
text : ""
textColor : Style.main.textBlue
onClicked : root.hide()
Accessible.description : qsTr("Close dialog %1", "Click to exit modal.").arg(title.text)
}
Text {
id: title
text : qsTr("List of errors")
font {
pointSize: Style.dialog.titleSize * Style.pt
}
anchors {
top : parent.top
topMargin : 0.5*Style.main.rightMargin
horizontalCenter : parent.horizontalCenter
}
}
ListView {
id: errorView
anchors {
left : parent.left
right : parent.right
top : title.bottom
bottom : detailBtn.top
margins : Style.main.rightMargin
}
clip : true
flickableDirection : Flickable.HorizontalAndVerticalFlick
contentWidth : errorView.rWall
boundsBehavior : Flickable.StopAtBounds
ScrollBar.vertical: ScrollBar {
anchors {
right : parent.right
top : parent.top
rightMargin : Style.main.rightMargin/4
topMargin : Style.main.rightMargin
}
width: Style.main.rightMargin/3
Accessible.ignored: true
}
ScrollBar.horizontal: ScrollBar {
anchors {
bottom : parent.bottom
right : parent.right
bottomMargin : Style.main.rightMargin/4
rightMargin : Style.main.rightMargin
}
height: Style.main.rightMargin/3
Accessible.ignored: true
}
property real rW1 : 150 *Style.px
property real rW2 : 150 *Style.px
property real rW3 : 100 *Style.px
property real rW4 : 150 *Style.px
property real rW5 : 550 *Style.px
property real rWall : errorView.rW1+errorView.rW2+errorView.rW3+errorView.rW4+errorView.rW5
property real pH : .5*Style.main.rightMargin
model : errorList
delegate : Rectangle {
width : Math.max(errorView.width, row.width)
height : row.height
Row {
id: row
spacing : errorView.pH
leftPadding : errorView.pH
rightPadding : errorView.pH
topPadding : errorView.pH
bottomPadding : errorView.pH
ImportReportCell { width : errorView.rW1; text : mailSubject }
ImportReportCell { width : errorView.rW2; text : mailDate }
ImportReportCell { width : errorView.rW3; text : inputFolder }
ImportReportCell { width : errorView.rW4; text : mailFrom }
ImportReportCell { width : errorView.rW5; text : errorMessage }
}
Rectangle {
color : Style.main.line
height : .8*Style.px
width : parent.width
anchors.left : parent.left
anchors.bottom : parent.bottom
}
}
headerPositioning: ListView.OverlayHeader
header: Rectangle {
height : viewHeader.height
width : Math.max(errorView.width, viewHeader.width)
color : Style.accounts.backgroundExpanded
z : 2
Row {
id: viewHeader
spacing : errorView.pH
leftPadding : errorView.pH
rightPadding : errorView.pH
topPadding : .5*errorView.pH
bottomPadding : .5*errorView.pH
ImportReportCell { width : errorView.rW1 ; text : qsTr ( "SUBJECT" ); isHeader: true }
ImportReportCell { width : errorView.rW2 ; text : qsTr ( "DATE/TIME" ); isHeader: true }
ImportReportCell { width : errorView.rW3 ; text : qsTr ( "FOLDER" ); isHeader: true }
ImportReportCell { width : errorView.rW4 ; text : qsTr ( "FROM" ); isHeader: true }
ImportReportCell { width : errorView.rW5 ; text : qsTr ( "ERROR" ); isHeader: true }
}
Rectangle {
color : Style.main.line
height : .8*Style.px
width : parent.width
anchors.left : parent.left
anchors.bottom : parent.bottom
}
}
}
Rectangle {
anchors{
fill : errorView
margins : -radius
}
radius : 2* Style.px
color : Style.transparent
border {
width : Style.px
color : Style.main.line
}
}
ButtonRounded {
id: detailBtn
fa_icon : Style.fa.file_text
text : qsTr("Detailed file")
color_main : Style.dialog.textBlue
onClicked : go.importLogFileName == "" ? go.openLogs() : go.openReport()
anchors {
bottom : parent.bottom
bottomMargin : 0.5*Style.main.rightMargin
horizontalCenter : parent.horizontalCenter
}
}
}
function show() {
root.visible = true
}
function hide() {
root.visible = false
}
}

View File

@ -0,0 +1,67 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Import report modal
import QtQuick 2.11
import QtQuick.Controls 2.4
import ProtonUI 1.0
import ImportExportUI 1.0
Rectangle {
id: root
property alias text : cellText.text
property bool isHeader : false
property bool isHovered : false
property bool isWider : cellText.contentWidth > root.width
width : 20*Style.px
height : cellText.height
z : root.isHovered ? 3 : 1
color : Style.transparent
Rectangle {
anchors {
fill : cellText
margins : -2*Style.px
}
color : root.isWider ? Style.main.background : Style.transparent
border {
color : root.isWider ? Style.main.textDisabled : Style.transparent
width : Style.px
}
}
Text {
id: cellText
color : root.isHeader ? Style.main.textDisabled : Style.main.text
elide : root.isHovered ? Text.ElideNone : Text.ElideRight
width : root.isHovered ? cellText.contentWidth : root.width
font {
pointSize : Style.main.textSize * Style.pt
family : Style.fontawesome.name
}
}
MouseArea {
anchors.fill : root
hoverEnabled : !root.isHeader
onEntered : { root.isHovered = true }
onExited : { root.isHovered = false }
}
}

View File

@ -0,0 +1,89 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Export dialog
import QtQuick 2.8
import QtQuick.Controls 2.2
import ProtonUI 1.0
import ImportExportUI 1.0
Button {
id: root
width : 200
height : icon.height + 4*tag.height
scale : pressed ? 0.95 : 1.0
property string iconText : Style.fa.ban
background: Rectangle { color: "transparent" }
contentItem: Rectangle {
id: wrapper
color: "transparent"
Image {
id: icon
anchors {
bottom : wrapper.bottom
bottomMargin : tag.height*2.5
horizontalCenter : wrapper.horizontalCenter
}
fillMode : Image.PreserveAspectFit
width : Style.main.fontSize * 7
mipmap : true
source : "images/"+iconText+".png"
}
Row {
spacing: Style.dialog.spacing
anchors {
bottom : wrapper.bottom
horizontalCenter : wrapper.horizontalCenter
}
Text {
id: tag
text : Style.fa.plus_circle
color : Qt.lighter( Style.dialog.textBlue, root.enabled ? 1.0 : 1.5)
font {
family : Style.fontawesome.name
pointSize : Style.main.fontSize * Style.pt * 1.2
}
}
Text {
text : root.text
color: tag.color
font {
family : tag.font.family
pointSize : tag.font.pointSize
weight : Font.DemiBold
underline : true
}
}
}
}
}

View File

@ -0,0 +1,148 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// List of import folder and their target
import QtQuick 2.8
import QtQuick.Controls 2.2
import ProtonUI 1.0
import ImportExportUI 1.0
Rectangle {
id: root
property string titleFrom
property string titleTo
property bool hasItems: true
color : Style.transparent
Rectangle {
anchors.fill: root
radius : Style.dialog.radiusButton
color : Style.transparent
border {
color : Style.main.line
width : 1.5*Style.dialog.borderInput
}
Text { // placeholder
visible: !root.hasItems
anchors.centerIn: parent
color: Style.main.textDisabled
font {
pointSize: Style.dialog.fontSize * Style.pt
}
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: qsTr("No emails found for this source.","todo")
}
}
anchors {
left : parent.left
right : parent.right
top : parent.top
bottom : parent.bottom
leftMargin : Style.main.leftMargin
rightMargin : Style.main.leftMargin
topMargin : Style.main.topMargin
bottomMargin : Style.main.bottomMargin
}
ListView {
id: listview
clip : true
orientation : ListView.Vertical
boundsBehavior : Flickable.StopAtBounds
model : transferRules
cacheBuffer : 10000
delegate : ImportDelegate {
width: root.width
}
anchors {
top: titleBox.bottom
bottom: root.bottom
left: root.left
right: root.right
margins : Style.dialog.borderInput
bottomMargin: Style.dialog.radiusButton
}
ScrollBar.vertical: ScrollBar {
anchors {
right: parent.right
rightMargin: Style.main.rightMargin/4
}
width: Style.main.rightMargin/3
Accessible.ignored: true
}
}
Rectangle {
id: titleBox
anchors {
top: parent.top
left: parent.left
right: parent.right
}
height: Style.main.fontSize *2
color : Style.transparent
Text {
id: textTitleFrom
anchors {
left: parent.left
verticalCenter: parent.verticalCenter
leftMargin: {
if (listview.currentItem === null) return 0
else return listview.currentItem.leftMargin1
}
}
text: "<b>"+qsTr("From:")+"</b> " + root.titleFrom
color: Style.main.text
width: listview.currentItem === null ? 0 : (listview.currentItem.leftMargin2 - listview.currentItem.leftMargin1 - Style.dialog.spacing)
elide: Text.ElideMiddle
}
Text {
id: textTitleTo
anchors {
left: parent.left
verticalCenter: parent.verticalCenter
leftMargin: {
if (listview.currentIndex<0) return root.width/3
else return listview.currentItem.leftMargin2
}
}
text: "<b>"+qsTr("To:")+"</b> " + root.titleTo
color: Style.main.text
}
}
Rectangle {
id: line
anchors {
left : titleBox.left
right : titleBox.right
top : titleBox.bottom
}
height: Style.dialog.borderInput
color: Style.main.line
}
}

View File

@ -0,0 +1,129 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// input for date range
import QtQuick 2.8
import QtQuick.Controls 2.2
import ProtonUI 1.0
import ImportExportUI 1.0
Row {
id: dateRange
property var structure : transferRules
property string sourceID : "-1"
property alias allDates : allDatesBox.checked
property alias inputDateFrom : inputDateFrom
property alias inputDateTo : inputDateTo
property alias labelWidth: label.width
function getRange() {common.getRange()}
function applyRange() {common.applyRange()}
DateRangeFunctions {id:common}
spacing: Style.dialog.spacing*2
Text {
id: label
anchors.verticalCenter: parent.verticalCenter
text : qsTr("Date range")
font {
bold: true
family: Style.fontawesome.name
pointSize: Style.main.fontSize * Style.pt
}
color: Style.main.text
}
DateInput {
id: inputDateFrom
label: ""
anchors.verticalCenter: parent.verticalCenter
currentDate: new Date(0) // default epoch start
maxDate: inputDateTo.currentDate
}
Text {
text : Style.fa.arrows_h
anchors.verticalCenter: parent.verticalCenter
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
color: Style.main.text
font.family: Style.fontawesome.name
}
DateInput {
id: inputDateTo
label: ""
anchors.verticalCenter: parent.verticalCenter
currentDate: new Date() // default now
minDate: inputDateFrom.currentDate
isMaxDateToday: true
}
CheckBoxLabel {
id: allDatesBox
text : qsTr("All dates")
anchors.verticalCenter : parent.verticalCenter
checkedSymbol : Style.fa.toggle_on
uncheckedSymbol : Style.fa.toggle_off
uncheckedColor : Style.main.textDisabled
symbolPointSize : Style.dialog.iconSize * Style.pt * 1.1
spacing : Style.dialog.spacing*2
TextMetrics {
id: metrics
text: allDatesBox.checkedSymbol
font {
family: Style.fontawesome.name
pointSize: allDatesBox.symbolPointSize
}
}
Rectangle {
color: allDatesBox.checked ? dotBackground.color : Style.exporting.sliderBackground
width: metrics.width*0.9
height: metrics.height*0.6
radius: height/2
z: -1
anchors {
left: allDatesBox.left
verticalCenter: allDatesBox.verticalCenter
leftMargin: 0.05 * metrics.width
}
Rectangle {
id: dotBackground
color : Style.exporting.background
height : parent.height
width : height
radius : height/2
anchors {
left : parent.left
verticalCenter : parent.verticalCenter
}
}
}
}
}

View File

@ -0,0 +1,227 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// input for date range
import QtQuick 2.8
import QtQuick.Controls 2.2
import ProtonUI 1.0
import ImportExportUI 1.0
Row {
id: root
spacing: Style.dialog.spacing
property alias labelWidth : label.width
property string labelName : ""
property string labelColor : ""
property alias labelSelected : masterLabelCheckbox.checked
Text {
id: label
text : qsTr("Add import label")
font {
bold: true
family: Style.fontawesome.name
pointSize: Style.main.fontSize * Style.pt
}
color: Style.main.text
anchors.verticalCenter: parent.verticalCenter
}
InfoToolTip {
info: qsTr( "When master import lablel is selected then all imported email will have this label.", "Tooltip text for master import label")
anchors.verticalCenter: parent.verticalCenter
}
CheckBoxLabel {
id: masterLabelCheckbox
text : ""
anchors.verticalCenter : parent.verticalCenter
checkedSymbol : Style.fa.toggle_on
uncheckedSymbol : Style.fa.toggle_off
uncheckedColor : Style.main.textDisabled
symbolPointSize : Style.dialog.iconSize * Style.pt * 1.1
spacing : Style.dialog.spacing*2
TextMetrics {
id: metrics
text: masterLabelCheckbox.checkedSymbol
font {
family: Style.fontawesome.name
pointSize: masterLabelCheckbox.symbolPointSize
}
}
Rectangle {
color: parent.checked ? dotBackground.color : Style.exporting.sliderBackground
width: metrics.width*0.9
height: metrics.height*0.6
radius: height/2
z: -1
anchors {
left: masterLabelCheckbox.left
verticalCenter: masterLabelCheckbox.verticalCenter
leftMargin: 0.05 * metrics.width
}
Rectangle {
id: dotBackground
color : Style.exporting.background
height : parent.height
width : height
radius : height/2
anchors {
left : parent.left
verticalCenter : parent.verticalCenter
}
}
}
}
Rectangle {
// label
color : Style.transparent
radius : Style.dialog.radiusButton
border {
color : Style.dialog.line
width : Style.dialog.borderInput
}
anchors.verticalCenter : parent.verticalCenter
scale: area.pressed ? 0.95 : 1
width: content.width
height: content.height
Row {
id: content
spacing : Style.dialog.spacing
padding : Style.dialog.spacing
anchors.verticalCenter: parent.verticalCenter
// label icon color
Text {
text: Style.fa.tag
color: root.labelSelected ? root.labelColor : Style.dialog.line
anchors.verticalCenter: parent.verticalCenter
font {
family: Style.fontawesome.name
pointSize: Style.main.fontSize * Style.pt
}
}
TextMetrics {
id:labelMetrics
text: root.labelName
elide: Text.ElideRight
elideWidth:gui.winMain.width*0.303
font {
pointSize: Style.main.fontSize * Style.pt
family: Style.fontawesome.name
}
}
// label text
Text {
text: labelMetrics.elidedText
color: root.labelSelected ? Style.dialog.text : Style.dialog.line
font: labelMetrics.font
anchors.verticalCenter: parent.verticalCenter
}
// edit icon
Text {
text: Style.fa.edit
color: root.labelSelected ? Style.main.textBlue : Style.dialog.line
anchors.verticalCenter: parent.verticalCenter
font {
family: Style.fontawesome.name
pointSize: Style.main.fontSize * Style.pt
}
}
}
MouseArea {
id: area
anchors.fill: parent
enabled: root.labelSelected
onClicked : {
if (!root.labelSelected) return
// NOTE: "createLater" is hack
winMain.popupFolderEdit.show(root.labelName, "createLater", root.labelColor, gui.enums.folderTypeLabel, "")
}
}
}
function reset(){
labelColor = go.leastUsedColor()
labelName = qsTr("Imported", "default name of global label followed by date") + " " + gui.niceDateTime()
labelSelected=true
}
Connections {
target: winMain.popupFolderEdit
onEdited : {
if (newName!="") root.labelName = newName
if (newColor!="") root.labelColor = newColor
}
}
/*
SelectLabelsMenu {
id: labelMenu
width : winMain.width/5
sourceID : root.sourceID
selectedIDs : root.structure.getTargetLabelIDs(root.sourceID)
anchors.verticalCenter: parent.verticalCenter
}
LabelIconList {
id: iconList
selectedIDs : root.structure.getTargetLabelIDs(root.sourceID)
anchors.verticalCenter: parent.verticalCenter
}
Connections {
target: structureExternal
onDataChanged: {
iconList.selectedIDs = root.structure.getTargetLabelIDs(root.sourceID)
labelMenu.selectedIDs = root.structure.getTargetLabelIDs(root.sourceID)
}
}
Connections {
target: structurePM
onDataChanged:{
iconList.selectedIDs = root.structure.getTargetLabelIDs(root.sourceID)
labelMenu.selectedIDs = root.structure.getTargetLabelIDs(root.sourceID)
}
}
*/
}

View File

@ -0,0 +1,65 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// List of icons for selected folders
import QtQuick 2.8
import QtQuick.Controls 2.2
import QtQml.Models 2.2
import ProtonUI 1.0
import ImportExportUI 1.0
Rectangle {
id: root
width: Style.main.fontSize * 2
height: metrics.height
property var colorList
color: "transparent"
DelegateModel {
id: selectedLabels
model : colorList
delegate : Text {
text : metrics.text
font : metrics.font
color : modelData
}
}
TextMetrics {
id: metrics
text: Style.fa.tag
font {
pointSize: Style.main.fontSize * Style.pt
family: Style.fontawesome.name
}
}
Row {
anchors.left : root.left
spacing : {
var n = Math.max(2,root.colorList.length)
var tagWidth = Math.max(1.0,metrics.width)
var space = Math.min(1*Style.px, (root.width - n*tagWidth)/(n-1)) // not more than 1px
space = Math.max(space,-tagWidth) // not less than tag width
return space
}
Repeater {
model: selectedLabels
}
}
}

View File

@ -0,0 +1,475 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// This is main window
import QtQuick 2.8
import QtQuick.Window 2.2
import QtQuick.Controls 2.1
import QtQuick.Layouts 1.3
import ImportExportUI 1.0
import ProtonUI 1.0
// Main Window
Window {
id : root
property alias tabbar : tabbar
property alias viewContent : viewContent
property alias viewAccount : viewAccount
property alias dialogAddUser : dialogAddUser
property alias dialogGlobal : dialogGlobal
property alias dialogCredits : dialogCredits
property alias dialogVersionInfo : dialogVersionInfo
property alias dialogUpdate : dialogUpdate
property alias popupMessage : popupMessage
property alias popupFolderEdit : popupFolderEdit
property alias updateState : infoBar.state
property alias dialogExport : dialogExport
property alias dialogImport : dialogImport
property alias addAccountTip : addAccountTip
property int heightContent : height-titleBar.height
property real innerWindowBorder : go.goos=="darwin" ? 0 : Style.main.border
// main window appearance
width : Style.main.width
height : Style.main.height
flags : go.goos=="darwin" ? Qt.Window : Qt.Window | Qt.FramelessWindowHint
color: go.goos=="windows" ? Style.main.background : Style.transparent
title: go.programTitle
minimumWidth : Style.main.width
minimumHeight : Style.main.height
property bool isOutdateVersion : root.updateState == "forceUpgrade"
property bool activeContent :
!dialogAddUser .visible &&
!dialogCredits .visible &&
!dialogVersionInfo .visible &&
!dialogGlobal .visible &&
!dialogUpdate .visible &&
!dialogImport .visible &&
!dialogExport .visible &&
!popupFolderEdit .visible &&
!popupMessage .visible
Accessible.role: Accessible.Grouping
Accessible.description: qsTr("Window %1").arg(title)
Accessible.name: Accessible.description
WindowTitleBar {
id: titleBar
window: root
visible: go.goos!="darwin"
}
Rectangle {
anchors {
top : titleBar.bottom
left : parent.left
right : parent.right
bottom : parent.bottom
}
color: Style.title.background
}
InformationBar {
id: infoBar
anchors {
left : parent.left
right : parent.right
top : titleBar.bottom
leftMargin: innerWindowBorder
rightMargin: innerWindowBorder
}
}
TabLabels {
id: tabbar
currentIndex : 0
enabled: root.activeContent
anchors {
top : infoBar.bottom
right : parent.right
left : parent.left
leftMargin: innerWindowBorder
rightMargin: innerWindowBorder
}
model: [
{ "title" : qsTr("Import-Export" , "title of tab that shows account list" ), "iconText": Style.fa.home },
{ "title" : qsTr("Settings" , "title of tab that allows user to change settings" ), "iconText": Style.fa.cogs },
{ "title" : qsTr("Help" , "title of tab that shows the help menu" ), "iconText": Style.fa.life_ring }
]
}
// Content of tabs
StackLayout {
id: viewContent
enabled: root.activeContent
// dimensions
anchors {
left : parent.left
right : parent.right
top : tabbar.bottom
bottom : parent.bottom
leftMargin: innerWindowBorder
rightMargin: innerWindowBorder
bottomMargin: innerWindowBorder
}
// attributes
currentIndex : { return root.tabbar.currentIndex}
clip : true
// content
AccountView {
id : viewAccount
onAddAccount : dialogAddUser.show()
model : accountsModel
hasFooter : false
delegate : AccountDelegate {
row_width : viewContent.width
}
}
SettingsView { id: viewSettings; }
HelpView { id: viewHelp; }
}
// Bubble prevent action
Rectangle {
anchors {
left: parent.left
right: parent.right
top: titleBar.bottom
bottom: parent.bottom
}
visible: bubbleNote.visible
color: "#aa222222"
MouseArea {
anchors.fill: parent
hoverEnabled: true
}
}
BubbleNote {
id : bubbleNote
visible : false
Component.onCompleted : {
bubbleNote.place(0)
}
}
BubbleNote {
id:addAccountTip
anchors.topMargin: viewAccount.separatorNoAccount - 2*Style.main.fontSize
text : qsTr("Click here to start", "on first launch, this is displayed above the Add Account button to tell the user what to do first")
state: (go.isFirstStart && viewAccount.numAccounts==0 && root.viewContent.currentIndex==0) ? "Visible" : "Invisible"
bubbleColor: Style.main.textBlue
Component.onCompleted : {
addAccountTip.place(-1)
}
enabled: false
states: [
State {
name: "Visible"
// hack: opacity 100% makes buttons dialog windows quit wrong color
PropertyChanges{target: addAccountTip; opacity: 0.999; visible: true}
},
State {
name: "Invisible"
PropertyChanges{target: addAccountTip; opacity: 0.0; visible: false}
}
]
transitions: [
Transition {
from: "Visible"
to: "Invisible"
SequentialAnimation{
NumberAnimation {
target: addAccountTip
property: "opacity"
duration: 0
easing.type: Easing.InOutQuad
}
NumberAnimation {
target: addAccountTip
property: "visible"
duration: 0
}
}
},
Transition {
from: "Invisible"
to: "Visible"
SequentialAnimation{
NumberAnimation {
target: addAccountTip
property: "visible"
duration: 300
}
NumberAnimation {
target: addAccountTip
property: "opacity"
duration: 500
easing.type: Easing.InOutQuad
}
}
}
]
}
// Dialogs
DialogAddUser {
id: dialogAddUser
anchors {
top : infoBar.bottom
bottomMargin: innerWindowBorder
leftMargin: innerWindowBorder
rightMargin: innerWindowBorder
}
onCreateAccount: Qt.openUrlExternally("https://protonmail.com/signup")
}
DialogUpdate {
id: dialogUpdate
title: root.isOutdateVersion ?
qsTr("%1 is outdated", "title of outdate dialog").arg(go.programTitle):
qsTr("%1 update to %2", "title of update dialog").arg(go.programTitle).arg(go.newversion)
introductionText: {
if (root.isOutdateVersion) {
if (go.goos=="linux") {
return qsTr('You are using an outdated version of our software.<br>
Please dowload and install the latest version to continue using %1.<br><br>
<a href="%2">%2</a>',
"Message for force-update in Linux").arg(go.programTitle).arg(go.landingPage)
} else {
return qsTr('You are using an outdated version of our software.<br>
Please dowload and install the latest version to continue using %1.<br><br>
You can continue with update or download and install the new version manually from<br><br>
<a href="%2">%2</a>',
"Message for force-update in Win/Mac").arg(go.programTitle).arg(go.landingPage)
}
} else {
if (go.goos=="linux") {
return qsTr('A new version of %1 is available.<br>
Check <a href="%2">release notes</a> to learn what is new in %3.<br>
Use your package manager to update or download and install new the version manually from<br><br>
<a href="%4">%4</a>',
"Message for update in Linux").arg(go.programTitle).arg("releaseNotes").arg(go.newversion).arg(go.landingPage)
} else {
return qsTr('A new version of %1 is available.<br>
Check <a href="%2">release notes</a> to learn what is new in %3.<br>
You can continue with update or download and install new the version manually from<br><br>
<a href="%4">%4</a>',
"Message for update in Win/Mac").arg(go.programTitle).arg("releaseNotes").arg(go.newversion).arg(go.landingPage)
}
}
}
}
DialogExport {
id: dialogExport
anchors {
top : infoBar.bottom
bottomMargin: innerWindowBorder
leftMargin: innerWindowBorder
rightMargin: innerWindowBorder
}
}
DialogImport {
id: dialogImport
anchors {
top : infoBar.bottom
bottomMargin: innerWindowBorder
leftMargin: innerWindowBorder
rightMargin: innerWindowBorder
}
}
Dialog {
id: dialogCredits
anchors {
top : infoBar.bottom
bottomMargin: innerWindowBorder
leftMargin: innerWindowBorder
rightMargin: innerWindowBorder
}
title: qsTr("Credits", "title for list of credited libraries")
Credits { }
}
Dialog {
id: dialogVersionInfo
anchors {
top : infoBar.bottom
bottomMargin: innerWindowBorder
leftMargin: innerWindowBorder
rightMargin: innerWindowBorder
}
property bool checkVersion : false
title: qsTr("Information about", "title of release notes page") + " v" + go.newversion
VersionInfo { }
onShow : {
// Hide information bar with olde version
if ( infoBar.state=="oldVersion" ) {
infoBar.state="upToDate"
dialogVersionInfo.checkVersion = true
}
}
onHide : {
// Reload current version based on online status
if (dialogVersionInfo.checkVersion) go.runCheckVersion(false)
dialogVersionInfo.checkVersion = false
}
}
DialogYesNo {
id: dialogGlobal
question : ""
answer : ""
z: 100
}
PopupEditFolder {
id: popupFolderEdit
anchors {
left: parent.left
right: parent.right
top: infoBar.bottom
bottom: parent.bottom
}
}
// Popup
PopupMessage {
id: popupMessage
anchors {
left : parent.left
right : parent.right
top : infoBar.bottom
bottom : parent.bottom
}
onClickedNo: popupMessage.hide()
onClickedOkay: popupMessage.hide()
onClickedCancel: popupMessage.hide()
onClickedYes: {
if (popupMessage.text == gui.areYouSureYouWantToQuit) Qt.quit()
}
}
// resize
MouseArea { // bottom
id: resizeBottom
property int diff: 0
anchors {
bottom : parent.bottom
left : parent.left
right : parent.right
}
cursorShape: Qt.SizeVerCursor
height: Style.main.fontSize
onPressed: {
var globPos = mapToGlobal(mouse.x, mouse.y)
resizeBottom.diff = root.height
resizeBottom.diff -= globPos.y
}
onMouseYChanged : {
var globPos = mapToGlobal(mouse.x, mouse.y)
root.height = Math.max(root.minimumHeight, globPos.y + resizeBottom.diff)
}
}
MouseArea { // right
id: resizeRight
property int diff: 0
anchors {
top : titleBar.bottom
bottom : parent.bottom
right : parent.right
}
cursorShape: Qt.SizeHorCursor
width: Style.main.fontSize/2
onPressed: {
var globPos = mapToGlobal(mouse.x, mouse.y)
resizeRight.diff = root.width
resizeRight.diff -= globPos.x
}
onMouseXChanged : {
var globPos = mapToGlobal(mouse.x, mouse.y)
root.width = Math.max(root.minimumWidth, globPos.x + resizeRight.diff)
}
}
function showAndRise(){
go.loadAccounts()
root.show()
root.raise()
if (!root.active) {
root.requestActivate()
}
}
// Toggle window
function toggle() {
go.loadAccounts()
if (root.visible) {
if (!root.active) {
root.raise()
root.requestActivate()
} else {
root.hide()
}
} else {
root.show()
root.raise()
}
}
onClosing : {
close.accepted=false
if (
(dialogImport.visible && dialogImport.currentIndex == 4 && go.progress!=1) ||
(dialogExport.visible && dialogExport.currentIndex == 2 && go.progress!=1)
) {
popupMessage.buttonOkay .visible = false
popupMessage.buttonYes .visible = false
popupMessage.buttonQuit .visible = true
popupMessage.buttonCancel .visible = true
popupMessage.show ( gui.areYouSureYouWantToQuit )
return
}
close.accepted=true
go.processFinished()
}
}

View File

@ -0,0 +1,84 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick 2.8
import QtQuick.Controls 2.2
import ProtonUI 1.0
import ImportExportUI 1.0
Column {
spacing: Style.dialog.spacing
property string checkedText : group.checkedButton.text
Text {
id: formatLabel
font {
pointSize: Style.dialog.fontSize * Style.pt
bold: true
}
color: Style.dialog.text
text: qsTr("Select format of exported email:")
InfoToolTip {
info: qsTr("MBOX exports one file for each folder", "todo") + "\n" + qsTr("EML exports one file for each email", "todo")
anchors {
left: parent.right
leftMargin: Style.dialog.spacing
verticalCenter: parent.verticalCenter
}
}
}
Row {
spacing : Style.main.leftMargin
ButtonGroup {
id: group
}
Repeater {
model: [ "MBOX", "EML" ]
delegate : RadioButton {
id: radioDelegate
checked: modelData=="MBOX"
width: 5*Style.dialog.fontSize // hack due to bold
text: modelData
ButtonGroup.group: group
spacing: Style.main.spacing
indicator: Text {
text : radioDelegate.checked ? Style.fa.check_circle : Style.fa.circle_o
color : radioDelegate.checked ? Style.main.textBlue : Style.main.textInactive
font {
pointSize: Style.dialog.iconSize * Style.pt
family: Style.fontawesome.name
}
anchors.verticalCenter: parent.verticalCenter
}
contentItem: Text {
text: radioDelegate.text
color: Style.main.text
font {
pointSize: Style.dialog.fontSize * Style.pt
bold: checked
}
horizontalAlignment : Text.AlignHCenter
verticalAlignment : Text.AlignVCenter
leftPadding: Style.dialog.iconSize
}
}
}
}
}

View File

@ -0,0 +1,311 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// popup to edit folders or labels
import QtQuick 2.8
import QtQuick.Controls 2.1
import ImportExportUI 1.0
import ProtonUI 1.0
Rectangle {
id: root
visible: false
color: "#aa223344"
property string folderType : gui.enums.folderTypeFolder
property bool isFolder : folderType == gui.enums.folderTypeFolder
property bool isNew : currentId == ""
property bool isCreateLater : currentId == "createLater" // NOTE: "createLater" is hack because folder id should be base64 string
property string currentName : ""
property string currentId : ""
property string currentColor : ""
property string sourceID : ""
property string selectedColor : colorList[0]
property color textColor : Style.main.background
property color backColor : Style.bubble.paneBackground
signal edited(string newName, string newColor)
property var colorList : [ "#7272a7", "#8989ac", "#cf5858", "#cf7e7e", "#c26cc7", "#c793ca", "#7569d1", "#9b94d1", "#69a9d1", "#a8c4d5", "#5ec7b7", "#97c9c1", "#72bb75", "#9db99f", "#c3d261", "#c6cd97", "#e6c04c", "#e7d292", "#e6984c", "#dfb286" ]
MouseArea { // prevent action below aka modal: true
anchors.fill: parent
hoverEnabled: true
}
Rectangle {
id:background
anchors {
fill: root
leftMargin: winMain.width/6
topMargin: winMain.height/6
rightMargin: anchors.leftMargin
bottomMargin: anchors.topMargin
}
color: backColor
radius: Style.errorDialog.radius
}
Column { // content
anchors {
top : background.top
horizontalCenter : background.horizontalCenter
}
topPadding : Style.main.topMargin
bottomPadding : topPadding
spacing : (background.height - title.height - inputField.height - view.height - buttonRow.height - topPadding - bottomPadding) / children.length
Text {
id: title
font.pointSize: Style.dialog.titleSize * Style.pt
color: textColor
text: {
if ( root.isFolder && root.isNew ) return qsTr ( "Create new folder" )
if ( !root.isFolder && root.isNew ) return qsTr ( "Create new label" )
if ( root.isFolder && !root.isNew ) return qsTr ( "Edit folder %1" ) .arg( root.currentName )
if ( !root.isFolder && !root.isNew ) return qsTr ( "Edit label %1" ) .arg( root.currentName )
}
width : parent.width
elide : Text.ElideRight
horizontalAlignment : Text.AlignHCenter
Rectangle {
anchors {
top: parent.bottom
topMargin: Style.dialog.spacing
horizontalCenter: parent.horizontalCenter
}
color: textColor
height: Style.main.borderInput
}
}
TextField {
id: inputField
anchors {
horizontalCenter: parent.horizontalCenter
}
width : parent.width
height : Style.dialog.button
rightPadding : Style.dialog.spacing
leftPadding : height + rightPadding
bottomPadding : rightPadding
topPadding : rightPadding
selectByMouse : true
color : textColor
font.pointSize : Style.dialog.fontSize * Style.pt
background: Rectangle {
color: backColor
border {
color: textColor
width: Style.dialog.borderInput
}
radius : Style.dialog.radiusButton
Text {
anchors {
left: parent.left
verticalCenter: parent.verticalCenter
}
font {
family: Style.fontawesome.name
pointSize: Style.dialog.titleSize * Style.pt
}
text : folderType == gui.enums.folderTypeFolder ? Style.fa.folder : Style.fa.tag
color : root.selectedColor
width : parent.height
horizontalAlignment: Text.AlignHCenter
}
Rectangle {
anchors {
left: parent.left
top: parent.top
leftMargin: parent.height
}
width: parent.border.width/2
height: parent.height
}
}
}
GridView {
id: view
anchors {
horizontalCenter: parent.horizontalCenter
}
model : colorList
cellWidth : 2*Style.dialog.titleSize
cellHeight : cellWidth
width : 10*cellWidth
height : 2*cellHeight
delegate: Rectangle {
width: view.cellWidth*0.8
height: width
radius: width/2
color: modelData
border {
color: indicator.visible ? textColor : modelData
width: 2*Style.px
}
Text {
id: indicator
anchors.centerIn : parent
text: Style.fa.check
color: textColor
font {
family: Style.fontawesome.name
pointSize: Style.dialog.titleSize * Style.pt
}
visible: modelData == root.selectedColor
}
MouseArea {
anchors.fill: parent
onClicked : {
root.selectedColor = modelData
}
}
}
}
Row {
id: buttonRow
anchors {
horizontalCenter: parent.horizontalCenter
}
spacing: Style.main.leftMargin
ButtonRounded {
text: "Cancel"
color_main : textColor
onClicked :{
root.hide()
}
}
ButtonRounded {
text: "Okay"
color_main: Style.dialog.background
color_minor: Style.dialog.textBlue
isOpaque: true
onClicked :{
root.okay()
}
}
}
}
function hide() {
root.visible=false
root.currentId = ""
root.currentName = ""
root.currentColor = ""
root.folderType = ""
root.sourceID = ""
inputField.text = ""
}
function show(currentName, currentId, currentColor, folderType, sourceID) {
root.currentId = currentId
root.currentName = currentName
root.currentColor = currentColor=="" ? go.leastUsedColor() : currentColor
root.selectedColor = root.currentColor
root.folderType = folderType
root.sourceID = sourceID
inputField.text = currentName
root.visible=true
//console.log(title.text , root.currentName, root.currentId, root.currentColor, root.folderType, root.sourceID)
}
function okay() {
// check inpupts
if (inputField.text == "") {
go.notifyError(gui.enums.errFillFolderName)
return
}
if (colorList.indexOf(root.selectedColor)<0) {
go.notifyError(gui.enums.errSelectFolderColor)
return
}
var isLabel = root.folderType == gui.enums.folderTypeLabel
if (!isLabel && !root.isFolder){
console.log("Unknown folder type: ", root.folderType)
go.notifyError(gui.enums.errUpdateLabelFailed)
root.hide()
return
}
if (winMain.dialogImport.address == "") {
console.log("Unknown address", winMain.dialogImport.address)
go.onNotifyError(gui.enums.errUpdateLabelFailed)
root.hide()
}
if (root.isCreateLater) {
root.edited(inputField.text, root.selectedColor)
root.hide()
return
}
// TODO send request (as timer)
if (root.isNew) {
var isOK = go.createLabelOrFolder(winMain.dialogImport.address, inputField.text, root.selectedColor, isLabel, root.sourceID)
if (isOK) {
root.hide()
}
} else {
// TODO: check there was some change
go.updateLabelOrFolder(winMain.dialogImport.address, root.currentId, inputField.text, root.selectedColor)
}
// waiting for finish
// TODO: waiting wheel of doom
// TODO: on close add source to sourceID
}
}

View File

@ -0,0 +1,339 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// This is global combo box which can be adjusted to choose folder target, folder label or global label
import QtQuick 2.8
import QtQuick.Controls 2.2
import ProtonUI 1.0
import ImportExportUI 1.0
ComboBox {
id: root
//fixme rounded
height: Style.main.fontSize*2 //fixme
property string folderType: gui.enums.folderTypeFolder
property string sourceID
property var targets
property bool isFolderType: root.folderType == gui.enums.folderTypeFolder
property bool below: true
signal doNotImport()
signal importToFolder(string newTargetID)
signal addTargetLabel(string newTargetID)
signal removeTargetLabel(string newTargetID)
leftPadding: Style.dialog.spacing
onDownChanged : {
root.below = popup.y>0
}
contentItem : Text {
id: boxText
verticalAlignment: Text.AlignVCenter
font {
family: Style.fontawesome.name
pointSize : Style.dialog.fontSize * Style.pt
bold: root.down
}
elide: Text.ElideRight
textFormat: Text.StyledText
text : root.displayText
color: !root.enabled ? Style.main.textDisabled : ( root.down ? Style.main.background : Style.main.text )
}
displayText: {
if (view.currentIndex >= 0) {
if (!root.isFolderType) return Style.fa.tags + " " + qsTr("Add/Remove labels")
var tgtName = view.currentItem.folderName
var tgtIcon = view.currentItem.folderIcon
var tgtColor = view.currentItem.folderColor
if (tgtIcon != Style.fa.folder_open) {
return tgtIcon + " " + tgtName
}
return '<font color="'+tgtColor+'">'+ tgtIcon + "</font> " + tgtName
}
if (root.isFolderType) return qsTr("No folder selected")
return qsTr("No labels selected")
}
background : RoundedRectangle {
fillColor : root.down ? Style.main.textBlue : Style.transparent
strokeColor : root.down ? fillColor : Style.main.line
radiusTopLeft : root.down && !root.below ? 0 : Style.dialog.radiusButton
radiusBottomLeft : root.down && root.below ? 0 : Style.dialog.radiusButton
radiusTopRight : radiusTopLeft
radiusBottomRight : radiusBottomLeft
MouseArea {
anchors.fill: parent
onClicked : {
if (root.down) root.popup.close()
else root.popup.open()
}
}
}
indicator : Text {
text: (root.down && root.below) || (!root.down && !root.below) ? Style.fa.chevron_up : Style.fa.chevron_down
anchors {
right: parent.right
verticalCenter: parent.verticalCenter
rightMargin: Style.dialog.spacing
}
font {
family : Style.fontawesome.name
pointSize : Style.dialog.fontSize * Style.pt
}
color: root.enabled && !root.down ? Style.main.textBlue : root.contentItem.color
}
// Popup row
delegate: Rectangle {
id: thisDelegate
height : Style.main.fontSize * 2
width : selectNone.width
property bool isHovered: area.containsMouse
color: isHovered ? root.popup.hoverColor : root.popup.backColor
property bool isSelected : isActive
property string folderName: name
property string folderIcon: gui.folderIcon(name,type)
property string folderColor: (type == gui.enums.folderTypeLabel || type == gui.enums.folderTypeFolder) ? iconColor : root.popup.textColor
Text {
id: targetIcon
text: thisDelegate.folderIcon
color : thisDelegate.folderColor
anchors {
verticalCenter: parent.verticalCenter
left: parent.left
leftMargin: root.leftPadding
}
font {
family : Style.fontawesome.name
pointSize : Style.dialog.fontSize * Style.pt
}
}
Text {
id: targetName
anchors {
verticalCenter: parent.verticalCenter
left: targetIcon.right
right: parent.right
leftMargin: Style.dialog.spacing
rightMargin: Style.dialog.spacing
}
text: thisDelegate.folderName
color : root.popup.textColor
elide: Text.ElideRight
font {
family : Style.fontawesome.name
pointSize : Style.dialog.fontSize * Style.pt
}
}
Text {
id: targetIndicator
anchors {
right: parent.right
verticalCenter: parent.verticalCenter
}
text : thisDelegate.isSelected ? Style.fa.check_square : Style.fa.square_o
visible : thisDelegate.isSelected || !root.isFolderType
color : root.popup.textColor
font {
family : Style.fontawesome.name
pointSize : Style.dialog.fontSize * Style.pt
}
}
Rectangle {
id: line
anchors {
bottom : parent.bottom
left : parent.left
right : parent.right
}
height : Style.main.lineWidth
color : Style.main.line
}
MouseArea {
id: area
anchors.fill: parent
onClicked: {
//console.log(" click delegate")
if (root.isFolderType) { // don't update if selected
root.popup.close()
if (!isActive) {
root.importToFolder(mboxID)
}
} else {
if (isActive) {
root.removeTargetLabel(mboxID)
} else {
root.addTargetLabel(mboxID)
}
}
}
hoverEnabled: true
}
}
popup : Popup {
y: root.height
width: root.width
modal: true
closePolicy: Popup.CloseOnPressOutside | Popup.CloseOnEscape
padding: Style.dialog.spacing
property var textColor : Style.main.background
property var backColor : Style.main.text
property var hoverColor : Style.main.textBlue
contentItem : Column {
// header
Rectangle {
id: selectNone
width: root.popup.width - 2*root.popup.padding
//height: root.isFolderType ? 2* Style.main.fontSize : 0
height: 2*Style.main.fontSize
color: area.containsMouse ? root.popup.hoverColor : root.popup.backColor
visible : root.isFolderType
Text {
anchors {
left : parent.left
leftMargin : Style.dialog.spacing
verticalCenter : parent.verticalCenter
}
text: root.isFolderType ? qsTr("Do not import") : ""
color: root.popup.textColor
font {
pointSize: Style.dialog.fontSize * Style.pt
bold: true
}
}
Rectangle {
id: line
anchors {
bottom : parent.bottom
left : parent.left
right : parent.right
}
height : Style.dialog.borderInput
color : Style.main.line
}
MouseArea {
id: area
anchors.fill: parent
onClicked: {
//console.log(" click no set")
root.doNotImport()
root.popup.close()
}
hoverEnabled: true
}
}
// scroll area
Rectangle {
width: selectNone.width
height: winMain.height/4
color: root.popup.backColor
ListView {
id: view
clip : true
anchors.fill : parent
model : root.targets
delegate : root.delegate
currentIndex: view.model.selectedIndex
}
}
// footer
Rectangle {
id: addFolderOrLabel
width: selectNone.width
height: addButton.height + 3*Style.dialog.spacing
color: root.popup.backColor
Rectangle {
anchors {
top : parent.top
left : parent.left
right : parent.right
}
height : Style.dialog.borderInput
color : Style.main.line
}
ButtonRounded {
id: addButton
anchors.centerIn: addFolderOrLabel
width: parent.width * 0.681
fa_icon : Style.fa.plus_circle
text : root.isFolderType ? qsTr("Create new folder") : qsTr("Create new label")
color_main : root.popup.textColor
}
MouseArea {
anchors.fill : parent
onClicked : {
//console.log("click", addButton.text)
var newName = name
winMain.popupFolderEdit.show(newName, "", "", root.folderType, sourceID)
root.popup.close()
}
}
}
}
background : RoundedRectangle {
strokeColor : root.popup.backColor
fillColor : root.popup.backColor
radiusTopLeft : root.below ? 0 : Style.dialog.radiusButton
radiusBottomLeft : !root.below ? 0 : Style.dialog.radiusButton
radiusTopRight : radiusTopLeft
radiusBottomRight : radiusBottomLeft
}
}
}

View File

@ -0,0 +1,29 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// List of import folder and their target
import QtQuick 2.8
import QtQuick.Controls 2.2
import ProtonUI 1.0
import ImportExportUI 1.0
SelectFolderMenu {
id: root
folderType: gui.enums.folderTypeLabel
}

View File

@ -0,0 +1,148 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// List the settings
import QtQuick 2.8
import ProtonUI 1.0
import ImportExportUI 1.0
Item {
id: root
// must have wrapper
Rectangle {
id: wrapper
anchors.centerIn: parent
width: parent.width
height: parent.height
color: Style.main.background
// content
Column {
anchors.left : parent.left
ButtonIconText {
id: cacheKeychain
text: qsTr("Clear Keychain")
leftIcon.text : Style.fa.chain_broken
rightIcon {
text : qsTr("Clear")
color: Style.main.text
font {
pointSize : Style.settings.fontSize * Style.pt
underline : true
}
}
onClicked: {
dialogGlobal.state="clearChain"
dialogGlobal.show()
}
}
ButtonIconText {
id: logs
anchors.left: parent.left
text: qsTr("Logs")
leftIcon.text : Style.fa.align_justify
rightIcon.text : Style.fa.chevron_circle_right
rightIcon.font.pointSize : Style.settings.toggleSize * Style.pt
onClicked: go.openLogs()
}
ButtonIconText {
id: bugreport
anchors.left: parent.left
text: qsTr("Report Bug")
leftIcon.text : Style.fa.bug
rightIcon.text : Style.fa.chevron_circle_right
rightIcon.font.pointSize : Style.settings.toggleSize * Style.pt
onClicked: bugreportWin.show()
}
/*
ButtonIconText {
id: cacheClear
text: qsTr("Clear Cache")
leftIcon.text : Style.fa.times
rightIcon {
text : qsTr("Clear")
color: Style.main.text
font {
pointSize : Style.settings.fontSize * Style.pt
underline : true
}
}
onClicked: {
dialogGlobal.state="clearCache"
dialogGlobal.show()
}
}
ButtonIconText {
id: autoStart
text: qsTr("Automatically Start Bridge")
leftIcon.text : Style.fa.rocket
rightIcon {
font.pointSize : Style.settings.toggleSize * Style.pt
text : go.isAutoStart!=0 ? Style.fa.toggle_on : Style.fa.toggle_off
color : go.isAutoStart!=0 ? Style.main.textBlue : Style.main.textDisabled
}
onClicked: {
go.toggleAutoStart()
}
}
ButtonIconText {
id: advancedSettings
property bool isAdvanced : !go.isDefaultPort
text: qsTr("Advanced settings")
leftIcon.text : Style.fa.cogs
rightIcon {
font.pointSize : Style.settings.toggleSize * Style.pt
text : isAdvanced!=0 ? Style.fa.chevron_circle_up : Style.fa.chevron_circle_right
color : isAdvanced!=0 ? Style.main.textDisabled : Style.main.textBlue
}
onClicked: {
isAdvanced = !isAdvanced
}
}
ButtonIconText {
id: changePort
visible: advancedSettings.isAdvanced
text: qsTr("Change SMTP/IMAP Ports")
leftIcon.text : Style.fa.plug
rightIcon {
text : qsTr("Change")
color: Style.main.text
font {
pointSize : Style.settings.fontSize * Style.pt
underline : true
}
}
onClicked: {
dialogChangePort.show()
}
}
*/
}
}
}

View File

@ -0,0 +1,125 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// credits
import QtQuick 2.8
import ProtonUI 1.0
import ImportExportUI 1.0
Item {
id: root
Rectangle {
id: wrapper
anchors.centerIn: parent
width: 2*Style.main.width/3
height: Style.main.height - 6*Style.dialog.titleSize
color: "transparent"
Flickable {
anchors.fill : wrapper
contentWidth : wrapper.width
contentHeight : content.height
flickableDirection : Flickable.VerticalFlick
clip : true
Column {
id: content
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
width: wrapper.width
spacing: 5
Text {
visible: go.changelog != ""
anchors {
left: parent.left
}
font.bold: true
font.pointSize: Style.main.fontSize * Style.pt
color: Style.main.text
text: qsTr("Release notes:")
}
Text {
anchors {
left: parent.left
leftMargin: Style.main.leftMargin
}
font.pointSize: Style.main.fontSize * Style.pt
width: wrapper.width - anchors.leftMargin
wrapMode: Text.Wrap
color: Style.main.text
text: go.changelog
}
Text {
visible: go.bugfixes != ""
anchors {
left: parent.left
}
font.bold: true
font.pointSize: Style.main.fontSize * Style.pt
color: Style.main.text
text: qsTr("Fixed bugs:")
}
Repeater {
anchors.fill: parent
model: go.bugfixes.split(";")
Text {
visible: go.bugfixes!=""
anchors {
left: parent.left
leftMargin: Style.main.leftMargin
}
font.pointSize: Style.main.fontSize * Style.pt
width: wrapper.width - anchors.leftMargin
wrapMode: Text.Wrap
color: Style.main.text
text: modelData
}
}
Rectangle{id:spacer; color:"transparent"; width:10; height: buttonClose.height}
ButtonRounded {
id: buttonClose
anchors.horizontalCenter: content.horizontalCenter
text: "Close"
onClicked: {
root.parent.hide()
}
}
AccessibleSelectableText {
anchors.horizontalCenter: content.horizontalCenter
font {
pointSize : Style.main.fontSize * Style.pt
}
color: Style.main.textDisabled
text: "\n Current: "+go.fullversion
}
}
}
}
}

View File

@ -0,0 +1,31 @@
module ImportExportUI
AccountDelegate 1.0 AccountDelegate.qml
Credits 1.0 Credits.qml
DateBox 1.0 DateBox.qml
DateInput 1.0 DateInput.qml
DateRangeMenu 1.0 DateRangeMenu.qml
DateRange 1.0 DateRange.qml
DateRangeFunctions 1.0 DateRangeFunctions.qml
DialogExport 1.0 DialogExport.qml
DialogImport 1.0 DialogImport.qml
DialogYesNo 1.0 DialogYesNo.qml
ExportStructure 1.0 ExportStructure.qml
FilterStructure 1.0 FilterStructure.qml
FolderRowButton 1.0 FolderRowButton.qml
HelpView 1.0 HelpView.qml
IEStyle 1.0 IEStyle.qml
ImportDelegate 1.0 ImportDelegate.qml
ImportSourceButton 1.0 ImportSourceButton.qml
ImportStructure 1.0 ImportStructure.qml
ImportReport 1.0 ImportReport.qml
ImportReportCell 1.0 ImportReportCell.qml
InlineDateRange 1.0 InlineDateRange.qml
InlineLabelSelect 1.0 InlineLabelSelect.qml
LabelIconList 1.0 LabelIconList.qml
MainWindow 1.0 MainWindow.qml
OutputFormat 1.0 OutputFormat.qml
PopupEditFolder 1.0 PopupEditFolder.qml
SelectFolderMenu 1.0 SelectFolderMenu.qml
SelectLabelsMenu 1.0 SelectLabelsMenu.qml
SettingsView 1.0 SettingsView.qml
VersionInfo 1.0 VersionInfo.qml

View File

@ -87,7 +87,7 @@ Item {
Text { // Status
anchors {
left : parent.left
leftMargin : Style.accounts.leftMargin2
leftMargin : viewContent.width/2
verticalCenter : parent.verticalCenter
}
visible: root.numAccounts!=0
@ -99,7 +99,7 @@ Item {
Text { // Actions
anchors {
left : parent.left
leftMargin : Style.accounts.leftMargin3
leftMargin : 5.5*viewContent.width/8
verticalCenter : parent.verticalCenter
}
visible: root.numAccounts!=0

View File

@ -106,6 +106,7 @@ Rectangle {
}
MouseArea {
anchors.fill: mainText
cursorShape: mainText.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
acceptedButtons: Qt.NoButton
}

View File

@ -39,7 +39,7 @@ Window {
color : "transparent"
flags : Qt.Window | Qt.Dialog | Qt.FramelessWindowHint
title : "ProtonMail Bridge - Bug report"
title : "Bug report"
visible : false
WindowTitleBar {
@ -327,6 +327,7 @@ Window {
function show() {
prefill()
description.focus=true
root.visible=true
}

View File

@ -30,10 +30,12 @@ CheckBox {
property color uncheckedColor : Style.main.textInactive
property string checkedSymbol : Style.fa.check_square_o
property string uncheckedSymbol : Style.fa.square_o
property alias symbolPointSize : symbol.font.pointSize
background: Rectangle {
color: Style.transparent
}
indicator: Text {
id: symbol
text : root.checked ? root.checkedSymbol : root.uncheckedSymbol
color : root.checked ? root.checkedColor : root.uncheckedColor
font {

View File

@ -123,12 +123,12 @@ Dialog {
wrapMode: Text.Wrap
text: {
switch (go.progressDescription) {
case 1: return qsTr("Checking the current version.")
case 2: return qsTr("Downloading the update files.")
case 3: return qsTr("Verifying the update files.")
case 4: return qsTr("Unpacking the update files.")
case 5: return qsTr("Starting the update.")
case 6: return qsTr("Quitting the application.")
case "1": return qsTr("Checking the current version.")
case "2": return qsTr("Downloading the update files.")
case "3": return qsTr("Verifying the update files.")
case "4": return qsTr("Unpacking the update files.")
case "5": return qsTr("Starting the update.")
case "6": return qsTr("Quitting the application.")
default: return ""
}
}
@ -220,7 +220,7 @@ Dialog {
function clear() {
root.hasError = false
go.progress = 0.0
go.progressDescription = 0
go.progressDescription = "0"
}
function finished(hasError) {

View File

@ -60,7 +60,7 @@ Row {
FileDialog {
id: pathDialog
title: root.title + ":"
title: root.title
folder: shortcuts.home
onAccepted: sanitizePath(pathDialog.fileUrl.toString())
selectFolder: true

View File

@ -138,6 +138,11 @@ Column {
}
}
function clear() {
inputField.text = ""
rightIcon = ""
}
function checkNonEmpty() {
if (inputField.text == "") {
rightIcon = Style.fa.exclamation_triangle
@ -154,6 +159,17 @@ Column {
if (root.isPassword) inputField.echoMode = TextInput.Password
}
function checkIsANumber(){
if (/^\d+$/.test(inputField.text)) {
rightIcon = Style.fa.check_circle
return true
}
rightIcon = Style.fa.exclamation_triangle
root.placeholderText = ""
inputField.focus = true
return false
}
function forceFocus() {
inputField.forceActiveFocus()
}

View File

@ -23,9 +23,26 @@ import ProtonUI 1.0
Rectangle {
id: root
color: Style.transparent
property alias text: message.text
property alias text : message.text
property alias checkbox : checkbox
property alias buttonQuit : buttonQuit
property alias buttonOkay : buttonOkay
property alias buttonYes : buttonYes
property alias buttonNo : buttonNo
property alias buttonRetry : buttonRetry
property alias buttonSkip : buttonSkip
property alias buttonCancel : buttonCancel
property alias msgWidth : backgroundInp.width
property string msgID : ""
visible: false
signal clickedOkay()
signal clickedYes()
signal clickedNo()
signal clickedRetry()
signal clickedSkip()
signal clickedCancel()
MouseArea { // prevent action below
anchors.fill: parent
hoverEnabled: true
@ -58,14 +75,29 @@ Rectangle {
wrapMode: Text.Wrap
}
ButtonRounded {
text : qsTr("Okay", "todo")
isOpaque : true
color_main : Style.dialog.background
color_minor : Style.dialog.textBlue
onClicked : root.hide()
CheckBoxLabel {
id: checkbox
text: ""
checked: false
visible: (text != "")
textColor : Style.errorDialog.text
checkedColor: Style.errorDialog.text
uncheckedColor: Style.errorDialog.text
anchors.horizontalCenter : parent.horizontalCenter
}
Row {
spacing: Style.dialog.spacing
anchors.horizontalCenter : parent.horizontalCenter
ButtonRounded { id : buttonQuit ; text : qsTr ( "Stop & quit", "" ) ; onClicked : root.clickedYes ( ) ; visible : false ; isOpaque : true ; color_main : Style.errorDialog.text ; color_minor : Style.dialog.textBlue ; }
ButtonRounded { id : buttonNo ; text : qsTr ( "No" , "Button No" ) ; onClicked : root.clickedNo ( ) ; visible : false ; isOpaque : false ; color_main : Style.errorDialog.text ; color_minor : Style.transparent ; }
ButtonRounded { id : buttonYes ; text : qsTr ( "Yes" , "Button Yes" ) ; onClicked : root.clickedYes ( ) ; visible : false ; isOpaque : true ; color_main : Style.errorDialog.text ; color_minor : Style.dialog.textBlue ; }
ButtonRounded { id : buttonRetry ; text : qsTr ( "Retry" , "Button Retry" ) ; onClicked : root.clickedRetry ( ) ; visible : false ; isOpaque : false ; color_main : Style.errorDialog.text ; color_minor : Style.transparent ; }
ButtonRounded { id : buttonSkip ; text : qsTr ( "Skip" , "Button Skip" ) ; onClicked : root.clickedSkip ( ) ; visible : false ; isOpaque : false ; color_main : Style.errorDialog.text ; color_minor : Style.transparent ; }
ButtonRounded { id : buttonCancel ; text : qsTr ( "Cancel" , "Button Cancel" ) ; onClicked : root.clickedCancel ( ) ; visible : false ; isOpaque : true ; color_main : Style.errorDialog.text ; color_minor : Style.dialog.textBlue ; }
ButtonRounded { id : buttonOkay ; text : qsTr ( "Okay" , "Button Okay" ) ; onClicked : root.clickedOkay ( ) ; visible : true ; isOpaque : true ; color_main : Style.errorDialog.text ; color_minor : Style.dialog.textBlue ; }
}
}
}
@ -75,7 +107,16 @@ Rectangle {
}
function hide() {
root.state = "Okay"
root.visible=false
root .text = ""
checkbox .text = ""
buttonNo .visible = false
buttonYes .visible = false
buttonRetry .visible = false
buttonSkip .visible = false
buttonCancel .visible = false
buttonOkay .visible = true
}
}

View File

@ -0,0 +1,115 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick 2.8
import ProtonUI 1.0
Rectangle {
id: root
color: Style.transparent
property color fillColor : Style.main.background
property color strokeColor : Style.main.line
property real strokeWidth : Style.dialog.borderInput
property real radiusTopLeft : Style.dialog.radiusButton
property real radiusBottomLeft : Style.dialog.radiusButton
property real radiusTopRight : Style.dialog.radiusButton
property real radiusBottomRight : Style.dialog.radiusButton
function paint() {
canvas.requestPaint()
}
onFillColorChanged : root.paint()
onStrokeColorChanged : root.paint()
onStrokeWidthChanged : root.paint()
onRadiusTopLeftChanged : root.paint()
onRadiusBottomLeftChanged : root.paint()
onRadiusTopRightChanged : root.paint()
onRadiusBottomRightChanged : root.paint()
Canvas {
id: canvas
anchors.fill: root
onPaint: {
var ctx = getContext("2d")
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = root.fillColor
ctx.strokeStyle = root.strokeColor
ctx.lineWidth = root.strokeWidth
var dimensions = {
x: ctx.lineWidth,
y: ctx.lineWidth,
w: canvas.width-2*ctx.lineWidth,
h: canvas.height-2*ctx.lineWidth,
}
var radius = {
tl: root.radiusTopLeft,
tr: root.radiusTopRight,
bl: root.radiusBottomLeft,
br: root.radiusBottomRight,
}
root.roundRect(
ctx,
dimensions,
radius, true, true
)
}
}
// adapted from: https://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas/3368118#3368118
function roundRect(ctx, dim, radius, fill, stroke) {
if (typeof stroke == 'undefined') {
stroke = true;
}
if (typeof radius === 'undefined') {
radius = 5;
}
if (typeof radius === 'number') {
radius = {tl: radius, tr: radius, br: radius, bl: radius};
} else {
var defaultRadius = {tl: 0, tr: 0, br: 0, bl: 0};
for (var side in defaultRadius) {
radius[side] = radius[side] || defaultRadius[side];
}
}
ctx.beginPath();
ctx.moveTo(dim.x + radius.tl, dim.y);
ctx.lineTo(dim.x + dim.w - radius.tr, dim.y);
ctx.quadraticCurveTo(dim.x + dim.w, dim.y, dim.x + dim.w, dim.y + radius.tr);
ctx.lineTo(dim.x + dim.w, dim.y + dim.h - radius.br);
ctx.quadraticCurveTo(dim.x + dim.w, dim.y + dim.h, dim.x + dim.w - radius.br, dim.y + dim.h);
ctx.lineTo(dim.x + radius.bl, dim.y + dim.h);
ctx.quadraticCurveTo(dim.x, dim.y + dim.h, dim.x, dim.y + dim.h - radius.bl);
ctx.lineTo(dim.x, dim.y + radius.tl);
ctx.quadraticCurveTo(dim.x, dim.y, dim.x + radius.tl, dim.y);
ctx.closePath();
if (fill) {
ctx.fill();
}
if (stroke) {
ctx.stroke();
}
}
Component.onCompleted: root.paint()
}

View File

@ -221,6 +221,31 @@ QtObject {
property real leftMargin3 : 30 * px
}
property QtObject importing : QtObject {
property color rowBackground : dialog.background
property color rowLine : dialog.line
}
property QtObject dropDownLight: QtObject {
property color background : dialog.background
property color text : dialog.text
property color inactive : dialog.line
property color highlight : dialog.textBlue
property color separator : dialog.line
property color line : dialog.line
property bool labelBold : true
}
property QtObject dropDownDark : QtObject {
property color background : dialog.text
property color text : dialog.background
property color inactive : dialog.line
property color highlight : dialog.textBlue
property color separator : dialog.line
property color line : dialog.line
property bool labelBold : true
}
property int okInfoBar : 0
property int warnInfoBar : 1
property int warnBubbleMessage : 2

View File

@ -23,7 +23,9 @@ import ProtonUI 1.0
Rectangle {
id: root
height: root.isDarwin ? Style.titleMacOS.height : Style.title.height
height: visible ? (
root.isDarwin ? Style.titleMacOS.height : Style.title.height
) : 0
color: "transparent"
property bool isDarwin : (go.goos == "darwin")
property QtObject window

View File

@ -23,6 +23,7 @@ InputField 1.0 InputField.qml
InstanceExistsWindow 1.0 InstanceExistsWindow.qml
LogoHeader 1.0 LogoHeader.qml
PopupMessage 1.0 PopupMessage.qml
RoundedRectangle 1.0 RoundedRectangle.qml
TabButton 1.0 TabButton.qml
TabLabels 1.0 TabLabels.qml
TextLabel 1.0 TextLabel.qml

View File

@ -110,7 +110,7 @@ Window {
ListElement { title: "Internet off" }
ListElement { title: "NeedUpdate" }
ListElement { title: "UpToDate" }
ListElement { title: "ForceUpdate" }
ListElement { title: "ForceUpdate" }
ListElement { title: "Linux" }
ListElement { title: "Windows" }
ListElement { title: "Macos" }
@ -122,6 +122,7 @@ Window {
ListElement { title: "Minimize this" }
ListElement { title: "SendAlertPopup" }
ListElement { title: "TLSCertError" }
ListElement { title: "IMAPCertError" }
}
ListView {
@ -208,6 +209,9 @@ Window {
case "TLSCertError" :
go.showCertIssue()
break;
case "IMAPCertError" :
go.showIMAPCertTroubleshoot()
break;
default :
console.log("Not implemented " + data)
}
@ -310,6 +314,7 @@ Window {
signal failedAutostartCode(string code)
signal showCertIssue()
signal showIMAPCertTroubleshoot()
signal updateFinished(bool hasError)

File diff suppressed because it is too large Load Diff

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