Compare commits

...

456 Commits

Author SHA1 Message Date
M66B 6d69e390c9 If DKIM fails, DMARC fails 2024-05-20 08:23:51 +02:00
M66B 7171455db7 Require DMARC for BIMI images 2024-05-20 08:02:49 +02:00
M66B 0934759a77 Workaround invalid tracking image width/height 2024-05-20 07:36:29 +02:00
M66B 323da90fc4 Updated changelog 2024-05-19 22:40:40 +02:00
M66B 2b6423c01d Added summarize rule action 2024-05-19 22:28:17 +02:00
M66B 4f22f269c8 List NOT rule conditions 2024-05-19 21:40:25 +02:00
M66B 325db7faa2 Set subject op priority 2024-05-19 20:28:31 +02:00
M66B 194a86f07f Updated FAQ 2024-05-19 20:20:50 +02:00
M66B cd14ca4577 Refactoring 2024-05-19 20:15:39 +02:00
M66B f0f5fd0b31 Revert "BIMI: require TLS if enabled"
This reverts commit 0db73c90a0.
2024-05-19 17:26:54 +02:00
M66B 98e31ba411 AI refactoring 2024-05-19 16:55:28 +02:00
M66B d116d72552 Refactoring 2024-05-19 15:55:35 +02:00
M66B a454f9cbed Cleanup 2024-05-19 15:47:55 +02:00
M66B f04cbba28d Debug: edge to edge 2024-05-19 14:14:18 +02:00
M66B 0db73c90a0 BIMI: require TLS if enabled 2024-05-19 14:04:46 +02:00
M66B 720649957a Debug: prevent cursor window full 2024-05-19 13:22:48 +02:00
M66B 0703bb232d Refactoring 2024-05-19 12:46:23 +02:00
M66B 93e192a309 Experiment: bottom edge 2024-05-19 12:14:29 +02:00
M66B b8d2d72387 Fixed multiple pro fragments 2024-05-19 11:39:37 +02:00
M66B 9726da053c Fixed tabular background color 2024-05-18 23:18:47 +02:00
M66B fbc120d099 Disallow DKIM L 2024-05-18 15:38:02 +02:00
M66B 74a92c9e67 Updated .gitignore 2024-05-18 15:13:05 +02:00
M66B 9f5593d3d3 Fixed white theme 2024-05-18 15:12:15 +02:00
M66B 7453751d12 Updated changelog 2024-05-18 14:26:55 +02:00
M66B b80578bd7a Save the icecream for later 2024-05-18 14:24:19 +02:00
M66B ff409a6a40 Merge branch 'master' into a15 2024-05-18 12:55:14 +02:00
M66B 5c089b7838 Disabled integrity check by default 2024-05-18 12:54:56 +02:00
M66B b51f5b3be0 Merge branch 'master' into a15 2024-05-18 11:49:01 +02:00
M66B e18b6196e7 Updated changelog 2024-05-18 11:48:48 +02:00
M66B 9bedac3787 Merge branch 'master' into a15 2024-05-18 11:14:53 +02:00
M66B eb29775909 Revert AndroidX fragment to version 1.6.2
https://issuetracker.google.com/issues/341313071
2024-05-18 11:14:17 +02:00
M66B f0a384b714 Merge branch 'master' into a15 2024-05-18 08:33:10 +02:00
M66B 4acbe416d6 Updated changelog 2024-05-18 08:32:55 +02:00
M66B 4935753f72 Fixed primary folders button theme 2024-05-18 08:10:52 +02:00
M66B d992fe9788 Added fail safes 2024-05-18 08:07:02 +02:00
M66B 4458b24fad Show toolbar on changing fragments 2024-05-18 08:04:50 +02:00
M66B 0155db1a4a Fixed black background 2024-05-17 22:16:10 +02:00
M66B 77d4278aea Merge branch 'master' into a15 2024-05-17 21:17:14 +02:00
M66B b80b223d0c Added branch to GitHub workflow 2024-05-17 21:16:19 +02:00
M66B 9150468a64 Merge branch 'master' into a15 2024-05-17 21:13:45 +02:00
M66B 43b5b8231c Added branch to GitHub workflow 2024-05-17 21:08:45 +02:00
M66B d5349c118a Cleanup 2024-05-17 20:42:07 +02:00
M66B a2fd648dcf Added option to disable appbar scroll 2024-05-17 20:40:51 +02:00
M66B 79b458f708 Added collapsible toolbar 2024-05-17 20:20:33 +02:00
M66B f66effc2e5 Small improvements 2024-05-17 18:13:57 +02:00
M66B 57d5b42ada Fixed folder search button theme 2024-05-17 17:49:00 +02:00
M66B 978e5d0d5e Removed custom toolbar hide 2024-05-17 17:23:19 +02:00
M66B f1f53f2d5d Merge branch 'master' into a15 2024-05-17 17:05:41 +02:00
M66B 3fe601006a Notification flag: skip marking read 2024-05-17 17:05:19 +02:00
M66B e0e2ca9ab1 Animate composer keyboard inset 2024-05-17 17:02:12 +02:00
M66B 648cb4621f Added remark 2024-05-17 15:10:04 +02:00
M66B 8f97864131 Fixed keyboard inset 2024-05-17 14:30:33 +02:00
M66B 153bf8b577 Time for icecream 2024-05-17 13:48:43 +02:00
M66B 1489dcb775 Enabled edge-to-edge 2024-05-17 13:41:34 +02:00
M66B a25ab1367f Set background color in activity 2024-05-17 12:54:03 +02:00
M66B 1083865e77 Switched to app bar 2024-05-17 12:53:26 +02:00
M66B 00ab42f973 Debug: added logging 2024-05-16 08:51:22 +02:00
M66B 5a65365f2c Debug: more logging 2024-05-16 07:23:04 +02:00
M66B a06e4aa0a3 Debug: keep logs for 24h 2024-05-15 22:06:03 +02:00
M66B b1d0aa4ec6 onTimeout logging 2024-05-15 20:44:52 +02:00
M66B 4d27446860 Limit changelog size 2024-05-15 19:14:42 +02:00
M66B c9f22fbf97 Fixed changelog 2024-05-15 19:06:50 +02:00
M66B 3546ab9cc7 b 2024-05-15 18:15:36 +02:00
M66B 7bdac5b902 1.2182 release 2024-05-15 17:48:02 +02:00
M66B f79b1579f9 Force crash reporting for DB lock 2024-05-15 17:47:28 +02:00
M66B 9173ec038c Crowdin sync 2024-05-15 17:41:53 +02:00
M66B 68a95a4f90 Updated changelog 2024-05-15 17:18:59 +02:00
M66B 43d3c3804e Updated FAQ 2024-05-15 17:10:25 +02:00
M66B 12f4a44239 Crowdin sync 2024-05-15 16:56:37 +02:00
M66B ebfb64d714 Revert "Segment tick"
This reverts commit 2cc73a7927.
2024-05-15 16:21:48 +02:00
M66B 2cc73a7927 Segment tick 2024-05-15 16:12:37 +02:00
M66B 2aa8f93f5d Updated FAQ 2024-05-15 16:00:20 +02:00
M66B b3f455ff7c Small improvement 2024-05-15 14:34:56 +02:00
M66B 127128db19 OpenAI: rate limiting logging 2024-05-15 14:26:12 +02:00
M66B cffbad0d0d Updated changelog 2024-05-15 13:50:25 +02:00
M66B 1a9962072d OpenAI: prevent empty text parts 2024-05-15 13:48:11 +02:00
M66B 6f925548e0 OpenAI summarize: added subject 2024-05-15 13:14:56 +02:00
M66B 5f188ef0f3 FairEmail quotes 2024-05-15 13:08:46 +02:00
M66B 0378df51d7 Web.de quotes 2024-05-15 13:01:43 +02:00
M66B 62bb023103 Updated text 2024-05-15 12:51:36 +02:00
M66B 60721c8139 Limit text to summarize 2024-05-15 12:45:39 +02:00
M66B 77ccdd48c3 Remove quotes 2024-05-15 12:40:06 +02:00
M66B 3a9b56438d Updated changelog 2024-05-15 09:57:31 +02:00
M66B a9efded85c Cloud services costs 2024-05-15 09:40:25 +02:00
M66B 22ae019243 Updated FAQ 2024-05-15 08:37:31 +02:00
M66B edaedbfe7d OpenAI: added failsafe 2024-05-15 08:16:46 +02:00
M66B 0244ef3f82 Added option to turn off multimodality 2024-05-15 08:09:20 +02:00
M66B 40daebe326 Updated AndroidX 2024-05-15 07:49:10 +02:00
M66B 63e8bd160c Refactoring 2024-05-14 21:48:44 +02:00
M66B 73097569f5 OpenAI: scale images 2024-05-14 21:30:08 +02:00
M66B 32964f1edd OpenAI: multimodal composer 2024-05-14 21:09:01 +02:00
M66B dc63ec78fd Refactoring 2024-05-14 20:44:36 +02:00
M66B 10211970fb Updated changelog 2024-05-14 20:29:17 +02:00
M66B 252ad4c2a5 OpenAI: image support 2024-05-14 20:21:54 +02:00
M66B 229a45d87b Hour glass finished 2024-05-14 19:49:59 +02:00
M66B 6bdfffcce5 Debug: doze 2024-05-14 18:26:35 +02:00
M66B db4a298b3c Enabled OpenAI/Gemeni for Play Store 2024-05-14 17:46:34 +02:00
M66B c86802c380 Refactoring 2024-05-14 17:37:53 +02:00
M66B f86b572eae OpenAI: optional multi modal 2024-05-14 17:08:23 +02:00
M66B 08d845887c Small fix 2024-05-14 17:08:11 +02:00
M66B a5a5cadafa Small layout improvement 2024-05-14 16:45:30 +02:00
M66B 53090f2f44 Summarize: error handling 2024-05-14 16:39:23 +02:00
M66B fef76265be BIMI: added unverified logging 2024-05-14 16:12:46 +02:00
M66B 82c64f3171 Updated changelog 2024-05-14 14:48:35 +02:00
M66B ae25f0017c Refactoring 2024-05-14 14:47:19 +02:00
M66B 3f06073664 Removed OpenAI moderation 2024-05-14 14:45:31 +02:00
M66B a0775944a1 Updated FAQ 2024-05-14 14:43:22 +02:00
M66B 8733388ce7 Default model/temperature 2024-05-14 14:35:13 +02:00
M66B 5c7f366e30 Added swipe to summarize 2024-05-14 14:17:00 +02:00
M66B 20f9d67b88 Debug DKIM failures 2024-05-14 13:27:50 +02:00
M66B f7148dad57 Added from/subject to summarize dialog 2024-05-14 13:11:59 +02:00
M66B 90e527cb60 Updated changelog 2024-05-14 10:02:52 +02:00
M66B c4442cccf3 Delete marked: remove styles first 2024-05-14 09:59:17 +02:00
M66B 1cbebf7870 OpenAI: prepare multi-modal support 2024-05-14 09:25:01 +02:00
M66B 4a52109212 Show summarize prompt 2024-05-14 08:41:59 +02:00
M66B f4c2025c39 Updated changelog 2024-05-14 08:17:16 +02:00
M66B 34342bc96b Summarize: show duration 2024-05-14 08:08:44 +02:00
M66B 0b66c0a45e Added summarize quick action 2024-05-14 07:58:39 +02:00
M66B caef8803f2 Updated FAQ 2024-05-13 23:18:43 +02:00
M66B 6a9091d596 DB process lock 2024-05-13 21:56:32 +02:00
M66B a906750374 b 2024-05-13 17:54:37 +02:00
M66B 94b514d58f a 2024-05-13 17:53:19 +02:00
M66B 29ac27aa45 Revert "Fixed modifying ROOM view tables"
This reverts commit 951a26d7b3.
2024-05-13 17:52:57 +02:00
M66B 45267c0f72 b 2024-05-13 17:50:08 +02:00
M66B e8e72d03d2 1.2181 release 2024-05-13 17:45:50 +02:00
M66B 413964da62 Crowdin sync 2024-05-13 17:44:56 +02:00
M66B a313268b1c Added debug classes 2024-05-13 17:33:14 +02:00
M66B 5bc84a0482 Revert "Revert "Revert "Updated room"""
This reverts commit 07f6fa6095.
2024-05-13 17:27:47 +02:00
M66B e6dff96061 Revert "Updated ROOM patches"
This reverts commit dc5d26151a.
2024-05-13 17:27:21 +02:00
M66B 5b210a238e Revert "Fixed logging"
This reverts commit 3d367c429f.
2024-05-13 17:26:54 +02:00
M66B a805164231 Revert "Added debug classes"
This reverts commit 18c85872f3.
2024-05-13 17:26:34 +02:00
M66B 048c558ebd Revert "ROOM patch: refactoring"
This reverts commit 3a0083ad44.
2024-05-13 17:26:18 +02:00
M66B a88379a09d Signed by 2024-05-13 16:45:27 +02:00
M66B 01621724cd Show auth info for verified sender 2024-05-13 16:43:20 +02:00
M66B 7659fde011 Fixed check first 2024-05-13 16:35:24 +02:00
M66B 4918e87ff7 DKIM: added logging 2024-05-13 16:03:28 +02:00
M66B 0a4c7ae54f DKIM: check for revoked key 2024-05-13 15:42:03 +02:00
M66B 35d5324c1b Updated FAQ 2024-05-13 11:32:40 +02:00
M66B b86420ad9d From exim 2024-05-13 10:35:41 +02:00
M66B e312b06a10 Updated PSL 2024-05-13 10:16:36 +02:00
M66B a74fefee45 Debug: prevent crash 2024-05-13 09:49:29 +02:00
M66B 1ec195d1eb b 2024-05-13 09:01:15 +02:00
M66B 3ed031cfa3 1.2180 release 2024-05-13 08:52:10 +02:00
M66B 7ac91b3497 Updated minimum cmake version 2024-05-13 08:42:47 +02:00
M66B 77044946e7 Updated NDK 2024-05-13 08:32:58 +02:00
M66B e9937c7308 Crowdin sync 2024-05-13 07:31:13 +02:00
M66B 2634ed276b Limit number of keywords/labels 2024-05-12 21:49:14 +02:00
M66B 4836366b48 Debug: legacy queries 2024-05-12 20:35:00 +02:00
M66B 77c56905d9 Refactoring 2024-05-12 20:01:01 +02:00
M66B 0385883390 Insert "AI" response at the end 2024-05-12 18:13:21 +02:00
M66B be73157316 Updated FAQ 2024-05-12 15:02:09 +02:00
M66B 384249a248 DKIM logging 2024-05-12 12:38:03 +02:00
M66B 5a44e99a06 Updated changelog 2024-05-12 12:26:50 +02:00
M66B 944a2a9a09 Crowdin sync 2024-05-12 12:24:10 +02:00
M66B 1e483b7438 Show auth details in case of auth failures 2024-05-12 09:51:06 +02:00
M66B f2882193d7 Merge addresses 2024-05-12 08:56:59 +02:00
M66B 3a0083ad44 ROOM patch: refactoring 2024-05-11 23:27:48 +02:00
M66B 8cb5a52ae7 Added DKIM logging 2024-05-11 08:07:56 +02:00
M66B 18c85872f3 Added debug classes 2024-05-11 08:06:13 +02:00
M66B 3d367c429f Fixed logging 2024-05-11 07:50:17 +02:00
M66B 43d89b6c13 Updated PSL 2024-05-11 07:38:52 +02:00
M66B 3300463e79 Fixed native ARC 2024-05-10 08:45:54 +02:00
M66B 93fc9b169c Formatting 2024-05-10 08:43:50 +02:00
M66B 04fe7b3743 Last touched 2024-05-10 07:08:05 +02:00
M66B 1fad4ad80c Gemini: default model temperature 2024-05-09 22:00:47 +02:00
M66B b20b7dee10 Save initialized state 2024-05-09 21:36:32 +02:00
M66B a207b73082 Gemini: fixed temperature range 2024-05-09 20:20:00 +02:00
M66B 12e79ce7e9 Use invisible text for preview text
Like Gmail, Outlook and Apple do
2024-05-09 17:58:33 +02:00
M66B bfa347aa71 Gemini: links 2024-05-09 17:15:20 +02:00
M66B e93c0de346 Small fix 2024-05-09 17:08:34 +02:00
M66B 8cab3786e9 Gemini: refactoring 2024-05-09 10:37:55 +02:00
M66B 861716aad0 Updated changelog 2024-05-09 09:01:39 +02:00
M66B a2b05e420d Gemini: disable safety 2024-05-09 09:00:00 +02:00
M66B a54f0f3f03 Skip resetting API keys 2024-05-09 08:50:35 +02:00
M66B 4442619c73 Added Gemini temperature 2024-05-09 08:49:48 +02:00
M66B 497ce2fda6 Touched hours 2024-05-08 21:01:23 +02:00
M66B 8a4e6aa939 Gemini: allow (local) http connections 2024-05-08 18:15:53 +02:00
M66B 18d69965ef OpenAI: allow (local) http connections 2024-05-08 18:15:33 +02:00
M66B 89b2b1f88c Refactoring 2024-05-08 15:02:37 +02:00
M66B 3b1a31621f Android 15 preparations 2024-05-08 14:49:37 +02:00
M66B 3994216cc5 Updated changelog 2024-05-08 14:06:51 +02:00
M66B df5fbe9f35 Prepend from/to to senders/recipients 2024-05-08 12:59:51 +02:00
M66B cd07035e71 Revert "Changed title"
This reverts commit 458d52353e.
2024-05-08 12:49:09 +02:00
M66B 458d52353e Changed title 2024-05-08 11:27:55 +02:00
M66B 8d7fb057fd Removed thread sub query 2024-05-08 09:49:17 +02:00
M66B d83dde8abc Updated privacy policy 2024-05-08 08:22:15 +02:00
M66B 096077d8b7 Updated links 2024-05-08 07:45:39 +02:00
M66B 3aaf7ba072 b 2024-05-08 07:36:12 +02:00
M66B dfc871007f 1.2179 release 2024-05-08 07:18:15 +02:00
M66B 0157efb61d Fixed touched order 2024-05-07 21:19:09 +02:00
M66B b56045b66e Crowdin sync 2024-05-07 20:33:32 +02:00
M66B 0691568835 Experiment: touched 2024-05-07 18:59:27 +02:00
M66B 3b11de947c Auto-select identity for folder 2024-05-07 16:15:44 +02:00
M66B f9cc6bdae1 Updated PSL 2024-05-07 16:08:01 +02:00
M66B 2154dd261e Filter unreasonable quotas 2024-05-06 16:50:44 +02:00
M66B f3e586b112 Small fix 2024-05-06 16:39:29 +02:00
M66B 1cd05f1fd1 Updated PSL 2024-05-05 16:31:11 +02:00
M66B 4a21502651 Added quick search button for hidden messages 2024-05-05 16:30:58 +02:00
M66B 4bb1284c52 Fixed loading saved search 2024-05-05 16:20:16 +02:00
M66B 374f121d5f Primary inbox: fixed external search 2024-05-05 08:08:51 +02:00
M66B 864c522ee5 Empty 2024-05-04 20:08:29 +02:00
M66B c8965a64a8 Made no viewer dialog fields selectable 2024-05-04 09:21:19 +02:00
M66B 5382a2580f Consistent quick action button order 2024-05-04 07:52:28 +02:00
M66B 69dcb2b9e8 Fixed wildcard mime types 2024-05-03 21:25:49 +02:00
M66B 2c2e35bc0a Updated FAQ 2024-05-03 11:04:21 +02:00
M66B 5dcc52424c Crowdin sync 2024-05-03 08:01:44 +02:00
M66B 951a26d7b3 Fixed modifying ROOM view tables 2024-05-02 20:08:05 +02:00
M66B 56d566e292 Updated PSL 2024-05-02 18:38:40 +02:00
M66B 932e1513ac Added freenet.de link 2024-05-02 18:38:03 +02:00
M66B a88ceaf533 Consistent ordering 2024-05-02 11:19:21 +02:00
M66B dc5d26151a Updated ROOM patches 2024-05-02 10:27:48 +02:00
M66B 07f6fa6095 Revert "Revert "Updated room""
This reverts commit 03a331175f.
2024-05-02 10:26:33 +02:00
M66B 03a331175f Revert "Updated room"
This reverts commit 58a63a366d.
2024-05-02 08:50:03 +02:00
M66B 8f2bab4c48 Revert "Updated ROOM patch"
This reverts commit b8250fe6a0.
2024-05-02 08:49:53 +02:00
M66B b8250fe6a0 Updated ROOM patch 2024-05-02 08:49:29 +02:00
M66B 58a63a366d Updated room 2024-05-01 22:56:33 +02:00
M66B d636a73bc3 Updated AndroidX 2024-05-01 20:23:27 +02:00
M66B a8f1ba0705 Updated changelog 2024-05-01 10:24:48 +02:00
M66B 794cb9b27c Updated PSL 2024-05-01 10:23:02 +02:00
M66B ea03f8c5f6 Added options to configure "AI" summarize prompt 2024-04-30 21:28:35 +02:00
M66B 0f7283f9ec Updated gradle 2024-04-30 20:58:22 +02:00
M66B 04265c6a7d Added received time to expression 2024-04-30 09:51:26 +02:00
M66B fb02b318e0 Updated changelog 2024-04-30 07:52:55 +02:00
M66B 5f4c1f1ce1 Refactoring 2024-04-30 07:45:25 +02:00
M66B c131de5f0e Expression: added known() 2024-04-29 21:35:12 +02:00
M66B 8c8337270a Fixed build dir 2024-04-29 21:04:08 +02:00
M66B 563f2a1951 Added option to disable swiping all messages to trash 2024-04-29 19:04:16 +02:00
M66B 13ff382ba4 Remove legacy keyword colors 2024-04-29 18:10:24 +02:00
M66B d9b06148b0 Default color/title for $Phishing keyword 2024-04-29 18:08:35 +02:00
M66B 600f8c0b00 Debug 2024-04-29 17:40:44 +02:00
M66B b8b36604d8 Updated build features 2024-04-29 16:56:25 +02:00
M66B 5b547b6ba0 Updated build tools 2024-04-29 16:52:32 +02:00
M66B 168484ddb5 Updated FAQ 2024-04-29 16:43:45 +02:00
M66B ad53d3d1ea Added expression function size() 2024-04-29 16:02:48 +02:00
M66B efff870d97 Updated changelog 2024-04-29 15:38:09 +02:00
M66B 21defedb76 Refactoring 2024-04-29 15:35:47 +02:00
M66B 0aa628dc99 Added Jsoup expression function 2024-04-29 15:32:58 +02:00
M66B 8270c9abbf Updated FAQ 2024-04-29 15:17:06 +02:00
M66B 7c8548616a Updated Bouncy Castle 2024-04-29 14:47:04 +02:00
M66B 97e30f3a71 Updated changelog 2024-04-29 14:40:59 +02:00
M66B ebd9003634 Updated FAQ 2024-04-29 14:17:53 +02:00
M66B ce0cc44025 Updated CHANGELOG 2024-04-29 13:33:09 +02:00
M66B 5d16bd8d7d Updated FAQ 2024-04-29 13:32:15 +02:00
M66B 700f3740f9 Expressions: added attachment count function 2024-04-29 13:31:38 +02:00
M66B d57eae01f2 Eval: added hasMx, refactoring 2024-04-29 13:15:55 +02:00
M66B 10b736c2e7 Small fix 2024-04-29 12:21:07 +02:00
M66B 46ff530412 Update Bugsnag/DSL-JSON 2024-04-29 11:45:03 +02:00
M66B 3989504073 Updated Bugsnag 2024-04-29 11:37:18 +02:00
M66B caab02b0d6 Added deletion of send operations 2024-04-29 11:26:22 +02:00
M66B 4995900afc Updated libraries 2024-04-29 11:11:50 +02:00
M66B 5c161ed230 b 2024-04-29 07:34:30 +02:00
M66B e48326ba44 1.2178 release 2024-04-29 07:32:49 +02:00
M66B c5b6a3e8dc Crowdin sync 2024-04-29 07:29:02 +02:00
M66B ab36faa82b NOOP logging 2024-04-29 07:24:07 +02:00
M66B 1840bc0fdd Crowdin sync 2024-04-28 22:46:08 +02:00
M66B e46c9cbd47 Added French no-reply addresses 2024-04-28 20:07:16 +02:00
M66B aab3de3dd2 Skip storing no-reply addresses 2024-04-28 19:49:22 +02:00
M66B 18df2e7d0d Material 2024-04-28 18:56:42 +02:00
M66B f902e252f0 Debug 2024-04-28 18:15:28 +02:00
M66B bb442b14c0 Cleanup 2024-04-28 16:01:44 +02:00
M66B b37d97c516 Cleanup 2024-04-28 16:00:46 +02:00
M66B 957276c1d4 Small improvement 2024-04-28 15:35:28 +02:00
M66B a18199e420 Warn for email addresses in names 2024-04-28 08:29:23 +02:00
M66B ee110b35ed Email addresses should not contain <> 2024-04-28 08:28:59 +02:00
M66B 855b9d56da Updated changelog 2024-04-27 19:58:34 +02:00
M66B 15caf283d3 Pwned: enable for Play Store version 2024-04-27 19:53:10 +02:00
M66B 6edffb9a61 Refactoring 2024-04-27 19:50:42 +02:00
M66B 1a9748191b Updated changelog 2024-04-27 19:46:49 +02:00
M66B 93835d886b Added AI summarization 2024-04-27 19:08:22 +02:00
M66B 98326a520e Updated FAQ 2024-04-27 17:18:02 +02:00
M66B 253ded0be4 Added remark 2024-04-27 14:14:46 +02:00
M66B ebc5800703 Updated changelog 2024-04-27 11:56:41 +02:00
M66B 99a88bb14e Pwned: added remark 2024-04-27 11:39:54 +02:00
M66B 21ac49f1c4 Pwned: link caption 2024-04-27 09:44:56 +02:00
M66B 2589a5003f Pwned: enable padding 2024-04-27 09:33:38 +02:00
M66B 9b85d39460 Updated changelog 2024-04-27 08:49:54 +02:00
M66B eef306cf3c Updated privacy policy 2024-04-27 08:40:46 +02:00
M66B f20b4c978d Updated changelog 2024-04-27 08:26:50 +02:00
M66B dd7f3d8f4d b 2024-04-27 08:19:38 +02:00
M66B 765d06241d 1.2177 release 2024-04-27 08:17:00 +02:00
M66B cf136ec762 Crowdin sync 2024-04-27 08:11:05 +02:00
M66B 70455dcf17 Updated FAQ 2024-04-27 07:52:20 +02:00
M66B 6bff3e1349 Small improvement 2024-04-27 07:35:31 +02:00
M66B dd6e85cee2 Added Pwned info button 2024-04-26 21:26:50 +02:00
M66B fdb1dcfd52 Updated release name 2024-04-26 11:14:11 +02:00
M66B 48913a1a3e Updated chaneglog 2024-04-26 11:11:31 +02:00
M66B b48532f8bd Refactoring 2024-04-26 10:54:47 +02:00
M66B 2a1b403636 Oops 2024-04-26 08:59:57 +02:00
M66B 178553b0f6 Update privacy policy 2024-04-26 08:54:03 +02:00
M66B 6f399b39ad Added Have I been pawned support 2024-04-26 08:46:16 +02:00
M66B a0098357a8 Crowdin sync 2024-04-26 07:22:27 +02:00
M66B 8529e6c012 Added input field for MAIL FROM 2024-04-25 11:01:34 +02:00
M66B a32d71273b Updated changelog 2024-04-25 08:42:31 +02:00
M66B 3401f301ad Go to unified inbox when using primary inbox 2024-04-25 08:39:52 +02:00
M66B 9fb426303b Crowdin sync 2024-04-25 07:40:08 +02:00
M66B 3ebc257728 Fix 2024-04-24 21:36:29 +02:00
M66B 9787c379ef Prevent crash 2024-04-24 20:27:35 +02:00
M66B 028ffba4ab Improved debug info 2024-04-24 15:29:53 +02:00
M66B bddae1af77 Prepare envelope from 2024-04-24 14:20:03 +02:00
M66B 4569ff5b2b Accessibility: more actions 2024-04-24 13:35:17 +02:00
M66B 26ee785be3 Removed move-to accessiblity action 2024-04-24 13:24:10 +02:00
M66B 0ebfbc584c TalkBalk: always move-to 2024-04-24 13:05:25 +02:00
M66B 1bb28fc4e4 Enable theme/force sync for primary inbox 2024-04-24 12:55:20 +02:00
M66B 3f106c4d30 Oops 2024-04-24 11:56:43 +02:00
M66B 069064502c Added fail-safe 2024-04-24 08:32:35 +02:00
M66B 0b44482f5b RLAH: not La Réunion
https://en.wikipedia.org/wiki/European_Union_roaming_regulations#Areas_not_covered
2024-04-24 08:13:12 +02:00
M66B 7a066d774c All child folders 2024-04-24 08:03:39 +02:00
M66B 433790bbd0 Accessibility: move to 2024-04-24 07:42:40 +02:00
M66B 265d5d1299 Decreased font size of duplicate message layout 2024-04-22 20:55:17 +02:00
M66B be4802bccc Updated FAQ 2024-04-22 15:18:19 +02:00
M66B c0e2a360ee b 2024-04-22 13:03:15 +02:00
M66B 58199e930f 1.2176 release 2024-04-22 13:02:00 +02:00
M66B b97ae1882c Debug: ignore formatted size 2024-04-22 07:28:43 +02:00
M66B 9ce09e3e72 Fixed translations 2024-04-21 08:49:09 +02:00
M66B bf9eb91959 Expression: added function blocklist 2024-04-21 08:38:21 +02:00
M66B 99145fd0d9 b 2024-04-20 08:24:05 +02:00
M66B d1935ae5a9 1.2175 release 2024-04-20 08:12:51 +02:00
M66B 43d3f432f2 Crowdin sync 2024-04-20 08:10:35 +02:00
M66B 34efdd46ad Update PSL 2024-04-19 15:29:11 +02:00
M66B ec888beb85 Delay checking intent for primary inbox 2024-04-19 14:16:02 +02:00
M66B 145b5c9214 b 2024-04-19 07:55:36 +02:00
M66B ee690add25 1.2174 release 2024-04-19 07:51:37 +02:00
M66B 0a107467ec Updated FAQ 2024-04-19 07:44:49 +02:00
M66B 9b8638f3f4 Expression: plain text for subject and text 2024-04-19 07:41:30 +02:00
M66B 9bac559ee0 Update PSL 2024-04-19 07:36:12 +02:00
M66B 7cbad6c1b4 Crowdin sync 2024-04-19 07:35:31 +02:00
M66B f547423b8b Expression: allow single value for contains/matches 2024-04-18 20:59:45 +02:00
M66B eae8e33fad Updated changelog 2024-04-18 08:44:48 +02:00
M66B bd60de816c Update FAQ 2024-04-18 08:44:38 +02:00
M66B 785387955f Expressions: hasAttachments 2024-04-18 08:44:02 +02:00
M66B e2f3da5629 Updated AndroidX 2024-04-18 08:26:36 +02:00
M66B 31401f9fd2 Expression: replace message structure by function 2024-04-18 07:59:03 +02:00
M66B 538a630c31 Expression: message properties 2024-04-17 22:17:37 +02:00
M66B 710fd04781 Expression: fixed text 2024-04-17 20:01:33 +02:00
M66B 441812c561 Expressions: experiment 2024-04-17 19:51:52 +02:00
M66B 55779ac915 Expression: attachment count 2024-04-17 19:42:14 +02:00
M66B ac55dcc31b Refactoring 2024-04-17 19:31:50 +02:00
M66B 613f72e769 Refactoring 2024-04-17 19:30:11 +02:00
M66B d337763a77 Refactoring 2024-04-17 19:27:07 +02:00
M66B 726ea077ff Refactoring 2024-04-17 19:23:33 +02:00
M66B 9150746f75 Added expression conditions 2024-04-17 16:27:33 +02:00
M66B 7d0da8d5fa List expressions 2024-04-17 14:13:34 +02:00
M66B 9b656434bd PoC rule expressions 2024-04-17 12:53:07 +02:00
M66B 27e955a13f Start foreground not allowed 2024-04-16 12:39:35 +02:00
M66B 96be6d30a1 Primary inbox type 2024-04-16 12:35:27 +02:00
M66B 760da17971 Updated FAQ 2024-04-16 12:26:44 +02:00
M66B a9f87270ce b 2024-04-16 08:04:39 +02:00
M66B 5d64c94dce 1.2173 release 2024-04-16 08:02:24 +02:00
M66B 7ca74f1899 Added dark theme to FAQ
Thanks @o101010
2024-04-16 07:59:03 +02:00
M66B 57c6b804f5 Fixed image links 2024-04-16 07:49:01 +02:00
M66B 435b3d75c8 Crowdin sync 2024-04-16 07:39:21 +02:00
M66B a9db609a80 Crowdin sync 2024-04-15 18:08:26 +02:00
M66B ae8f03e0cd Updated PSL 2024-04-15 15:44:14 +02:00
M66B 80692be12d Added rule not header condition 2024-04-15 09:11:36 +02:00
M66B 6fef2235fd Updated changelog 2024-04-15 07:59:09 +02:00
M66B 2baa11161d Improved geo link pattern 2024-04-15 07:56:58 +02:00
M66B c72a5194c2 Rule NOT regex 2024-04-15 07:21:45 +02:00
M66B 0181493b4a Crowdin sync 2024-04-14 22:14:49 +02:00
M66B 27ddf4f6e0 Updated FAQ 2024-04-14 21:53:15 +02:00
M66B ccd32b15ca Fixed swipe/action in some cases 2024-04-13 17:58:44 +02:00
M66B 6ce4c32bbe Updated changelog 2024-04-13 10:00:59 +02:00
M66B 6c52d14966 Added primary inbox start screen 2024-04-13 09:56:27 +02:00
M66B 704a994c7d Log foreground status 2024-04-13 09:24:22 +02:00
M66B 6422b7924f Prevent crash 2024-04-13 09:24:01 +02:00
M66B ee9b5aa901 Reduced logging 2024-04-13 09:03:30 +02:00
M66B ef0c01e84e Updated FAQ 2024-04-13 08:29:20 +02:00
M66B 0f63effad1 Log on timeout only 2024-04-13 08:12:34 +02:00
M66B 43d6821520 Forwarders: match identity 2024-04-13 08:11:18 +02:00
M66B bc76f5b391 Refactoring 2024-04-11 17:28:12 +02:00
M66B 9b317defe5 Added Service.onTimeout 2024-04-11 07:50:39 +02:00
M66B c5099952bb Updated PSL 2024-04-10 17:48:01 +02:00
M66B fb30aa9fe0 Remove newlines when opening links 2024-04-10 17:17:40 +02:00
M66B c3e088048b Require empty filename for inline text parts 2024-04-10 16:33:43 +02:00
M66B 5830ce1461 Updated PSL 2024-04-10 09:50:22 +02:00
M66B 9653454018 Revert "Require inline for inline text"
This reverts commit 8dbf37884f.
2024-04-10 08:06:09 +02:00
M66B 8dbf37884f Require inline for inline text 2024-04-10 07:36:30 +02:00
M66B 3e15d628e7 Updated gradle 2024-04-10 07:30:43 +02:00
M66B 71f1dd4d5f Removed Play Billing from GitHub version 2024-04-09 13:12:55 +02:00
M66B 04244d2832 F-Droid sync 2024-04-09 13:00:35 +02:00
M66B 87d6a507ce Disabled Firebase analytics 2024-04-09 12:28:14 +02:00
M66B 64ad1c90dc Added comment 2024-04-09 11:47:35 +02:00
M66B cfc2e27270 Merge branch 'master' of github.com:M66B/FairEmail 2024-04-09 11:47:31 +02:00
M66B 03006bbadb Updated comment 2024-04-09 11:44:17 +02:00
M66B 04106dd5b2 Added IAB network error 2024-04-09 11:43:53 +02:00
Marcel Bokhorst d34f5035ee
Fixed F-Droid build 2024-04-08 15:08:51 +02:00
M66B 9a1e4cd904 b 2024-04-08 07:49:10 +02:00
M66B 4e0e8a544a Merge branch 'master' of github.com:M66B/FairEmail 2024-04-08 07:44:56 +02:00
M66B 5c4152afad 1.2172 release 2024-04-08 07:40:13 +02:00
M66B fa4a02bc6e Crowdin sync 2024-04-08 07:32:56 +02:00
Marcel Bokhorst 70d66232ac
Merge pull request #214 from yurtpage/i18n
app metadata: i18n ru
2024-04-07 19:39:07 +02:00
Yurt Page 76e38e9f5c app metadata: i18n ru 2024-04-07 20:09:24 +03:00
M66B fe2e8dc794 Updated changelog 2024-04-07 12:18:32 +02:00
M66B a278433b13 Updated changelog 2024-04-07 11:38:41 +02:00
M66B 8558f2f123 Handle invalid response codes 2024-04-07 11:27:32 +02:00
M66B 0577fed9ab Crowdin sync 2024-04-07 10:40:18 +02:00
M66B a431a0c00a Handle old activation links 2024-04-07 10:30:40 +02:00
M66B 08b29f9473 Cloud: always logout 2024-04-04 22:31:45 +02:00
M66B b1dfcdfd59 Revert "Separate inbox filters"
This reverts commit c8e91ea24a.
2024-04-04 21:35:01 +02:00
M66B 6e9a1b8a18 Updated link 2024-04-04 08:25:23 +02:00
M66B 37d466f4b6 Updated changelog 2024-04-04 08:11:17 +02:00
M66B 2421f5eaee Updated AndroidX 2024-04-04 08:11:11 +02:00
M66B 4653b104e0 Use original address when forwarded for images/full 2024-04-03 07:56:47 +02:00
M66B b45eb556d7 Updated FAQ 2024-04-02 19:16:33 +02:00
M66B 129b0dcaf9 Revert "Enabled ChatGPT and Gemini in Play Store build"
This reverts commit 86cc0c4649.
2024-04-02 19:15:12 +02:00
M66B c8e91ea24a Separate inbox filters 2024-04-02 18:36:55 +02:00
M66B 7286b628d1 Crowdin sync 2024-04-02 17:18:25 +02:00
M66B 17a3c538de Updated FAQ 2024-04-01 09:53:05 +02:00
M66B 86cc0c4649 Enabled ChatGPT and Gemini in Play Store build 2024-03-31 08:37:51 +02:00
M66B 9cf52b72a7 Enabled rule raw attachments 2024-03-30 15:44:30 +01:00
M66B bb0b97b2fc Updated changelog 2024-03-30 09:30:55 +01:00
M66B ac59cb23b4 Updated FAQ 2024-03-30 09:29:29 +01:00
M66B c8e5cccea3 b 2024-03-30 08:24:34 +01:00
M66B c8083ca5fa 1.2171 release 2024-03-30 08:19:08 +01:00
M66B 637dec7f7e Crowdin sync 2024-03-30 07:58:00 +01:00
M66B 76ab574055 Gemini: updated documentation 2024-03-29 09:49:22 +01:00
M66B cca1d13237 Updated FAQ 2024-03-29 09:32:05 +01:00
M66B 9fd01d48f2 Prevent NPE 2024-03-29 08:33:32 +01:00
M66B e45b90d9a7 Gemini: updated parameters 2024-03-29 08:02:34 +01:00
M66B 589f222481 Crowdin sync 2024-03-29 08:02:23 +01:00
M66B 6470b22c71 Gemini: basic error handling 2024-03-28 18:33:05 +01:00
M66B 98aea61a37 Gemini: use auth header 2024-03-28 17:48:35 +01:00
M66B ce4b044be1 Updated PSL 2024-03-28 17:33:56 +01:00
M66B 6535467df1 Updated PSL 2024-03-28 17:33:08 +01:00
M66B 5d641d48e2 Updated FAQ 2024-03-28 10:35:35 +01:00
M66B c0a0fef08c Gemini integration improvements 2024-03-28 09:57:25 +01:00
M66B b141489e61 Updated changelog 2024-03-27 11:33:43 +01:00
M66B ab072056be Gemini integration 2024-03-27 11:32:36 +01:00
M66B d77057a7a7 Updated PSL 2024-03-27 07:21:34 +01:00
M66B 78384293d3 Updated changelog 2024-03-26 07:44:16 +01:00
M66B b2859a06d4 Crowdin sync 2024-03-26 07:42:48 +01:00
M66B 1b18ce2066 Reply with options 2024-03-26 07:34:00 +01:00
M66B ac5830d111 Added reply button option 2024-03-26 07:28:59 +01:00
M66B 3667bc468e Limit server browse to 100,000 messages 2024-03-24 19:21:17 +01:00
M66B ce17db1e83 Remove duplicate tracking images 2024-03-23 18:39:15 +01:00
M66B d0bbc63198 Updated changelog 2024-03-23 10:35:40 +01:00
M66B c120bcb4ae b 2024-03-23 10:29:57 +01:00
M66B b385a52cbf 1.2170 release 2024-03-23 10:28:38 +01:00
M66B 1a2ac920c5 Crowdin sync 2024-03-23 10:24:54 +01:00
M66B 8746e5362f Added intent to download AdGuard lists 2024-03-22 08:31:58 +01:00
M66B ef2578bd83 Fixed auto update downloaded times 2024-03-22 08:30:07 +01:00
M66B 02ccd0d93a Improved error reporting 2024-03-22 07:26:27 +01:00
M66B 81a5ebfd02 Updated PSL 2024-03-21 22:00:30 +01:00
M66B e3cc93528a AndroidX 2024-03-20 22:25:23 +01:00
M66B 188d1ec811 Haptic feedback on select by date 2024-03-20 15:09:12 +01:00
M66B a3da6c711e Updated Virgin media config 2024-03-20 10:52:19 +01:00
M66B c038d8a182 Updated FAQ 2024-03-20 10:42:00 +01:00
M66B 2f9b14a6da Crowdin sync 2024-03-20 07:32:04 +01:00
M66B d7043c13f2 Updated changelog 2024-03-20 07:30:22 +01:00
M66B 8a4226174f Protect header on unencrypt 2024-03-19 19:25:03 +01:00
M66B e3c215b2e1 Updated gradle 2024-03-19 07:09:32 +01:00
M66B dbbc7111e9 Reset options on import 2024-03-18 13:23:11 +01:00
M66B 0ba95456d9 DeepL: added Arabic 2024-03-18 08:07:50 +01:00
M66B f1215287e4 Use reliable DNS client when not using DNSSEC 2024-03-18 07:54:31 +01:00
M66B 0c5c960588 Updated comment 2024-03-18 07:34:42 +01:00
M66B 8d21c68afc MOD for Android 14 beta only 2024-03-17 08:48:36 +01:00
M66B b61ea3303b b 2024-03-16 09:47:29 +01:00
283 changed files with 53245 additions and 1879 deletions

View File

@ -7,7 +7,10 @@ on:
password:
description: 'Password'
required: true
branch:
description: 'Branch'
required: true
default: 'master'
jobs:
build:
@ -17,6 +20,8 @@ jobs:
#https://github.com/actions/setup-java
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.branch }}
- name: set up JDK 17
uses: actions/setup-java@v4
with:
@ -49,9 +54,9 @@ jobs:
run: ./gradlew assembleGithubRelease assembleLargeRelease assemblePlayRelease uploadBugsnagGithub-releaseMapping uploadBugsnagLarge-releaseMapping uploadBugsnagPlay-releaseMapping
- name: Upload to BitBucket
run: |
./gradlew upload -Ptarget=play-preview
./gradlew upload -Ptarget=github-snapshot
./gradlew upload -Ptarget=large-snapshot
./gradlew upload -Ptarget=play-preview-${{ github.event.inputs.branch }}
./gradlew upload -Ptarget=github-snapshot-${{ github.event.inputs.branch }}
./gradlew upload -Ptarget=large-snapshot-${{ github.event.inputs.branch }}
#https://github.com/actions/upload-artifact
- uses: actions/upload-artifact@v4
with:

1
.gitignore vendored
View File

@ -17,6 +17,7 @@ crowdin.properties
.idea/vcs.xml
.idea/workspace.xml
.idea/deploymentTargetDropDown.xml
.idea/deploymentTargetSelector.xml
app/.cxx/
build
captures

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.9.22" />
<option name="version" value="1.9.23" />
</component>
</project>

View File

@ -57,3 +57,4 @@ FairEmail uses parts or all of:
* [ZXing](https://github.com/zxing/zxing). Copyright (C) 2014 ZXing authors. [Apache License 2.0](https://github.com/zxing/zxing/blob/master/LICENSE).
* [commonmark-java](https://github.com/commonmark/commonmark-java). Copyright (c) 2015, Atlassian Pty Ltd. All rights reserved. [BSD-2-Clause license](https://github.com/commonmark/commonmark-java/blob/main/LICENSE.txt).
* [flexmark-java](https://github.com/vsch/flexmark-java). Copyright (c) 2016-2018, Vladimir Schneider. All rights reserved. [BSD-2-Clause license](https://github.com/vsch/flexmark-java/blob/master/LICENSE.txt).
* [EvalEx](https://github.com/ezylang/EvalEx). Copyright 2012-2022 Udo Klimaschewski. [Apache License 2.0](https://github.com/ezylang/EvalEx/blob/main/LICENSE).

View File

@ -4,9 +4,136 @@
For support you can use [the contact form](https://contact.faircode.eu/?product=fairemailsupport).
### [Acantholipan](https://en.wikipedia.org/wiki/Acantholipan)
### Next version
* Prepared for Android 15
* Added "AI" summarize rule action
* Listing NOT rule conditions
* Reverted AndroidX fragment to version 1.6.2
* Small improvements and minor bug fixes
* Updated [translations](https://crowdin.com/project/open-source-email)
Preview versions are available [here](https://bitbucket.org/M66B/fairemail-test/downloads/).
### 1.2182 - 2024-05-15
* Added optional "AI" summarize quick action
* Added optional "AI" summarize swipe action
* Changed default OpenAI model to [gpt-4o](https://openai.com/index/hello-gpt-4o/)
* Improved OpenAI integration (added multimodal support)
* Improved Gemini integration
* Made "AI" integrations available in the Play Store version
* Updated [AndroidX](https://developer.android.com/jetpack/androidx/versions/all-channel)
* Small improvements and minor bug fixes
* Updated [translations](https://crowdin.com/project/open-source-email)
*The use of "AI" integrations is and will remain completely optional*
### 1.2181 - 2024-05-13
* Reverted [AndroidX ROOM](https://developer.android.com/jetpack/androidx/releases/room#2.6.1) to version 2.4.3 to solve locking issues (*)
* Small improvements and minor bug fixes
* Updated [Public Suffix List](https://github.com/publicsuffix/list)
* Updated [translations](https://crowdin.com/project/open-source-email)
### 1.2180 - 2024-05-13
* Improved [Gemini](https://m66b.github.io/FairEmail/#faq204) integration
* Performance improvements
* Small improvements and minor bug fixes
* Updated [NDK](https://developer.android.com/ndk/)
* Updated [Public Suffix List](https://github.com/publicsuffix/list)
* Updated [translations](https://crowdin.com/project/open-source-email)
### 1.2179 - 2024-05-08
* Added option to change "AI" summarize prompt
* Added expression condition functions, see [the FAQ](https://m66b.github.io/FairEmail/#faq71)
* Small improvements and minor bug fixes
* Updated build tools
* Updated [AndroidX](https://developer.android.com/jetpack/androidx/versions/all-channel)
* Updated libraries (including [Bouncy Castle](https://www.bouncycastle.org/))
* Updated [Public Suffix List](https://github.com/publicsuffix/list)
* Updated [translations](https://crowdin.com/project/open-source-email)
### 1.2178 - 2024-04-29
* Added "AI" summarization of received messages (*)
* Small improvements and minor bug fixes
* Updated [translations](https://crowdin.com/project/open-source-email)
<sup>(*) Via the horizontal three-dots button just above the message text. ChatGPT or Gemini needs to be configured in the integrations-settings tab page for this.</sub>
### 1.2177 - 2024-04-27
* Added [Have I Been Pwned?](https://haveibeenpwned.com/) **<ins>password</ins>** check (*)
* Added identity option to configure envelope-from (*MAIL FROM*)
* Small improvements and minor bug fixes
* Updated [translations](https://crowdin.com/project/open-source-email)
<sub>(*) Via the three-dots overflow menu of the account list under "*Manual setup and account options*" in the main settings screen (GitHub version only)</sub>
### [Zby](https://en.wikipedia.org/wiki/Zby)
### 1.2169 - 2024-03-16 *
### 1.2176 - 2024-04-22 *
* Fixed British English translation
* Small improvements and minor bug fixes
### 1.2175 - 2024-04-20
* Fixed primary inbox navigation
* Updated [Public Suffix List](https://github.com/publicsuffix/list)
* Updated [translations](https://crowdin.com/project/open-source-email)
### 1.2174 - 2024-04-19
* Added expression conditions to rules, see [the FAQ](https://m66b.github.io/FairEmail/#faq71)
* Small improvements and minor bug fixes
* Updated [AndroidX](https://developer.android.com/jetpack/androidx/versions/all-channel)
* Updated [translations](https://crowdin.com/project/open-source-email)
### 1.2173 - 2024-04-16
* Added *primary inbox* start screen option
* Added *NOT* option to rule conditions
* Small improvements and minor bug fixes
* Updated [Public Suffix List](https://github.com/publicsuffix/list)
* Updated [translations](https://crowdin.com/project/open-source-email)
### 1.2172 - 2024-04-08
* Improved handling of messages via email forwarders (*)
* Small improvements and minor bug fixes
* Updated [AndroidX](https://developer.android.com/jetpack/androidx/versions/all-channel)
* Updated [translations](https://crowdin.com/project/open-source-email)
<sub>(*) Currently supported email forwarders:</sub>
* [addy.io](https://addy.io/)
* [DuckDuckGo Email Protection](https://duckduckgo.com/email/)
* [Firefox Relay](https://relay.firefox.com/)
* [SimpleLogin](https://simplelogin.io/)
### 1.2171 - 2024-03-30
* Added [Gemini](https://m66b.github.io/FairEmail/#faq204) integration
* Added answer button to buttons configuration
* Small improvements and minor bug fixes
* Updated [Public Suffix List](https://github.com/publicsuffix/list)
* Updated [translations](https://crowdin.com/project/open-source-email)
### 1.2170 - 2024-03-23
* Added Arabic to [DeepL translation](https://github.com/M66B/FairEmail/blob/master/FAQ.md#faq167) targets
* Small improvements and minor bug fixes
* Updated build tools
* Updated [Public Suffix List](https://github.com/publicsuffix/list)
* Updated [translations](https://crowdin.com/project/open-source-email)
### 1.2169 - 2024-03-16
* Small improvements and minor bug fixes
* Updated [translations](https://crowdin.com/project/open-source-email)
@ -175,6 +302,8 @@ For support you can use [the contact form](https://contact.faircode.eu/?product=
* Updated libraries (Apache Compress, Bugsnag, Bouncy Castle, Jsoup)
* Updated [translations](https://crowdin.com/project/open-source-email)
<!-- truncate here -->
### 1.2145 - 2023-12-30
* Added Adguard filter list to remove tracking parameters from links, see [the FAQ](https://github.com/M66B/FairEmail/blob/master/FAQ.md#faq200)
@ -1585,7 +1714,7 @@ For support you can use [the contact form](https://contact.faircode.eu/?product=
* Small improvements and minor bug fixes
* Updated translations
(*) Due to Play store policies this feature is not available in the Play store version; Android version 6 or later is required
<sub>(*) Due to Play store policies this feature is not available in the Play store version; Android version 6 or later is required</sub>
### 1.1930 - 2022-07-04

158
FAQ.md
View File

@ -415,6 +415,7 @@ Anything on this list is in random order and *might* be added in the near future
* [(201) What is certificate transparency?](#faq201)
* [(202) What is DNSSEC and what is DANE?](#faq202)
* [(203) Where is my sent message?](#faq203)
* [(204) How do I use Gemini?](#faq204)
[I have another question.](#get-support)
@ -549,6 +550,8 @@ The low priority status bar notification shows the number of pending operations,
* *rule*: execute rule on body text
* *expunge*: permanently delete messages
* *report*: process delivery or read receipt (experimental)
* *download*: async download of text and attachments (experimental)
* *subject*: update subject
Operations are processed only when there is a connection to the email server or when manually synchronizing.
See also [this FAQ](#faq16).
@ -708,12 +711,13 @@ If you use the Play store or GitHub version of FairEmail,
you can use the quick setup wizard to easily setup a Gmail account and identity.
The Gmail quick setup wizard is not available for third party builds, like the F-Droid build
because Google approved the use of OAuth for official builds only.
OAuth is also not available on devices without Google services, such as recent Huawei devices, in which case selecting an account will fail.
The Gmail quick setup wizard won't work if the Android account manager doesn't work or doesn't support Google accounts,
When using OAuth with multiple Google accounts, other Google accounts probably need to be logged out first.
The "*Gmail (Android)*" quick setup wizard won't work if the Android account manager doesn't work or doesn't support Google accounts,
which is typically the case if the account selection is being *canceled* right away.
If you don't want to use or can't use an on-device Google account, for example on recent Huawei devices,
If you don't want to use or can't use OAuth or an on-device Google account, for example on recent Huawei devices,
you can ~~either enable access for "less secure apps" and use your account password (not advised)~~
or enable two factor authentication and use an app specific password.
To use a password you can use the quick setup wizard and select *Other provider*.
@ -911,7 +915,9 @@ POP3 is supported!
Communication with email servers is always encrypted, unless you explicitly turned this off.
This question is about optional end-to-end encryption with PGP or S/MIME.
The sender and recipient should first agree on this and exchange signed messages to transfer their public key to be able to send encrypted messages.
There is a gesture icon button just above the text of a received message on the right to verify a signature and store the public key.
<br />
@ -1076,6 +1082,7 @@ In case the certificate chain is incorrect, you can tap on the little info butto
After the certificate details the issuer or "selfSign" is shown.
A certificate is self signed when the subject and the issuer are the same.
Certificates from a certificate authority (CA) are marked with "[keyCertSign](https://tools.ietf.org/html/rfc5280#section-4.2.1.3)".
You can find the description of other key usage bits, like *cRLSign*, via this same link.
Certificates found in the Android key store are marked with "Android".
A valid chain looks like this:
@ -1095,8 +1102,6 @@ If you are looking for a free (test) S/MIME certificate, see [here](http://kb.mo
Please be sure to [read this first](https://davidroessli.com/logs/2019/09/free-smime-certificates-in-2019/#update20191219)
if you want to request an S/MIME Actalis certificate.
S/MIME certificates can for example be purchased via [Xolphin](https://www.xolphin.com/).
How to extract a public key from a S/MIME certificate:
```
@ -1472,6 +1477,12 @@ Sometimes it is necessary to enable external access (IMAP/SMTP) on the website o
Other possible causes are that the account is blocked or that logging in has been administratively restricted in some way,
for example by allowing to login from certain networks / IP addresses only.
<br />
**In the case of an existing account with an authentication error, please try to use the quick setup wizard button to authenticate the account again.**
<br />
* **Free.fr**: please see [this FAQ](#faq157)
* **Gmail / G suite**: please see [this FAQ](#faq6)
* **iCloud**: please see [this FAQ](#faq148)
@ -1707,6 +1718,7 @@ You can disable this feature in the advanced account settings.
When a menu item to select/open/save a file is disabled (dimmed) or when you get the message *Storage access framework not available*,
the [storage access framework](https://developer.android.com/guide/topics/providers/document-provider), a standard Android component, is probably not present.
This might be because your custom ROM does not include it or because it was actively removed (debloated).
Note that this will result in similar problems in other apps too.
FairEmail does not request storage permissions, so this framework is required to select files and folders.
No app, except maybe file managers, targeting Android 4.4 KitKat or later should ask for storage permissions because it would allow access to *all* files.
@ -1745,15 +1757,15 @@ please [contact me](https://contact.faircode.eu/?product=fairemailsupport).
External image:
<img alt="External image" src="https://github.com/M66B/FairEmail/blob/master/images/baseline_image_black_48dp.png" width="48" height="48" />
<img alt="External image" src="https://raw.githubusercontent.com/M66B/FairEmail/master/images/baseline_image_black_48dp.png" width="48" height="48" />
Embedded image:
<img alt="Embedded image" src="https://github.com/M66B/FairEmail/blob/master/images/baseline_photo_library_black_48dp.png" width="48" height="48" />
<img alt="Embedded image" src="https://raw.githubusercontent.com/M66B/FairEmail/master/images/baseline_photo_library_black_48dp.png" width="48" height="48" />
Broken image:
<img alt="Broken image" src="https://github.com/M66B/FairEmail/blob/master/images/baseline_broken_image_black_48dp.png" width="48" height="48" />
<img alt="Broken image" src="https://raw.githubusercontent.com/M66B/FairEmail/master/images/baseline_broken_image_black_48dp.png" width="48" height="48" />
Note that downloading external images from a remote server can be used to record you did see a message, which you likely don't want if the message is spam or malicious.
@ -1865,6 +1877,8 @@ Note that this is independent of receiving messages.
&#x1F30E; [Google Translate](https://translate.google.com/translate?sl=en&u=https%3A%2F%2Fm66b.github.io%2FFairEmail%2F%23faq34)
Matched identities are used to select the correct (matched) identity when replying to a message.
Identities are as expected matched by account.
For incoming messages the *to*, *cc*, *bcc*, *from* and *(X-)delivered/envelope/original-to* addresses will be checked (in this order)
and for outgoing messages (drafts, outbox and sent) only the *from* addresses will be checked.
@ -2381,6 +2395,8 @@ Then use the three dot action button to execute the desired action.
There are almost no providers offering the [JMAP](https://jmap.io/) protocol,
so it is not worth a lot of effort to add support for this to FairEmail.
Moreover, the only available [Java JMAP library](https://github.com/iNPUTmice/jmap) seems not to be maintained anymore.
<br />
<a name="faq57"></a>
@ -2424,11 +2440,11 @@ so it is better to resize images with an image editor first.
The email icon in the folder list can be open (outlined) or closed (solid):
<img src="https://github.com/M66B/FairEmail/blob/master/images/baseline_mail_outline_black_48dp.png" width="48" height="48" />
<img alt="Mail outline image" src="https://raw.githubusercontent.com/M66B/FairEmail/master/images/baseline_mail_outline_black_48dp.png" width="48" height="48" />
Message bodies and attachments are not downloaded by default.
<img src="https://github.com/M66B/FairEmail/blob/master/images/baseline_email_black_48dp.png" width="48" height="48" />
<img alt="Mail image" src="https://raw.githubusercontent.com/M66B/FairEmail/master/images/baseline_email_black_48dp.png" width="48" height="48" />
Message bodies and attachments are downloaded by default.
@ -2689,6 +2705,10 @@ If a rule is part of a group, stop processing means stop processing the group.
Since version 1.2018 there is a rule option to run rules daily on messages (around 1:00am) older than xxx.
<br>
**Conditions**
The following rule conditions are available:
* Sender (from, reply-to) contains or sender is contact
@ -2699,9 +2719,11 @@ The following rule conditions are available:
* Text contains (since version 1.1785)
* Absolute time (received) between (since version 1.1540)
* Relative time (received) between
* Expression (since version 1.2174)
All the conditions of a rule need to be true for the rule action to be executed.
All conditions are optional, but there needs to be at least one condition, to prevent matching all messages.
If you want to match all senders or all recipients, you can just use the @ character as condition because all email addresses will contain this character.
If you want to match a domain name, you can use as a condition something like *@example.org*
@ -2724,6 +2746,7 @@ jsoup:html > body > div > a[href=https://example.org]
```
You can use multiple rules, possibly with a *stop processing*, for an *or* or a *not* condition.
Since version 1.2173 there is a *NOT* option for conditions that accept a regex.
Matching is not case sensitive, unless you use [regular expressions](https://en.wikipedia.org/wiki/Regular_expression).
Please see [here](https://developer.android.com/reference/java/util/regex/Pattern) for the documentation of Java regular expressions.
@ -2739,6 +2762,49 @@ Note that a regular expression supports an *or* operator, so if you want to matc
Note that [dot all mode](https://developer.android.com/reference/java/util/regex/Pattern#DOTALL) is enabled
to be able to match [unfolded headers](https://tools.ietf.org/html/rfc2822#section-3.2.3).
<br />
**Expressions**
Since version 1.2174 it is possible to use expression conditions, which is [experimental](#faq125) for now.
Please [see here](https://ezylang.github.io/EvalEx/references/references.html) about which constants, operators and functions are available.
The following extra variables are available:
* *received* (long, unix epoch in milliseconds; since version 1.2179)
* *from* (array of strings)
* *to* (array of strings)
* *subject* (string)
* *text* (string)
* *hasAttachments* (boolean; deprecated, use function *attachments()* instead)
The following extra operators are available:
* *contains* (string/array of strings contains substring)
* *matches* (string/array of strings matches regex)
The following extra functions are available:
* *header(name)* (returns an array of header values for the named header)
* *blocklist()* (version 1.2176-1.2178; deprecated, use *onBlocklist()* instead)
* *onBlocklist()* (returns a boolean indicating if the sender/server is on a DNS blocklist; since version 1.2179)
* *hasMx()* (returns a boolean indicating if the from/reply-to address has an associated MX record; since version 1.2179)
* *attachments()* (returns an integer indicating number of attachments; since version 1.2179)
* *Jsoup()* (returns an array of selected strings; since version 1.2179)
* *Size(array)* (returns the number of items in an array; since version 1.2179)
* *knownContact()* (returns a boolean indicating that the from/reply-to address is in the Android address book or in the local contacts database)
Example conditions:
```header("X-Mailer") contains "Open-Xchange" && from matches ".*service@.*"```
```!onBlocklist() && hasMx() && attachments() > 0```
<br>
**Actions**
You can select one of these actions to apply to matching messages:
* No action (useful for *not*)
@ -3160,7 +3226,7 @@ The BBC article '[Spy pixels in emails have become endemic](https://www.bbc.com/
FairEmail will in most cases automatically recognize tracking images and replace them by this icon:
<img src="https://github.com/M66B/FairEmail/blob/master/images/baseline_my_location_black_48dp.png" width="48" height="48" />
<img alt="Tracking image" src="https://raw.githubusercontent.com/M66B/FairEmail/master/images/baseline_my_location_black_48dp.png" width="48" height="48" />
Automatic recognition of tracking images can be disabled in the privacy settings.
@ -4039,6 +4105,12 @@ Composing messages using [Markdown](https://en.wikipedia.org/wiki/Markdown) can
<br />
*Rule expression condition (1.2174+)*
See [this FAQ](#faq71)
<br />
<a name="faq126"></a>
**(126) Can message previews be sent to my smartwatch?**
@ -4415,7 +4487,7 @@ Note that trashing a message will permanently remove it from the server and that
To record voice notes you can press this icon in the bottom action bar of the message composer:
<img src="https://github.com/M66B/FairEmail/blob/master/images/baseline_record_voice_over_black_48dp.png" width="48" height="48" />
<img alt="Record image" src="https://raw.githubusercontent.com/M66B/FairEmail/master/images/baseline_record_voice_over_black_48dp.png" width="48" height="48" />
This requires a compatible audio recorder app to be installed.
In particular [this common intent](https://developer.android.com/reference/android/provider/MediaStore.Audio.Media.html#RECORD_SOUND_ACTION)
@ -4496,7 +4568,7 @@ You likely came here because you are using a third party build of FairEmail.
There is **only support** on the latest Play store version, the latest GitHub release and
the F-Droid build, but **only if** the version number of the F-Droid build is the same as the version number of the latest GitHub release.
F-Droid builds irregularly, which can be problematic when there is an important update.
F-Droid builds irregularly, which can be problematic if there is an important update.
Therefore you are advised to switch to the GitHub release.
Note that developers have no control over F-Droid builds and the F-Droid infrastructure (apps, forums, etc.).
@ -4625,7 +4697,7 @@ You might need to change [the Gmail IMAP settings](https://mail.google.com/mail/
* When I mark a message in IMAP as deleted: Auto-Expunge off - Wait for the client to update the server.
* When a message is marked as deleted and expunged from the last visible IMAP folder: Immediately delete the message forever
<img alt="External image" src="https://github.com/M66B/FairEmail/blob/master/images/Gmail_IMAP_delete_settings.png" width="600" height="333" />
<img alt="External image" src="https://raw.githubusercontent.com/M66B/FairEmail/master/images/Gmail_IMAP_delete_settings.png" width="600" height="333" />
Note that archived messages can be deleted only by moving them to the trash folder first.
@ -5502,6 +5574,10 @@ Cloud sync is an experimental feature. It is not available for the Play Store ve
OpenAI can only be used if configured and enabled.
**Note that using OpenAI is not free (anymore) !**
<br>
**Setup**
* Create an account [here](https://platform.openai.com/signup)
@ -5513,6 +5589,8 @@ OpenAI can only be used if configured and enabled.
**Usage**
*Editor*
Tap on the robot button in the top action bar of the message editor.
The text in the message editor (if any) and the first part of the message being replied to (if any)
will be used for [chat completion](https://platform.openai.com/docs/guides/chat/introduction).
@ -5523,25 +5601,32 @@ For example: create a new draft and enter the text "*How far is the sun?*", and
<br>
*Summarize* (since version 1.2178)
You can request a summary via the horizontal three-dots button just above the message text.
It is possible to configure a button for this (a robot icon).
The summary prompt text can be configured in the receive-settings tab page.
The default is *Summarize the following text:*.
Since version 1.2182 is is possible to configure a quick action button and a swipe action to summarize a message.
<br>
OpenAI isn't very fast, so be patient. Sometimes a timeout error occurs because the app is not receiving a response from OpenAI.
<br>
Depending on the ChatGPT account (free or paid) there are usage limits. If you exceed the limit, there will be an error message like this:
If you exceed [your usage limit](https://platform.openai.com/docs/guides/rate-limits), there will be an error message like this:
*Error 429: Too Many Requests insufficient_quota: You exceeded your current quota, please check your plan and billing details*
In this case, you'll either need to wait, or upgrade your ChatGPT plan.
Please [see here](https://help.openai.com/en/articles/6843909-rate-limits-and-429-too-many-requests-errors) for details.
Note that you are required to switch to a [paid plan](https://openai.com/api/pricing/) after the testing period.
<br>
You can select the [model](https://platform.openai.com/docs/models/overview),
configure the [temperature](https://platform.openai.com/docs/api-reference/chat/create#chat/create-temperature)
and enable [moderation](https://platform.openai.com/docs/api-reference/moderations) in the integration settings.
If you have access to GPT-4, you can change the model name to [gpt-4](https://platform.openai.com/docs/models/gpt-4) in the integration settings.
There is currently a [waitlist](https://openai.com/waitlist/gpt-4-api) for API GPT-4 access.
and configure the [temperature](https://platform.openai.com/docs/api-reference/chat/create#chat/create-temperature).
Please read the [privacy policy](https://openai.com/policies/privacy-policy) of OpenAI,
and perhaps [this article](https://katedowninglaw.com/2023/03/10/openais-massive-data-grab/)
@ -5558,7 +5643,7 @@ It is possible to use **DeepInfra** too (since version 1.2132).
<br>
This feature is experimental and available in the GitHub version only and requires version 1.2053 or later.
This feature is experimental and requires version 1.2053 or later for the GitHub version and version 1.2182 or later for the Play Store version.
<br>
@ -5803,6 +5888,33 @@ Basically, an outgoing message is either in the draft messages folder, the outbo
<br>
<a name="faq204"></a>
**(204) How do I use Gemini?**
Gemini can only be used if configured and enabled.
**Note that using Gemini is not free (anymore) !**
<br>
To use [Gemini](https://gemini.google.com/), please follow these steps:
1. Check if your country [is supported](https://ai.google.dev/available_regions)
1. Get an API key via [here](https://ai.google.dev/tutorials/setup)
1. Enter the API key in the integration settings tab page
1. Enable Gemini integration in the integration settings tab page
<br>
For usage instructions, please see [this FAQ](#faq190).
Please read the privacy policy of [Gemini](https://support.google.com/gemini/answer/13594961).
FairEmail does not use third-party libraries to avoid being tracked when Gemini is not being used.
This feature is experimental and requires version 1.2171 or later for the GitHub version and version 1.2182 or later for the Play Store version.
<br>
<h2><a name="get-support"></a>Get support</h2>
&#x1F30E; [Google Translate](https://translate.google.com/translate?sl=en&u=https%3A%2F%2Fm66b.github.io%2FFairEmail%2F%23get-support)

View File

@ -3,5 +3,27 @@ header-includes: |
<link rel="shortcut icon" href="https://raw.githubusercontent.com/M66B/FairEmail/master/app/src/main/ic_launcher-web.png">
<style>
body { font-family: Arial, sans-serif; }
@media (prefers-color-scheme: light) {
body {
color: #212121;
background: #FFFFFF;
}
a:link { color: #C68400; }
a:visited { color: #ff6f00; }
a:hover { color: #039BE5; }
a:active { color: #039BE5; }
img { filter: invert(0%); }
}
@media (prefers-color-scheme: dark) {
body {
color: #FFFFFF;
background: #424242;
}
a:link { color: #FFB300; }
a:visited { color: #FF6F00; }
a:hover { color: #01579B; }
a:active { color: #01579B; }
img { filter: invert(75%); }
}
</style>
---

View File

@ -37,7 +37,7 @@ FairEmail **does not** send account information and message data elsewhere than
FairEmail **does not** allow other apps access to message data without your approval.
FairEmail **does not** require unnecessary permissions.
For more information on permissions, see [this FAQ](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq1).
For more information on permissions, see [this FAQ](https://m66b.github.io/FairEmail/#faq1).
FairEmail **does** use modern and secure transport protocols by default.
@ -47,7 +47,7 @@ FairEmail **does** follow the recommendations of [this EFF article](https://www.
FairEmail is 100 % **open source**, see [the license](https://github.com/M66B/FairEmail/blob/master/LICENSE).
Error reporting via Bugsnag **is opt-in**, see [here](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq104) for more information.
Error reporting via Bugsnag **is opt-in**, see [here](https://m66b.github.io/FairEmail/#faq104) for more information.
FairEmail **adheres** to the [Google API Services User Data Policy](https://developers.google.com/terms/api-services-user-data-policy),
including the [Limited Use requirements](https://developers.google.com/terms/api-services-user-data-policy#additional_requirements_for_specific_api_scopes).
@ -69,10 +69,13 @@ FairEmail **can use** these services if they are explicitly enabled (off by defa
* [DeepL](https://www.deepl.com/) &#8211; [Privacy policy](https://www.deepl.com/privacy/)
* [LanguageTool](https://languagetool.org/) &#8211; [Privacy policy](https://languagetool.org/legal/privacy)
* [VirusTotal](https://www.virustotal.com/) &#8211; [Privacy policy](https://support.virustotal.com/hc/en-us/articles/115002168385-Privacy-Policy)
* [OpenAI](https://openai.com/) (GitHub version only) &#8211; [Privacy policy](https://openai.com/policies/privacy-policy)
* [OpenAI](https://openai.com/) &#8211; [Privacy policy](https://openai.com/policies/privacy-policy)
* [Google Gemini](https://gemini.google.com/) &#8211; [Privacy policy](https://support.google.com/gemini/answer/13594961)
* [Gravatar](https://gravatar.com/) (GitHub version only) &#8211; [Privacy policy](https://automattic.com/privacy/)
* [Libravatar](https://www.libravatar.org/) (GitHub version only) &#8211; [Privacy policy](https://www.libravatar.org/privacy/)
* [GitHub](https://github.com/) (GitHub version only) &#8211; [Privacy policy](https://docs.github.com/en/site-policy/privacy-policies/github-privacy-statement)
* [Have I Been Pwned?](https://haveibeenpwned.com/) &#8211; [Privacy policy](https://haveibeenpwned.com/Privacy)
* [Bugsnag](https://www.bugsnag.com/) &#8211; [Privacy policy](https://smartbear.com/privacy/)
FairEmail **can access** the websites at the domain names of email addresses (username@domain.name)
if [Brand Indicators for Message Identification](https://en.wikipedia.org/wiki/Brand_Indicators_for_Message_Identification) (BIMI)
@ -105,12 +108,14 @@ This table provides a complete overview of all shared data and the conditions un
| LanguageTool | Entered message texts | If LanguageTools is enabled, upon long pressing the save draft button |
| VirusTotal | [SHA-256 hash](https://en.wikipedia.org/wiki/SHA-2) of attachments | If VirusTotal is enabled, upon long pressing a scan button (*) |
| VirusTotal | Attached file contents | If VirusTotal is enabled, upon long pressing an upload button (*) |
| OpenAI | Received and entered message texts | Upen pressing a button in a navigation bar (*) |
| OpenAI/ChatGPT | Received and entered message texts | If configured and upon pressing a button or using a menu item |
| Google Gemini | Received and entered message texts | If configured and upon pressing a button or using a menu item |
| Gravatar | [MD5 hash](https://en.wikipedia.org/wiki/MD5) of email addresses | If Gravatars are enabled, upon receiving a message (*) |
| Libravatar | [MD5 hash](https://en.wikipedia.org/wiki/MD5) of email addresses | If Libravatars are enabled, upon receiving a message (*) |
| GitHub | None, but see the remarks below | Upon downloading AdGuard tracking parameter list |
| | | Upon downloading Disconnect's Tracker Protection lists |
| | | Upon checking for updates (*) |
| Have I Been Pwned? | The first 5 characters of the SHA1 hash of passwords | Upon checking for being pwned |
| BIMI | Domain name of email addresses | If BIMI is enabled, upon receiving a message (*) |
| Favicons | Domain name of email addresses | If favicons are enabled, upon receiving a message |
| Link title | Link address | Upon pressing a download button in the insert link dialog |
@ -193,13 +198,13 @@ The sub-processors are:
#### V. Permissions
The app only requests permissions that are necessary for the expected behavior of an email app.
For more information on permissions, see [this FAQ](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq1).
For more information on permissions, see [this FAQ](https://m66b.github.io/FairEmail/#faq1).
#### VI. Logging
The app does not send any log entries to the data processor by default.
The error reporting system utilizes Bugsnag and is disabled by default.
See [this FAQ](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq104) for more information.
See [this FAQ](https://m66b.github.io/FairEmail/#faq104) for more information.
#### VII. Legal basis

View File

@ -57,7 +57,7 @@ This app starts a foreground service with a low-priority status bar notification
* Confirm showing images to prevent tracking
* Confirm opening links to prevent tracking and phishing
* Attempt to recognize and disable tracking images
* Warning if messages could not be [authenticated](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq92)
* Warning if messages could not be [authenticated](https://m66b.github.io/FairEmail/#faq92)
## Simple
@ -74,7 +74,7 @@ This app starts a foreground service with a low-priority status bar notification
* Confirm opening links, images and attachments
* No special permissions required
* No advertisements
* No analytics and no tracking ([error reporting](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq104) via Bugsnag is opt-in)
* No analytics and no tracking ([error reporting](https://m66b.github.io/FairEmail/#faq104) via Bugsnag is opt-in)
* No [Google backup](https://developer.android.com/guide/topics/data/backup)
* No [Firebase Cloud Messaging](https://firebase.google.com/docs/cloud-messaging)
* FairEmail is an original work, not a fork or a clone
@ -90,21 +90,21 @@ This app starts a foreground service with a low-priority status bar notification
All pro features are convenience or advanced features.
* Account/identity/folder colors
* Colored stars ([instructions](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq107))
* Notification settings (sounds) per account/folder/sender (requires Android 8 Oreo) ([instructions](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq145))
* Colored stars ([instructions](https://m66b.github.io/FairEmail/#faq107))
* Notification settings (sounds) per account/folder/sender (requires Android 8 Oreo) ([instructions](https://m66b.github.io/FairEmail/#faq145))
* Configurable notification actions
* Snooze messages ([instructions](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq67))
* Snooze messages ([instructions](https://m66b.github.io/FairEmail/#faq67))
* Send messages after selected time
* Synchronization scheduling ([instructions](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq78))
* Reply templates ([instructions](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq179))
* Synchronization scheduling ([instructions](https://m66b.github.io/FairEmail/#faq78))
* Reply templates ([instructions](https://m66b.github.io/FairEmail/#faq179))
* Accept/decline calendar invitations
* Add message to calendar
* Automatically generate vCard attachments
* Filter rules ([instructions](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq71))
* Automatic message classification ([instructions](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq163))
* Search indexing ([instructions](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq13))
* S/MIME sign/encrypt ([instructions](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq12))
* Biometric/PIN authentication ([instructions](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq113))
* Filter rules ([instructions](https://m66b.github.io/FairEmail/#faq71))
* Automatic message classification ([instructions](https://m66b.github.io/FairEmail/#faq163))
* Search indexing ([instructions](https://m66b.github.io/FairEmail/#faq13))
* S/MIME sign/encrypt ([instructions](https://m66b.github.io/FairEmail/#faq12))
* Biometric/PIN authentication ([instructions](https://m66b.github.io/FairEmail/#faq113))
* Message list widget
* Export settings
@ -123,10 +123,10 @@ Supported download locations:
* ~~[AppGallery](https://wap3.hispace.hicloud.com/uowap/index.jsp#/detailApp/C101678151) (the AppGallery app can be downloaded [here](https://huaweimobileservices.com/appgallery/))~~
* ~~[Amazon](https://www.amazon.com/gp/product/B0983R6MH2)~~ (the APK file repackaged by Amazon is incomplete! An issue report was never answered by Amazon.)
Please see [this FAQ](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq173) about the differences between the different releases.
Please see [this FAQ](https://m66b.github.io/FairEmail/#faq173) about the differences between the different releases.
**Important**: after enrolling in the [Advanced Protection Program](https://landing.google.com/advancedprotection/)
you cannot use third party email apps anymore, please see [this FAQ](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq22) for more information.
you cannot use third party email apps anymore, please see [this FAQ](https://m66b.github.io/FairEmail/#faq22) for more information.
The Gmail quick setup wizard can be used in official releases only (Play store or GitHub) because Google approved the use of OAuth for one app signature only.
@ -143,7 +143,7 @@ F-Droid builds new versions irregularly and you'll need the F-Droid client to ge
To get updates in a timely fashion you are advised to use the GitHub release.
**Important**: There is support on the F-Droid build only if the version number of the F-Droid build is the same as the version number of the latest GitHub release.
Please [see here](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq147) for more information on third-party builds.
Please [see here](https://m66b.github.io/FairEmail/#faq147) for more information on third-party builds.
Because F-Droid builds and GitHub releases are signed differently, an F-Droid build needs to be uninstalled first to be able to update to a GitHub release.
@ -178,7 +178,7 @@ because earlier Android versions do not support notification channels.
FairEmail will work properly on devices without any Google service installed.
Please see [here](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-known-problems) for known problems.
Please see [here](https://m66b.github.io/FairEmail/#known-problems) for known problems.
## Privacy
@ -186,7 +186,7 @@ Please see [here](https://github.com/M66B/FairEmail/blob/master/PRIVACY.md#faire
## Support
Please see [here](https://github.com/M66B/FairEmail/blob/master/FAQ.md) for a list of often asked questions and about how to get support.
Please see [here](https://m66b.github.io/FairEmail/) for a list of often asked questions and about how to get support.
## Contributing
@ -223,7 +223,7 @@ Error reporting is sponsored by:
![Bugsnag logo](/images/bugsnag_logo.png)
[Bugsnag](https://www.bugsnag.com/) monitors application stability
and is used to [help improve FairEmail](https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq104).
and is used to [help improve FairEmail](https://m66b.github.io/FairEmail/#faq104).
Error reporting is disabled by default, see also [the privacy policy](https://github.com/M66B/FairEmail/blob/master/PRIVACY.md#fairemail).
## License

View File

@ -2,7 +2,7 @@
# https://developer.android.com/studio/projects/configure-cmake
project(fairemail)
cmake_minimum_required(VERSION 3.4.1)
cmake_minimum_required(VERSION 3.22.1)
add_library(fairemail SHARED
src/main/jni/fairemail.cc

View File

@ -3,9 +3,9 @@ apply plugin: 'com.bugsnag.android.gradle'
apply plugin: 'kotlin-android'
apply plugin: 'de.undercouch.download'
def getVersionCode = { -> return 2169 }
def getRevision = { -> return "a" }
def getReleaseName = { -> return "Zby" }
def getVersionCode = { -> return 2182 }
def getRevision = { -> return "b" }
def getReleaseName = { -> return "Acantholipan" }
// https://en.wikipedia.org/wiki/List_of_dinosaur_genera
def keystoreProperties = new Properties()
@ -18,6 +18,7 @@ if (rootProject.file("local.properties").exists())
android {
//compileSdkExtension 4 // https://developer.android.com/guide/sdk-extensions
//compileSdkPreview "VanillaIceCream"
namespace 'eu.faircode.email'
// https://apilevels.com/
@ -26,6 +27,7 @@ android {
compileSdk 34
minSdkVersion 21
targetSdkVersion 34
//targetSdkPreview "VanillaIceCream"
versionCode getVersionCode()
versionName "1." + getVersionCode()
@ -51,7 +53,7 @@ android {
}
// https://developer.android.com/ndk/downloads
ndkVersion "25.2.9519653" // r25c
ndkVersion "26.3.11579264" // r26d
ndk {
// Bugsnag, sqlite
// https://developer.android.com/ndk/guides/abis
@ -65,10 +67,10 @@ android {
sourceSets {
github {
java.srcDirs = ['src/main/java', 'src/play/java', 'src/extra/java']
java.srcDirs = ['src/main/java', 'src/github/java', 'src/extra/java']
}
large {
java.srcDirs = ['src/main/java', 'src/play/java', 'src/extra/java']
java.srcDirs = ['src/main/java', 'src/github/java', 'src/extra/java']
}
fdroid {
java.srcDirs = ['src/main/java', 'src/fdroid/java', 'src/extra/java']
@ -116,7 +118,9 @@ android {
}
packagingOptions {
jniLibs {
useLegacyPackaging = true // https://issuetracker.google.com/issues/127691101
// https://issuetracker.google.com/issues/127691101
// https://developer.android.com/guide/practices/page-sizes#build
useLegacyPackaging = true
excludes += [
'org/apache/**'
]
@ -153,15 +157,17 @@ android {
}
}
// https://developer.android.com/reference/tools/gradle-api/8.3/com/android/build/api/dsl/BuildFeatures
buildFeatures {
buildConfig = true
viewBinding = false
dataBinding = false
aidl = true
buildConfig = true
compose = false
prefab = false
renderScript = false
resValues = false
shaders = false
compose = false
viewBinding = false
dataBinding = false
}
buildTypes {
@ -213,6 +219,10 @@ android {
buildConfigField "String", "CLOUD_EMAIL", "\"cloud@in.faircode.eu\""
buildConfigField "String", "OPENAI_ENDPOINT", "\"https://api.openai.com/v1/\""
buildConfigField "String", "OPENAI_PRIVACY", "\"https://openai.com/policies/privacy-policy\""
buildConfigField "String", "GEMINI_ENDPOINT", "\"https://generativelanguage.googleapis.com/v1beta/\""
buildConfigField "String", "GEMINI_PRIVACY", "\"https://support.google.com/gemini/answer/13594961\""
buildConfigField "String", "PWNED_ENDPOINT", "\"https://api.pwnedpasswords.com/\""
buildConfigField "String", "PWNED_URI", "\"https://haveibeenpwned.com/\""
buildConfigField "String", "FDROID", "\"https://f-droid.org/packages/%s/\""
}
large {
@ -235,6 +245,10 @@ android {
buildConfigField "String", "CLOUD_EMAIL", "\"cloud@in.faircode.eu\""
buildConfigField "String", "OPENAI_ENDPOINT", "\"https://api.openai.com/v1/\""
buildConfigField "String", "OPENAI_PRIVACY", "\"https://openai.com/policies/privacy-policy\""
buildConfigField "String", "GEMINI_ENDPOINT", "\"https://generativelanguage.googleapis.com/v1beta/\""
buildConfigField "String", "GEMINI_PRIVACY", "\"https://support.google.com/gemini/answer/13594961\""
buildConfigField "String", "PWNED_ENDPOINT", "\"https://api.pwnedpasswords.com/\""
buildConfigField "String", "PWNED_URI", "\"https://haveibeenpwned.com/\""
buildConfigField "String", "FDROID", "\"https://f-droid.org/packages/%s/\""
}
fdroid {
@ -266,6 +280,10 @@ android {
buildConfigField "String", "CLOUD_EMAIL", "\"cloud@in.faircode.eu\""
buildConfigField "String", "OPENAI_ENDPOINT", "\"https://api.openai.com/v1/\""
buildConfigField "String", "OPENAI_PRIVACY", "\"https://openai.com/policies/privacy-policy\""
buildConfigField "String", "GEMINI_ENDPOINT", "\"https://generativelanguage.googleapis.com/v1beta/\""
buildConfigField "String", "GEMINI_PRIVACY", "\"https://support.google.com/gemini/answer/13594961\""
buildConfigField "String", "PWNED_ENDPOINT", "\"https://api.pwnedpasswords.com/\""
buildConfigField "String", "PWNED_URI", "\"https://haveibeenpwned.com/\""
buildConfigField "String", "FDROID", "\"https://f-droid.org/packages/%s/\""
}
play {
@ -287,8 +305,12 @@ android {
buildConfigField "String", "ANNOUNCEMENT_URI", "\"\""
buildConfigField "String", "CLOUD_URI", "\"\""
buildConfigField "String", "CLOUD_EMAIL", "\"\""
buildConfigField "String", "OPENAI_ENDPOINT", "\"\""
buildConfigField "String", "OPENAI_PRIVACY", "\"\""
buildConfigField "String", "OPENAI_ENDPOINT", "\"https://api.openai.com/v1/\""
buildConfigField "String", "OPENAI_PRIVACY", "\"https://openai.com/policies/privacy-policy\""
buildConfigField "String", "GEMINI_ENDPOINT", "\"https://generativelanguage.googleapis.com/v1beta/\""
buildConfigField "String", "GEMINI_PRIVACY", "\"https://support.google.com/gemini/answer/13594961\""
buildConfigField "String", "PWNED_ENDPOINT", "\"https://api.pwnedpasswords.com/\""
buildConfigField "String", "PWNED_URI", "\"https://haveibeenpwned.com/\""
buildConfigField "String", "FDROID", "\"\""
getIsDefault().set(true)
}
@ -313,6 +335,10 @@ android {
buildConfigField "String", "CLOUD_EMAIL", "\"\""
buildConfigField "String", "OPENAI_ENDPOINT", "\"\""
buildConfigField "String", "OPENAI_PRIVACY", "\"\""
buildConfigField "String", "GEMINI_ENDPOINT", "\"\""
buildConfigField "String", "GEMINI_PRIVACY", "\"\""
buildConfigField "String", "PWNED_ENDPOINT", "\"https://api.pwnedpasswords.com/\""
buildConfigField "String", "PWNED_URI", "\"https://haveibeenpwned.com/\""
buildConfigField "String", "FDROID", "\"\""
}
}
@ -395,7 +421,7 @@ preBuild.dependsOn copyChangelog
tasks.register('updateFAQ', Exec) {
workingDir "${rootDir}"
commandLine 'sh', '-c', 'pandoc --standalone --metadata-file FAQ.yaml FAQ.md -o index.html'
commandLine 'sh', '-c', 'pandoc --standalone --columns=10000 -M document-css=false --metadata-file FAQ.yaml FAQ.md -o index.html'
}
tasks.register('updatePrivacy', Exec) {
@ -451,11 +477,11 @@ tasks.register('upload') {
doLast {
println "\nhttps://bitbucket.org/M66B/fairemail-test/downloads/FairEmail-v1." + getVersionCode() + getRevision() + "-" + target + "-release.apk\n"
exec {
workingDir "${buildDir}"
workingDir "${rootDir}/app/build"
commandLine 'curl',
'-o', '/dev/null',
'-X', 'POST', "https://M66B:" + localProperties.getProperty("bb.pwd", "") + "@api.bitbucket.org/2.0/repositories/M66B/fairemail-test/downloads",
'--form', "files=@${buildDir}/outputs/apk/" + target.split('-')[0] + "/release/FairEmail-v1." + getVersionCode() + getRevision() + "-" + target.split('-')[0] + "-release.apk;" +
'--form', "files=@${rootDir}/app/build/outputs/apk/" + target.split('-')[0] + "/release/FairEmail-v1." + getVersionCode() + getRevision() + "-" + target.split('-')[0] + "-release.apk;" +
"filename=FairEmail-v1." + getVersionCode() + getRevision() + "-" + target + "-release.apk"
}
}
@ -517,51 +543,51 @@ dependencies {
def desugar_version = "2.0.4"
def startup_version = "1.2.0-alpha02"
def annotation_version_experimental = "1.4.0"
def core_version = "1.12.0" // 1.13.0-alpha04
def appcompat_version = "1.6.1" // 1.7.0-alpha03
def annotation_version_experimental = "1.4.1"
def core_version = "1.13.1" // 1.14.0-alpha01
def appcompat_version = "1.6.1" // 1.7.0-rc01
def emoji_version = "1.4.0" // 1.5.0-alpha01
def flatbuffers_version = "2.0.0"
def activity_version = "1.8.2" // 1.9.0-alpha03
def fragment_version = "1.6.2" // 1.7.0-alpha10
def windows_version = "1.2.0" // 1.3.0-alpha03
def webkit_version = "1.10.0" // 1.11.0-alpha02
def activity_version = " 1.9.0"
def fragment_version = "1.6.2" // 1.7.1 // 1.8.0-beta01
def windows_version = "1.2.0" // 1.3.0-rc01
def webkit_version = "1.10.0" // 1.11.0
def recyclerview_version = "1.3.2" // 1.4.0-alpha01
def coordinatorlayout_version = "1.2.0" // 1.3.0-alpha02
def constraintlayout_version = "2.1.4" // 2.2.0-alpha13
def material_version = "1.11.0" // 1.11.0-alpha01
def material_version = "1.11.0" // 1.12.0-rc01 / 1.13.0-alpha01
def browser_version = "1.8.0"
def lbm_version = "1.1.0"
def swiperefresh_version = "1.2.0-alpha01"
def documentfile_version = "1.1.0-alpha01"
def lifecycle_version = "2.7.0" // 2.8.0-alpha02
def lifecycle_version = "2.7.0" // 2.8.0
def lifecycle_extensions_version = "2.2.0"
def room_version = "2.4.3" // 2.5.2/2.6.1
def sqlite_version = "2.4.0"
def room_version = "2.4.3" // 2.5.2/2.6.1/2.7.0-alpha02
def sqlite_version = "2.4.0" // 2.5.0-alpha02
def requery_version = "3.39.2"
def paging_version = "2.1.2" // 3.3.0-alpha04
def paging_version = "2.1.2" // 3.3.0-rc01
def preference_version = "1.2.1"
def work_version = "2.9.0" // 2.10.0-alpha01
def work_version = "2.9.0" // 2.10.0-alpha02
def exif_version = "1.3.7"
def biometric_version = "1.2.0-alpha05"
def billingclient_version = "6.0.1" // 6.1.0
def playservicesbase_version = "18.3.0";
def billingclient_version = "6.0.1" // 6.2.0
def playservicesbasement_version = "18.3.0";
def transparency_version = "2.5.19"
def javamail_version = "1.6.7"
def jsoup_version = "1.17.2"
def jsonpath_version = "2.9.0"
def css_version = "0.9.30"
def jax_version = "2.3.0-jaxb-1.0.6"
def minidns_version = "1.0.4"
def minidns_version = "1.0.5"
def openpgp_version = "12.0"
def badge_version = "1.1.22"
def bugsnag_version = "6.1.0"
def bugsnag_version = "6.4.0"
def biweekly_version = "0.6.8"
def vcard_version = "0.12.1"
def relinker_version = "1.4.5"
def markwon_version = "4.6.2"
def commonmark_version = "0.21.0"
def bouncycastle_version = "1.77"
def commonmark_version = "0.22.0"
def bouncycastle_version = "1.78.1"
def colorpicker_version = "0.0.15"
def overscroll_version = "1.1.1"
def appauth_version = "0.11.1"
@ -571,12 +597,13 @@ dependencies {
def reactivestreams_version = "1.0.3"
def rxjava2_version = "2.2.21"
def svg_version = "1.4"
def compress_version = "1.25.0"
def ipaddress_version = "5.4.0"
def canary_version = "2.13"
def compress_version = "1.26.1"
def ipaddress_version = "5.5.0"
def canary_version = "2.14"
def ws_version = "2.14"
def tinylog_version = "2.6.2"
def zxing_version = "3.5.3"
def evalex_version = "3.2.0"
// https://mvnrepository.com/artifact/com.android.tools/desugar_jdk_libs?repo=google
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:$desugar_version"
@ -677,7 +704,8 @@ dependencies {
// https://mvnrepository.com/artifact/androidx.work/work-runtime
implementation "androidx.work:work-runtime:$work_version"
// implementation "com.google.guava:listenablefuture:1.0"
implementation "com.google.guava:guava:31.1-android" // ListenableFuture
// https://mvnrepository.com/artifact/com.google.guava/guava
implementation "com.google.guava:guava:33.1.0-android" // ListenableFuture
// https://mvnrepository.com/artifact/androidx.exifinterface/exifinterface
implementation "androidx.exifinterface:exifinterface:$exif_version"
@ -688,14 +716,12 @@ dependencies {
// https://developer.android.com/google/play/billing/billing_library_releases_notes
// https://android-developers.googleblog.com/2020/06/meet-google-play-billing-library.html
githubImplementation "com.android.billingclient:billing:$billingclient_version"
largeImplementation "com.android.billingclient:billing:$billingclient_version"
playImplementation "com.android.billingclient:billing:$billingclient_version"
// https://mvnrepository.com/artifact/com.google.android.gms/play-services-base
githubImplementation "com.google.android.gms:play-services-basement:$playservicesbase_version"
largeImplementation "com.google.android.gms:play-services-basement:$playservicesbase_version"
playImplementation "com.google.android.gms:play-services-basement:$playservicesbase_version"
// https://mvnrepository.com/artifact/com.google.android.gms/play-services-basement
githubImplementation "com.google.android.gms:play-services-basement:$playservicesbasement_version"
largeImplementation "com.google.android.gms:play-services-basement:$playservicesbasement_version"
playImplementation "com.google.android.gms:play-services-basement:$playservicesbasement_version"
// https://github.com/appmattus/certificatetransparency
// https://mvnrepository.com/artifact/com.appmattus.certificatetransparency/certificatetransparency-android
@ -780,6 +806,7 @@ dependencies {
implementation "org.commonmark:commonmark-ext-gfm-strikethrough:$commonmark_version";
// https://github.com/vsch/flexmark-java
// https://mvnrepository.com/artifact/com.vladsch.flexmark/flexmark
implementation "com.vladsch.flexmark:flexmark-html2md-converter:0.64.8"
// // https://github.com/QuadFlask/colorpicker
@ -849,4 +876,7 @@ dependencies {
// https://github.com/zxing/zxing
// https://mvnrepository.com/artifact/com.google.zxing/core
implementation "com.google.zxing:core:$zxing_version"
// https://github.com/ezylang/EvalEx
implementation "com.ezylang:EvalEx:$evalex_version"
}

View File

@ -161,3 +161,6 @@
-dontwarn java.lang.**
-dontwarn javax.naming.**
-dontwarn sun.reflect.Reflection
#EvalEx
-dontwarn lombok.Generated

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -131,6 +131,15 @@
android:theme="@style/Theme.AppCompat.TranslucentSplash">
<!-- https://developer.samsung.com/samsung-dex/modify-optimizing.html -->
<!-- https://firebase.google.com/docs/analytics/configure-data-collection?platform=android -->
<meta-data
android:name="firebase_analytics_collection_deactivated"
android:value="true" />
<!-- https://firebase.google.com/docs/perf-mon/disable-sdk?platform=android -->
<meta-data
android:name="firebase_performance_collection_deactivated"
android:value="true" />
<!-- https://developer.android.com/guide/webapps/managing-webview#metrics -->
<meta-data
android:name="android.webkit.WebView.MetricsOptOut"
@ -622,6 +631,7 @@
<action android:name="${applicationId}.RULE" />
<action android:name="${applicationId}.TEMPLATE" />
<action android:name="${applicationId}.DISCONNECT.ME" />
<action android:name="${applicationId}.ADGUARD" />
</intent-filter>
</service>

View File

@ -80,9 +80,12 @@ public class ActivityBilling extends ActivityBase implements PurchasingListener,
if (standalone) {
setContentView(R.layout.activity_billing);
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, new FragmentPro()).addToBackStack("pro");
fragmentTransaction.commit();
int count = getSupportFragmentManager().getBackStackEntryCount();
if (count == 0) {
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, new FragmentPro()).addToBackStack("pro");
fragmentTransaction.commit();
}
getSupportActionBar().setDisplayHomeAsUpEnabled(true);

View File

@ -139,6 +139,15 @@
android:theme="@style/Theme.AppCompat.TranslucentSplash">
<!-- https://developer.samsung.com/samsung-dex/modify-optimizing.html -->
<!-- https://firebase.google.com/docs/analytics/configure-data-collection?platform=android -->
<meta-data
android:name="firebase_analytics_collection_deactivated"
android:value="true" />
<!-- https://firebase.google.com/docs/perf-mon/disable-sdk?platform=android -->
<meta-data
android:name="firebase_performance_collection_deactivated"
android:value="true" />
<!-- https://developer.android.com/guide/webapps/managing-webview#metrics -->
<meta-data
android:name="android.webkit.WebView.MetricsOptOut"
@ -629,6 +638,7 @@
<action android:name="${applicationId}.RULE" />
<action android:name="${applicationId}.TEMPLATE" />
<action android:name="${applicationId}.DISCONNECT.ME" />
<action android:name="${applicationId}.ADGUARD" />
</intent-filter>
</service>

View File

@ -366,6 +366,9 @@ public class Bimi {
}
}
if (bitmap != null && !verified)
Log.i("BIMI unverified");
return (bitmap == null ? null : new Pair<>(bitmap, verified));
}

View File

@ -138,6 +138,15 @@
android:theme="@style/Theme.AppCompat.TranslucentSplash">
<!-- https://developer.samsung.com/samsung-dex/modify-optimizing.html -->
<!-- https://firebase.google.com/docs/analytics/configure-data-collection?platform=android -->
<meta-data
android:name="firebase_analytics_collection_deactivated"
android:value="true" />
<!-- https://firebase.google.com/docs/perf-mon/disable-sdk?platform=android -->
<meta-data
android:name="firebase_performance_collection_deactivated"
android:value="true" />
<!-- https://developer.android.com/guide/webapps/managing-webview#metrics -->
<meta-data
android:name="android.webkit.WebView.MetricsOptOut"
@ -628,6 +637,7 @@
<action android:name="${applicationId}.RULE" />
<action android:name="${applicationId}.TEMPLATE" />
<action android:name="${applicationId}.DISCONNECT.ME" />
<action android:name="${applicationId}.ADGUARD" />
</intent-filter>
</service>

View File

@ -98,25 +98,72 @@ public class ActivityBilling extends ActivityBase implements
if (standalone) {
setContentView(R.layout.activity_billing);
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, new FragmentPro()).addToBackStack("pro");
fragmentTransaction.commit();
int count = getSupportFragmentManager().getBackStackEntryCount();
if (count == 0) {
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, new FragmentPro()).addToBackStack("pro");
fragmentTransaction.commit();
}
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportFragmentManager().addOnBackStackChangedListener(this);
}
if (Helper.isPlayStoreInstall() || isTesting(this)) {
Log.i("IAB start");
if (Helper.isPlayStoreInstall() || isTesting(this))
try {
Log.i("IAB start");
/*
billingClient = BillingClient.newBuilder(getApplicationContext()
.enablePendingPurchases()
.setListener(this)
.build();
billingClient.startConnection(this);
billingClient = BillingClient.newBuilder(getApplicationContext())
.enablePendingPurchases()
.setListener(this)
.build();
billingClient.startConnection(this);
getLifecycle().addObserver(new LifecycleObserver() {
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
public void onDestroyed() {
getLifecycle().removeObserver(this);
if (billingClient != null)
try {
Log.i("IAB end");
billingClient.endConnection();
billingClient = null;
} catch (Throwable ex) {
Log.e(ex);
}
}
});
*/
}
} catch (Throwable ex) {
Log.e(ex);
/*
Exception java.lang.RuntimeException:
at android.app.ActivityThread.performLaunchActivity (ActivityThread.java:4171)
at android.app.ActivityThread.handleLaunchActivity (ActivityThread.java:4317)
at android.app.servertransaction.LaunchActivityItem.execute (LaunchActivityItem.java:101)
at android.app.servertransaction.TransactionExecutor.executeCallbacks (TransactionExecutor.java:135)
at android.app.servertransaction.TransactionExecutor.execute (TransactionExecutor.java:95)
at android.app.ActivityThread$H.handleMessage (ActivityThread.java:2576)
at android.os.Handler.dispatchMessage (Handler.java:106)
at android.os.Looper.loopOnce (Looper.java:226)
at android.os.Looper.loop (Looper.java:313)
at android.app.ActivityThread.main (ActivityThread.java:8772)
at java.lang.reflect.Method.invoke (Method.java)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:571)
at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1067)
Caused by java.lang.IllegalStateException: Too many bind requests(999+) for service Intent { act=com.android.vending.billing.InAppBillingService.BIND pkg=com.android.vending cmp=com.android.vending/com.google.android.finsky.billing.iab.InAppBillingService (has extras) }
at android.app.ContextImpl.bindServiceCommon (ContextImpl.java:2115)
at android.app.ContextImpl.bindService (ContextImpl.java:2024)
at android.content.ContextWrapper.bindService (ContextWrapper.java:870)
at com.android.billingclient.api.BillingClientImpl.startConnection (com.android.billingclient:billing@@4.1.0:52)
at eu.faircode.email.ActivityBilling.onCreate (ActivityBilling.java:116)
at eu.faircode.email.ActivityView.onCreate (ActivityView.java:192)
at android.app.Activity.performCreate (Activity.java:8565)
at android.app.Activity.performCreate (Activity.java:8544)
at android.app.Instrumentation.callActivityOnCreate (Instrumentation.java:1384)
at android.app.ActivityThread.performLaunchActivity (ActivityThread.java:4152)
*/
}
}
@Override
@ -152,14 +199,6 @@ public class ActivityBilling extends ActivityBase implements
}
}
@Override
protected void onDestroy() {
//if (billingClient != null)
// billingClient.endConnection();
super.onDestroy();
}
@NonNull
static String getSkuPro(Context context) {
if (isTesting(context))
@ -176,7 +215,7 @@ public class ActivityBilling extends ActivityBase implements
prefs.getBoolean("test_iab", false));
}
private static String getChallenge(Context context) throws NoSuchAlgorithmException {
static String getChallenge(Context context) throws NoSuchAlgorithmException {
String android_id = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
if (android_id == null) {
Log.e("Android ID empty");
@ -334,6 +373,8 @@ public class ActivityBilling extends ActivityBase implements
@Override
public void delegate() {
try {
if (!getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED))
return;
boolean ready = billingClient.isReady();
Log.i("IAB ready=" + ready);
if (!ready)
@ -450,6 +491,8 @@ public class ActivityBilling extends ActivityBase implements
if (isPurchaseValid(purchase)) {
editor.putBoolean("pro", true);
editor.putLong(sku + ".cached", new Date().getTime());
editor.putString("iab_json", purchase.getOriginalJson());
editor.putString("iab_signature", purchase.getSignature());
}
if (!purchase.isAcknowledged())
@ -643,6 +686,10 @@ public class ActivityBilling extends ActivityBase implements
// User pressed back or canceled a dialog
return "USER_CANCELED";
case BillingClient.BillingResponseCode.NETWORK_ERROR:
// A network error occurred during the operation
return "NETWORK_ERROR";
default:
return Integer.toString(result.getResponseCode());
}

View File

@ -138,6 +138,15 @@
android:theme="@style/Theme.AppCompat.TranslucentSplash">
<!-- https://developer.samsung.com/samsung-dex/modify-optimizing.html -->
<!-- https://firebase.google.com/docs/analytics/configure-data-collection?platform=android -->
<meta-data
android:name="firebase_analytics_collection_deactivated"
android:value="true" />
<!-- https://firebase.google.com/docs/perf-mon/disable-sdk?platform=android -->
<meta-data
android:name="firebase_performance_collection_deactivated"
android:value="true" />
<!-- https://developer.android.com/guide/webapps/managing-webview#metrics -->
<meta-data
android:name="android.webkit.WebView.MetricsOptOut"
@ -628,6 +637,7 @@
<action android:name="${applicationId}.RULE" />
<action android:name="${applicationId}.TEMPLATE" />
<action android:name="${applicationId}.DISCONNECT.ME" />
<action android:name="${applicationId}.ADGUARD" />
</intent-filter>
</service>

View File

@ -0,0 +1,699 @@
package eu.faircode.email;
/*
This file is part of FairEmail.
FairEmail 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.
FairEmail 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 FairEmail. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018-2024 by Marcel Bokhorst (M66B)
*/
import android.annotation.SuppressLint;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.Base64;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.OnLifecycleEvent;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.preference.PreferenceManager;
/*
import com.android.billingclient.api.AcknowledgePurchaseParams;
import com.android.billingclient.api.AcknowledgePurchaseResponseListener;
import com.android.billingclient.api.BillingClient;
import com.android.billingclient.api.BillingClientStateListener;
import com.android.billingclient.api.BillingFlowParams;
import com.android.billingclient.api.BillingResult;
import com.android.billingclient.api.ConsumeParams;
import com.android.billingclient.api.ConsumeResponseListener;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.PurchasesResponseListener;
import com.android.billingclient.api.PurchasesUpdatedListener;
import com.android.billingclient.api.SkuDetails;
import com.android.billingclient.api.SkuDetailsParams;
import com.android.billingclient.api.SkuDetailsResponseListener;
*/
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
public class ActivityBilling extends ActivityBase implements
/* BillingClientStateListener, SkuDetailsResponseListener, PurchasesResponseListener, PurchasesUpdatedListener, */
FragmentManager.OnBackStackChangedListener {
private boolean standalone = false;
private int backoff = 4; // seconds
//private BillingClient billingClient = null;
private List<IBillingListener> listeners = new ArrayList<>();
static final String ACTION_PURCHASE = BuildConfig.APPLICATION_ID + ".ACTION_PURCHASE";
static final String ACTION_PURCHASE_CONSUME = BuildConfig.APPLICATION_ID + ".ACTION_PURCHASE_CONSUME";
static final String ACTION_PURCHASE_ERROR = BuildConfig.APPLICATION_ID + ".ACTION_PURCHASE_ERROR";
private static final String SKU_TEST = "android.test.purchased";
private static final long MAX_SKU_CACHE_DURATION = 24 * 3600 * 1000L; // milliseconds
private static final long MAX_SKU_NOACK_DURATION = 24 * 3600 * 1000L; // milliseconds
@Override
@SuppressLint("MissingSuperCall")
protected void onCreate(Bundle savedInstanceState) {
onCreate(savedInstanceState, true);
}
protected void onCreate(Bundle savedInstanceState, boolean standalone) {
super.onCreate(savedInstanceState);
this.standalone = standalone;
if (standalone) {
setContentView(R.layout.activity_billing);
int count = getSupportFragmentManager().getBackStackEntryCount();
if (count == 0) {
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, new FragmentPro()).addToBackStack("pro");
fragmentTransaction.commit();
}
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportFragmentManager().addOnBackStackChangedListener(this);
}
if (Helper.isPlayStoreInstall() || isTesting(this))
try {
Log.i("IAB start");
/*
billingClient = BillingClient.newBuilder(getApplicationContext())
.enablePendingPurchases()
.setListener(this)
.build();
billingClient.startConnection(this);
getLifecycle().addObserver(new LifecycleObserver() {
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
public void onDestroyed() {
getLifecycle().removeObserver(this);
if (billingClient != null)
try {
Log.i("IAB end");
billingClient.endConnection();
billingClient = null;
} catch (Throwable ex) {
Log.e(ex);
}
}
});
*/
} catch (Throwable ex) {
Log.e(ex);
/*
Exception java.lang.RuntimeException:
at android.app.ActivityThread.performLaunchActivity (ActivityThread.java:4171)
at android.app.ActivityThread.handleLaunchActivity (ActivityThread.java:4317)
at android.app.servertransaction.LaunchActivityItem.execute (LaunchActivityItem.java:101)
at android.app.servertransaction.TransactionExecutor.executeCallbacks (TransactionExecutor.java:135)
at android.app.servertransaction.TransactionExecutor.execute (TransactionExecutor.java:95)
at android.app.ActivityThread$H.handleMessage (ActivityThread.java:2576)
at android.os.Handler.dispatchMessage (Handler.java:106)
at android.os.Looper.loopOnce (Looper.java:226)
at android.os.Looper.loop (Looper.java:313)
at android.app.ActivityThread.main (ActivityThread.java:8772)
at java.lang.reflect.Method.invoke (Method.java)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:571)
at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1067)
Caused by java.lang.IllegalStateException: Too many bind requests(999+) for service Intent { act=com.android.vending.billing.InAppBillingService.BIND pkg=com.android.vending cmp=com.android.vending/com.google.android.finsky.billing.iab.InAppBillingService (has extras) }
at android.app.ContextImpl.bindServiceCommon (ContextImpl.java:2115)
at android.app.ContextImpl.bindService (ContextImpl.java:2024)
at android.content.ContextWrapper.bindService (ContextWrapper.java:870)
at com.android.billingclient.api.BillingClientImpl.startConnection (com.android.billingclient:billing@@4.1.0:52)
at eu.faircode.email.ActivityBilling.onCreate (ActivityBilling.java:116)
at eu.faircode.email.ActivityView.onCreate (ActivityView.java:192)
at android.app.Activity.performCreate (Activity.java:8565)
at android.app.Activity.performCreate (Activity.java:8544)
at android.app.Instrumentation.callActivityOnCreate (Instrumentation.java:1384)
at android.app.ActivityThread.performLaunchActivity (ActivityThread.java:4152)
*/
}
}
@Override
public void onBackStackChanged() {
if (getSupportFragmentManager().getBackStackEntryCount() == 0)
finish();
}
@Override
protected void onResume() {
super.onResume();
if (standalone) {
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(this);
IntentFilter iff = new IntentFilter();
iff.addAction(ACTION_PURCHASE);
iff.addAction(ACTION_PURCHASE_CONSUME);
iff.addAction(ACTION_PURCHASE_ERROR);
lbm.registerReceiver(receiver, iff);
}
//if (billingClient != null && billingClient.isReady())
// queryPurchases();
}
@Override
protected void onPause() {
super.onPause();
if (standalone) {
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(this);
lbm.unregisterReceiver(receiver);
}
}
@NonNull
static String getSkuPro(Context context) {
if (isTesting(context))
return SKU_TEST;
else
return BuildConfig.APPLICATION_ID + ".pro";
}
static boolean isTesting(Context context) {
if (context == null)
return false;
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
return (BuildConfig.DEBUG && BuildConfig.TEST_RELEASE &&
prefs.getBoolean("test_iab", false));
}
static String getChallenge(Context context) throws NoSuchAlgorithmException {
String android_id = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
if (android_id == null) {
Log.e("Android ID empty");
android_id = Long.toHexString(System.currentTimeMillis() / (24 * 3600 * 1000L));
}
return Helper.sha256(android_id);
}
private static String getResponse(Context context) throws NoSuchAlgorithmException {
return Helper.sha256(BuildConfig.APPLICATION_ID.replace(".debug", "") + getChallenge(context));
}
static boolean activatePro(Context context, Uri data) throws NoSuchAlgorithmException {
String response = data.getQueryParameter("response");
return activatePro(context, response);
}
static boolean activatePro(Context context, String response) throws NoSuchAlgorithmException {
String challenge = getChallenge(context);
Log.i("IAB challenge=" + challenge);
Log.i("IAB response=" + response);
String expected = getResponse(context);
if (expected.equals(response)) {
Log.i("IAB response valid");
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
prefs.edit()
.putBoolean("pro", true)
.putBoolean("play_store", false)
.apply();
WidgetUnified.updateData(context);
return true;
} else {
Log.i("IAB response invalid");
return false;
}
}
static boolean isPro(Context context) {
if (BuildConfig.DEBUG && false)
return true;
return PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean("pro", false);
}
private BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)) {
if (ACTION_PURCHASE.equals(intent.getAction()))
onPurchase(intent);
else if (ACTION_PURCHASE_CONSUME.equals(intent.getAction()))
;//onPurchaseConsume(intent);
else if (ACTION_PURCHASE_ERROR.equals(intent.getAction()))
;//onPurchaseError(intent);
}
}
};
private void onPurchase(Intent intent) {
if (Helper.isPlayStoreInstall() || isTesting(this)) {
String skuPro = getSkuPro(this);
Log.i("IAB purchase SKU=" + skuPro);
/*
SkuDetailsParams.Builder builder = SkuDetailsParams.newBuilder();
builder.setSkusList(Arrays.asList(skuPro));
builder.setType(BillingClient.SkuType.INAPP);
billingClient.querySkuDetailsAsync(builder.build(),
new SkuDetailsResponseListener() {
@Override
public void onSkuDetailsResponse(@NonNull BillingResult r, List<SkuDetails> skuDetailsList) {
if (r.getResponseCode() == BillingClient.BillingResponseCode.OK) {
if (skuDetailsList.size() == 0)
reportError(null, "Unknown SKU=" + skuPro);
else {
SkuDetails skuDetails = skuDetailsList.get(0);
Log.i("IAB purchase details=" + skuDetails);
BillingFlowParams.Builder flowParams = BillingFlowParams.newBuilder();
flowParams.setSkuDetails(skuDetails);
BillingResult result = billingClient.launchBillingFlow(ActivityBilling.this, flowParams.build());
if (result.getResponseCode() != BillingClient.BillingResponseCode.OK)
reportError(result, "IAB launch billing flow");
}
} else
reportError(r, "IAB query SKUs");
}
});
*/
} else
try {
Uri uri = Uri.parse(BuildConfig.PRO_FEATURES_URI +
"?challenge=" + getChallenge(this) +
"&version=" + BuildConfig.VERSION_CODE);
Helper.view(this, uri, true);
} catch (NoSuchAlgorithmException ex) {
Log.unexpectedError(getSupportFragmentManager(), ex);
}
}
/*
private void onPurchaseConsume(Intent intent) {
billingClient.queryPurchasesAsync(BillingClient.SkuType.INAPP, new PurchasesResponseListener() {
@Override
public void onQueryPurchasesResponse(@NonNull BillingResult result, @NonNull List<Purchase> list) {
if (result.getResponseCode() == BillingClient.BillingResponseCode.OK) {
for (Purchase purchase : list)
consumePurchase(purchase);
} else
reportError(result, "IAB onPurchaseConsume");
}
});
}
private void onPurchaseError(Intent intent) {
String message = intent.getStringExtra("message");
boolean play = Helper.hasPlayStore(this);
Uri uri = Helper.getSupportUri(this, "Purchase:error");
if (!TextUtils.isEmpty(message))
uri = uri
.buildUpon()
.appendQueryParameter("message", "IAB: " + message + " Play: " + play)
.build();
Helper.view(this, uri, true);
}
@Override
public void onBillingSetupFinished(BillingResult result) {
if (result.getResponseCode() == BillingClient.BillingResponseCode.OK) {
EntityLog.log(this, "IAB connected");
for (IBillingListener listener : listeners)
listener.onConnected();
backoff = 4;
queryPurchases();
} else
reportError(result, "IAB connected");
}
@Override
public void onBillingServiceDisconnected() {
EntityLog.log(this, "IAB disconnected");
for (IBillingListener listener : listeners)
listener.onDisconnected();
backoff *= 2;
retry(backoff);
}
private void retry(int backoff) {
Log.i("IAB connect retry in " + backoff + " s");
getMainHandler().postDelayed(new RunnableEx("IAB retry") {
@Override
public void delegate() {
try {
if (!getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED))
return;
boolean ready = billingClient.isReady();
Log.i("IAB ready=" + ready);
if (!ready)
billingClient.startConnection(ActivityBilling.this);
} catch (Throwable ex) {
Log.e(ex);
}
}
}, backoff * 1000L);
}
@Override
public void onPurchasesUpdated(BillingResult result, @Nullable List<Purchase> purchases) {
Log.i("IAB purchases updated");
if (result.getResponseCode() == BillingClient.BillingResponseCode.OK)
checkPurchases(purchases);
else
reportError(result, "IAB purchases updated");
}
private void queryPurchases() {
billingClient.queryPurchasesAsync(BillingClient.SkuType.INAPP, this);
}
@Override
public void onQueryPurchasesResponse(@NonNull BillingResult result, @NonNull List<Purchase> list) {
if (result.getResponseCode() == BillingClient.BillingResponseCode.OK)
checkPurchases(list);
else
reportError(result, "IAB query purchases");
}
*/
interface IBillingListener {
void onConnected();
void onDisconnected();
void onSkuDetails(String sku, String price);
void onPurchasePending(String sku);
void onPurchased(String sku, boolean purchased);
void onError(String message);
}
void addBillingListener(final IBillingListener listener, LifecycleOwner owner) {
Log.i("IAB adding billing listener=" + listener);
listeners.add(listener);
//if (billingClient != null)
// if (billingClient.isReady()) {
// listener.onConnected();
// queryPurchases();
// } else
// listener.onDisconnected();
owner.getLifecycle().addObserver(new LifecycleObserver() {
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
public void onDestroyed() {
Log.i("IAB removing billing listener=" + listener);
listeners.remove(listener);
}
});
}
/*
private void checkPurchases(List<Purchase> purchases) {
Log.i("IAB purchases=" + (purchases == null ? null : purchases.size()));
List<String> query = new ArrayList<>();
query.add(getSkuPro(this));
if (purchases != null) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
SharedPreferences.Editor editor = prefs.edit();
if (prefs.getBoolean("play_store", true)) {
long cached = prefs.getLong(getSkuPro(this) + ".cached", 0);
if (cached + MAX_SKU_CACHE_DURATION < new Date().getTime()) {
Log.i("IAB cache expired=" + new Date(cached));
editor.remove("pro");
} else
Log.i("IAB caching until=" + new Date(cached + MAX_SKU_CACHE_DURATION));
}
for (Purchase purchase : purchases)
for (String sku : purchase.getSkus())
try {
query.remove(sku);
long time = purchase.getPurchaseTime();
Log.i("IAB SKU=" + sku +
" purchased=" + isPurchased(purchase) +
" valid=" + isPurchaseValid(purchase) +
" time=" + new Date(time));
Log.i("IAB json=" + purchase.getOriginalJson());
for (IBillingListener listener : listeners)
if (isPurchaseValid(purchase))
listener.onPurchased(sku, true);
else
listener.onPurchasePending(sku);
if (isPurchased(purchase)) {
byte[] decodedKey = Base64.decode(getString(R.string.public_key), Base64.DEFAULT);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));
Signature sig = Signature.getInstance("SHA1withRSA");
sig.initVerify(publicKey);
sig.update(purchase.getOriginalJson().getBytes());
if (SKU_TEST.equals(sku) ||
sig.verify(Base64.decode(purchase.getSignature(), Base64.DEFAULT))) {
Log.i("IAB valid signature");
if (getSkuPro(this).equals(sku)) {
if (isPurchaseValid(purchase)) {
editor.putBoolean("pro", true);
editor.putLong(sku + ".cached", new Date().getTime());
editor.putString("iab_json", purchase.getOriginalJson());
editor.putString("iab_signature", purchase.getSignature());
}
if (!purchase.isAcknowledged())
acknowledgePurchase(purchase, 0);
}
} else {
Log.w("IAB invalid signature");
editor.putBoolean("pro", false);
reportError(null, "Invalid purchase");
}
}
} catch (Throwable ex) {
reportError(null, Log.formatThrowable(ex, false));
}
editor.apply();
WidgetUnified.updateData(this);
}
if (query.size() > 0)
querySkus(query);
}
private void querySkus(List<String> query) {
Log.i("IAB query SKUs");
SkuDetailsParams.Builder builder = SkuDetailsParams.newBuilder();
builder.setSkusList(query);
builder.setType(BillingClient.SkuType.INAPP);
billingClient.querySkuDetailsAsync(builder.build(), this);
}
@Override
public void onSkuDetailsResponse(@NonNull BillingResult result, List<SkuDetails> skuDetailsList) {
if (result.getResponseCode() == BillingClient.BillingResponseCode.OK) {
for (SkuDetails skuDetail : skuDetailsList) {
Log.i("IAB SKU detail=" + skuDetail);
for (IBillingListener listener : listeners)
listener.onSkuDetails(skuDetail.getSku(), skuDetail.getPrice());
}
} else
reportError(result, "IAB query SKUs");
}
private void consumePurchase(final Purchase purchase) {
for (String sku : purchase.getSkus()) {
Log.i("IAB consuming SKU=" + sku);
ConsumeParams params = ConsumeParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.build();
billingClient.consumeAsync(params, new ConsumeResponseListener() {
@Override
public void onConsumeResponse(@NonNull BillingResult result, @NonNull String purchaseToken) {
if (result.getResponseCode() == BillingClient.BillingResponseCode.OK) {
for (IBillingListener listener : listeners)
listener.onPurchased(sku, false);
} else
reportError(result, "IAB consuming SKU=" + sku);
}
});
}
}
private void acknowledgePurchase(final Purchase purchase, int retry) {
for (String sku : purchase.getSkus()) {
Log.i("IAB acknowledging purchase SKU=" + sku);
AcknowledgePurchaseParams params =
AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.build();
billingClient.acknowledgePurchase(params, new AcknowledgePurchaseResponseListener() {
@Override
public void onAcknowledgePurchaseResponse(@NonNull BillingResult result) {
if (result.getResponseCode() == BillingClient.BillingResponseCode.OK) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ActivityBilling.this);
SharedPreferences.Editor editor = prefs.edit();
editor.putBoolean("pro", true);
editor.putLong(sku + ".cached", new Date().getTime());
editor.apply();
for (IBillingListener listener : listeners)
listener.onPurchased(sku, true);
WidgetUnified.updateData(ActivityBilling.this);
} else {
if (retry < 3) {
new Handler().postDelayed(new RunnableEx("IAB ack retry") {
@Override
public void delegate() {
acknowledgePurchase(purchase, retry + 1);
}
}, (retry + 1) * 10 * 1000L);
} else
reportError(result, "IAB acknowledged SKU=" + sku);
}
}
});
}
}
private boolean isPurchased(Purchase purchase) {
return (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED);
}
private boolean isPurchaseValid(Purchase purchase) {
return (isPurchased(purchase) &&
(purchase.isAcknowledged() ||
purchase.getSkus().contains(SKU_TEST) ||
purchase.getPurchaseTime() + MAX_SKU_NOACK_DURATION > new Date().getTime()));
}
private void reportError(BillingResult result, String stage) {
String message;
if (result == null)
message = stage;
else {
message = getBillingResponseText(result);
if (result.getResponseCode() == BillingClient.BillingResponseCode.BILLING_UNAVAILABLE)
message += " Is the Play Store app logged into the account used to install the app?";
String debug = result.getDebugMessage();
if (!TextUtils.isEmpty(debug))
message += " " + debug;
message += " " + stage;
}
EntityLog.log(this, message);
if (result != null) {
// https://developer.android.com/reference/com/android/billingclient/api/BillingClient.BillingResponse#service_disconnected
if (result.getResponseCode() == BillingClient.BillingResponseCode.SERVICE_DISCONNECTED)
retry(60);
if (result.getResponseCode() == BillingClient.BillingResponseCode.USER_CANCELED)
return;
}
for (IBillingListener listener : listeners)
listener.onError(message);
}
private static String getBillingResponseText(BillingResult result) {
switch (result.getResponseCode()) {
case BillingClient.BillingResponseCode.BILLING_UNAVAILABLE:
// Billing API version is not supported for the type requested
return "BILLING_UNAVAILABLE";
case BillingClient.BillingResponseCode.DEVELOPER_ERROR:
// Invalid arguments provided to the API.
return "DEVELOPER_ERROR";
case BillingClient.BillingResponseCode.ERROR:
// Fatal error during the API action
return "ERROR";
case BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED:
// Requested feature is not supported by Play Store on the current device.
return "FEATURE_NOT_SUPPORTED";
case BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED:
// Failure to purchase since item is already owned
return "ITEM_ALREADY_OWNED";
case BillingClient.BillingResponseCode.ITEM_NOT_OWNED:
// Failure to consume since item is not owned
return "ITEM_NOT_OWNED";
case BillingClient.BillingResponseCode.ITEM_UNAVAILABLE:
// Requested product is not available for purchase
return "ITEM_UNAVAILABLE";
case BillingClient.BillingResponseCode.OK:
// Success
return "OK";
case BillingClient.BillingResponseCode.SERVICE_DISCONNECTED:
// Play Store service is not connected now - potentially transient state.
return "SERVICE_DISCONNECTED";
case BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE:
// Network connection is down
return "SERVICE_UNAVAILABLE";
case BillingClient.BillingResponseCode.SERVICE_TIMEOUT:
// The request has reached the maximum timeout before Google Play responds.
return "SERVICE_TIMEOUT";
case BillingClient.BillingResponseCode.USER_CANCELED:
// User pressed back or canceled a dialog
return "USER_CANCELED";
case BillingClient.BillingResponseCode.NETWORK_ERROR:
// A network error occurred during the operation
return "NETWORK_ERROR";
default:
return Integer.toString(result.getResponseCode());
}
}
*/
}

View File

@ -0,0 +1,74 @@
package eu.faircode.email;
/*
This file is part of FairEmail.
FairEmail 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.
FairEmail 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 FairEmail. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018-2024 by Marcel Bokhorst (M66B)
*/
import android.content.Intent;
import android.content.SharedPreferences;
import androidx.preference.PreferenceManager;
import com.google.android.gms.security.ProviderInstaller;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class ApplicationSecure extends ApplicationEx implements ProviderInstaller.ProviderInstallListener {
private static final CountDownLatch lock = new CountDownLatch(1);
private static final long WAIT_INSTALLED = 750L; // milliseconds
@Override
public void onCreate() {
super.onCreate();
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
boolean ssl_update = prefs.getBoolean("ssl_update", true);
if (ssl_update) {
Log.i("Security provider check");
ProviderInstaller.installIfNeededAsync(this, this);
} else
lock.countDown();
}
@Override
public void onProviderInstalled() {
Log.i("Security provider installed");
lock.countDown();
}
@Override
public void onProviderInstallFailed(int errorCode, Intent recoveryIntent) {
Log.i("Security provider install failed" +
" errorCode=" + errorCode +
" recoveryIntent=" + recoveryIntent);
lock.countDown();
}
public static boolean waitProviderInstalled() {
Log.i("Security provider wait");
try {
boolean succeeded = lock.await(WAIT_INSTALLED, TimeUnit.MILLISECONDS);
Log.i("Security provider wait succeeded=" + succeeded);
return succeeded;
} catch (InterruptedException ex) {
Log.i(ex);
return false;
}
}
}

View File

@ -131,6 +131,15 @@
android:theme="@style/Theme.AppCompat.TranslucentSplash">
<!-- https://developer.samsung.com/samsung-dex/modify-optimizing.html -->
<!-- https://firebase.google.com/docs/analytics/configure-data-collection?platform=android -->
<meta-data
android:name="firebase_analytics_collection_deactivated"
android:value="true" />
<!-- https://firebase.google.com/docs/perf-mon/disable-sdk?platform=android -->
<meta-data
android:name="firebase_performance_collection_deactivated"
android:value="true" />
<!-- https://developer.android.com/guide/webapps/managing-webview#metrics -->
<meta-data
android:name="android.webkit.WebView.MetricsOptOut"
@ -623,6 +632,7 @@
<action android:name="${applicationId}.RULE" />
<action android:name="${applicationId}.TEMPLATE" />
<action android:name="${applicationId}.DISCONNECT.ME" />
<action android:name="${applicationId}.ADGUARD" />
</intent-filter>
</service>

View File

@ -57,3 +57,4 @@ FairEmail uses parts or all of:
* [ZXing](https://github.com/zxing/zxing). Copyright (C) 2014 ZXing authors. [Apache License 2.0](https://github.com/zxing/zxing/blob/master/LICENSE).
* [commonmark-java](https://github.com/commonmark/commonmark-java). Copyright (c) 2015, Atlassian Pty Ltd. All rights reserved. [BSD-2-Clause license](https://github.com/commonmark/commonmark-java/blob/main/LICENSE.txt).
* [flexmark-java](https://github.com/vsch/flexmark-java). Copyright (c) 2016-2018, Vladimir Schneider. All rights reserved. [BSD-2-Clause license](https://github.com/vsch/flexmark-java/blob/master/LICENSE.txt).
* [EvalEx](https://github.com/ezylang/EvalEx). Copyright 2012-2022 Udo Klimaschewski. [Apache License 2.0](https://github.com/ezylang/EvalEx/blob/main/LICENSE).

View File

@ -4,9 +4,136 @@
For support you can use [the contact form](https://contact.faircode.eu/?product=fairemailsupport).
### [Acantholipan](https://en.wikipedia.org/wiki/Acantholipan)
### Next version
* Prepared for Android 15
* Added "AI" summarize rule action
* Listing NOT rule conditions
* Reverted AndroidX fragment to version 1.6.2
* Small improvements and minor bug fixes
* Updated [translations](https://crowdin.com/project/open-source-email)
Preview versions are available [here](https://bitbucket.org/M66B/fairemail-test/downloads/).
### 1.2182 - 2024-05-15
* Added optional "AI" summarize quick action
* Added optional "AI" summarize swipe action
* Changed default OpenAI model to [gpt-4o](https://openai.com/index/hello-gpt-4o/)
* Improved OpenAI integration (added multimodal support)
* Improved Gemini integration
* Made "AI" integrations available in the Play Store version
* Updated [AndroidX](https://developer.android.com/jetpack/androidx/versions/all-channel)
* Small improvements and minor bug fixes
* Updated [translations](https://crowdin.com/project/open-source-email)
*The use of "AI" integrations is and will remain completely optional*
### 1.2181 - 2024-05-13
* Reverted [AndroidX ROOM](https://developer.android.com/jetpack/androidx/releases/room#2.6.1) to version 2.4.3 to solve locking issues (*)
* Small improvements and minor bug fixes
* Updated [Public Suffix List](https://github.com/publicsuffix/list)
* Updated [translations](https://crowdin.com/project/open-source-email)
### 1.2180 - 2024-05-13
* Improved [Gemini](https://m66b.github.io/FairEmail/#faq204) integration
* Performance improvements
* Small improvements and minor bug fixes
* Updated [NDK](https://developer.android.com/ndk/)
* Updated [Public Suffix List](https://github.com/publicsuffix/list)
* Updated [translations](https://crowdin.com/project/open-source-email)
### 1.2179 - 2024-05-08
* Added option to change "AI" summarize prompt
* Added expression condition functions, see [the FAQ](https://m66b.github.io/FairEmail/#faq71)
* Small improvements and minor bug fixes
* Updated build tools
* Updated [AndroidX](https://developer.android.com/jetpack/androidx/versions/all-channel)
* Updated libraries (including [Bouncy Castle](https://www.bouncycastle.org/))
* Updated [Public Suffix List](https://github.com/publicsuffix/list)
* Updated [translations](https://crowdin.com/project/open-source-email)
### 1.2178 - 2024-04-29
* Added "AI" summarization of received messages (*)
* Small improvements and minor bug fixes
* Updated [translations](https://crowdin.com/project/open-source-email)
<sup>(*) Via the horizontal three-dots button just above the message text. ChatGPT or Gemini needs to be configured in the integrations-settings tab page for this.</sub>
### 1.2177 - 2024-04-27
* Added [Have I Been Pwned?](https://haveibeenpwned.com/) **<ins>password</ins>** check (*)
* Added identity option to configure envelope-from (*MAIL FROM*)
* Small improvements and minor bug fixes
* Updated [translations](https://crowdin.com/project/open-source-email)
<sub>(*) Via the three-dots overflow menu of the account list under "*Manual setup and account options*" in the main settings screen (GitHub version only)</sub>
### [Zby](https://en.wikipedia.org/wiki/Zby)
### 1.2169 - 2024-03-16 *
### 1.2176 - 2024-04-22 *
* Fixed British English translation
* Small improvements and minor bug fixes
### 1.2175 - 2024-04-20
* Fixed primary inbox navigation
* Updated [Public Suffix List](https://github.com/publicsuffix/list)
* Updated [translations](https://crowdin.com/project/open-source-email)
### 1.2174 - 2024-04-19
* Added expression conditions to rules, see [the FAQ](https://m66b.github.io/FairEmail/#faq71)
* Small improvements and minor bug fixes
* Updated [AndroidX](https://developer.android.com/jetpack/androidx/versions/all-channel)
* Updated [translations](https://crowdin.com/project/open-source-email)
### 1.2173 - 2024-04-16
* Added *primary inbox* start screen option
* Added *NOT* option to rule conditions
* Small improvements and minor bug fixes
* Updated [Public Suffix List](https://github.com/publicsuffix/list)
* Updated [translations](https://crowdin.com/project/open-source-email)
### 1.2172 - 2024-04-08
* Improved handling of messages via email forwarders (*)
* Small improvements and minor bug fixes
* Updated [AndroidX](https://developer.android.com/jetpack/androidx/versions/all-channel)
* Updated [translations](https://crowdin.com/project/open-source-email)
<sub>(*) Currently supported email forwarders:</sub>
* [addy.io](https://addy.io/)
* [DuckDuckGo Email Protection](https://duckduckgo.com/email/)
* [Firefox Relay](https://relay.firefox.com/)
* [SimpleLogin](https://simplelogin.io/)
### 1.2171 - 2024-03-30
* Added [Gemini](https://m66b.github.io/FairEmail/#faq204) integration
* Added answer button to buttons configuration
* Small improvements and minor bug fixes
* Updated [Public Suffix List](https://github.com/publicsuffix/list)
* Updated [translations](https://crowdin.com/project/open-source-email)
### 1.2170 - 2024-03-23
* Added Arabic to [DeepL translation](https://github.com/M66B/FairEmail/blob/master/FAQ.md#faq167) targets
* Small improvements and minor bug fixes
* Updated build tools
* Updated [Public Suffix List](https://github.com/publicsuffix/list)
* Updated [translations](https://crowdin.com/project/open-source-email)
### 1.2169 - 2024-03-16
* Small improvements and minor bug fixes
* Updated [translations](https://crowdin.com/project/open-source-email)
@ -175,6 +302,8 @@ For support you can use [the contact form](https://contact.faircode.eu/?product=
* Updated libraries (Apache Compress, Bugsnag, Bouncy Castle, Jsoup)
* Updated [translations](https://crowdin.com/project/open-source-email)
<!-- truncate here -->
### 1.2145 - 2023-12-30
* Added Adguard filter list to remove tracking parameters from links, see [the FAQ](https://github.com/M66B/FairEmail/blob/master/FAQ.md#faq200)
@ -1585,7 +1714,7 @@ For support you can use [the contact form](https://contact.faircode.eu/?product=
* Small improvements and minor bug fixes
* Updated translations
(*) Due to Play store policies this feature is not available in the Play store version; Android version 6 or later is required
<sub>(*) Due to Play store policies this feature is not available in the Play store version; Android version 6 or later is required</sub>
### 1.1930 - 2022-07-04

View File

@ -1,6 +1,6 @@
# Einrichtungshilfe
Es ist ziemlich einfach, FairEmail einzurichten. Sie müssen mindestens ein Konto hinzufügen, um E-Mails zu empfangen, und mindestens eine Identität, um E-Mails zu senden. Die Schnelleinrichtung wird ein Konto und eine Identität in einem Schritt für die meisten großen Anbieter hinzufügen.
Die Einrichtung von FairEmail ist ziemlich einfach. Sie müssen mindestens ein Konto hinzufügen, um E-Mails zu empfangen, und mindestens eine Identität, um E-Mails zu senden. Die Schnelleinrichtung wird ein Konto und eine Identität in einem Schritt für die meisten großen Anbieter hinzufügen.
## Anforderungen
@ -10,7 +10,7 @@ Für die Einrichtung von Konten und Identitäten ist eine Internetverbindung erf
Wählen Sie einfach den passenden Anbieter oder *»Anderer Anbieter«* aus, geben Ihren Namen, Ihre E-Mail-Adresse und Ihr Passwort ein und tippen auf *»Überprüfen«*.
Dies funktioniert für die meisten E-Mail-Anbieter.
Das funktioniert für die meisten großen E-Mail-Anbieter.
Wenn die Schnelleinrichtung nicht funktioniert, müssen Sie Konto und Identität manuell einrichten, siehe Anweisungen unten.

View File

@ -1,6 +1,6 @@
# Guida di configurazione
Configurare FairEmail è abbastanza semplice. Dovrai aggiungere almeno un profilo per ricevere le email e almeno un'identità se vuoi inviarle. La configurazione rapida aggiungerà un profilo e un'identità in una sola volta per gran parte dei principali fornitori.
Configurare FairEmail è semplice. Aggiungi almeno un profilo (account di posta) per ricevere le email e almeno un'identità se vuoi inviarle. La configurazione rapida aggiungerà un profilo e un'identità in modalità guidata (valida per i principali provider di posta).
## Requisiti
@ -8,34 +8,34 @@ Configurare FairEmail è abbastanza semplice. Dovrai aggiungere almeno un profil
## Configurazione rapida
Basta selezionare il provider appropriato o *Altri provider* e inserire il tuo nome, l'indirizzo email e la password, e toccare *Controlla*.
Basta selezionare il provider appropriato o *Altri provider* e inserire il tuo nome, l'indirizzo email e la password, e toccare *Controlla*:
Questo funzionerà per gran parte dei provider email.
non sarà necessario configurare altro.
Se la configurazione rapida non funziona, dovrai configurare manualmente un profilo e un'identità, vedi sotto per le istruzioni.
Se la configurazione rapida non funziona, dovrai impostare manualmente un profilo e un'identità: leggi le istruzioni qui sotto.
## Configura il profilo - per ricevere le email
Per aggiungere un profilo, tocca *Configurazione manuale e altre opzioni*, tocca *Profili* e il pulsante 'più' in fondo e seleziona IMAP (o POP3). Seleziona un provider dall'elenco, inserisci il nome utente, che è prevalentemente il tuo indirizzo email e inserisci la tua password. Tocca *Controlla* per far connettere FairEmail al server email e recuperare un elenco delle cartelle di sistema. Dopo aver revisionato la selezione delle cartelle di sistema, puoi aggiungere il profilo toccando *Salva*.
Per aggiungere un profilo, tocca *Configurazione manuale e altre opzioni*, tocca *Profili* e il pulsante 'più' in fondo e seleziona IMAP (o POP3). Seleziona un provider dall'elenco, inserisci il nome utente, che è quasi sempre il tuo indirizzo email, e la tua password. Tocca *Controlla* per collegare FairEmail al server e recuperare l'elenco delle cartelle di posta. Controlla le cartelle selezionate, poi conferma l'aggiunta dell'account con il tasto *Salva*.
Se il tuo provider non è nell'elenco, ce ne sono a migliaia, seleziona *Personalizzato*. Inserisci il nome del dominio, ad esempio *gmail.com* e tocca *Ottieni le impostazioni*. Se il tuo provider supporta [auto-discovery](https://tools.ietf.org/html/rfc6186), FairEmail compilerà il nome dell'host e il numero di porta, altrimenti controlla le istruzioni di configurazione del tuo provider per il giusto nome dell'host IMAP, numero di porta e protocollo di crittografia (SSL/TLS o STARTTLS). Per altro a riguardo, sei pregato di vedere [qui](https://github.com/M66B/FairEmail/blob/master/FAQ.md#authorizing-accounts).
I provider di posta sono migliaia: se il tuo non è nell'elenco, seleziona *Personalizzato*. Inserisci il nome del dominio (es. *gmail.com*) e tocca *Scarica le impostazioni*. Se il tuo provider supporta [auto-discovery](https://tools.ietf.org/html/rfc6186), FairEmail compilerà il nome dell'host e il numero di porta; altrimenti cerca le impostazioni più recenti sul sito del tuo provider. I parametri obbligatori sono: host IMAP, numero di porta e protocollo di crittografia (SSL/TLS o STARTTLS). Clicca [qui](https://github.com/M66B/FairEmail/blob/master/FAQ.md#authorizing-accounts) se ti occorrono altre informazioni.
## Configura l'identità - per inviare le email
Similmente, per aggiungere un'identità, tocca *Configurazione manuale e altre opzioni*, tocca *Identità* e il pulsante 'più' in fondo. Inserisci il nome che vuoi compaia nell'indirizzo del mittente delle email che invii e seleziona un profilo collegato. Tocca *Salva* per aggiungere l'identità.
Per aggiungere un'identità, tocca *Configurazione manuale e altre opzioni*, poi *Identità* e infine il pulsante + ('più') in fondo. Inserisci il tuo nome (quello che apparirà ai destinatari come mittente), poi scegli un account a cui collegarlo. Tocca *Salva* per aggiungere l'identità.
Se il profilo è stato configurato manualmente, potresti dover configurare manualmente anche l'identità. Inserisci il nome del dominio, ad esempio *gmail.com* e tocca *Ottieni le impostazioni*. Se il tuo provider supporta [auto-discovery](https://tools.ietf.org/html/rfc6186), FairEmail compilerà il nome dell'host e il numero di porta, altrimenti controlla le istruzioni di configurazione del tuo provider per il giusto nome dell'host SMTP, numero di porta e protocollo di crittografia (SSL/TLS o STARTTLS).
Se il profilo è stato configurato manualmente, potresti dover configurare manualmente anche l'identità. Inserisci il nome del dominio, ad esempio *gmail.com* e tocca *Scarica le impostazioni*. Se il tuo provider supporta [auto-discovery](https://tools.ietf.org/html/rfc6186), FairEmail compilerà il nome dell'host e il numero di porta; altrimenti cerca le impostazioni più recenti sul sito del tuo provider. I parametri obbligatori sono: host IMAP, numero di porta e protocollo di crittografia (SSL/TLS o STARTTLS).
Vedi [questa FAQ](https://github.com/M66B/FairEmail/blob/master/FAQ.md#FAQ9) sull'uso degli alias.
Per l'utilizzo degli alias, vedi [questa FAQ](https://github.com/M66B/FairEmail/blob/master/FAQ.md#FAQ9).
## Concedi le autorizzazioni - per accedere alle informazioni di contatto
## Configura l'accesso ai contatti (Rubrica di sistema)
Se vuoi cercare gli indirizzi email, visualizzare le foto di contatto, etc., dovrai concedere le autorizzazioni per leggere le informazioni di contatto a FairEmail. Basta toccare su *Autorizza* e selezionare *Consenti*.
Se vuoi cercare gli indirizzi email dei tuoi contatti Google, visualizzare le foto e rendere più semplice l'interazione con la rubrica di sistema, dovrai autorizzare FairEmail a leggere le informazioni di contatto. Basta toccare *Autorizza* e di seguito *Consenti*.
## Configura le ottimizzazioni della batteria - per ricevere costantemente le email
## Consentire a FairEmail di ricevere le email in tempo reale
Sulle versioni recenti di Android, Android metterà in standby le app quando lo schermo è spento per un po' di tempo per ridurre l'uso della batteria. Se vuoi ricevere le nuove email senza ritardo, dovresti disabilitare le ottimizzazioni della batteria per FairEmail. Tocca *Gestisci* e segui le istruzioni.
Per limitare al massimo l'uso della batteria, Android chiude automaticamente le app in background dopo un certo numero di minuti. Le uniche app che possono funzionare in background sono quelle di sistema e quelle a cui l'utente concede le autorizzazioni necessarie. Tocca *Gestisci* e segui le istruzioni.
## Domande o problemi
## Altro
Se hai una domanda o un problema, sei pregato di [vedere qui](https://github.com/M66B/FairEmail/blob/master/FAQ.md) per aiuto.
Se hai una domanda o un problema, sei pregato di [controllare qui](https://github.com/M66B/FairEmail/blob/master/FAQ.md) per trovare una risposta o chiedere assistenza.

View File

@ -1,4 +1,9 @@
[
{
"language": "AR",
"name": "Arabic",
"supports_formality": false
},
{
"language": "BG",
"name": "Bulgarian",

View File

@ -6710,7 +6710,7 @@ org.zw
// newGTLDs
// List of new gTLDs imported from https://www.icann.org/resources/registries/gtlds/v2/gtlds.json on 2024-03-06T15:14:58Z
// List of new gTLDs imported from https://www.icann.org/resources/registries/gtlds/v2/gtlds.json on 2024-05-04T15:12:50Z
// This list is auto-generated, don't edit it manually.
// aaa : American Automobile Association, Inc.
// https://www.iana.org/domains/root/db/aaa.html
@ -6896,7 +6896,7 @@ anquan
// https://www.iana.org/domains/root/db/anz.html
anz
// aol : Oath Inc.
// aol : Yahoo Inc.
// https://www.iana.org/domains/root/db/aol.html
aol
@ -6988,10 +6988,6 @@ auto
// https://www.iana.org/domains/root/db/autos.html
autos
// avianca : Avianca Inc.
// https://www.iana.org/domains/root/db/avianca.html
avianca
// aws : AWS Registry LLC
// https://www.iana.org/domains/root/db/aws.html
aws
@ -11124,7 +11120,7 @@ xyz
// https://www.iana.org/domains/root/db/yachts.html
yachts
// yahoo : Oath Inc.
// yahoo : Yahoo Inc.
// https://www.iana.org/domains/root/db/yahoo.html
yahoo
@ -11204,6 +11200,10 @@ ltd.ua
// 611coin : https://611project.org/
611.to
// AAA workspace : https://aaa.vodka
// Submitted by Kirill Rezraf <admin@aaa.vodka>
aaa.vodka
// A2 Hosting
// Submitted by Tyler Hall <sysadmin@a2hosting.com>
a2hosted.com
@ -11350,23 +11350,28 @@ cloudfront.net
// Amazon Cognito
// Submitted by AWS Security <psl-maintainers@amazon.com>
// Reference: 7bee1013-f456-47df-bfe8-03c78d946d61
// Reference: 09588633-91fe-49d8-b4e7-ec36496d11f3
auth.af-south-1.amazoncognito.com
auth.ap-northeast-1.amazoncognito.com
auth.ap-northeast-2.amazoncognito.com
auth.ap-northeast-3.amazoncognito.com
auth.ap-south-1.amazoncognito.com
auth.ap-south-2.amazoncognito.com
auth.ap-southeast-1.amazoncognito.com
auth.ap-southeast-2.amazoncognito.com
auth.ap-southeast-3.amazoncognito.com
auth.ap-southeast-4.amazoncognito.com
auth.ca-central-1.amazoncognito.com
auth.eu-central-1.amazoncognito.com
auth.eu-central-2.amazoncognito.com
auth.eu-north-1.amazoncognito.com
auth.eu-south-1.amazoncognito.com
auth.eu-south-2.amazoncognito.com
auth.eu-west-1.amazoncognito.com
auth.eu-west-2.amazoncognito.com
auth.eu-west-3.amazoncognito.com
auth.il-central-1.amazoncognito.com
auth.me-central-1.amazoncognito.com
auth.me-south-1.amazoncognito.com
auth.sa-east-1.amazoncognito.com
auth.us-east-1.amazoncognito.com
@ -11389,7 +11394,7 @@ us-east-1.amazonaws.com
// Amazon EMR
// Submitted by AWS Security <psl-maintainers@amazon.com>
// Reference: 597f3f8e-9283-4e48-8e32-7ee25a1ff6ab
// Reference: 82f43f9f-bbb8-400e-8349-854f5a62f20d
emrappui-prod.cn-north-1.amazonaws.com.cn
emrnotebooks-prod.cn-north-1.amazonaws.com.cn
emrstudio-prod.cn-north-1.amazonaws.com.cn
@ -11414,6 +11419,9 @@ emrstudio-prod.ap-northeast-3.amazonaws.com
emrappui-prod.ap-south-1.amazonaws.com
emrnotebooks-prod.ap-south-1.amazonaws.com
emrstudio-prod.ap-south-1.amazonaws.com
emrappui-prod.ap-south-2.amazonaws.com
emrnotebooks-prod.ap-south-2.amazonaws.com
emrstudio-prod.ap-south-2.amazonaws.com
emrappui-prod.ap-southeast-1.amazonaws.com
emrnotebooks-prod.ap-southeast-1.amazonaws.com
emrstudio-prod.ap-southeast-1.amazonaws.com
@ -11423,18 +11431,30 @@ emrstudio-prod.ap-southeast-2.amazonaws.com
emrappui-prod.ap-southeast-3.amazonaws.com
emrnotebooks-prod.ap-southeast-3.amazonaws.com
emrstudio-prod.ap-southeast-3.amazonaws.com
emrappui-prod.ap-southeast-4.amazonaws.com
emrnotebooks-prod.ap-southeast-4.amazonaws.com
emrstudio-prod.ap-southeast-4.amazonaws.com
emrappui-prod.ca-central-1.amazonaws.com
emrnotebooks-prod.ca-central-1.amazonaws.com
emrstudio-prod.ca-central-1.amazonaws.com
emrappui-prod.ca-west-1.amazonaws.com
emrnotebooks-prod.ca-west-1.amazonaws.com
emrstudio-prod.ca-west-1.amazonaws.com
emrappui-prod.eu-central-1.amazonaws.com
emrnotebooks-prod.eu-central-1.amazonaws.com
emrstudio-prod.eu-central-1.amazonaws.com
emrappui-prod.eu-central-2.amazonaws.com
emrnotebooks-prod.eu-central-2.amazonaws.com
emrstudio-prod.eu-central-2.amazonaws.com
emrappui-prod.eu-north-1.amazonaws.com
emrnotebooks-prod.eu-north-1.amazonaws.com
emrstudio-prod.eu-north-1.amazonaws.com
emrappui-prod.eu-south-1.amazonaws.com
emrnotebooks-prod.eu-south-1.amazonaws.com
emrstudio-prod.eu-south-1.amazonaws.com
emrappui-prod.eu-south-2.amazonaws.com
emrnotebooks-prod.eu-south-2.amazonaws.com
emrstudio-prod.eu-south-2.amazonaws.com
emrappui-prod.eu-west-1.amazonaws.com
emrnotebooks-prod.eu-west-1.amazonaws.com
emrstudio-prod.eu-west-1.amazonaws.com
@ -11444,6 +11464,9 @@ emrstudio-prod.eu-west-2.amazonaws.com
emrappui-prod.eu-west-3.amazonaws.com
emrnotebooks-prod.eu-west-3.amazonaws.com
emrstudio-prod.eu-west-3.amazonaws.com
emrappui-prod.il-central-1.amazonaws.com
emrnotebooks-prod.il-central-1.amazonaws.com
emrstudio-prod.il-central-1.amazonaws.com
emrappui-prod.me-central-1.amazonaws.com
emrnotebooks-prod.me-central-1.amazonaws.com
emrstudio-prod.me-central-1.amazonaws.com
@ -11474,9 +11497,11 @@ emrstudio-prod.us-west-2.amazonaws.com
// Amazon Managed Workflows for Apache Airflow
// Submitted by AWS Security <psl-maintainers@amazon.com>
// Reference: 4ab55e6f-90c0-4a8d-b6a0-52ca5dbb1c2e
// Reference: 87f24ece-a77e-40e8-bb4a-f6b74fe9f975
*.cn-north-1.airflow.amazonaws.com.cn
*.cn-northwest-1.airflow.amazonaws.com.cn
*.af-south-1.airflow.amazonaws.com
*.ap-east-1.airflow.amazonaws.com
*.ap-northeast-1.airflow.amazonaws.com
*.ap-northeast-2.airflow.amazonaws.com
*.ap-south-1.airflow.amazonaws.com
@ -11485,12 +11510,15 @@ emrstudio-prod.us-west-2.amazonaws.com
*.ca-central-1.airflow.amazonaws.com
*.eu-central-1.airflow.amazonaws.com
*.eu-north-1.airflow.amazonaws.com
*.eu-south-1.airflow.amazonaws.com
*.eu-west-1.airflow.amazonaws.com
*.eu-west-2.airflow.amazonaws.com
*.eu-west-3.airflow.amazonaws.com
*.me-south-1.airflow.amazonaws.com
*.sa-east-1.airflow.amazonaws.com
*.us-east-1.airflow.amazonaws.com
*.us-east-2.airflow.amazonaws.com
*.us-west-1.airflow.amazonaws.com
*.us-west-2.airflow.amazonaws.com
// Amazon S3
@ -11784,9 +11812,25 @@ s3-fips.us-west-2.amazonaws.com
s3-object-lambda.us-west-2.amazonaws.com
s3-website.us-west-2.amazonaws.com
// Amazon SageMaker Ground Truth
// Submitted by AWS Security <psl-maintainers@amazon.com>
// Reference: 98dbfde4-7802-48c3-8751-b60f204e0d9c
labeling.ap-northeast-1.sagemaker.aws
labeling.ap-northeast-2.sagemaker.aws
labeling.ap-south-1.sagemaker.aws
labeling.ap-southeast-1.sagemaker.aws
labeling.ap-southeast-2.sagemaker.aws
labeling.ca-central-1.sagemaker.aws
labeling.eu-central-1.sagemaker.aws
labeling.eu-west-1.sagemaker.aws
labeling.eu-west-2.sagemaker.aws
labeling.us-east-1.sagemaker.aws
labeling.us-east-2.sagemaker.aws
labeling.us-west-2.sagemaker.aws
// Amazon SageMaker Notebook Instances
// Submitted by AWS Security <psl-maintainers@amazon.com>
// Reference: ce8ae0b1-0070-496d-be88-37c31837af9d
// Reference: b5ea56df-669e-43cc-9537-14aa172f5dfc
notebook.af-south-1.sagemaker.aws
notebook.ap-east-1.sagemaker.aws
notebook.ap-northeast-1.sagemaker.aws
@ -11823,6 +11867,7 @@ notebook-fips.us-gov-east-1.sagemaker.aws
notebook.us-gov-west-1.sagemaker.aws
notebook-fips.us-gov-west-1.sagemaker.aws
notebook.us-west-1.sagemaker.aws
notebook-fips.us-west-1.sagemaker.aws
notebook.us-west-2.sagemaker.aws
notebook-fips.us-west-2.sagemaker.aws
notebook.cn-north-1.sagemaker.com.cn
@ -11830,7 +11875,7 @@ notebook.cn-northwest-1.sagemaker.com.cn
// Amazon SageMaker Studio
// Submitted by AWS Security <psl-maintainers@amazon.com>
// Reference: 057ee397-6bf8-4f20-b807-d7bc145ac980
// Reference: 69c723d9-6e1a-4bff-a203-48eecd203183
studio.af-south-1.sagemaker.aws
studio.ap-east-1.sagemaker.aws
studio.ap-northeast-1.sagemaker.aws
@ -11844,6 +11889,7 @@ studio.ca-central-1.sagemaker.aws
studio.eu-central-1.sagemaker.aws
studio.eu-north-1.sagemaker.aws
studio.eu-south-1.sagemaker.aws
studio.eu-south-2.sagemaker.aws
studio.eu-west-1.sagemaker.aws
studio.eu-west-2.sagemaker.aws
studio.eu-west-3.sagemaker.aws
@ -11955,6 +12001,11 @@ webview-assets.aws-cloud9.us-west-2.amazonaws.com
vfs.cloud9.us-west-2.amazonaws.com
webview-assets.cloud9.us-west-2.amazonaws.com
// AWS Directory Service
// Submitted by AWS Security <psl-maintainers@amazon.com>
// Reference: a13203e8-42dc-4045-a0d2-2ee67bed1068
awsapps.com
// AWS Elastic Beanstalk
// Submitted by AWS Security <psl-maintainers@amazon.com>
// Reference: bb5a965c-dec3-4967-aa22-e306ad064797
@ -12080,6 +12131,7 @@ autocode.dev
// AVM : https://avm.de
// Submitted by Andreas Weise <a.weise@avm.de>
myfritz.link
myfritz.net
// AVStack Pte. Ltd. : https://avstack.io
@ -12147,6 +12199,10 @@ pages.gay
// Submitted by Adrian <adrian@betainabox.com>
betainabox.com
// University of Bielsko-Biala regional domain: http://dns.bielsko.pl/
// Submitted by Marcin <dns@ath.bielsko.pl>
bielsko.pl
// BinaryLane : http://www.binarylane.com
// Submitted by Nathan O'Sullivan <nathan@mammoth.com.au>
bnr.la
@ -12193,7 +12249,8 @@ square7.net
*.s.brave.io
// Brendly : https://brendly.rs
// Submitted by Dusan Radovanovic <dusan.radovanovic@brendly.rs>
// Submitted by Dusan Radovanovic <administracija@brendly.rs>
shop.brendly.hr
shop.brendly.rs
// BrowserSafetyMark
@ -12310,7 +12367,10 @@ discourse.team
// Clever Cloud : https://www.clever-cloud.com/
// Submitted by Quentin Adam <noc@clever-cloud.com>
cleverapps.cc
*.services.clever-cloud.com
cleverapps.io
cleverapps.tech
// Clerk : https://www.clerk.dev
// Submitted by Colin Sidoti <systems@clerk.dev>
@ -12357,6 +12417,12 @@ pages.dev
r2.dev
workers.dev
// cloudscale.ch AG : https://www.cloudscale.ch/
// Submitted by Gaudenz Steinlin <support@cloudscale.ch>
cust.cloudscale.ch
objects.lpg.cloudscale.ch
objects.rma.cloudscale.ch
// Clovyr : https://clovyr.io
// Submitted by Patrick Nielsen <patrick@clovyr.io>
wnext.app
@ -12374,22 +12440,33 @@ co.cz
// CDN77.com : http://www.cdn77.com
// Submitted by Jan Krpes <jan.krpes@cdn77.com>
c.cdn77.org
cdn77-storage.com
rsc.contentproxy9.cz
cdn77-ssl.net
r.cdn77.net
rsc.cdn77.org
ssl.origin.cdn77-secure.org
c.cdn77.org
rsc.cdn77.org
// Cloud DNS Ltd : http://www.cloudns.net
// Submitted by Aleksander Hristov <noc@cloudns.net>
// Submitted by Aleksander Hristov <noc@cloudns.net> & Boyan Peychev <boyan@cloudns.net>
cloudns.asia
cloudns.be
cloudns.biz
cloudns.club
cloudns.cc
cloudns.ch
cloudns.cl
cloudns.club
dnsabr.com
cloudns.cx
cloudns.eu
cloudns.in
cloudns.info
dns-cloud.net
dns-dynamic.net
cloudns.nz
cloudns.org
cloudns.ph
cloudns.pro
cloudns.pw
cloudns.us
@ -12402,6 +12479,11 @@ cnpy.gdn
// Submitted by Moritz Marquardt <git@momar.de>
codeberg.page
// CodeSandbox B.V. : https://codesandbox.io
// Submitted by Ives van Hoorne <abuse@codesandbox.io>
csb.app
preview.csb.app
// CoDNS B.V.
co.nl
co.no
@ -12520,6 +12602,7 @@ dyndns.dappnode.io
// Dark, Inc. : https://darklang.com
// Submitted by Paul Biggar <ops@darklang.com>
builtwithdark.com
darklang.io
// DataDetect, LLC. : https://datadetect.com
// Submitted by Andrew Banchich <abanchich@sceven.com>
@ -12918,6 +13001,10 @@ ondigitalocean.app
// Submitted by Robin H. Johnson <psl-maintainers@digitalocean.com>
*.digitaloceanspaces.com
// DigitalPlat : https://www.digitalplat.org/
// Submitted by Edward Hsing <contact@digitalplat.org>
us.kg
// dnstrace.pro : https://dnstrace.pro/
// Submitted by Chris Partridge <chris@partridge.tech>
bci.dnstrace.pro
@ -12959,6 +13046,14 @@ easypanel.host
// Submitted by <infracloudteam@namecheap.com>
*.ewp.live
// Electromagnetic Field : https://www.emfcamp.org
// Submitted by <noc@emfcamp.org>
at.emf.camp
// Elefunc, Inc. : https://elefunc.com
// Submitted by Cetin Sert <domains@elefunc.com>
rt.ht
// Elementor : Elementor Ltd.
// Submitted by Anton Barkan <antonb@elementor.com>
elementor.cloud
@ -13061,6 +13156,11 @@ us-2.evennode.com
us-3.evennode.com
us-4.evennode.com
// Expo : https://expo.dev/
// Submitted by James Ide <psl@expo.dev>
expo.app
staging.expo.app
// eDirect Corp. : https://hosting.url.com.tw/
// Submitted by C.S. chang <cschang@corp.url.com.tw>
twmail.cc
@ -13250,7 +13350,8 @@ forgeblocks.com
id.forgerock.io
// Framer : https://www.framer.com
// Submitted by Koen Rouwhorst <koenrh@framer.com>
// Submitted by Koen Rouwhorst <security@framer.com>
framer.ai
framer.app
framercanvas.com
framer.media
@ -13291,6 +13392,24 @@ freemyip.com
// Submitted by Daniel A. Maierhofer <vorstand@funkfeuer.at>
wien.funkfeuer.at
// Future Versatile Group. https://www.fvg-on.net/
// T.Kabu <webmaster@fvg-on.net>
daemon.asia
dix.asia
mydns.bz
0am.jp
0g0.jp
0j0.jp
0t0.jp
mydns.jp
pgw.jp
wjg.jp
keyword-on.net
live-on.net
server-on.net
mydns.tw
mydns.vc
// Futureweb GmbH : https://www.futureweb.at
// Submitted by Andreas Schnederle-Wagner <schnederle@futureweb.at>
*.futurecms.at
@ -13334,9 +13453,11 @@ gentlentapis.com
lab.ms
cdn-edges.net
// Ghost Foundation : https://ghost.org
// Submitted by Matt Hanley <security@ghost.org>
ghost.io
// Getlocalcert: https://www.getlocalcert.net
// Submitted by Robert Alexander <support@getlocalcert.net>
localcert.net
localhostcert.net
corpnet.work
// GignoSystemJapan: http://gsj.bz
// Submitted by GignoSystemJapan <kakutou-ec@gsj.bz>
@ -13480,6 +13601,10 @@ whitesnow.jp
zombie.jp
heteml.net
// GoDaddy Registry : https://registry.godaddy
// Submitted by Rohan Durrant <tldns@registry.godaddy>
graphic.design
// GOV.UK Platform as a Service : https://www.cloud.service.gov.uk/
// Submitted by Tom Whitwell <gov-uk-paas-support@digital.cabinet-office.gov.uk>
cloudapps.digital
@ -13498,7 +13623,8 @@ ro.im
goip.de
// Google, Inc.
// Submitted by Eduardo Vela <evn@google.com>
// Submitted by Shannon McCabe <public-suffix-editors@google.com>
*.hosted.app
*.run.app
web.app
*.0emm.com
@ -13599,6 +13725,10 @@ goupile.fr
// Submitted by <domeinnaam@minaz.nl>
gov.nl
// GrayJay Web Solutions Inc. : https://grayjaysports.ca
// Submitted by Matt Yamkowy <info@grayjaysports.ca>
grayjayleagues.com
// Group 53, LLC : https://www.group53.com
// Submitted by Tyler Todd <noc@nova53.net>
awsmppl.com
@ -13633,6 +13763,11 @@ hasura-app.io
// Submitted by Richard Zowalla <mi-admin@hs-heilbronn.de>
pages.it.hs-heilbronn.de
// Helio Networks : https://heliohost.org
// Submitted by Ben Frede <admin@heliohost.org>
helioho.st
heliohost.us
// Hepforge : https://www.hepforge.org
// Submitted by David Grellscheid <admin@hepforge.org>
hepforge.org
@ -13646,7 +13781,6 @@ herokussl.com
// Submitted by Oren Eini <oren@ravendb.net>
ravendb.cloud
ravendb.community
ravendb.me
development.run
ravendb.run
@ -13737,7 +13871,7 @@ biz.at
info.at
// info.cx : http://info.cx
// Submitted by Jacob Slater <whois@igloo.to>
// Submitted by June Slater <whois@igloo.to>
info.cx
// Interlegis : http://www.interlegis.leg.br
@ -13786,6 +13920,14 @@ iopsys.se
// Submitted by Matthew Hardeman <mhardeman@ipifony.com>
ipifony.net
// is-a.dev : https://www.is-a.dev
// Submitted by William Harrison <admin@maintainers.is-a.dev>
is-a.dev
// ir.md : https://nic.ir.md
// Submitted by Ali Soizi <info@nic.ir.md>
ir.md
// IServ GmbH : https://iserv.de
// Submitted by Mario Hoberg <info@iserv.de>
iservschule.de
@ -13894,6 +14036,11 @@ myjino.ru
// Submitted by Daniel Fariña <ingenieria@jotelulu.com>
jotelulu.cloud
// JouwWeb B.V. : https://www.jouwweb.nl
// Submitted by Camilo Sperberg <tech@webador.com>
jouwweb.site
webadorsite.com
// Joyent : https://www.joyent.com/
// Submitted by Brian Bennett <brian.bennett@joyent.com>
*.triton.zone
@ -13967,6 +14114,10 @@ lpusercontent.com
// Submitted by Lelux Admin <publisuffix@lelux.site>
lelux.site
// Libre IT Ltd : https://libre.nz
// Submitted by Tomas Maggio <support@libre.nz>
runcontainers.dev
// Lifetime Hosting : https://Lifetime.Hosting/
// Submitted by Mike Fillator <support@lifetime.hosting>
co.business
@ -13977,10 +14128,6 @@ co.network
co.place
co.technology
// Lightmaker Property Manager, Inc. : https://app.lmpm.com/
// Submitted by Greg Holland <greg.holland@lmpm.com>
app.lmpm.com
// linkyard ldt: https://www.linkyard.ch/
// Submitted by Mario Siegenthaler <mario.siegenthaler@linkyard.ch>
linkyard.cloud
@ -14141,7 +14288,6 @@ co.pl
// Managed by Corporate Domains
// Microsoft Azure : https://home.azure
*.azurecontainer.io
*.cloudapp.azure.com
azure-api.net
azureedge.net
azurefd.net
@ -14227,6 +14373,10 @@ netlify.app
// Submitted by Trung Tran <Trung.Tran@neustar.biz>
4u.com
// NGO.US Registry : https://nic.ngo.us
// Submitted by Alstra Solutions Ltd. Networking Team <admin@alstra.org>
ngo.us
// ngrok : https://ngrok.com/
// Submitted by Alan Shreve <alan@ngrok.com>
ngrok.app
@ -14248,13 +14398,18 @@ ngrok.pro
torun.pl
// Nimbus Hosting Ltd. : https://www.nimbushosting.co.uk/
// Submitted by Nicholas Ford <nick@nimbushosting.co.uk>
// Submitted by Nicholas Ford <dev@nimbushosting.co.uk>
nh-serv.co.uk
nimsite.uk
// NFSN, Inc. : https://www.NearlyFreeSpeech.NET/
// Submitted by Jeff Wheelhouse <support@nearlyfreespeech.net>
nfshost.com
// NFT.Storage : https://nft.storage/
// Submitted by Vasco Santos <vasco.santos@protocol.ai> or <support@nft.storage>
ipfs.nftstorage.link
// Noop : https://noop.app
// Submitted by Nathaniel Schweinberg <noop@rearc.io>
*.developer.app
@ -14272,6 +14427,10 @@ noop.app
// Submitted by Laurent Pellegrino <security@noticeable.io>
noticeable.news
// Notion Labs, Inc : https://www.notion.so/
// Submitted by Jess Yao <trust-core-team@makenotion.com>
notion.site
// Now-DNS : https://now-dns.com
// Submitted by Steve Russell <steve@now-dns.com>
dnsking.ch
@ -14405,8 +14564,13 @@ pcloud.host
// Submitted by Matthew Brown <mattbrown@nyc.mn>
nyc.mn
// O3O.Foundation : https://o3o.foundation/
// Submitted by the prvcy.page Registry Team <psl@registry.prvcy.page>
prvcy.page
// Observable, Inc. : https://observablehq.com
// Submitted by Mike Bostock <dns@observablehq.com>
observablehq.cloud
static.observableusercontent.com
// Octopodal Solutions, LLC. : https://ulterius.io/
@ -14434,7 +14598,6 @@ omniwe.site
123minsida.se
123miweb.es
123paginaweb.pt
123sait.ru
123siteweb.fr
123webseite.at
123webseite.de
@ -14452,6 +14615,13 @@ simplesite.pl
// Submitted by Eddie Jones <eddie@onefoldmedia.com>
nid.io
// Open Domains : https://open-domains.net
// Submitted by William Harrison <admin@open-domains.net>
is-cool.dev
is-not-a.dev
localplayer.dev
is-local.org
// Open Social : https://www.getopensocial.com/
// Submitted by Alexander Varwijk <security@getopensocial.com>
opensocial.site
@ -14472,6 +14642,11 @@ operaunite.com
// Submitted by Alexandre Linte <alexandre.linte@orange.com>
tech.orange
// OsSav Technology Ltd. : https://ossav.com/
// TLD Nic: http://nic.can.re - TLD Whois Server: whois.can.re
// Submitted by OsSav Technology Ltd. <support@ossav.com>
can.re
// Oursky Limited : https://authgear.com/, https://skygear.io/
// Submitted by Authgear Team <hello@authgear.com>, Skygear Developer <hello@skygear.io>
authgear-staging.com
@ -14522,10 +14697,11 @@ pagexl.com
// pcarrier.ca Software Inc: https://pcarrier.ca/
// Submitted by Pierre Carrier <pc@rrier.ca>
bar0.net
bar1.net
bar2.net
rdv.to
*.xmit.co
xmit.dev
srv.us
gh.srv.us
gl.srv.us
// .pl domains (grandfathered)
art.pl
@ -14613,10 +14789,6 @@ xen.prgmr.com
// Submitted by registry <lendl@nic.at>
priv.at
// privacytools.io : https://www.privacytools.io/
// Submitted by Jonah Aragon <jonah@privacytools.io>
prvcy.page
// Protocol Labs : https://protocol.ai/
// Submitted by Michael Burns <noc@protocol.ai>
*.dweb.link
@ -14683,9 +14855,12 @@ qcx.io
*.sys.qcx.io
// QNAP System Inc : https://www.qnap.com
// Submitted by Nick Chang <nickchang@qnap.com>
dev-myqnapcloud.com
// Submitted by Nick Chang <cloudadmin@qnap.com>
myqnapcloud.cn
alpha-myqnapcloud.com
dev-myqnapcloud.com
mycloudnas.com
mynascloud.com
myqnapcloud.com
// Quip : https://quip.com
@ -14915,6 +15090,10 @@ service.gov.scot
// Submitted by Shante Adam <shante@skyhat.io>
scrysec.com
// Scrypted : https://scrypted.app
// Submitted by Koushik Dutta <public-suffix-list@scrypted.app>
client.scrypted.io
// Securepoint GmbH : https://www.securepoint.de
// Submitted by Erik Anders <erik.anders@securepoint.de>
firewall-gateway.com
@ -14954,6 +15133,10 @@ biz.ua
co.ua
pp.ua
// Sheezy.Art : https://sheezy.art
// Submitted by Nyoom <admin@sheezy.art>
sheezy.games
// Shift Crypto AG : https://shiftcrypto.ch
// Submitted by alex <alex@shiftcrypto.ch>
shiftcrypto.dev
@ -15024,9 +15207,9 @@ small-web.org
vp4.me
// Snowflake Inc : https://www.snowflake.com/
// Submitted by Faith Olapade <faith.olapade@snowflake.com>
snowflake.app
privatelink.snowflake.app
// Submitted by Sam Haar <psl@snowflake.com>
*.snowflake.app
*.privatelink.snowflake.app
streamlit.app
streamlitapp.com
@ -15038,6 +15221,12 @@ try-snowplow.com
// Submitted by Drew DeVault <sir@cmpwn.com>
srht.site
// StackBlitz : https://stackblitz.com
// Submitted by Dominic Elm <hello@stackblitz.com>
w-corp-staticblitz.com
w-credentialless-staticblitz.com
w-staticblitz.com
// Stackhero : https://www.stackhero.io
// Submitted by Adrien Gillon <adrien+public-suffix-list@stackhero.io>
stackhero-network.com
@ -15339,6 +15528,10 @@ inc.hk
// Submitted by ITComdomains <to@it.com>
it.com
// Unison Computing, PBC : https://unison.cloud
// Submitted by Simon Højberg <security@unison.cloud>
unison-services.cloud
// UNIVERSAL DOMAIN REGISTRY : https://www.udr.org.yt/
// see also: whois -h whois.udr.org.yt help
// Submitted by Atanunu Igbunuroghene <publicsuffixlist@udr.org.yt>
@ -15366,6 +15559,11 @@ dnsupdate.info
// Submitted by Ed Moore <Ed.Moore@lib.de.us>
lib.de.us
// Val Town, Inc : https://val.town/
// Submitted by Tom MacWright <security@val.town>
express.val.run
web.val.run
// VeryPositive SIA : http://very.lv
// Submitted by Danko Aleksejevs <danko@very.lv>
2038.io
@ -15388,47 +15586,6 @@ v-info.info
// Submitted by Nathan van Bakel <info@voorloper.com>
voorloper.cloud
// Voxel.sh DNS : https://voxel.sh/dns/
// Submitted by Mia Rehlinger <dns@voxel.sh>
neko.am
nyaa.am
be.ax
cat.ax
es.ax
eu.ax
gg.ax
mc.ax
us.ax
xy.ax
nl.ci
xx.gl
app.gp
blog.gt
de.gt
to.gt
be.gy
cc.hn
io.kg
jp.kg
tv.kg
uk.kg
us.kg
de.ls
at.md
de.md
jp.md
to.md
indie.porn
vxl.sh
ch.tc
me.tc
we.tc
nyan.to
at.vg
blog.vu
dev.vu
me.vu
// V.UA Domain Administrator : https://domain.v.ua/
// Submitted by Serhii Rostilo <sergey@rostilo.kiev.ua>
v.ua
@ -15457,6 +15614,10 @@ reserve-online.com
bookonline.app
hotelwithflight.com
// WebWaddle Ltd: https://webwaddle.com/
// Submitted by Merlin Glander <hostmaster@webwaddle.com>
*.wadl.top
// WeDeploy by Liferay, Inc. : https://www.wedeploy.com
// Submitted by Henrique Vicente <security@wedeploy.com>
wedeploy.io
@ -15467,6 +15628,10 @@ wedeploy.sh
// Submitted by Jung Jin <jungseok.jin@wdc.com>
remotewd.com
// Whatbox Inc. : https://whatbox.ca/
// Submitted by Anthony Ryan <servers@whatbox.ca>
box.ca
// WIARD Enterprises : https://wiardweb.com
// Submitted by Kidd Hustle <kiddhustle@wiardweb.com>
pages.wiardweb.com
@ -15569,6 +15734,10 @@ za.org
// Submitted by Julian Alker <security@zap-hosting.com>
zap.cloud
// Zeabur : https://zeabur.com/
// Submitted by Zeabur Team <contact@zeabur.com>
zeabur.app
// Zine EOOD : https://zine.bg/
// Submitted by Martin Angelov <martin@zine.bg>
bss.design

View File

@ -118,7 +118,7 @@ class RoomTrackingLiveData<T> extends LiveData<T> {
if (isActive)
synchronized (lock) {
if (queued > 0)
eu.faircode.email.Log.persist(eu.faircode.email.EntityLog.Type.Debug,
eu.faircode.email.Log.persist(eu.faircode.email.EntityLog.Type.Debug1,
mComputeFunction + " queued=" + queued);
else {
queued++;

View File

@ -2,11 +2,26 @@ package com.bugsnag.android
import android.annotation.SuppressLint
import android.app.ActivityManager
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_CANT_SAVE_STATE
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_EMPTY
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_GONE
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_PERCEPTIBLE
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_PERCEPTIBLE_PRE_26
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_SERVICE
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_TOP_SLEEPING
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_TOP_SLEEPING_PRE_28
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE
import android.app.ActivityManager.RunningAppProcessInfo.REASON_PROVIDER_IN_USE
import android.app.ActivityManager.RunningAppProcessInfo.REASON_SERVICE_IN_USE
import android.app.Application
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import android.os.Process
import android.os.SystemClock
import com.bugsnag.android.internal.ImmutableConfig
@ -49,12 +64,57 @@ internal class AppDataCollector(
)
}
@SuppressLint("SwitchIntDef")
@Suppress("DEPRECATION")
private fun getProcessImportance(): String? {
try {
val appInfo = ActivityManager.RunningAppProcessInfo()
if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
ActivityManager.getMyMemoryState(appInfo)
} else {
val expectedPid = Process.myPid()
activityManager?.runningAppProcesses
?.find { it.pid == expectedPid }
?.let {
appInfo.importance = it.importance
appInfo.pid = expectedPid
}
}
if (appInfo.pid == 0) {
return null
}
return when (appInfo.importance) {
IMPORTANCE_FOREGROUND -> "foreground"
IMPORTANCE_FOREGROUND_SERVICE -> "foreground service"
IMPORTANCE_TOP_SLEEPING -> "top sleeping"
IMPORTANCE_TOP_SLEEPING_PRE_28 -> "top sleeping"
IMPORTANCE_VISIBLE -> "visible"
IMPORTANCE_PERCEPTIBLE -> "perceptible"
IMPORTANCE_PERCEPTIBLE_PRE_26 -> "perceptible"
IMPORTANCE_CANT_SAVE_STATE -> "can't save state"
IMPORTANCE_CANT_SAVE_STATE_PRE_26 -> "can't save state"
IMPORTANCE_SERVICE -> "service"
IMPORTANCE_CACHED -> "cached/background"
IMPORTANCE_GONE -> "gone"
IMPORTANCE_EMPTY -> "empty"
REASON_PROVIDER_IN_USE -> "provider in use"
REASON_SERVICE_IN_USE -> "service in use"
else -> "unknown importance (${appInfo.importance})"
}
} catch (e: Exception) {
return null
}
}
fun getAppDataMetadata(): MutableMap<String, Any?> {
val map = HashMap<String, Any?>()
map["name"] = appName
map["activeScreen"] = sessionTracker.contextActivity
map["lowMemory"] = memoryTrimState.isLowMemory
map["memoryTrimLevel"] = memoryTrimState.trimLevelDescription
map["processImportance"] = getProcessImportance()
populateRuntimeMemoryMetadata(map)
@ -128,6 +188,7 @@ internal class AppDataCollector(
packageManager != null && copy != null -> {
packageManager.getApplicationLabel(copy).toString()
}
else -> null
}
}
@ -156,6 +217,7 @@ internal class AppDataCollector(
VERSION.SDK_INT >= VERSION_CODES.P -> {
Application.getProcessName()
}
else -> {
// see https://stackoverflow.com/questions/19631894
val clz = Class.forName("android.app.ActivityThread")
@ -179,5 +241,7 @@ internal class AppDataCollector(
* good approximation for how long the app has been running.
*/
fun getDurationMs(): Long = SystemClock.elapsedRealtime() - startTimeMs
private const val IMPORTANCE_CANT_SAVE_STATE_PRE_26 = 170
}
}

View File

@ -29,12 +29,14 @@ internal class BugsnagEventMapper(
event.userImpl = convertUser(map.readEntry("user"))
// populate metadata
val metadataMap: Map<String, Map<String, Any?>> = map.readEntry("metaData")
val metadataMap: Map<String, Map<String, Any?>> =
(map["metaData"] as? Map<String, Map<String, Any?>>).orEmpty()
metadataMap.forEach { (key, value) ->
event.addMetadata(key, value)
}
val featureFlagsList: List<Map<String, Any?>> = map.readEntry("featureFlags")
val featureFlagsList: List<Map<String, Any?>> =
(map["featureFlags"] as? List<Map<String, Any?>>).orEmpty()
featureFlagsList.forEach { featureFlagMap ->
event.addFeatureFlag(
featureFlagMap.readEntry("featureFlag"),
@ -43,7 +45,8 @@ internal class BugsnagEventMapper(
}
// populate breadcrumbs
val breadcrumbList: List<MutableMap<String, Any?>> = map.readEntry("breadcrumbs")
val breadcrumbList: List<MutableMap<String, Any?>> =
(map["breadcrumbs"] as? List<MutableMap<String, Any?>>).orEmpty()
breadcrumbList.mapTo(event.breadcrumbs) {
Breadcrumb(
convertBreadcrumbInternal(it),
@ -226,8 +229,7 @@ internal class BugsnagEventMapper(
is T -> return value
null -> throw IllegalStateException("cannot find json property '$key'")
else -> throw IllegalArgumentException(
"json property '$key' not " +
"of expected type, found ${value.javaClass.name}"
"json property '$key' not of expected type, found ${value.javaClass.name}"
)
}
}

View File

@ -25,6 +25,7 @@ internal class ConfigInternal(
var releaseStage: String? = null
var sendThreads: ThreadSendPolicy = ThreadSendPolicy.ALWAYS
var persistUser: Boolean = true
var generateAnonymousId: Boolean = true
var launchDurationMillis: Long = DEFAULT_LAUNCH_CRASH_THRESHOLD_MS

View File

@ -178,6 +178,26 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware, F
impl.setPersistUser(persistUser);
}
/**
* Set whether or not Bugsnag should generate an anonymous ID and persist it in local storage
*
* If disabled, any device ID that has been persisted will not be retrieved, and no new
* device ID will be generated or stored
*/
public boolean getGenerateAnonymousId() {
return impl.getGenerateAnonymousId();
}
/**
* Set whether or not Bugsnag should generate an anonymous ID and persist it in local storage
*
* If disabled, any device ID that has been persisted will not be retrieved, and no new
* device ID will be generated or stored
*/
public void setGenerateAnonymousId(boolean generateAnonymousId) {
impl.setGenerateAnonymousId(generateAnonymousId);
}
/**
* Sets the directory where event and session JSON payloads should be persisted if a network
* request is not successful. If you use Bugsnag in multiple processes, then a unique

View File

@ -1,6 +1,7 @@
package com.bugsnag.android
import android.content.Context
import com.bugsnag.android.internal.ImmutableConfig
import java.io.File
import java.util.UUID
@ -8,18 +9,20 @@ import java.util.UUID
* This class is responsible for persisting and retrieving the device ID and internal device ID,
* which uniquely identify this device in various contexts.
*/
internal class DeviceIdStore @JvmOverloads constructor(
internal class DeviceIdStore @JvmOverloads @Suppress("LongParameterList") constructor(
context: Context,
deviceIdfile: File = File(context.filesDir, "device-id"),
deviceIdGenerator: () -> UUID = { UUID.randomUUID() },
internalDeviceIdfile: File = File(context.filesDir, "internal-device-id"),
internalDeviceIdGenerator: () -> UUID = { UUID.randomUUID() },
private val sharedPrefMigrator: SharedPrefMigrator,
config: ImmutableConfig,
logger: Logger
) {
private val persistence: DeviceIdPersistence
private val internalPersistence: DeviceIdPersistence
private val generateId = config.generateAnonymousId
init {
persistence = DeviceIdFilePersistence(deviceIdfile, deviceIdGenerator, logger)
@ -35,6 +38,12 @@ internal class DeviceIdStore @JvmOverloads constructor(
* be used. If no value is present then a random UUID will be generated and persisted.
*/
fun loadDeviceId(): String? {
// If generateAnonymousId = false, return null
// so that a previously persisted device ID is not returned,
// or a new one is not generated and persisted
if (!generateId) {
return null
}
var result = persistence.loadDeviceId(false)
if (result != null) {
return result
@ -47,6 +56,12 @@ internal class DeviceIdStore @JvmOverloads constructor(
}
fun loadInternalDeviceId(): String? {
// If generateAnonymousId = false, return null
// so that a previously persisted device ID is not returned,
// or a new one is not generated and persisted
if (!generateId) {
return null
}
return internalPersistence.loadDeviceId(true)
}
}

View File

@ -1,42 +1,113 @@
package com.bugsnag.android
import java.io.IOException
import kotlin.math.max
internal class FeatureFlags(
internal val store: MutableMap<String, String?> = mutableMapOf()
internal class FeatureFlags private constructor(
@Volatile
private var flags: Array<FeatureFlag>
) : JsonStream.Streamable, FeatureFlagAware {
private val emptyVariant = "__EMPTY_VARIANT_SENTINEL__"
@Synchronized override fun addFeatureFlag(name: String) {
/*
* Implemented as *effectively* a CopyOnWriteArrayList - but since FeatureFlags are
* key/value pairs, CopyOnWriteArrayList would require external locking (in addition to it's
* internal locking) for us to be sure we are not adding duplicates.
*
* This class aims to have similar performance while also ensuring that the FeatureFlag object
* themselves don't leak, as they are mutable and we want 'copy' to be an O(1) snapshot
* operation for when an Event is created.
*
* It's assumed that *most* FeatureFlags will be added up-front, or during the normal app
* lifecycle (not during an Event).
*
* As such a copy-on-write structure allows an Event to simply capture a reference to the
* "snapshot" of FeatureFlags that were active when the Event was created.
*/
constructor() : this(emptyArray<FeatureFlag>())
override fun addFeatureFlag(name: String) {
addFeatureFlag(name, null)
}
@Synchronized override fun addFeatureFlag(name: String, variant: String?) {
store[name] = variant ?: emptyVariant
}
override fun addFeatureFlag(name: String, variant: String?) {
synchronized(this) {
val flagArray = flags
val index = flagArray.indexOfFirst { it.name == name }
@Synchronized override fun addFeatureFlags(featureFlags: Iterable<FeatureFlag>) {
featureFlags.forEach { (name, variant) ->
addFeatureFlag(name, variant)
flags = when {
// this is a new FeatureFlag
index == -1 -> flagArray + FeatureFlag(name, variant)
// this is a change to an existing FeatureFlag
flagArray[index].variant != variant -> flagArray.copyOf().also {
// replace the existing FeatureFlag in-place
it[index] = FeatureFlag(name, variant)
}
// no actual change, so we return
else -> return
}
}
}
@Synchronized override fun clearFeatureFlag(name: String) {
store.remove(name)
override fun addFeatureFlags(featureFlags: Iterable<FeatureFlag>) {
synchronized(this) {
val flagArray = flags
val newFlags = ArrayList<FeatureFlag>(
// try to guess a reasonable upper-bound for the output array
if (featureFlags is Collection<*>) flagArray.size + featureFlags.size
else max(flagArray.size * 2, flagArray.size)
)
newFlags.addAll(flagArray)
featureFlags.forEach { (name, variant) ->
val existingIndex = newFlags.indexOfFirst { it.name == name }
when (existingIndex) {
// add a new flag to the end of the list
-1 -> newFlags.add(FeatureFlag(name, variant))
// replace the existing flag
else -> newFlags[existingIndex] = FeatureFlag(name, variant)
}
}
flags = newFlags.toTypedArray()
}
}
@Synchronized override fun clearFeatureFlags() {
store.clear()
override fun clearFeatureFlag(name: String) {
synchronized(this) {
val flagArray = flags
val index = flagArray.indexOfFirst { it.name == name }
if (index == -1) {
return
}
val out = arrayOfNulls<FeatureFlag>(flagArray.size - 1)
flagArray.copyInto(out, 0, 0, index)
flagArray.copyInto(out, index, index + 1)
@Suppress("UNCHECKED_CAST")
flags = out as Array<FeatureFlag>
}
}
override fun clearFeatureFlags() {
synchronized(this) {
flags = emptyArray()
}
}
@Throws(IOException::class)
override fun toStream(stream: JsonStream) {
val storeCopy = synchronized(this) { store.toMap() }
val storeCopy = flags
stream.beginArray()
storeCopy.forEach { (name, variant) ->
stream.beginObject()
stream.name("featureFlag").value(name)
if (variant != emptyVariant) {
if (variant != null) {
stream.name("variant").value(variant)
}
stream.endObject()
@ -44,9 +115,7 @@ internal class FeatureFlags(
stream.endArray()
}
@Synchronized fun toList(): List<FeatureFlag> = store.entries.map { (name, variant) ->
FeatureFlag(name, variant.takeUnless { it == emptyVariant })
}
fun toList(): List<FeatureFlag> = flags.map { (name, variant) -> FeatureFlag(name, variant) }
@Synchronized fun copy() = FeatureFlags(store.toMutableMap())
fun copy() = FeatureFlags(flags)
}

View File

@ -145,6 +145,7 @@ class JsonWriter implements Closeable, Flushable {
*/
private static final String[] REPLACEMENT_CHARS;
private static final String[] HTML_SAFE_REPLACEMENT_CHARS;
static {
REPLACEMENT_CHARS = new String[128];
for (int i = 0; i <= 0x1f; i++) {
@ -165,11 +166,14 @@ class JsonWriter implements Closeable, Flushable {
HTML_SAFE_REPLACEMENT_CHARS['\''] = "\\u0027";
}
/** The output data, containing at most one top-level array or object. */
/**
* The output data, containing at most one top-level array or object.
*/
private final Writer out;
private int[] stack = new int[32];
private int stackSize = 0;
{
push(EMPTY_DOCUMENT);
}
@ -337,7 +341,7 @@ class JsonWriter implements Closeable, Flushable {
* given bracket.
*/
private JsonWriter close(int empty, int nonempty, String closeBracket)
throws IOException {
throws IOException {
int context = peek();
if (context != nonempty && context != empty) {
throw new IllegalStateException("Nesting problem.");
@ -437,7 +441,7 @@ class JsonWriter implements Closeable, Flushable {
}
writeDeferredName();
beforeValue();
out.append(value);
out.write(value);
return this;
}
@ -490,17 +494,18 @@ class JsonWriter implements Closeable, Flushable {
/**
* Encodes {@code value}.
*
* @param value a finite value. May not be {@link Double#isNaN() NaNs} or
* {@link Double#isInfinite() infinities}.
* @param value a finite value.
* @return this writer.
*/
public JsonWriter value(double value) throws IOException {
writeDeferredName();
if (!lenient && (Double.isNaN(value) || Double.isInfinite(value))) {
throw new IllegalArgumentException("Numeric values must be finite, but was " + value);
// omit these values instead of attempting to write them
deferredName = null;
} else {
writeDeferredName();
beforeValue();
out.write(Double.toString(value));
}
beforeValue();
out.append(Double.toString(value));
return this;
}
@ -520,7 +525,7 @@ class JsonWriter implements Closeable, Flushable {
* Encodes {@code value}.
*
* @param value a finite value. May not be {@link Double#isNaN() NaNs} or
* {@link Double#isInfinite() infinities}.
* {@link Double#isInfinite() infinities}.
* @return this writer.
*/
public JsonWriter value(Number value) throws IOException {
@ -528,14 +533,16 @@ class JsonWriter implements Closeable, Flushable {
return nullValue();
}
writeDeferredName();
String string = value.toString();
if (!lenient
&& (string.equals("-Infinity") || string.equals("Infinity") || string.equals("NaN"))) {
throw new IllegalArgumentException("Numeric values must be finite, but was " + value);
&& (string.equals("-Infinity") || string.equals("Infinity") || string.equals("NaN"))) {
// omit this value
deferredName = null;
} else {
writeDeferredName();
beforeValue();
out.write(string);
}
beforeValue();
out.append(string);
return this;
}
@ -634,7 +641,7 @@ class JsonWriter implements Closeable, Flushable {
case NONEMPTY_DOCUMENT:
if (!lenient) {
throw new IllegalStateException(
"JSON must have only one top-level value.");
"JSON must have only one top-level value.");
}
// fall-through
case EMPTY_DOCUMENT: // first in document
@ -647,12 +654,12 @@ class JsonWriter implements Closeable, Flushable {
break;
case NONEMPTY_ARRAY: // another in array
out.append(',');
out.write(',');
newline();
break;
case DANGLING_NAME: // value for name
out.append(separator);
out.write(separator);
replaceTop(NONEMPTY_OBJECT);
break;

View File

@ -20,6 +20,7 @@ internal class ManifestConfigLoader {
private const val AUTO_DETECT_ERRORS = "$BUGSNAG_NS.AUTO_DETECT_ERRORS"
private const val PERSIST_USER = "$BUGSNAG_NS.PERSIST_USER"
private const val SEND_THREADS = "$BUGSNAG_NS.SEND_THREADS"
private const val GENERATE_ANONYMOUS_ID = "$BUGSNAG_NS.GENERATE_ANONYMOUS_ID"
// endpoints
private const val ENDPOINT_NOTIFY = "$BUGSNAG_NS.ENDPOINT_NOTIFY"
@ -108,6 +109,7 @@ internal class ManifestConfigLoader {
autoTrackSessions = data.getBoolean(AUTO_TRACK_SESSIONS, autoTrackSessions)
autoDetectErrors = data.getBoolean(AUTO_DETECT_ERRORS, autoDetectErrors)
persistUser = data.getBoolean(PERSIST_USER, persistUser)
generateAnonymousId = data.getBoolean(GENERATE_ANONYMOUS_ID, generateAnonymousId)
val str = data.getString(SEND_THREADS)

View File

@ -7,7 +7,7 @@ import java.io.IOException
*/
class Notifier @JvmOverloads constructor(
var name: String = "Android Bugsnag Notifier",
var version: String = "6.1.0",
var version: String = "6.4.0",
var url: String = "https://bugsnag.com"
) : JsonStream.Streamable {

View File

@ -11,9 +11,11 @@ internal class ObjectJsonStreamer {
companion object {
internal const val REDACTED_PLACEHOLDER = "[REDACTED]"
internal const val OBJECT_PLACEHOLDER = "[OBJECT]"
internal val DEFAULT_REDACTED_KEYS = setOf(Pattern.compile(".*password.*", Pattern.CASE_INSENSITIVE))
}
var redactedKeys = setOf(Pattern.compile(".*password.*", Pattern.CASE_INSENSITIVE))
var redactedKeys = DEFAULT_REDACTED_KEYS
// Write complex/nested values to a JsonStreamer
@Throws(IOException::class)

View File

@ -36,6 +36,7 @@ class SessionTracker extends BaseObservable implements ForegroundDetector.OnActi
private volatile Session currentSession = null;
final BackgroundTaskService backgroundTaskService;
final Logger logger;
private boolean shouldSuppressFirstAutoSession = false;
SessionTracker(ImmutableConfig configuration,
CallbackState callbackState,
@ -76,7 +77,7 @@ class SessionTracker extends BaseObservable implements ForegroundDetector.OnActi
@VisibleForTesting
Session startNewSession(@NonNull Date date, @Nullable User user,
boolean autoCaptured) {
if (client.getConfig().shouldDiscardSession(autoCaptured)) {
if (shouldDiscardSession(autoCaptured)) {
return null;
}
String id = UUID.randomUUID().toString();
@ -92,12 +93,29 @@ class SessionTracker extends BaseObservable implements ForegroundDetector.OnActi
}
Session startSession(boolean autoCaptured) {
if (client.getConfig().shouldDiscardSession(autoCaptured)) {
if (shouldDiscardSession(autoCaptured)) {
return null;
}
return startNewSession(new Date(), client.getUser(), autoCaptured);
}
private boolean shouldDiscardSession(boolean autoCaptured) {
if (client.getConfig().shouldDiscardSession(autoCaptured)) {
return true;
} else {
Session existingSession = currentSession;
if (autoCaptured
&& existingSession != null
&& !existingSession.isAutoCaptured()
&& shouldSuppressFirstAutoSession) {
shouldSuppressFirstAutoSession = true;
return true;
}
}
return false;
}
void pauseSession() {
Session session = currentSession;

View File

@ -19,7 +19,8 @@ internal class StorageModule(
DeviceIdStore(
appContext,
sharedPrefMigrator = sharedPrefMigrator,
logger = logger
logger = logger,
config = immutableConfig
)
}

View File

@ -30,7 +30,9 @@ internal class UserStore @JvmOverloads constructor(
* [Configuration.getPersistUser] is true.
*
* If no user is stored on disk, then a default [User] is used which uses the device ID
* as its ID.
* as its ID (unless the generateAnonymousId config option is set to false, in which case the
* device ID and therefore the user ID is set to
* null).
*
* The [UserState] provides a mechanism for observing value changes to its user property,
* so to avoid interfering with this the method should only be called once for each [Client].
@ -46,6 +48,8 @@ internal class UserStore @JvmOverloads constructor(
val userState = when {
loadedUser != null && validUser(loadedUser) -> UserState(loadedUser)
// if generateAnonymousId config option is false, the deviceId should already be null
// here
else -> UserState(User(deviceId, null, null))
}

View File

@ -57,6 +57,7 @@ data class ImmutableConfig(
val persistenceDirectory: Lazy<File>,
val sendLaunchCrashesSynchronously: Boolean,
val attemptDeliveryOnCrash: Boolean,
val generateAnonymousId: Boolean,
// results cached here to avoid unnecessary lookups in Client.
val packageInfo: PackageInfo?,
@ -166,6 +167,7 @@ internal fun convertToImmutableConfig(
delivery = config.delivery,
endpoints = config.endpoints,
persistUser = config.persistUser,
generateAnonymousId = config.generateAnonymousId,
launchDurationMillis = config.launchDurationMillis,
logger = config.logger!!,
maxBreadcrumbs = config.maxBreadcrumbs,

View File

@ -39,7 +39,7 @@ public abstract class BinaryConverter {
@SuppressWarnings("unchecked")
public static ArrayList<byte[]> deserializeCollection(final JsonReader reader) throws IOException {
return reader.deserializeCollection(Base64Reader);
return reader.deserializeCollectionCustom(Base64Reader);
}
public static void deserializeCollection(final JsonReader reader, final Collection<byte[]> res) throws IOException {
@ -48,7 +48,7 @@ public abstract class BinaryConverter {
@SuppressWarnings("unchecked")
public static ArrayList<byte[]> deserializeNullableCollection(final JsonReader reader) throws IOException {
return reader.deserializeNullableCollection(Base64Reader);
return reader.deserializeNullableCollectionCustom(Base64Reader);
}
public static void deserializeNullableCollection(final JsonReader reader, final Collection<byte[]> res) throws IOException {

View File

@ -110,7 +110,7 @@ public abstract class BoolConverter {
@SuppressWarnings("unchecked")
public static ArrayList<Boolean> deserializeCollection(final JsonReader reader) throws IOException {
return reader.deserializeCollection(READER);
return reader.deserializeCollectionCustom(READER);
}
public static void deserializeCollection(final JsonReader reader, final Collection<Boolean> res) throws IOException {
@ -119,7 +119,7 @@ public abstract class BoolConverter {
@SuppressWarnings("unchecked")
public static ArrayList<Boolean> deserializeNullableCollection(final JsonReader reader) throws IOException {
return reader.deserializeNullableCollection(READER);
return reader.deserializeNullableCollectionCustom(READER);
}
public static void deserializeNullableCollection(final JsonReader reader, final Collection<Boolean> res) throws IOException {

View File

@ -804,7 +804,7 @@ public class DslJson<TContext> implements UnknownSerializer, TypeLookup {
for (ClassLoader loader : loaders) {
try {
Class<?> external = loader.loadClass(name);
Configuration instance = (Configuration) external.newInstance();
Configuration instance = (Configuration) external.getDeclaredConstructor().newInstance();
instance.configure(json);
} catch (NoClassDefFoundError ignore) {
} catch (Exception ignore) {
@ -813,6 +813,8 @@ public class DslJson<TContext> implements UnknownSerializer, TypeLookup {
}
static void registerJavaSpecifics(final DslJson json) {
json.registerReader(Element.class, XmlConverter.Reader);
json.registerWriter(Element.class, XmlConverter.Writer);
}
private final Map<Type, Object> defaults = new ConcurrentHashMap<Type, Object>();
@ -1653,7 +1655,7 @@ public class DslJson<TContext> implements UnknownSerializer, TypeLookup {
}
final JsonReader.ReadObject<?> contentReader = tryFindReader(content);
if (contentReader != null) {
final ArrayList<?> result = json.deserializeNullableCollection(contentReader);
final ArrayList<?> result = json.deserializeNullableCollectionCustom(contentReader);
if (container.isArray()) {
return returnAsArray(content, result);
}
@ -1673,7 +1675,7 @@ public class DslJson<TContext> implements UnknownSerializer, TypeLookup {
}
final JsonReader.ReadObject<?> contentReader = tryFindReader(content);
if (contentReader != null) {
final ArrayList<?> result = json.deserializeNullableCollection(contentReader);
final ArrayList<?> result = json.deserializeNullableCollectionCustom(contentReader);
return returnAsArray(content, result);
}
}
@ -1768,7 +1770,7 @@ public class DslJson<TContext> implements UnknownSerializer, TypeLookup {
}
final JsonReader.ReadObject<?> simpleReader = tryFindReader(manifest);
if (simpleReader != null) {
return json.deserializeNullableCollection(simpleReader);
return json.deserializeNullableCollectionCustom(simpleReader);
}
if (fallback != null) {
final Object array = Array.newInstance(manifest, 0);
@ -1883,7 +1885,7 @@ public class DslJson<TContext> implements UnknownSerializer, TypeLookup {
}
final JsonReader.ReadObject simpleReader = tryFindReader(manifest);
if (simpleReader != null) {
return json.deserializeNullableCollection(simpleReader);
return json.deserializeNullableCollectionCustom(simpleReader);
}
if (fallback != null) {
final Object array = Array.newInstance(manifest, 0);
@ -2009,7 +2011,7 @@ public class DslJson<TContext> implements UnknownSerializer, TypeLookup {
}
final JsonReader.ReadObject<?> simpleElementReader = tryFindReader(elementManifest);
if (simpleElementReader != null) {
List<?> list = json.deserializeNullableCollection(simpleElementReader);
List<?> list = json.deserializeNullableCollectionCustom(simpleElementReader);
return (TResult) convertResultToArray(elementManifest, list);
}
}
@ -2267,7 +2269,7 @@ public class DslJson<TContext> implements UnknownSerializer, TypeLookup {
}
final JsonReader.ReadObject<?> simpleReader = tryFindReader(manifest);
if (simpleReader != null) {
return json.iterateOver(simpleReader);
return json.iterateOverCustom(simpleReader);
}
if (fallback != null) {
final Object array = Array.newInstance(manifest, 0);

View File

@ -1,5 +1,6 @@
package com.bugsnag.android.repackaged.dslplatform.json;
import java.lang.reflect.InvocationTargetException;
import java.util.*;
class ExternalConverterAnalyzer {
@ -19,14 +20,16 @@ class ExternalConverterAnalyzer {
try {
Class<?> converterClass = cl.loadClass(ccn);
if (!Configuration.class.isAssignableFrom(converterClass)) continue;
Configuration converter = (Configuration) converterClass.newInstance();
Configuration converter = (Configuration) converterClass.getDeclaredConstructor().newInstance();
converter.configure(dslJson);
return true;
} catch (ClassNotFoundException ignored) {
} catch (IllegalAccessException ignored) {
} catch (InstantiationException ignored) {
}
}
} catch (InvocationTargetException e) {
} catch (NoSuchMethodException e) {
}
}
}
return false;
}

View File

@ -1594,12 +1594,13 @@ public final class JsonReader<TContext> {
return res.toArray(emptyArray);
}
public final <T, S extends T> ArrayList<T> deserializeCollection(final ReadObject<S> readObject) throws IOException {
public final <T, S extends T> ArrayList<T> deserializeCollectionCustom(final ReadObject<S> readObject) throws IOException {
final ArrayList<T> res = new ArrayList<T>(4);
deserializeCollection(readObject, res);
return res;
}
@SuppressWarnings("overloads")
public final <T, S extends T> void deserializeCollection(final ReadObject<S> readObject, final Collection<T> res) throws IOException {
res.add(readObject.read(this));
while (getNextToken() == ',') {
@ -1609,12 +1610,13 @@ public final class JsonReader<TContext> {
checkArrayEnd();
}
public final <T, S extends T> ArrayList<T> deserializeNullableCollection(final ReadObject<S> readObject) throws IOException {
public final <T, S extends T> ArrayList<T> deserializeNullableCollectionCustom(final ReadObject<S> readObject) throws IOException {
final ArrayList<T> res = new ArrayList<T>(4);
deserializeNullableCollection(readObject, res);
return res;
}
@SuppressWarnings("overloads")
public final <T, S extends T> void deserializeNullableCollection(final ReadObject<S> readObject, final Collection<T> res) throws IOException {
if (wasNull()) {
res.add(null);
@ -1638,6 +1640,7 @@ public final class JsonReader<TContext> {
return res;
}
@SuppressWarnings("overloads")
public final <T extends JsonObject> void deserializeCollection(final ReadJsonObject<T> readObject, final Collection<T> res) throws IOException {
if (last == '{') {
getNextToken();
@ -1658,6 +1661,7 @@ public final class JsonReader<TContext> {
return res;
}
@SuppressWarnings("overloads")
public final <T extends JsonObject> void deserializeNullableCollection(final ReadJsonObject<T> readObject, final Collection<T> res) throws IOException {
if (last == '{') {
getNextToken();
@ -1676,7 +1680,7 @@ public final class JsonReader<TContext> {
checkArrayEnd();
}
public final <T> Iterator<T> iterateOver(final JsonReader.ReadObject<T> reader) {
public final <T> Iterator<T> iterateOverCustom(final JsonReader.ReadObject<T> reader) {
return new WithReader<T>(reader, this);
}

View File

@ -70,7 +70,7 @@ public abstract class MapConverter {
@SuppressWarnings("unchecked")
public static ArrayList<Map<String, String>> deserializeCollection(final JsonReader reader) throws IOException {
return reader.deserializeCollection(TypedMapReader);
return reader.deserializeCollectionCustom(TypedMapReader);
}
public static void deserializeCollection(final JsonReader reader, final Collection<Map<String, String>> res) throws IOException {
@ -79,7 +79,7 @@ public abstract class MapConverter {
@SuppressWarnings("unchecked")
public static ArrayList<Map<String, String>> deserializeNullableCollection(final JsonReader reader) throws IOException {
return reader.deserializeNullableCollection(TypedMapReader);
return reader.deserializeNullableCollectionCustom(TypedMapReader);
}
public static void deserializeNullableCollection(final JsonReader reader, final Collection<Map<String, String>> res) throws IOException {

View File

@ -56,7 +56,7 @@ public abstract class NetConverter {
@SuppressWarnings("unchecked")
public static ArrayList<URI> deserializeUriCollection(final JsonReader reader) throws IOException {
return reader.deserializeCollection(UriReader);
return reader.deserializeCollectionCustom(UriReader);
}
public static void deserializeUriCollection(final JsonReader reader, final Collection<URI> res) throws IOException {
@ -65,7 +65,7 @@ public abstract class NetConverter {
@SuppressWarnings("unchecked")
public static ArrayList<URI> deserializeUriNullableCollection(final JsonReader reader) throws IOException {
return reader.deserializeNullableCollection(UriReader);
return reader.deserializeNullableCollectionCustom(UriReader);
}
public static void deserializeUriNullableCollection(final JsonReader reader, final Collection<URI> res) throws IOException {
@ -92,7 +92,7 @@ public abstract class NetConverter {
@SuppressWarnings("unchecked")
public static ArrayList<InetAddress> deserializeIpCollection(final JsonReader reader) throws IOException {
return reader.deserializeCollection(AddressReader);
return reader.deserializeCollectionCustom(AddressReader);
}
public static void deserializeIpCollection(final JsonReader reader, final Collection<InetAddress> res) throws IOException {
@ -101,7 +101,7 @@ public abstract class NetConverter {
@SuppressWarnings("unchecked")
public static ArrayList<InetAddress> deserializeIpNullableCollection(final JsonReader reader) throws IOException {
return reader.deserializeNullableCollection(AddressReader);
return reader.deserializeNullableCollectionCustom(AddressReader);
}
public static void deserializeIpNullableCollection(final JsonReader reader, final Collection<InetAddress> res) throws IOException {

View File

@ -570,7 +570,7 @@ public abstract class NumberConverter {
@SuppressWarnings("unchecked")
public static ArrayList<Double> deserializeDoubleCollection(final JsonReader reader) throws IOException {
return reader.deserializeCollection(DOUBLE_READER);
return reader.deserializeCollectionCustom(DOUBLE_READER);
}
public static void deserializeDoubleCollection(final JsonReader reader, final Collection<Double> res) throws IOException {
@ -579,7 +579,7 @@ public abstract class NumberConverter {
@SuppressWarnings("unchecked")
public static ArrayList<Double> deserializeDoubleNullableCollection(final JsonReader reader) throws IOException {
return reader.deserializeNullableCollection(DOUBLE_READER);
return reader.deserializeNullableCollectionCustom(DOUBLE_READER);
}
public static void deserializeDoubleNullableCollection(final JsonReader reader, final Collection<Double> res) throws IOException {
@ -772,7 +772,7 @@ public abstract class NumberConverter {
@SuppressWarnings("unchecked")
public static ArrayList<Float> deserializeFloatCollection(final JsonReader reader) throws IOException {
return reader.deserializeCollection(FLOAT_READER);
return reader.deserializeCollectionCustom(FLOAT_READER);
}
public static void deserializeFloatCollection(final JsonReader reader, Collection<Float> res) throws IOException {
@ -781,7 +781,7 @@ public abstract class NumberConverter {
@SuppressWarnings("unchecked")
public static ArrayList<Float> deserializeFloatNullableCollection(final JsonReader reader) throws IOException {
return reader.deserializeNullableCollection(FLOAT_READER);
return reader.deserializeNullableCollectionCustom(FLOAT_READER);
}
public static void deserializeFloatNullableCollection(final JsonReader reader, final Collection<Float> res) throws IOException {
@ -981,7 +981,7 @@ public abstract class NumberConverter {
@SuppressWarnings("unchecked")
public static ArrayList<Integer> deserializeIntCollection(final JsonReader reader) throws IOException {
return reader.deserializeCollection(INT_READER);
return reader.deserializeCollectionCustom(INT_READER);
}
public static int[] deserializeIntArray(final JsonReader reader) throws IOException {
@ -1080,7 +1080,7 @@ public abstract class NumberConverter {
@SuppressWarnings("unchecked")
public static ArrayList<Short> deserializeShortNullableCollection(final JsonReader reader) throws IOException {
return reader.deserializeNullableCollection(SHORT_READER);
return reader.deserializeNullableCollectionCustom(SHORT_READER);
}
public static void deserializeShortNullableCollection(final JsonReader reader, final Collection<Short> res) throws IOException {
@ -1093,7 +1093,7 @@ public abstract class NumberConverter {
@SuppressWarnings("unchecked")
public static ArrayList<Integer> deserializeIntNullableCollection(final JsonReader reader) throws IOException {
return reader.deserializeNullableCollection(INT_READER);
return reader.deserializeNullableCollectionCustom(INT_READER);
}
public static void deserializeIntNullableCollection(final JsonReader reader, final Collection<Integer> res) throws IOException {
@ -1317,7 +1317,7 @@ public abstract class NumberConverter {
@SuppressWarnings("unchecked")
public static ArrayList<Long> deserializeLongCollection(final JsonReader reader) throws IOException {
return reader.deserializeCollection(LONG_READER);
return reader.deserializeCollectionCustom(LONG_READER);
}
public static void deserializeLongCollection(final JsonReader reader, final Collection<Long> res) throws IOException {
@ -1326,7 +1326,7 @@ public abstract class NumberConverter {
@SuppressWarnings("unchecked")
public static ArrayList<Long> deserializeLongNullableCollection(final JsonReader reader) throws IOException {
return reader.deserializeNullableCollection(LONG_READER);
return reader.deserializeNullableCollectionCustom(LONG_READER);
}
public static void deserializeLongNullableCollection(final JsonReader reader, final Collection<Long> res) throws IOException {
@ -1682,7 +1682,7 @@ public abstract class NumberConverter {
@SuppressWarnings("unchecked")
public static ArrayList<BigDecimal> deserializeDecimalCollection(final JsonReader reader) throws IOException {
return reader.deserializeCollection(DecimalReader);
return reader.deserializeCollectionCustom(DecimalReader);
}
public static void deserializeDecimalCollection(final JsonReader reader, final Collection<BigDecimal> res) throws IOException {
@ -1691,7 +1691,7 @@ public abstract class NumberConverter {
@SuppressWarnings("unchecked")
public static ArrayList<BigDecimal> deserializeDecimalNullableCollection(final JsonReader reader) throws IOException {
return reader.deserializeNullableCollection(DecimalReader);
return reader.deserializeNullableCollectionCustom(DecimalReader);
}
public static void deserializeDecimalNullableCollection(final JsonReader reader, final Collection<BigDecimal> res) throws IOException {

View File

@ -117,7 +117,7 @@ public abstract class ObjectConverter {
@SuppressWarnings("unchecked")
public static ArrayList<Map<String, Object>> deserializeMapCollection(final JsonReader reader) throws IOException {
return reader.deserializeCollection(TypedMapReader);
return reader.deserializeCollectionCustom(TypedMapReader);
}
public static void deserializeMapCollection(final JsonReader reader, final Collection<Map<String, Object>> res) throws IOException {
@ -126,7 +126,7 @@ public abstract class ObjectConverter {
@SuppressWarnings("unchecked")
public static ArrayList<Map<String, Object>> deserializeNullableMapCollection(final JsonReader reader) throws IOException {
return reader.deserializeNullableCollection(TypedMapReader);
return reader.deserializeNullableCollectionCustom(TypedMapReader);
}
public static void deserializeNullableMapCollection(final JsonReader reader, final Collection<Map<String, Object>> res) throws IOException {

View File

@ -89,7 +89,7 @@ public abstract class StringConverter {
@SuppressWarnings("unchecked")
public static ArrayList<String> deserializeCollection(final JsonReader reader) throws IOException {
return reader.deserializeCollection(READER);
return reader.deserializeCollectionCustom(READER);
}
public static void deserializeCollection(final JsonReader reader, final Collection<String> res) throws IOException {
@ -98,7 +98,7 @@ public abstract class StringConverter {
@SuppressWarnings("unchecked")
public static ArrayList<String> deserializeNullableCollection(final JsonReader reader) throws IOException {
return reader.deserializeNullableCollection(READER);
return reader.deserializeNullableCollectionCustom(READER);
}
public static void deserializeNullableCollection(final JsonReader reader, final Collection<String> res) throws IOException {

View File

@ -180,7 +180,7 @@ public abstract class UUIDConverter {
@SuppressWarnings("unchecked")
public static ArrayList<UUID> deserializeCollection(final JsonReader reader) throws IOException {
return reader.deserializeCollection(READER);
return reader.deserializeCollectionCustom(READER);
}
public static void deserializeCollection(final JsonReader reader, final Collection<UUID> res) throws IOException {
@ -189,7 +189,7 @@ public abstract class UUIDConverter {
@SuppressWarnings("unchecked")
public static ArrayList<UUID> deserializeNullableCollection(final JsonReader reader) throws IOException {
return reader.deserializeNullableCollection(READER);
return reader.deserializeNullableCollectionCustom(READER);
}
public static void deserializeNullableCollection(final JsonReader reader, final Collection<UUID> res) throws IOException {

View File

@ -0,0 +1,217 @@
package com.bugsnag.android.repackaged.dslplatform.json;
import androidx.annotation.Nullable;
import org.w3c.dom.*;
import org.w3c.dom.ls.DOMImplementationLS;
import org.w3c.dom.ls.LSOutput;
import org.w3c.dom.ls.LSSerializer;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.*;
@SuppressWarnings({"rawtypes", "unchecked"}) // suppress pre-existing warnings
public abstract class XmlConverter {
static final JsonReader.ReadObject<Element> Reader = new JsonReader.ReadObject<Element>() {
@Nullable
@Override
public Element read(JsonReader reader) throws IOException {
return reader.wasNull() ? null : deserialize(reader);
}
};
static final JsonWriter.WriteObject<Element> Writer = new JsonWriter.WriteObject<Element>() {
@Override
public void write(JsonWriter writer, @Nullable Element value) {
serializeNullable(value, writer);
}
};
private static final DocumentBuilder documentBuilder;
static {
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
try {
documentBuilder = dbFactory.newDocumentBuilder();
} catch (ParserConfigurationException e) {
throw new RuntimeException(e);
}
}
public static void serializeNullable(@Nullable final Element value, final JsonWriter sw) {
if (value == null)
sw.writeNull();
else
serialize(value, sw);
}
public static void serialize(final Element value, final JsonWriter sw) {
Document document = value.getOwnerDocument();
DOMImplementationLS domImplLS = (DOMImplementationLS) document.getImplementation();
LSSerializer serializer = domImplLS.createLSSerializer();
LSOutput lsOutput = domImplLS.createLSOutput();
lsOutput.setEncoding("UTF-8");
StringWriter writer = new StringWriter();
lsOutput.setCharacterStream(writer);
serializer.write(document, lsOutput);
StringConverter.serialize(writer.toString(), sw);
}
public static Element deserialize(final JsonReader reader) throws IOException {
if (reader.last() == '"') {
try {
InputSource source = new InputSource(new StringReader(reader.readString()));
return documentBuilder.parse(source).getDocumentElement();
} catch (SAXException ex) {
throw reader.newParseErrorAt("Invalid XML value", 0, ex);
}
} else {
final Map<String, Object> map = ObjectConverter.deserializeMap(reader);
return mapToXml(map);
}
}
public static Element mapToXml(final Map<String, Object> map) throws IOException {
final Set<String> xmlRootElementNames = map.keySet();
if (xmlRootElementNames.size() > 1) {
throw ParsingException.create("Invalid XML. Expecting root element", true);
}
final String rootName = xmlRootElementNames.iterator().next();
final Document document = createDocument();
final Element rootElement = document.createElement(rootName);
document.appendChild(rootElement);
buildXmlFromHashMap(document, rootElement, map.get(rootName));
return rootElement;
}
private static synchronized Document createDocument() {
try {
final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
final DocumentBuilder builder = factory.newDocumentBuilder();
return builder.newDocument();
} catch (ParserConfigurationException e) {
throw new ConfigurationException(e);
}
}
private static final String TEXT_NODE_TAG = "#text";
private static final String COMMENT_NODE_TAG = "#comment";
private static final String CDATA_NODE_TAG = "#cdata-section";
@SuppressWarnings("unchecked")
private static void buildXmlFromHashMap(
final Document doc,
final Element subtreeRootElement,
@Nullable final Object elementContent) {
if (elementContent instanceof HashMap) {
final HashMap<String, Object> elementContentMap = (HashMap<String, Object>) elementContent;
for (final Map.Entry<String, Object> childEntry : elementContentMap.entrySet()) {
final String key = childEntry.getKey();
if (key.startsWith("@")) {
subtreeRootElement.setAttribute(key.substring(1), childEntry.getValue().toString());
} else if (key.startsWith("#")) {
if (key.equals(TEXT_NODE_TAG)) {
if (childEntry.getValue() instanceof List) {
buildTextNodeList(doc, subtreeRootElement, (List<String>) childEntry.getValue());
} else {
final Node textNode = doc.createTextNode(childEntry.getValue().toString());
subtreeRootElement.appendChild(textNode);
}
} else if (key.equals(CDATA_NODE_TAG)) {
if (childEntry.getValue() instanceof List) {
buildCDataList(doc, subtreeRootElement, (List<String>) childEntry.getValue());
} else {
final Node cDataNode = doc.createCDATASection(childEntry.getValue().toString());
subtreeRootElement.appendChild(cDataNode);
}
} else if (key.equals(COMMENT_NODE_TAG)) {
if (childEntry.getValue() instanceof List) {
buildCommentList(doc, subtreeRootElement, (List<String>) childEntry.getValue());
} else {
final Node commentNode = doc.createComment(childEntry.getValue().toString());
subtreeRootElement.appendChild(commentNode);
}
} //else if (key.equals(WHITESPACE_NODE_TAG)
// || key.equals(SIGNIFICANT_WHITESPACE_NODE_TAG)) {
// Ignore
//} else {
/*
* All other nodes whose name starts with a '#' are invalid XML
* nodes, and thus ignored:
*/
//}
} else {
final Element newElement = doc.createElement(key);
subtreeRootElement.appendChild(newElement);
buildXmlFromHashMap(doc, newElement, childEntry.getValue());
}
}
} else if (elementContent instanceof List) {
buildXmlFromJsonArray(doc, subtreeRootElement, (List<Object>) elementContent);
} else {
if (elementContent != null) {
subtreeRootElement.setTextContent(elementContent.toString());
}
}
}
private static void buildTextNodeList(final Document doc, final Node subtreeRoot, final List<String> nodeValues) {
final StringBuilder sb = new StringBuilder();
for (final String nodeValue : nodeValues) {
sb.append(nodeValue);
}
subtreeRoot.appendChild(doc.createTextNode(sb.toString()));
}
private static void buildCDataList(final Document doc, final Node subtreeRoot, final List<String> nodeValues) {
for (final String nodeValue : nodeValues) {
subtreeRoot.appendChild(doc.createCDATASection(nodeValue));
}
}
private static void buildCommentList(final Document doc, final Node subtreeRoot, final List<String> nodeValues) {
for (final String nodeValue : nodeValues) {
subtreeRoot.appendChild(doc.createComment(nodeValue));
}
}
private static void buildXmlFromJsonArray(
final Document doc,
final Node listHeadNode,
final List<Object> elementContentList) {
final Node subtreeRootNode = listHeadNode.getParentNode();
/* The head node (already exists) */
buildXmlFromHashMap(doc, (Element) listHeadNode, elementContentList.get(0));
/* The rest of the list */
for (final Object elementContent : elementContentList.subList(1, elementContentList.size())) {
final Element newElement = doc.createElement(listHeadNode.getNodeName());
subtreeRootNode.appendChild(newElement);
buildXmlFromHashMap(doc, newElement, elementContent);
}
}
@SuppressWarnings("unchecked")
public static ArrayList<Element> deserializeCollection(final JsonReader reader) throws IOException {
return reader.deserializeCollectionCustom(Reader);
}
public static void deserializeCollection(final JsonReader reader, final Collection<Element> res) throws IOException {
reader.deserializeCollection(Reader, res);
}
@SuppressWarnings("unchecked")
public static ArrayList<Element> deserializeNullableCollection(final JsonReader reader) throws IOException {
return reader.deserializeNullableCollectionCustom(Reader);
}
public static void deserializeNullableCollection(final JsonReader reader, final Collection<Element> res) throws IOException {
reader.deserializeNullableCollection(Reader, res);
}
}

View File

@ -0,0 +1,182 @@
package eu.faircode.email;
/*
This file is part of FairEmail.
FairEmail 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.
FairEmail 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 FairEmail. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018-2024 by Marcel Bokhorst (M66B)
*/
import android.content.Context;
import android.content.SharedPreferences;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import androidx.preference.PreferenceManager;
import org.json.JSONException;
import org.jsoup.nodes.Document;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class AI {
private static final int MAX_SUMMARIZE_TEXT_SIZE = 10 * 1024;
static boolean isAvailable(Context context) {
return (OpenAI.isAvailable(context) || Gemini.isAvailable(context));
}
static String completeChat(Context context, long id, CharSequence body) throws JSONException, IOException {
if (body == null || body.length() == 0)
return null;
if (OpenAI.isAvailable(context)) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
String model = prefs.getString("openai_model", OpenAI.DEFAULT_MODEL);
float temperature = prefs.getFloat("openai_temperature", OpenAI.DEFAULT_TEMPERATURE);
boolean multimodal = prefs.getBoolean("openai_multimodal", true);
OpenAI.Message message;
if (body instanceof Spannable)
message = new OpenAI.Message(OpenAI.USER,
OpenAI.Content.get((Spannable) body, id, multimodal, context));
else
message = new OpenAI.Message(OpenAI.USER, new OpenAI.Content[]{
new OpenAI.Content(OpenAI.CONTENT_TEXT, body.toString())});
OpenAI.Message[] completions =
OpenAI.completeChat(context, model, new OpenAI.Message[]{message}, temperature, 1);
StringBuilder sb = new StringBuilder();
for (OpenAI.Message completion : completions)
for (OpenAI.Content content : completion.getContent())
if (OpenAI.CONTENT_TEXT.equals(content.getType())) {
if (sb.length() > 0)
sb.append('\n');
sb.append(content.getContent()
.replaceAll("^\\n+", "")
.replaceAll("\\n+$", ""));
}
return sb.toString();
} else if (Gemini.isAvailable(context)) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
String model = prefs.getString("gemini_model", Gemini.DEFAULT_MODEL);
float temperature = prefs.getFloat("gemini_temperature", Gemini.DEFAULT_TEMPERATURE);
Gemini.Message message = new Gemini.Message(Gemini.USER,
new String[]{Gemini.truncateParagraphs(body.toString())});
Gemini.Message[] completions = Gemini.generate(context, model, new Gemini.Message[]{message}, temperature, 1);
StringBuilder sb = new StringBuilder();
for (Gemini.Message completion : completions)
for (String result : completion.getContent()) {
if (sb.length() > 0)
sb.append('\n');
sb.append(result
.replaceAll("^\\n+", "")
.replaceAll("\\n+$", ""));
}
return sb.toString();
} else
return null;
}
static String getSummarizePrompt(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
if (OpenAI.isAvailable(context))
return prefs.getString("openai_summarize", OpenAI.DEFAULT_SUMMARY_PROMPT);
else if (Gemini.isAvailable(context))
return prefs.getString("gemini_summarize", Gemini.DEFAULT_SUMMARY_PROMPT);
else
return context.getString(R.string.title_summarize);
}
static String getSummaryText(Context context, EntityMessage message) throws JSONException, IOException {
File file = message.getFile(context);
if (!file.exists())
return null;
Document d = JsoupEx.parse(file);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
boolean remove_signatures = prefs.getBoolean("remove_signatures", false);
if (remove_signatures)
HtmlHelper.removeSignatures(d);
HtmlHelper.removeQuotes(d);
d = HtmlHelper.sanitizeView(context, d, false);
HtmlHelper.truncate(d, MAX_SUMMARIZE_TEXT_SIZE);
if (OpenAI.isAvailable(context)) {
String model = prefs.getString("openai_model", OpenAI.DEFAULT_MODEL);
float temperature = prefs.getFloat("openai_temperature", OpenAI.DEFAULT_TEMPERATURE);
String prompt = prefs.getString("openai_summarize", OpenAI.DEFAULT_SUMMARY_PROMPT);
boolean multimodal = prefs.getBoolean("openai_multimodal", true);
List<OpenAI.Message> input = new ArrayList<>();
input.add(new OpenAI.Message(OpenAI.USER,
new OpenAI.Content[]{new OpenAI.Content(OpenAI.CONTENT_TEXT, prompt)}));
if (!TextUtils.isEmpty(message.subject))
input.add(new OpenAI.Message(OpenAI.USER,
new OpenAI.Content[]{new OpenAI.Content(OpenAI.CONTENT_TEXT, message.subject)}));
SpannableStringBuilder ssb = HtmlHelper.fromDocument(context, d, null, null);
input.add(new OpenAI.Message(OpenAI.USER,
OpenAI.Content.get(ssb, message.id, multimodal, context)));
OpenAI.Message[] completions =
OpenAI.completeChat(context, model, input.toArray(new OpenAI.Message[0]), temperature, 1);
StringBuilder sb = new StringBuilder();
for (OpenAI.Message completion : completions)
for (OpenAI.Content content : completion.getContent())
if (OpenAI.CONTENT_TEXT.equals(content.getType())) {
if (sb.length() != 0)
sb.append('\n');
sb.append(content.getContent());
}
return sb.toString();
} else if (Gemini.isAvailable(context)) {
String model = prefs.getString("gemini_model", Gemini.DEFAULT_MODEL);
float temperature = prefs.getFloat("gemini_temperature", Gemini.DEFAULT_TEMPERATURE);
String prompt = prefs.getString("gemini_summarize", Gemini.DEFAULT_SUMMARY_PROMPT);
String text = d.text();
if (TextUtils.isEmpty(text))
return null;
Gemini.Message content = new Gemini.Message(Gemini.USER, new String[]{prompt, text});
Gemini.Message[] completions =
Gemini.generate(context, model, new Gemini.Message[]{content}, temperature, 1);
StringBuilder sb = new StringBuilder();
for (Gemini.Message completion : completions)
for (String result : completion.getContent()) {
if (sb.length() != 0)
sb.append('\n');
sb.append(result);
}
return sb.toString();
} else
return null;
}
}

View File

@ -69,11 +69,11 @@ public class ActivityAMP extends ActivityBase {
if (savedInstanceState != null)
force_light = savedInstanceState.getBoolean("fair:force_light");
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
View view = LayoutInflater.from(this).inflate(R.layout.activity_amp, null);
setContentView(view);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
wvAmp = findViewById(R.id.wvAmp);
pbWait = findViewById(R.id.pbWait);
grpReady = findViewById(R.id.grpReady);

View File

@ -54,11 +54,11 @@ public class ActivityAnswer extends ActivityBase {
final CharSequence query = intent.getCharSequenceExtra(Intent.EXTRA_PROCESS_TEXT);
final boolean readonly = intent.getBooleanExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, false);
getSupportActionBar().setSubtitle(query == null ? null : query.toString());
View view = LayoutInflater.from(this).inflate(R.layout.activity_answer, null);
setContentView(view);
getSupportActionBar().setSubtitle(query == null ? null : query.toString());
ListView lvAnswer = view.findViewById(R.id.lvAnswer);
Group grpReady = view.findViewById(R.id.grpReady);
ContentLoadingProgressBar pbWait = view.findViewById(R.id.pbWait);

View File

@ -20,8 +20,6 @@ package eu.faircode.email;
*/
import android.Manifest;
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.app.ActivityManager;
import android.content.ComponentName;
import android.content.Context;
@ -40,19 +38,26 @@ import android.os.PowerManager;
import android.os.SystemClock;
import android.text.TextUtils;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Toast;
import androidx.activity.EdgeToEdge;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.core.graphics.ColorUtils;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsAnimationCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.documentfile.provider.DocumentFile;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
@ -63,6 +68,8 @@ import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.OnLifecycleEvent;
import androidx.preference.PreferenceManager;
import com.google.android.material.appbar.AppBarLayout;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
@ -92,6 +99,131 @@ abstract class ActivityBase extends AppCompatActivity implements SharedPreferenc
return originalContext;
}
@Override
public void setContentView(View view) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
boolean hide_toolbar = prefs.getBoolean("hide_toolbar", !BuildConfig.PLAY_STORE_RELEASE);
boolean edge_to_edge = prefs.getBoolean("edge_to_edge", false);
LayoutInflater inflater = LayoutInflater.from(this);
ViewGroup holder = (ViewGroup) inflater.inflate(R.layout.toolbar_holder, null);
if (BuildConfig.DEBUG)
holder.setBackgroundColor(Color.RED);
AppBarLayout appbar = holder.findViewById(R.id.appbar);
Toolbar toolbar = holder.findViewById(R.id.toolbar);
View placeholder = holder.findViewById(R.id.placeholder);
toolbar.setPopupTheme(getThemeId());
if (hide_toolbar) {
AppBarLayout.LayoutParams params = (AppBarLayout.LayoutParams) toolbar.getLayoutParams();
params.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL |
AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS);
toolbar.setLayoutParams(params);
getSupportFragmentManager().addOnBackStackChangedListener(new FragmentManager.OnBackStackChangedListener() {
@Override
public void onBackStackChanged() {
try {
appbar.setExpanded(true);
} catch (Throwable ex) {
Log.e(ex);
}
}
});
}
setSupportActionBar(toolbar);
holder.removeView(placeholder);
holder.addView(view, placeholder.getLayoutParams());
int abh = Helper.getActionBarHeight(this);
appbar.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
@Override
public void onOffsetChanged(AppBarLayout appBarLayout, int offset) {
try {
ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
mlp.topMargin = abh + offset;
view.setLayoutParams(mlp);
} catch (Throwable ex) {
Log.e(ex);
}
}
});
FragmentDialogTheme.setBackground(this, holder, this instanceof ActivityCompose);
ViewCompat.setOnApplyWindowInsetsListener(holder, (v, windowInsets) -> {
try {
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) v.getLayoutParams();
mlp.leftMargin = insets.left;
mlp.topMargin = insets.top;
mlp.rightMargin = insets.right;
if (!edge_to_edge)
mlp.bottomMargin = insets.bottom;
v.setLayoutParams(mlp);
if (edge_to_edge)
for (View child : Helper.getViewsWithTag(v, "inset")) {
mlp = (ViewGroup.MarginLayoutParams) child.getLayoutParams();
mlp.bottomMargin = insets.bottom;
child.setLayoutParams(mlp);
}
} catch (Throwable ex) {
Log.e(ex);
}
return WindowInsetsCompat.CONSUMED;
});
if (this instanceof ActivityCompose)
ViewCompat.setWindowInsetsAnimationCallback(
holder,
new WindowInsetsAnimationCompat.Callback(WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP) {
@NonNull
@Override
public WindowInsetsCompat onProgress(
@NonNull WindowInsetsCompat windowInsets,
@NonNull List<WindowInsetsAnimationCompat> runningAnimations) {
try {
// https://developer.android.com/develop/ui/views/layout/sw-keyboard
for (WindowInsetsAnimationCompat animation : runningAnimations)
if ((animation.getTypeMask() & WindowInsetsCompat.Type.ime()) != 0) {
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
int bottom = windowInsets.getInsets(WindowInsetsCompat.Type.ime()).bottom;
int pad = bottom - insets.bottom;
holder.setPaddingRelative(0, 0, 0, pad < 0 ? 0 : pad);
break;
}
} catch (Throwable ex) {
Log.e(ex);
}
return windowInsets;
}
});
super.setContentView(holder);
int colorPrimaryDark = Helper.resolveColor(this, androidx.appcompat.R.attr.colorPrimaryDark);
view.post(new RunnableEx("setBackgroundColor") {
@Override
public void delegate() {
getWindow().getDecorView().setBackgroundColor(colorPrimaryDark);
}
});
}
@Override
public void setContentView(int layoutResID) {
View view = LayoutInflater.from(this).inflate(layoutResID, null);
setContentView(view);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
EntityLog.log(this, "Activity create " + this.getClass().getName() +
@ -104,6 +236,8 @@ abstract class ActivityBase extends AppCompatActivity implements SharedPreferenc
getSupportFragmentManager().registerFragmentLifecycleCallbacks(lifecycleCallbacks, true);
int colorPrimaryDark = Helper.resolveColor(this, androidx.appcompat.R.attr.colorPrimaryDark);
this.contacts = hasPermission(Manifest.permission.READ_CONTACTS);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
@ -116,15 +250,9 @@ abstract class ActivityBase extends AppCompatActivity implements SharedPreferenc
themeId = FragmentDialogTheme.getTheme(this);
setTheme(themeId);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
boolean dark = Helper.isDarkTheme(this);
Window window = getWindow();
View view = window.getDecorView();
int flags = view.getSystemUiVisibility();
if (dark)
flags &= ~View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
view.setSystemUiVisibility(flags);
}
EdgeToEdge.enable(this);
WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView())
.setAppearanceLightStatusBars(false);
}
String requestKey = getRequestKey();
@ -153,8 +281,6 @@ abstract class ActivityBase extends AppCompatActivity implements SharedPreferenc
prefs.registerOnSharedPreferenceChangeListener(this);
int colorPrimaryDark = Helper.resolveColor(this, androidx.appcompat.R.attr.colorPrimaryDark);
try {
Drawable d = getDrawable(R.drawable.baseline_mail_24);
Bitmap bm = Bitmap.createBitmap(
@ -184,13 +310,6 @@ abstract class ActivityBase extends AppCompatActivity implements SharedPreferenc
Log.e(ex);
}
boolean navbar_colorize = prefs.getBoolean("navbar_colorize", false);
if (navbar_colorize) {
Window window = getWindow();
if (window != null)
window.setNavigationBarColor(colorPrimaryDark);
}
FragmentManager fm = getSupportFragmentManager();
Fragment bfragment = fm.findFragmentByTag("androidx.biometric.BiometricFragment");
@ -900,80 +1019,6 @@ abstract class ActivityBase extends AppCompatActivity implements SharedPreferenc
return super.shouldUpRecreateTask(targetIntent);
}
public boolean abShowing = true;
public ValueAnimator abAnimator = null;
public boolean isActionBarShown() {
return abShowing;
}
public void showActionBar(boolean show) {
ViewGroup abv = findViewById(androidx.appcompat.R.id.action_bar);
if (abv == null)
return;
if (abShowing == show)
return;
abShowing = show;
int height = Helper.getActionBarHeight(this);
int current = abv.getLayoutParams().height;
int target = (show ? height : 0);
Log.i("ActionBar height=" + current + "..." + target);
if (abAnimator != null)
abAnimator.cancel();
abAnimator = ValueAnimator.ofInt(current, target);
abAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator anim) {
try {
Integer v = (Integer) anim.getAnimatedValue();
Log.i("ActionBar height=" + v);
ViewGroup.LayoutParams lparam = abv.getLayoutParams();
if (lparam.height == v)
Log.i("ActionBar ---");
else {
lparam.height = v;
abv.requestLayout();
}
} catch (Throwable ex) {
Log.e(ex);
}
}
});
abAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(@NonNull Animator animation) {
Log.i("ActionBar start");
}
@Override
public void onAnimationEnd(@NonNull Animator animation) {
Log.i("ActionBar end");
abAnimator = null;
}
@Override
public void onAnimationCancel(@NonNull Animator animation) {
Log.i("ActionBar cancel");
abAnimator = null;
}
@Override
public void onAnimationRepeat(@NonNull Animator animation) {
Log.i("ActionBar repeat");
}
});
abAnimator.setDuration(ACTIONBAR_ANIMATION_DURATION * Math.abs(current - target) / height);
abAnimator.start();
}
Handler getMainHandler() {
return ApplicationEx.getMainHandler();
}

View File

@ -35,9 +35,10 @@ public class ActivityClear extends ActivityBase {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_clear);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setSubtitle(getString(R.string.title_advanced_clear_all));
setContentView(R.layout.activity_clear);
btnClearAll = findViewById(R.id.btnClearAll);
btnCancel = findViewById(R.id.btnCancel);

View File

@ -90,8 +90,6 @@ public class ActivityCode extends ActivityBase {
searching = savedInstanceState.getString("fair:searching");
}
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
@ -102,6 +100,8 @@ public class ActivityCode extends ActivityBase {
View view = LayoutInflater.from(this).inflate(R.layout.activity_code, null);
setContentView(view);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
wvCode = findViewById(R.id.wvCode);
pbWait = findViewById(R.id.pbWait);
grpReady = findViewById(R.id.grpReady);

View File

@ -56,8 +56,6 @@ public class ActivityCompose extends ActivityBase implements FragmentManager.OnB
setContentView(R.layout.activity_compose);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setCustomView(R.layout.action_bar);
getSupportActionBar().setDisplayShowCustomEnabled(true);
getSupportFragmentManager().addOnBackStackChangedListener(this);

View File

@ -64,18 +64,17 @@ public class ActivityDMARC extends ActivityBase {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setSubtitle(R.string.title_advanced_dmarc_viewer);
View view = LayoutInflater.from(this).inflate(R.layout.activity_dmarc, null);
setContentView(view);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setSubtitle(R.string.title_advanced_dmarc_viewer);
tvDmarc = findViewById(R.id.tvDmarc);
pbWait = findViewById(R.id.pbWait);
grpReady = findViewById(R.id.grpReady);
// Initialize
FragmentDialogTheme.setBackground(this, view, false);
grpReady.setVisibility(View.GONE);
load();

View File

@ -46,18 +46,17 @@ public class ActivityDSN extends ActivityBase {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setSubtitle("DSN");
View view = LayoutInflater.from(this).inflate(R.layout.activity_dsn, null);
setContentView(view);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setSubtitle("DSN");
tvHeaders = findViewById(R.id.tvHeaders);
pbWait = findViewById(R.id.pbWait);
grpReady = findViewById(R.id.grpReady);
// Initialize
FragmentDialogTheme.setBackground(this, view, false);
grpReady.setVisibility(View.GONE);
load();

View File

@ -109,12 +109,12 @@ public class ActivityEML extends ActivityBase {
junk = savedInstanceState.getBoolean("fair:junk");
}
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setSubtitle("EML");
View view = LayoutInflater.from(this).inflate(R.layout.activity_eml, null);
setContentView(view);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setSubtitle("EML");
tvFrom = findViewById(R.id.tvFrom);
tvTo = findViewById(R.id.tvTo);
tvReplyTo = findViewById(R.id.tvReplyTo);
@ -207,7 +207,6 @@ public class ActivityEML extends ActivityBase {
});
// Initialize
FragmentDialogTheme.setBackground(this, view, false);
vSeparatorAttachments.setVisibility(View.GONE);
grpReady.setVisibility(View.GONE);
cardHeaders.setVisibility(View.GONE);

View File

@ -51,9 +51,10 @@ public class ActivityError extends ActivityBase {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_error);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setSubtitle(getString(R.string.title_setup_error));
setContentView(R.layout.activity_error);
tvTitle = findViewById(R.id.tvTitle);
tvMessage = findViewById(R.id.tvMessage);

View File

@ -120,10 +120,7 @@ public class ActivitySetup extends ActivityBase implements FragmentManager.OnBac
view = LayoutInflater.from(this).inflate(R.layout.activity_setup, null);
setContentView(view);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setCustomView(R.layout.action_bar);
getSupportActionBar().setDisplayShowCustomEnabled(true);
drawerLayout = findViewById(R.id.drawer_layout);
drawerLayout.setScrimColor(Helper.resolveColor(this, R.attr.colorDrawerScrim));

View File

@ -88,13 +88,13 @@ public class ActivitySignature extends ActivityBase {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
boolean monospaced = prefs.getBoolean("monospaced", false);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setSubtitle(getString(R.string.title_edit_signature));
LayoutInflater inflater = LayoutInflater.from(this);
view = (ViewGroup) inflater.inflate(R.layout.activity_signature, null, false);
setContentView(view);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setSubtitle(getString(R.string.title_edit_signature));
tvHtmlRemark = findViewById(R.id.tvHtmlRemark);
etText = findViewById(R.id.etText);
ibFull = findViewById(R.id.ibFull);
@ -208,7 +208,6 @@ public class ActivitySignature extends ActivityBase {
});
// Initialize
FragmentDialogTheme.setBackground(this, view, true);
tvHtmlRemark.setVisibility(View.GONE);
style_bar.setVisibility(View.GONE);

View File

@ -155,6 +155,7 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
private AdapterNavMenu adapterNavMenu;
private AdapterNavMenu adapterNavMenuExtra;
private boolean initialized = false;
private boolean exit = false;
private boolean searching = false;
private int lastBackStackCount = 0;
@ -217,8 +218,10 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
iff.addAction(ACTION_NEW_MESSAGE);
lbm.registerReceiver(creceiver, iff);
if (savedInstanceState != null)
if (savedInstanceState != null) {
initialized = savedInstanceState.getBoolean("fair:initialized");
searching = savedInstanceState.getBoolean("fair:searching");
}
colorDrawerScrim = Helper.resolveColor(this, R.attr.colorDrawerScrim);
@ -266,8 +269,6 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
setContentView(view);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setCustomView(R.layout.action_bar);
getSupportActionBar().setDisplayShowCustomEnabled(true);
content_separator = findViewById(R.id.content_separator);
content_pane = findViewById(R.id.content_pane);
@ -744,12 +745,15 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
boolean unified = (intent != null && "unified".equals(intent.getAction()));
if (!search && !(standalone && !unified))
init();
else
initialized = true;
}
if (savedInstanceState != null)
drawerToggle.setDrawerIndicatorEnabled(savedInstanceState.getBoolean("fair:toggle"));
checkFirst();
if (!"inbox".equals(startup))
checkFirst();
checkBanner();
checkCrash();
@ -779,6 +783,42 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
private void init() {
Bundle args = new Bundle();
if ("inbox".equals(startup)) {
new SimpleTask<EntityFolder>() {
@Override
protected void onPreExecute(Bundle args) {
initialized = false;
}
@Override
protected EntityFolder onExecute(Context context, Bundle args) throws Throwable {
DB db = DB.getInstance(context);
return db.folder().getFolderPrimary(EntityFolder.INBOX);
}
@Override
protected void onExecuted(Bundle args, EntityFolder inbox) {
FragmentBase fragment = new FragmentMessages();
if (inbox != null) {
args.putString("type", inbox.type);
args.putLong("account", inbox.account);
args.putLong("folder", inbox.id);
}
fragment.setArguments(args);
setFragment(fragment);
checkIntent();
checkFirst();
initialized = true;
}
@Override
protected void onException(Bundle args, Throwable ex) {
Log.unexpectedError(getSupportFragmentManager(), ex);
}
}.execute(this, new Bundle(), "primary");
return;
}
FragmentBase fragment;
switch (startup) {
case "accounts":
@ -798,7 +838,10 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
}
fragment.setArguments(args);
setFragment(fragment);
}
private void setFragment(Fragment fragment) {
FragmentManager fm = getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fm.beginTransaction();
for (Fragment existing : fm.getFragments())
@ -811,6 +854,7 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
protected void onSaveInstanceState(Bundle outState) {
outState.putParcelable("fair:intent", getIntent());
outState.putBoolean("fair:toggle", drawerToggle == null || drawerToggle.isDrawerIndicatorEnabled());
outState.putBoolean("fair:initialized", initialized);
outState.putBoolean("fair:searching", searching);
super.onSaveInstanceState(outState);
}
@ -1127,7 +1171,8 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
checkUpdate(false);
checkAnnouncements(false);
checkIntent();
if (initialized || !"inbox".equals(startup))
checkIntent();
}
@Override
@ -1346,8 +1391,6 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
if (count == 0)
finish();
else {
showActionBar(true);
if (count < lastBackStackCount) {
Intent intent = getIntent();
intent.setAction(null);
@ -1485,6 +1528,8 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
String last = prefs.getString("changelog", null);
if (!Objects.equals(version, last) || BuildConfig.DEBUG) {
prefs.edit().putString("changelog", version).apply();
Bundle args = new Bundle();
args.putString("name", "CHANGELOG.md");
FragmentDialogMarkdown fragment = new FragmentDialogMarkdown();
@ -1492,8 +1537,6 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
fragment.show(getSupportFragmentManager(), "changelog");
}
}
prefs.edit().putString("changelog", version).apply();
}
private void checkBanner() {

View File

@ -109,9 +109,10 @@ public class ActivityWidget extends ActivityBase {
daynight = daynight && (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S);
setContentView(R.layout.activity_widget);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setSubtitle(R.string.title_widget_title_count);
setContentView(R.layout.activity_widget);
spAccount = findViewById(R.id.spAccount);
spFolder = findViewById(R.id.spFolder);

View File

@ -66,9 +66,10 @@ public class ActivityWidgetSync extends ActivityBase {
daynight = daynight && (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S);
setContentView(R.layout.activity_widget_sync);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setSubtitle(R.string.title_widget_title_sync);
setContentView(R.layout.activity_widget_sync);
cbDayNight = findViewById(R.id.cbDayNight);
cbSemiTransparent = findViewById(R.id.cbSemiTransparent);

View File

@ -127,9 +127,10 @@ public class ActivityWidgetUnified extends ActivityBase {
daynight = daynight && (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S);
setContentView(R.layout.activity_widget_unified);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setSubtitle(R.string.title_widget_title_list);
setContentView(R.layout.activity_widget_unified);
spAccount = findViewById(R.id.spAccount);
spFolder = findViewById(R.id.spFolder);

View File

@ -708,11 +708,11 @@ public class AdapterFolder extends RecyclerView.Adapter<AdapterFolder.ViewHolder
submenu.add(Menu.FIRST, R.string.title_notify_batch_disable, 5, R.string.title_notify_batch_disable);
submenu.add(Menu.FIRST, R.string.title_unified_inbox_add, 6, R.string.title_unified_inbox_add);
submenu.add(Menu.FIRST, R.string.title_unified_inbox_delete, 7, R.string.title_unified_inbox_delete);
submenu.add(Menu.FIRST, R.string.title_navigation_folder, 6, R.string.title_navigation_folder);
submenu.add(Menu.FIRST, R.string.title_navigation_folder_hide, 7, R.string.title_navigation_folder_hide);
submenu.add(Menu.FIRST, R.string.title_synchronize_more, 8, R.string.title_synchronize_more);
submenu.add(Menu.FIRST, R.string.title_download_batch_enable, 9, R.string.title_download_batch_enable);
submenu.add(Menu.FIRST, R.string.title_download_batch_disable, 10, R.string.title_download_batch_disable);
submenu.add(Menu.FIRST, R.string.title_navigation_folder, 8, R.string.title_navigation_folder);
submenu.add(Menu.FIRST, R.string.title_navigation_folder_hide, 9, R.string.title_navigation_folder_hide);
submenu.add(Menu.FIRST, R.string.title_synchronize_more, 10, R.string.title_synchronize_more);
submenu.add(Menu.FIRST, R.string.title_download_batch_enable, 11, R.string.title_download_batch_enable);
submenu.add(Menu.FIRST, R.string.title_download_batch_disable, 12, R.string.title_download_batch_disable);
}
if (folder.account != null && folder.accountProtocol == EntityAccount.TYPE_IMAP)
@ -869,11 +869,10 @@ public class AdapterFolder extends RecyclerView.Adapter<AdapterFolder.ViewHolder
EntityOperation.sync(context, folder.id, true, !children);
if (children) {
List<EntityFolder> folders = db.folder().getChildFolders(folder.id);
if (folders != null)
for (EntityFolder child : folders)
if (child.selectable)
EntityOperation.sync(context, child.id, true);
List<EntityFolder> folders = EntityFolder.getChildFolders(context, folder.id);
for (EntityFolder child : folders)
if (child.selectable)
EntityOperation.sync(context, child.id, true);
}
if (folder.account != null) {
@ -935,10 +934,8 @@ public class AdapterFolder extends RecyclerView.Adapter<AdapterFolder.ViewHolder
DB db = DB.getInstance(context);
try {
db.beginTransaction();
List<EntityFolder> children = db.folder().getChildFolders(id);
if (children == null)
return null;
List<EntityFolder> children = EntityFolder.getChildFolders(context, id);
for (EntityFolder child : children)
db.folder().setFolderSynchronize(child.id, enabled);
@ -973,10 +970,8 @@ public class AdapterFolder extends RecyclerView.Adapter<AdapterFolder.ViewHolder
DB db = DB.getInstance(context);
try {
db.beginTransaction();
List<EntityFolder> children = db.folder().getChildFolders(id);
if (children == null)
return null;
List<EntityFolder> children = EntityFolder.getChildFolders(context, id);
for (EntityFolder child : children)
db.folder().setFolderNotify(child.id, enabled);
@ -1009,10 +1004,8 @@ public class AdapterFolder extends RecyclerView.Adapter<AdapterFolder.ViewHolder
DB db = DB.getInstance(context);
try {
db.beginTransaction();
List<EntityFolder> children = db.folder().getChildFolders(id);
if (children == null)
return null;
List<EntityFolder> children = EntityFolder.getChildFolders(context, id);
for (EntityFolder child : children)
db.folder().setFolderUnified(child.id, add);
@ -1045,10 +1038,8 @@ public class AdapterFolder extends RecyclerView.Adapter<AdapterFolder.ViewHolder
DB db = DB.getInstance(context);
try {
db.beginTransaction();
List<EntityFolder> children = db.folder().getChildFolders(id);
if (children == null)
return null;
List<EntityFolder> children = EntityFolder.getChildFolders(context, id);
for (EntityFolder child : children)
db.folder().setFolderNavigation(child.id, enabled);
@ -1092,10 +1083,8 @@ public class AdapterFolder extends RecyclerView.Adapter<AdapterFolder.ViewHolder
DB db = DB.getInstance(context);
try {
db.beginTransaction();
List<EntityFolder> children = db.folder().getChildFolders(id);
if (children == null)
return null;
List<EntityFolder> children = EntityFolder.getChildFolders(context, id);
for (EntityFolder child : children)
db.folder().setFolderDownload(child.id, enabled);

View File

@ -229,6 +229,8 @@ public class AdapterKeyword extends RecyclerView.Adapter<AdapterKeyword.ViewHold
else
prefs.edit().putInt(key, keyword.color).apply();
prefs.edit().remove("keyword." + keyword.name);
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context);
lbm.sendBroadcast(new Intent(FragmentMessages.ACTION_KEYWORDS));
}

View File

@ -179,6 +179,7 @@ import java.util.Properties;
import java.util.SortedMap;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.regex.Matcher;
import javax.mail.Address;
import javax.mail.Session;
@ -478,6 +479,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
private ImageButton ibSearchText;
private ImageButton ibSearch;
private ImageButton ibTranslate;
private ImageButton ibSummarize;
private ImageButton ibFullScreen;
private ImageButton ibForceLight;
private ImageButton ibImportance;
@ -927,6 +929,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
ibSearchText = vsBody.findViewById(R.id.ibSearchText);
ibSearch = vsBody.findViewById(R.id.ibSearch);
ibTranslate = vsBody.findViewById(R.id.ibTranslate);
ibSummarize = vsBody.findViewById(R.id.ibSummarize);
ibFullScreen = vsBody.findViewById(R.id.ibFullScreen);
ibForceLight = vsBody.findViewById(R.id.ibForceLight);
ibImportance = vsBody.findViewById(R.id.ibImportance);
@ -1099,6 +1102,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
ibSearch.setOnClickListener(this);
ibTranslate.setOnClickListener(this);
ibTranslate.setOnLongClickListener(this);
ibSummarize.setOnClickListener(this);
ibFullScreen.setOnClickListener(this);
ibForceLight.setOnClickListener(this);
ibImportance.setOnClickListener(this);
@ -1221,6 +1225,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
ibSearch.setOnClickListener(null);
ibTranslate.setOnClickListener(null);
ibTranslate.setOnLongClickListener(null);
ibSummarize.setOnClickListener(null);
ibFullScreen.setOnClickListener(null);
ibForceLight.setOnClickListener(null);
ibImportance.setOnClickListener(null);
@ -1288,6 +1293,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
message.folderUnified && outgoing) ||
EntityFolder.isOutgoing(message.folderInheritedType));
String selector = (reverse ? null : message.bimi_selector);
boolean dmarc = (!reverse && Boolean.TRUE.equals(message.dmarc));
Address[] addresses = (reverse ? message.to : (message.isForwarder() ? message.submitter : message.from));
Address[] senders = ContactInfo.fillIn(
reverse && !show_recipients ? message.to : message.senders, prefer_contact, only_contact);
@ -1661,7 +1667,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
// Contact info
ContactInfo[] info = ContactInfo.getCached(context,
message.account, message.folderType, selector, addresses);
message.account, message.folderType, selector, dmarc, addresses);
if (info == null) {
if (taskContactInfo != null) {
taskContactInfo.cancel(context);
@ -1673,6 +1679,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
aargs.putLong("account", message.account);
aargs.putString("folderType", message.folderType);
aargs.putString("selector", selector);
aargs.putBoolean("dmarc", dmarc);
aargs.putSerializable("addresses", addresses);
taskContactInfo = new SimpleTask<ContactInfo[]>() {
@ -1681,8 +1688,9 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
long account = args.getLong("account");
String folderType = args.getString("folderType");
String selector = args.getString("selector");
boolean dmarc = args.getBoolean("dmarc");
Address[] addresses = (Address[]) args.getSerializable("addresses");
return ContactInfo.get(context, account, folderType, selector, addresses);
return ContactInfo.get(context, account, folderType, selector, dmarc, addresses);
}
@Override
@ -1839,6 +1847,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
ibSearchText.setVisibility(View.GONE);
ibSearch.setVisibility(View.GONE);
ibTranslate.setVisibility(View.GONE);
ibSummarize.setVisibility(View.GONE);
ibFullScreen.setVisibility(View.GONE);
ibForceLight.setVisibility(View.GONE);
ibImportance.setVisibility(View.GONE);
@ -2135,6 +2144,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
ibSearchText.setVisibility(View.GONE);
ibSearch.setVisibility(View.GONE);
ibTranslate.setVisibility(View.GONE);
ibSummarize.setVisibility(View.GONE);
ibFullScreen.setVisibility(View.GONE);
ibForceLight.setVisibility(View.GONE);
ibImportance.setVisibility(View.GONE);
@ -2335,6 +2345,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
boolean expand_all = prefs.getBoolean("expand_all", false);
boolean expand_one = prefs.getBoolean("expand_one", true);
boolean swipe_reply = prefs.getBoolean("swipe_reply", false);
boolean tools = prefs.getBoolean("message_tools", true);
boolean button_junk = prefs.getBoolean("button_junk", true);
boolean button_trash = prefs.getBoolean("button_trash", true);
@ -2348,6 +2359,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
boolean button_hide = prefs.getBoolean("button_hide", false);
boolean button_importance = prefs.getBoolean("button_importance", false);
boolean button_translate = prefs.getBoolean("button_translate", true);
boolean button_summarize = prefs.getBoolean("button_summarize", false);
boolean button_full_screen = prefs.getBoolean("button_full_screen", false);
boolean button_force_light = prefs.getBoolean("button_force_light", true);
boolean button_search = prefs.getBoolean("button_search", false);
@ -2361,7 +2373,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
boolean button_raw = prefs.getBoolean("button_raw", false);
boolean button_unsubscribe = prefs.getBoolean("button_unsubscribe", true);
boolean button_rule = prefs.getBoolean("button_rule", false);
boolean swipe_reply = prefs.getBoolean("swipe_reply", false);
boolean button_answer = prefs.getBoolean("button_answer", false);
int importance = (((message.ui_importance == null ? 1 : message.ui_importance) + 1) % 3);
@ -2375,7 +2387,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
ibInbox.setImageResource(inJunk ? R.drawable.twotone_report_off_24 : R.drawable.twotone_inbox_24);
ibUndo.setVisibility(outbox ? View.VISIBLE : View.GONE);
ibAnswer.setVisibility(!tools || outbox || (!expand_all && expand_one) || !threading || swipe_reply ? View.GONE : View.VISIBLE);
ibAnswer.setVisibility(!tools || outbox || (!expand_all && expand_one && !button_answer) || (!threading && !button_answer) || (swipe_reply && !button_answer) ? View.GONE : View.VISIBLE);
ibRule.setVisibility(tools && button_rule && !outbox && !message.folderReadOnly ? View.VISIBLE : View.GONE);
ibUnsubscribe.setVisibility(tools && button_unsubscribe && message.unsubscribe != null ? View.VISIBLE : View.GONE);
ibRaw.setVisibility(tools && button_raw && raw ? View.VISIBLE : View.GONE);
@ -2388,6 +2400,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
ibSearchText.setVisibility(tools && !outbox && button_search_text && message.content ? View.VISIBLE : View.GONE);
ibSearch.setVisibility(tools && !outbox && button_search && (froms > 0 || tos > 0) ? View.VISIBLE : View.GONE);
ibTranslate.setVisibility(tools && !outbox && button_translate && DeepL.isAvailable(context) && message.content ? View.VISIBLE : View.GONE);
ibSummarize.setVisibility(tools && !outbox && button_summarize && AI.isAvailable(context) && message.content ? View.VISIBLE : View.GONE);
ibFullScreen.setVisibility(tools && full && button_full_screen && message.content ? View.VISIBLE : View.GONE);
ibForceLight.setVisibility(tools && full && dark && button_force_light && message.content ? View.VISIBLE : View.GONE);
ibForceLight.setImageLevel(!(canDarken || fake_dark) || force_light ? 1 : 0);
@ -2545,7 +2558,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
}
} else {
boolean homoPersonal = TextHelper.isSingleScript(personal);
if (BuildConfig.DEBUG && !homoPersonal)
if (debug && !homoPersonal)
personal = TextHelper.getNonLatinCodepoints(personal);
ssb.append(personal);
if (!homoPersonal) {
@ -2553,6 +2566,13 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
ssb.setSpan(new StyleSpan(Typeface.BOLD), start, ssb.length(), 0);
ssb.setSpan(new ForegroundColorSpan(colorError), start, ssb.length(), 0);
}
Matcher m = Helper.EMAIL_ADDRESS.matcher(personal);
while (m.find()) {
ssb.setSpan(new StyleSpan(Typeface.BOLD), m.start(), m.end(), 0);
ssb.setSpan(new ForegroundColorSpan(colorError), m.start(), m.end(), 0);
}
if (full) {
ssb.append(" <");
if (!TextUtils.isEmpty(email)) {
@ -2840,8 +2860,10 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
properties.setValue("images_asked", message.id, true);
}
if (message.from != null)
for (Address sender : message.from) {
Address[] senders = (message.isForwarder() ? message.submitter : message.from);
if (senders != null)
for (Address sender : senders) {
String from = ((InternetAddress) sender).getAddress();
if (TextUtils.isEmpty(from))
continue;
@ -4437,7 +4459,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
else if (id == R.id.ibVerified)
onShowVerified(message);
else if (id == R.id.ibAuth)
onShowAuth(message);
onShowAuth(message, null);
else if (id == R.id.ibPriority)
onShowPriority(message);
else if (id == R.id.ibSensitivity)
@ -4526,6 +4548,8 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
onSearchContact(message, false);
} else if (id == R.id.ibTranslate) {
onActionTranslate(message);
} else if (id == R.id.ibSummarize) {
onActionSummarize(message);
} else if (id == R.id.ibFullScreen)
onActionOpenFull(message);
else if (id == R.id.ibForceLight) {
@ -4889,12 +4913,15 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
}
private void onShowVerified(TupleMessageEx message) {
ToastEx.makeText(context, ibVerified.getContentDescription(), Toast.LENGTH_LONG).show();
onShowAuth(message, ibVerified.getContentDescription().toString());
}
private void onShowAuth(TupleMessageEx message) {
private void onShowAuth(TupleMessageEx message, String title) {
StringBuilder sb = new StringBuilder();
if (title != null)
sb.append(title).append('\n');
List<String> result = new ArrayList<>();
if (Boolean.FALSE.equals(message.dkim))
result.add("DKIM");
@ -4908,61 +4935,48 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
result.add("MX");
if (result.size() > 0)
sb.append(context.getString(R.string.title_authentication_failed, TextUtils.join(", ", result)));
else {
sb.append(context.getString(R.string.title_authentication_failed, TextUtils.join(", ", result)))
.append('\n');
if (authentication_indicator) {
if (check_tls)
sb.append("TLS: ")
.append(message.tls == null ? "-" : (message.tls ? "" : ""))
.append('\n');
.append(message.tls == null ? "-" : (message.tls ? "" : "")).append('\n');
sb.append("DKIM: ")
.append(message.dkim == null ? "-" : (message.dkim ? "" : ""))
.append('\n');
.append(message.dkim == null ? "-" : (message.dkim ? "" : "")).append('\n');
sb.append("SPF: ")
.append(message.spf == null ? "-" : (message.spf ? "" : ""))
.append('\n');
.append(message.spf == null ? "-" : (message.spf ? "" : "")).append('\n');
sb.append("DMARC: ")
.append(message.dmarc == null ? "-" : (message.dmarc ? "" : ""))
.append('\n');
.append(message.dmarc == null ? "-" : (message.dmarc ? "" : "")).append('\n');
if (message.auth != null)
sb.append("SMTP: ").append(message.auth ? "" : "");
sb.append("SMTP: ")
.append(message.auth ? "" : "").append('\n');
if (check_mx)
sb.append('\n')
.append("MX: ")
.append(message.mx == null ? "-" : (message.mx ? "" : ""));
sb.append("MX: ")
.append(message.mx == null ? "-" : (message.mx ? "" : "")).append('\n');
}
if (native_dkim && !TextUtils.isEmpty(message.signedby)) {
if (sb.length() > 0)
sb.append('\n');
sb.append("Signed by:");
sb.append(context.getString(R.string.title_signed_by)).append(' ');
for (String signer : message.signedby.split(","))
sb.append('\n').append(signer);
sb.append(signer).append('\n');
}
if (Boolean.TRUE.equals(message.blocklist)) {
if (sb.length() > 0)
sb.append('\n');
sb.append(context.getString(R.string.title_on_blocklist));
}
if (Boolean.TRUE.equals(message.blocklist))
sb.append(context.getString(R.string.title_on_blocklist)).append('\n');
if (Boolean.FALSE.equals(message.from_domain) && message.smtp_from != null)
for (Address smtp_from : message.smtp_from) {
String domain = UriHelper.getEmailDomain(((InternetAddress) smtp_from).getAddress());
String root = UriHelper.getRootDomain(context, domain);
if (root != null) {
if (sb.length() > 0)
sb.append('\n');
sb.append(context.getString(R.string.title_via, root));
}
if (root != null)
sb.append(context.getString(R.string.title_via, root)).append('\n');
}
if (Boolean.FALSE.equals(message.reply_domain)) {
String[] warning = message.checkReplyDomain(context);
if (warning != null) {
if (sb.length() > 0)
sb.append('\n');
sb.append(context.getString(R.string.title_reply_domain, warning[0], warning[1]));
}
if (warning != null)
sb.append(context.getString(R.string.title_reply_domain, warning[0], warning[1])).append('\n');
}
if (message.from != null && message.from.length > 0) {
@ -4972,6 +4986,9 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
sb.insert(0, '\n').insert(0, domain);
}
if (sb.length() > 0 && sb.charAt(sb.length() - 1) == '\n')
sb.deleteCharAt(sb.length() - 1);
ToastEx.makeText(context, sb.toString(), Toast.LENGTH_LONG).show();
}
@ -5433,7 +5450,12 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
for (Address from : message.from) {
if (!(from instanceof InternetAddress))
continue;
if (!TextHelper.isSingleScript(((InternetAddress) from).getPersonal()))
String personal = ((InternetAddress) from).getPersonal();
if (TextUtils.isEmpty(personal))
continue;
if (!TextHelper.isSingleScript(personal))
return true;
if (Helper.EMAIL_ADDRESS.matcher(personal).find())
return true;
}
@ -5524,15 +5546,16 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
}
boolean junk = EntityFolder.JUNK.equals(message.folderType);
Address[] senders = (message.isForwarder() ? message.submitter : message.from);
boolean current = properties.getValue(full ? "full" : "images", message.id);
boolean asked = properties.getValue(full ? "full_asked" : "images_asked", message.id);
boolean confirm = prefs.getBoolean(full ? "confirm_html" : "confirm_images", true) || junk;
boolean ask = prefs.getBoolean(full ? "ask_html" : "ask_images", true) || junk;
if (current || asked || !confirm || !ask) {
if (current && message.from != null) {
if (current && senders != null) {
SharedPreferences.Editor editor = prefs.edit();
for (Address sender : message.from) {
for (Address sender : senders) {
String from = ((InternetAddress) sender).getAddress();
if (TextUtils.isEmpty(from))
continue;
@ -5562,13 +5585,13 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
cbNotAgainSender.setVisibility(View.GONE);
cbNotAgainDomain.setVisibility(View.GONE);
cbNotAgain.setVisibility(View.GONE);
} else if (message.from == null || message.from.length == 0) {
} else if (senders == null || senders.length == 0) {
cbNotAgainSender.setVisibility(View.GONE);
cbNotAgainDomain.setVisibility(View.GONE);
} else {
List<String> froms = new ArrayList<>();
List<String> domains = new ArrayList<>();
for (Address address : message.from) {
for (Address address : senders) {
String from = ((InternetAddress) address).getAddress();
froms.add(from);
int at = from.indexOf('@');
@ -5640,8 +5663,8 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
if (!junk) {
SharedPreferences.Editor editor = prefs.edit();
if (message.from != null)
for (Address sender : message.from) {
if (senders != null)
for (Address sender : senders) {
String from = ((InternetAddress) sender).getAddress();
if (TextUtils.isEmpty(from))
continue;
@ -6181,7 +6204,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
boolean experiments = prefs.getBoolean("experiments", false);
PopupMenuLifecycle popupMenu = new PopupMenuLifecycle(context, powner, ibMore);
PopupMenuLifecycle popupMenu = new PopupMenuLifecycle(context, powner, ibMore == null ? view : ibMore);
popupMenu.inflate(R.menu.popup_message_more);
popupMenu.getMenu().findItem(R.id.menu_unseen)
@ -6235,6 +6258,8 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
popupMenu.getMenu().findItem(R.id.menu_search_in_text).setEnabled(message.content && !full);
popupMenu.getMenu().findItem(R.id.menu_translate).setVisible(
DeepL.isAvailable(context) && message.content);
popupMenu.getMenu().findItem(R.id.menu_summarize).setVisible(
AI.isAvailable(context) && message.content);
popupMenu.getMenu().findItem(R.id.menu_force_light).setVisible(full && dark);
popupMenu.getMenu().findItem(R.id.menu_force_light).setChecked(force_light);
@ -6344,6 +6369,9 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
} else if (itemId == R.id.menu_translate) {
onActionTranslate(message);
return true;
} else if (itemId == R.id.menu_summarize) {
onActionSummarize(message);
return true;
} else if (itemId == R.id.menu_force_light) {
onActionForceLight(message);
return true;
@ -6420,6 +6448,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
Log.i("Opening uri=" + uri + " title=" + title + " always confirm=" + always_confirm);
try {
uri = Uri.parse(uri.toString().replaceAll("[\r\n]", ""));
if (UriHelper.isHyperLink(uri))
uri = Uri.parse(uri.toString().trim().replaceAll("\\s+", "+"));
@ -6456,8 +6485,12 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
try {
if (ActivityBilling.activatePro(context, uri))
ToastEx.makeText(context, R.string.title_pro_valid, Toast.LENGTH_LONG).show();
else
ToastEx.makeText(context, R.string.title_pro_invalid, Toast.LENGTH_LONG).show();
else {
Uri invalid = Uri.parse(BuildConfig.PRO_FEATURES_URI + "invalid.html" +
"?challenge=" + ActivityBilling.getChallenge(context) +
"&version=" + BuildConfig.VERSION_CODE);
Helper.view(context, invalid, true);
}
} catch (NoSuchAlgorithmException ex) {
Log.e(ex);
ToastEx.makeText(context, Log.formatThrowable(ex), Toast.LENGTH_LONG).show();
@ -6569,6 +6602,8 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
}
private boolean isActivate(Uri uri) {
if ("eu.faircode.email".equals(uri.getHost()))
return true;
return ("email.faircode.eu".equals(uri.getHost()) &&
"/activate/".equals(uri.getPath()));
}
@ -7245,6 +7280,10 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
}
}
private void onActionSummarize(TupleMessageEx message) {
FragmentDialogSummarize.summarize(message, parentFragment.getParentFragmentManager());
}
private void onActionForceLight(TupleMessageEx message) {
if (canDarken || fake_dark) {
boolean force_light = !properties.getValue("force_light", message.id);
@ -7466,6 +7505,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
args.putLong("account", message.account);
args.putString("folderType", message.folderType);
args.putString("selector", message.bimi_selector);
args.putBoolean("dmarc", Boolean.TRUE.equals(message.dmarc));
args.putSerializable("addresses", message.from);
new SimpleTask<ContactInfo[]>() {
@ -7474,8 +7514,9 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
long account = args.getLong("account");
String folderType = args.getString("folderType");
String selector = args.getString("selector");
boolean dmarc = args.getBoolean("dmarc");
Address[] addresses = (Address[]) args.getSerializable("addresses");
return ContactInfo.get(context, account, folderType, selector, addresses);
return ContactInfo.get(context, account, folderType, selector, dmarc, addresses);
}
@Override
@ -7750,7 +7791,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
return tvBody.getText().subSequence(start, end);
}
private View.AccessibilityDelegate accessibilityDelegateHeader = new View.AccessibilityDelegate() {
private final View.AccessibilityDelegate accessibilityDelegateHeader = new View.AccessibilityDelegate() {
@Override
public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
super.onInitializeAccessibilityEvent(host, event);
@ -7787,63 +7828,82 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
Log.e(ex);
}
TupleMessageEx message = getMessage();
if (message == null)
return;
try {
TupleMessageEx message = getMessage();
if (message == null)
return;
boolean expanded = properties.getValue("expanded", message.id);
boolean expanded = properties.getValue("expanded", message.id);
vwColor.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
vwColor.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
if (ibExpander.getVisibility() == View.VISIBLE)
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.ibExpander,
context.getString(expanded ? R.string.title_accessibility_collapse : R.string.title_accessibility_expand)));
ibExpander.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
if (ibExpander.getVisibility() == View.VISIBLE)
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.ibExpander,
context.getString(expanded ? R.string.title_accessibility_collapse : R.string.title_accessibility_expand)));
ibExpander.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.ibSeen,
context.getString(message.ui_seen ? R.string.title_unseen : R.string.title_seen)));
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.ibSeen,
context.getString(message.ui_seen ? R.string.title_unseen : R.string.title_seen)));
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.ibAnswer,
context.getString(R.string.title_reply)));
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.ibAnswer,
context.getString(R.string.title_reply)));
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.ibArchive,
context.getString(R.string.title_archive)));
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.ibArchive,
context.getString(R.string.title_archive)));
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.ibTrash,
context.getString(R.string.title_trash)));
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.ibTrash,
context.getString(R.string.title_trash)));
if (properties.getSelectionCount() > 0)
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.ibDelete,
context.getString(R.string.title_trash_selection)));
if (properties.getSelectionCount() > 0)
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.ibDelete,
context.getString(R.string.title_trash_selection)));
if (ibAvatar.getVisibility() == View.VISIBLE && ibAvatar.isEnabled())
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.ibAvatar,
context.getString(R.string.title_accessibility_view_contact)));
ibAvatar.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
if (ibAvatar != null) {
if (ibAvatar.getVisibility() == View.VISIBLE && ibAvatar.isEnabled())
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.ibAvatar,
context.getString(R.string.title_accessibility_view_contact)));
ibAvatar.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
}
if (ibFlagged.getVisibility() == View.VISIBLE && ibFlagged.isEnabled()) {
int flagged = (message.count - message.unflagged);
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.ibFlagged,
context.getString(flagged > 0 ? R.string.title_unflag : R.string.title_flag)));
if (ibFlagged != null) {
if (ibFlagged.getVisibility() == View.VISIBLE && ibFlagged.isEnabled()) {
int flagged = (message.count - message.unflagged);
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.ibFlagged,
context.getString(flagged > 0 ? R.string.title_unflag : R.string.title_flag)));
}
ibFlagged.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
}
if (ibAuth != null) {
if (ibAuth.getVisibility() == View.VISIBLE)
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.ibAuth,
context.getString(R.string.title_accessibility_show_authentication_result)));
ibAuth.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
}
if (ibSnoozed != null) {
if (ibSnoozed.getVisibility() == View.VISIBLE)
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.ibSnoozed,
context.getString(R.string.title_accessibility_show_snooze_time)));
ibSnoozed.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
}
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.ibMore,
context.getString(R.string.title_advanced_more)));
if (ibMore != null)
ibMore.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
if (ibError != null) {
if (ibError.getVisibility() == View.VISIBLE)
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.ibError,
context.getString(R.string.title_accessibility_view_help)));
ibError.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
}
info.setContentDescription(populateContentDescription(message));
} catch (Throwable ex) {
Log.e(ex);
}
ibFlagged.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
if (ibAuth.getVisibility() == View.VISIBLE)
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.ibAuth,
context.getString(R.string.title_accessibility_show_authentication_result)));
ibAuth.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
if (ibSnoozed.getVisibility() == View.VISIBLE)
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.ibSnoozed,
context.getString(R.string.title_accessibility_show_snooze_time)));
ibSnoozed.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
if (ibError.getVisibility() == View.VISIBLE)
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.ibError,
context.getString(R.string.title_accessibility_view_help)));
ibError.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
info.setContentDescription(populateContentDescription(message));
}
@Override
@ -7877,11 +7937,14 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
onToggleFlag(message);
return true;
} else if (action == R.id.ibAuth) {
onShowAuth(message);
onShowAuth(message, null);
return true;
} else if (action == R.id.ibSnoozed) {
onShowSnoozed(message);
return true;
} else if (action == R.id.ibMore) {
onActionMore(message);
return true;
} else if (action == R.id.ibError) {
onHelp(message);
return true;
@ -8470,6 +8533,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
same = false;
log("last_attempt changed " + prev.last_attempt + "/" + next.last_attempt, next.id);
}
// last_touched
// accountPop
if (!Objects.equals(prev.accountName, next.accountName)) {
@ -8709,12 +8773,12 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
if (message != null) {
keyPosition.put(message.id, i);
positionKey.put(i, message.id);
addExtra(message.from, message.extra);
if (threading) {
if (message.senders == null || message.senders.length == 0)
message.senders = message.from;
if (message.recipients == null || message.recipients.length == 0)
message.recipients = message.to;
message.senders = merge(message.from, message.senders);
message.recipients = merge(message.to, message.recipients);
addExtra(message.senders, message.extra);
} else {
message.senders = message.from;
@ -8742,6 +8806,28 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
});
}
static Address[] merge(Address[] base, Address[] addresses) {
if (base == null || base.length == 0)
return (addresses == null ? new Address[0] : addresses);
if (addresses == null)
return base;
List<Address> result = new ArrayList<>();
result.addAll(Arrays.asList(base));
for (Address a : addresses)
for (Address b : base) {
if (a.equals(b)) {
if (a instanceof InternetAddress && b instanceof InternetAddress) {
if (Objects.equals(((InternetAddress) a).getPersonal(), ((InternetAddress) b).getPersonal()))
continue;
} else
continue;
}
result.add(a);
}
return result.toArray(new Address[0]);
}
static void addExtra(Address[] addresses, String extra) {
if (addresses == null || addresses.length == 0)
return;

View File

@ -163,9 +163,17 @@ public class AdapterNavUnified extends RecyclerView.Adapter<AdapterNavUnified.Vi
if (folder == null)
return;
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
String startup = prefs.getString("startup", "unified");
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context);
if (EntityFolder.OUTBOX.equals(folder.type))
lbm.sendBroadcast(new Intent(ActivityView.ACTION_VIEW_OUTBOX));
else if ("inbox".equals(startup) && EntityFolder.INBOX.equals(folder.type))
lbm.sendBroadcast(
new Intent(ActivityView.ACTION_VIEW_MESSAGES)
.putExtra("type", (String) null)
.putExtra("unified", true));
else if (folder.folders > 1 || folder.type == null)
lbm.sendBroadcast(
new Intent(ActivityView.ACTION_VIEW_MESSAGES)

View File

@ -143,25 +143,30 @@ public class AdapterRule extends RecyclerView.Adapter<AdapterRule.ViewHolder> {
JSONObject jcondition = new JSONObject(rule.condition);
if (jcondition.has("sender"))
conditions.add(new Condition(context.getString(R.string.title_rule_sender),
jcondition.getJSONObject("sender").optBoolean("not"),
jcondition.getJSONObject("sender").optString("value"),
jcondition.getJSONObject("sender").optBoolean("regex")));
if (jcondition.has("recipient"))
conditions.add(new Condition(context.getString(R.string.title_rule_recipient),
jcondition.getJSONObject("recipient").optBoolean("not"),
jcondition.getJSONObject("recipient").optString("value"),
jcondition.getJSONObject("recipient").optBoolean("regex")));
if (jcondition.has("subject"))
conditions.add(new Condition(context.getString(R.string.title_rule_subject),
jcondition.getJSONObject("subject").optBoolean("not"),
jcondition.getJSONObject("subject").optString("value"),
jcondition.getJSONObject("subject").optBoolean("regex")));
if (jcondition.optBoolean("attachments"))
conditions.add(new Condition(context.getString(R.string.title_rule_attachments),
null, null));
false, null, null));
if (jcondition.has("header"))
conditions.add(new Condition(context.getString(R.string.title_rule_header),
jcondition.getJSONObject("header").optBoolean("not"),
jcondition.getJSONObject("header").optString("value"),
jcondition.getJSONObject("header").optBoolean("regex")));
if (jcondition.has("body"))
conditions.add(new Condition(context.getString(R.string.title_rule_body),
jcondition.getJSONObject("body").optBoolean("not"),
jcondition.getJSONObject("body").optString("value"),
jcondition.getJSONObject("body").optBoolean("regex")));
if (jcondition.has("date")) {
@ -173,7 +178,7 @@ public class AdapterRule extends RecyclerView.Adapter<AdapterRule.ViewHolder> {
range = DF.format(after) + " - " + DF.format(before);
}
conditions.add(new Condition(context.getString(R.string.title_rule_time_abs),
range, null));
false, range, null));
}
if (jcondition.has("schedule")) {
String range = null;
@ -185,14 +190,20 @@ public class AdapterRule extends RecyclerView.Adapter<AdapterRule.ViewHolder> {
Helper.formatHour(context, end % (24 * 60));
}
conditions.add(new Condition(context.getString(R.string.title_rule_time_rel),
range, null));
false, range, null));
}
if (jcondition.has("expression"))
conditions.add(new Condition(context.getString(R.string.title_rule_expression),
false, jcondition.getString("expression"), null));
SpannableStringBuilder ssb = new SpannableStringBuilderEx();
for (Condition condition : conditions) {
if (ssb.length() > 0)
ssb.append("\n");
ssb.append(condition.name);
if (condition.not)
ssb.append(' ').append(context.getString(R.string.title_rule_not));
if (!TextUtils.isEmpty(condition.condition)) {
ssb.append(" \"");
int start = ssb.length();
@ -605,6 +616,8 @@ public class AdapterRule extends RecyclerView.Adapter<AdapterRule.ViewHolder> {
return R.string.title_rule_url;
case EntityRule.TYPE_SILENT:
return R.string.title_rule_silent;
case EntityRule.TYPE_SUMMARIZE:
return R.string.title_rule_summarize;
default:
throw new IllegalArgumentException("Unknown action type=" + type);
}
@ -612,11 +625,13 @@ public class AdapterRule extends RecyclerView.Adapter<AdapterRule.ViewHolder> {
private class Condition {
private final String name;
private boolean not;
private final String condition;
private final Boolean regex;
Condition(String name, String condition, Boolean regex) {
Condition(String name, boolean not, String condition, Boolean regex) {
this.name = name;
this.not = not;
this.condition = condition;
this.regex = regex;
}

View File

@ -20,10 +20,12 @@ package eu.faircode.email;
*/
import android.content.Context;
import android.content.SharedPreferences;
import android.net.Uri;
import android.text.TextUtils;
import androidx.annotation.Nullable;
import androidx.preference.PreferenceManager;
import org.json.JSONException;
@ -40,6 +42,7 @@ import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.regex.Pattern;
@ -403,6 +406,9 @@ public class Adguard {
try (OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) {
Helper.copy(connection.getInputStream(), os);
}
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
prefs.edit().putLong("adguard_last", new Date().getTime()).apply();
} finally {
connection.disconnect();
}

View File

@ -406,19 +406,14 @@ public class ApplicationEx extends Application
case "watchdog":
ServiceSynchronize.scheduleWatchdog(this);
break;
case "secure": // privacy
case "load_emoji": // privacy
case "shortcuts": // misc
case "language": // misc
case "wal": // misc
// Should be excluded for import
restart(this, key);
break;
case "debug":
case "log_level":
Log.setLevel(this);
FairEmailLoggingProvider.setLevel(this);
break;
default:
if (FragmentOptionsBackup.RESTART_OPTIONS.contains(key))
restart(this, key);
}
} catch (Throwable ex) {
Log.e(ex);
@ -857,6 +852,13 @@ public class ApplicationEx extends Application
} else if (version < 2168) {
if (Helper.isGoogle())
editor.putBoolean("mod", true);
} else if (version < 2170) {
if (Build.PRODUCT == null || !Build.PRODUCT.endsWith("_beta") ||
Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
editor.putBoolean("mod", false);
} else if (version < 2180) {
if (Helper.isAndroid15())
editor.putInt("last_sdk", 0);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !BuildConfig.DEBUG)
@ -866,6 +868,11 @@ public class ApplicationEx extends Application
editor.putInt("previous_version", version);
editor.putInt("version", BuildConfig.VERSION_CODE);
int last_sdk = prefs.getInt("last_sdk", Build.VERSION.SDK_INT);
if (Helper.isAndroid15() && last_sdk <= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
editor.remove("setup_reminder");
editor.putInt("last_sdk", Build.VERSION.SDK_INT);
editor.apply();
}

View File

@ -104,6 +104,7 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
private static ExecutorService executor = Helper.getBackgroundExecutor(1, "boundary");
private static final int SEARCH_LIMIT_DEVICE = 1000;
private static final int FETCH_LIMIT_SERVER = 100000;
interface IBoundaryCallbackMessages {
void onLoading();
@ -357,6 +358,7 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
criteria.with_size,
criteria.after,
criteria.before,
criteria.touched == null ? null : new Date().getTime() - criteria.touched * 3600 * 1000L,
SEARCH_LIMIT_DEVICE, state.offset);
EntityLog.log(context, "Boundary device" +
" account=" + account +
@ -470,7 +472,7 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
and.add(new FlagTerm(new Flags(Flags.Flag.FLAGGED), true));
if (and.size() == 0)
state.imessages = state.ifolder.getMessages();
state.getMessages(FETCH_LIMIT_SERVER);
else
state.imessages = state.ifolder.search(new AndTerm(and.toArray(new SearchTerm[0])));
@ -723,8 +725,10 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
EntityLog.log(context, "Search utf8=" + utf8);
SearchTerm terms = criteria.getTerms(utf8, state.ifolder.getPermanentFlags(), keywords);
if (terms == null)
return state.ifolder.getMessages();
if (terms == null) {
state.getMessages(FETCH_LIMIT_SERVER);
return state.imessages;
}
SearchSequence ss = new SearchSequence(protocol);
Argument args = ss.generateSequence(terms, utf8 ? StandardCharsets.UTF_8.name() : null);
@ -802,15 +806,20 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
return false;
}
//
if (criteria.after != null) {
if (message.received < criteria.after)
return false;
}
//
if (criteria.before != null) {
if (message.received > criteria.before)
return false;
}
//
if (criteria.after != null) {
if (message.received < criteria.after)
if (criteria.touched != null) {
if (message.last_attempt == null || message.last_attempt < new Date().getTime() - criteria.touched * 3600 * 1000L)
return false;
}
@ -876,7 +885,7 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
if (criteria.in_message) {
// This won't match <p>An <b>example</b><p> when searching for "An example"
if (contains(html, criteria.query, partial, true)) {
String text = HtmlHelper.getFullText(html);
String text = HtmlHelper.getFullText(html, false);
if (contains(text, criteria.query, partial, false))
return true;
}
@ -991,6 +1000,13 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
IMAPFolder ifolder = null;
Message[] imessages = null;
void getMessages(int max) throws MessagingException {
int total = Math.min(ifolder.getMessageCount(), max);
imessages = new Message[total];
for (int i = 1; i <= total; i++)
imessages[i - 1] = ifolder.getMessage(i);
}
void reset() {
Log.i("Boundary reset");
queued.set(0);
@ -1032,6 +1048,7 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
boolean in_junk = true;
Long after = null;
Long before = null;
Integer touched = null;
private static final String FROM = "from:";
private static final String TO = "to:";
@ -1285,6 +1302,8 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
if (with_size != null)
flags.add(context.getString(R.string.title_search_flag_size,
Helper.humanReadableByteCount(with_size)));
if (touched != null)
flags.add(context.getString(R.string.title_search_flag_touched));
return (query == null ? "" : query + " ")
+ (flags.size() > 0 ? "+" : "")
+ TextUtils.join(",", flags);
@ -1316,7 +1335,8 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
this.in_trash == other.in_trash &&
this.in_junk == other.in_junk &&
Objects.equals(this.after, other.after) &&
Objects.equals(this.before, other.before));
Objects.equals(this.before, other.before) &&
Objects.equals(this.touched, other.touched));
} else
return false;
}
@ -1366,12 +1386,16 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
if (before != null)
json.put("before", before - now.getTimeInMillis());
if (touched != null)
json.put("touched", touched);
return json;
}
public static SearchCriteria fromJsonData(JSONObject json) throws JSONException {
SearchCriteria criteria = new SearchCriteria();
criteria.query = json.optString("query");
if (!json.isNull("query"))
criteria.query = json.optString("query");
criteria.fts = json.optBoolean("fts");
criteria.in_senders = json.optBoolean("in_senders");
criteria.in_recipients = json.optBoolean("in_recipients");
@ -1414,6 +1438,9 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
if (json.has("before"))
criteria.before = json.getLong("before") + now.getTimeInMillis();
if (json.has("touched"))
criteria.touched = json.getInt("touched");
return criteria;
}
@ -1442,7 +1469,8 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
" trash=" + in_trash +
" junk=" + in_junk +
" after=" + (after == null ? "" : new Date(after)) +
" before=" + (before == null ? "" : new Date(before));
" before=" + (before == null ? "" : new Date(before)) +
" touched=" + touched;
}
}
}

View File

@ -115,7 +115,6 @@ public class ConnectionHelper {
"NO", // Norway
"PL", // Poland
"PT", // Portugal
"RE", // La Réunion
"RO", // Romania
"SK", // Slovakia
"SI", // Slovenia

View File

@ -219,15 +219,15 @@ public class ContactInfo {
}
@NonNull
static ContactInfo[] get(Context context, long account, String folderType, String selector, Address[] addresses) {
return get(context, account, folderType, selector, addresses, false);
static ContactInfo[] get(Context context, long account, String folderType, String selector, boolean dmarc, Address[] addresses) {
return get(context, account, folderType, selector, dmarc, addresses, false);
}
static ContactInfo[] getCached(Context context, long account, String folderType, String selector, Address[] addresses) {
return get(context, account, folderType, selector, addresses, true);
static ContactInfo[] getCached(Context context, long account, String folderType, String selector, boolean dmarc, Address[] addresses) {
return get(context, account, folderType, selector, dmarc, addresses, true);
}
private static ContactInfo[] get(Context context, long account, String folderType, String selector, Address[] addresses, boolean cacheOnly) {
private static ContactInfo[] get(Context context, long account, String folderType, String selector, boolean dmarc, Address[] addresses, boolean cacheOnly) {
if (addresses == null || addresses.length == 0) {
ContactInfo anonymous = getAnonymous(context);
return new ContactInfo[]{anonymous == null ? new ContactInfo() : anonymous};
@ -235,7 +235,7 @@ public class ContactInfo {
ContactInfo[] result = new ContactInfo[addresses.length];
for (int i = 0; i < addresses.length; i++) {
result[i] = _get(context, account, folderType, selector, (InternetAddress) addresses[i], cacheOnly);
result[i] = _get(context, account, folderType, selector, dmarc, (InternetAddress) addresses[i], cacheOnly);
if (result[i] == null) {
if (cacheOnly)
return null;
@ -257,7 +257,7 @@ public class ContactInfo {
private static ContactInfo _get(
Context context,
long account, String folderType,
String selector, InternetAddress address, boolean cacheOnly) {
String selector, boolean dmarc, InternetAddress address, boolean cacheOnly) {
String key = MessageHelper.formatAddresses(new Address[]{address});
synchronized (emailContactInfo) {
ContactInfo info = emailContactInfo.get(key);
@ -277,7 +277,7 @@ public class ContactInfo {
boolean avatars = prefs.getBoolean("avatars", true);
boolean prefer_contact = prefs.getBoolean("prefer_contact", false);
boolean distinguish_contacts = prefs.getBoolean("distinguish_contacts", false);
boolean bimi = (prefs.getBoolean("bimi", false) && !BuildConfig.PLAY_STORE_RELEASE);
boolean bimi = (prefs.getBoolean("bimi", false) && dmarc && !BuildConfig.PLAY_STORE_RELEASE);
boolean gravatars = (prefs.getBoolean("gravatars", false) && !BuildConfig.PLAY_STORE_RELEASE);
boolean libravatars = (prefs.getBoolean("libravatars", false) && !BuildConfig.PLAY_STORE_RELEASE);
boolean favicons = prefs.getBoolean("favicons", false);

View File

@ -64,6 +64,7 @@ import com.sun.mail.pop3.POP3Store;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
@ -204,8 +205,10 @@ class Core {
@Override
public Object doCommand(IMAPProtocol protocol) throws ProtocolException {
long ago = System.currentTimeMillis() - protocol.getTimestamp();
if (ago > 20000)
if (ago > 20000) {
Log.i("NOOP ago=" + ago + " ms");
protocol.noop();
}
return null;
}
});
@ -2074,7 +2077,7 @@ class Core {
String body = parts.getHtml(context, plain_text, charset);
File file = message.getFile(context);
Helper.writeText(file, body);
String text = HtmlHelper.getFullText(body);
String text = HtmlHelper.getFullText(body, true);
message.preview = HtmlHelper.getPreview(text);
message.language = HtmlHelper.getLanguage(context, message.subject, text);
Integer plain_only = parts.isPlainOnly();
@ -2247,7 +2250,7 @@ class Core {
String body = parts.getHtml(context, download_plain);
File file = message.getFile(context);
Helper.writeText(file, body);
String text = HtmlHelper.getFullText(body);
String text = HtmlHelper.getFullText(body, true);
message.preview = HtmlHelper.getPreview(text);
message.language = HtmlHelper.getLanguage(context, message.subject, text);
@ -3134,6 +3137,7 @@ class Core {
if (!message.content)
throw new IllegalArgumentException("Message without content id=" + rule.id + ":" + rule.name);
rule.async = true;
rule.execute(context, message, null);
}
@ -3514,6 +3518,8 @@ class Core {
}
}
}
if (!Boolean.TRUE.equals(message.dkim))
message.dmarc = message.dkim;
}
if (message.size == null && message.total != null)
@ -3609,7 +3615,7 @@ class Core {
File file = message.getFile(context);
Helper.writeText(file, body);
String text = HtmlHelper.getFullText(body);
String text = HtmlHelper.getFullText(body, true);
message.preview = HtmlHelper.getPreview(text);
message.language = HtmlHelper.getLanguage(context, message.subject, text);
db.message().setMessageContent(message.id,
@ -4676,6 +4682,8 @@ class Core {
}
}
}
if (!Boolean.TRUE.equals(message.dkim))
message.dmarc = message.dkim;
}
// Borrow reply name from sender name
@ -4896,7 +4904,7 @@ class Core {
body = parts.getHtml(context, download_plain);
File file = message.getFile(context);
Helper.writeText(file, body);
String text = HtmlHelper.getFullText(body);
String text = HtmlHelper.getFullText(body, true);
message.content = true;
message.preview = HtmlHelper.getPreview(text);
message.language = HtmlHelper.getLanguage(context, message.subject, text);
@ -5204,14 +5212,16 @@ class Core {
if (message.from != null)
addresses.addAll(Arrays.asList(message.from));
} else {
Address[] senders = (message.isForwarder() ? message.submitter : message.from);
if (message.to != null)
addresses.addAll(Arrays.asList(message.to));
if (message.cc != null)
addresses.addAll(Arrays.asList(message.cc));
if (message.bcc != null)
addresses.addAll(Arrays.asList(message.bcc));
if (message.from != null)
addresses.addAll(Arrays.asList(message.from));
if (senders != null)
addresses.addAll(Arrays.asList(senders));
}
InternetAddress deliveredto = null;
@ -5414,7 +5424,7 @@ class Core {
String body = parts.getHtml(context);
File file = message.getFile(context);
Helper.writeText(file, body);
String text = HtmlHelper.getFullText(body);
String text = HtmlHelper.getFullText(body, true);
message.preview = HtmlHelper.getPreview(text);
message.language = HtmlHelper.getLanguage(context, message.subject, text);
db.message().setMessageContent(message.id,

View File

@ -30,7 +30,9 @@ import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.FileOutputStream;
import java.lang.reflect.Field;
import java.nio.channels.FileLock;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@ -68,7 +70,7 @@ import javax.mail.internet.InternetAddress;
// https://developer.android.com/topic/libraries/architecture/room.html
@Database(
version = 291,
version = 293,
entities = {
EntityIdentity.class,
EntityAccount.class,
@ -123,6 +125,7 @@ public abstract class DB extends RoomDatabase {
static final String DB_NAME = "fairemail";
static final int DEFAULT_QUERY_THREADS = 4; // AndroidX default thread count: 4
static final int DEFAULT_CACHE_SIZE = 20; // percentage of memory class
private static final long DB_LOCK_TIMEOUT = 60 * 1000L;
private static final int DB_JOURNAL_SIZE_LIMIT = 1048576; // requery/sqlite-android default
private static final int DB_CHECKPOINT = 1000; // requery/sqlite-android default
@ -148,7 +151,7 @@ public abstract class DB extends RoomDatabase {
File dbfile = configuration.context.getDatabasePath(DB_NAME);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(configuration.context);
boolean sqlite_integrity_check = prefs.getBoolean("sqlite_integrity_check", true);
boolean sqlite_integrity_check = prefs.getBoolean("sqlite_integrity_check", false);
// https://www.sqlite.org/pragma.html#pragma_integrity_check
if (sqlite_integrity_check && dbfile.exists()) {
@ -416,6 +419,8 @@ public abstract class DB extends RoomDatabase {
Log.i("Disabled view invalidation");
} catch (ReflectiveOperationException ex) {
// Should never happen
Log.forceCrashReporting();
Log.e(ex);
}
@ -425,6 +430,42 @@ public abstract class DB extends RoomDatabase {
Log.d("ROOM invalidated=" + TextUtils.join(",", tables));
}
});
// Ref: https://android-review.googlesource.com/c/platform/frameworks/support/+/1797472
Log.i("DB critical section start");
File dbDir = context.getDatabasePath(DB_NAME).getParentFile();
dbDir.mkdirs();
File lockFile = new File(dbDir, DB_NAME + ".lock");
try (FileOutputStream fos = new FileOutputStream(lockFile)) {
ObjectHolder<FileLock> lock = new ObjectHolder<>(null);
try {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
lock.value = fos.getChannel().lock();
} catch (Throwable ex) {
Log.e(ex);
}
}
});
thread.start();
thread.join(DB_LOCK_TIMEOUT);
if (thread.isAlive())
throw new IllegalArgumentException("DB critical section failed");
// Force migration
sInstance.getOpenHelper().getWritableDatabase();
} finally {
if (lock.value != null)
lock.value.release();
}
} catch (Throwable ex) {
// Should never happen
Log.forceCrashReporting();
Log.e(ex);
}
Log.i("DB critical section end");
}
return sInstance;
@ -547,6 +588,7 @@ public abstract class DB extends RoomDatabase {
at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper.getWritableDatabase(FrameworkSQLiteOpenHelper.kt:104)
at androidx.room.RoomDatabase.inTransaction(RoomDatabase.java:706)
*/
// Should never happen
Log.forceCrashReporting();
Log.e(ex);
// FrameworkSQLiteOpenHelper.innerGetDatabase will delete the database
@ -2950,6 +2992,18 @@ public abstract class DB extends RoomDatabase {
logMigration(startVersion, endVersion);
db.execSQL("ALTER TABLE `folder` ADD COLUMN `last_view` INTEGER");
}
}).addMigrations(new Migration(291, 292) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase db) {
logMigration(startVersion, endVersion);
db.execSQL("ALTER TABLE `identity` ADD COLUMN `envelopeFrom` TEXT");
}
}).addMigrations(new Migration(292, 293) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase db) {
logMigration(startVersion, endVersion);
db.execSQL("ALTER TABLE `message` ADD COLUMN `last_touched` INTEGER");
}
}).addMigrations(new Migration(998, 999) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase db) {

View File

@ -245,8 +245,8 @@ public interface DaoFolder {
" JOIN account ON account.id = folder.account" +
" WHERE account.synchronize" +
" AND account.`primary`" +
" AND type = '" + EntityFolder.DRAFTS + "'")
EntityFolder getPrimaryDrafts();
" AND type = :type")
EntityFolder getFolderPrimary(String type);
@Query("SELECT * FROM folder WHERE type = '" + EntityFolder.OUTBOX + "'")
EntityFolder getOutbox();

View File

@ -32,8 +32,8 @@ public interface DaoLog {
" WHERE time > :from" +
" AND (:type IS NULL OR type = :type)" +
" ORDER BY time DESC" +
" LIMIT 2000")
LiveData<List<EntityLog>> liveLogs(long from, Integer type);
" LIMIT :limit")
LiveData<List<EntityLog>> liveLogs(long from, int limit, Integer type);
@Query("SELECT * FROM log" +
" WHERE time > :from" +

View File

@ -32,8 +32,6 @@ import androidx.room.Update;
import java.util.List;
import javax.mail.Address;
@Dao
public interface DaoMessage {
@ -77,15 +75,7 @@ public interface DaoMessage {
" OR (NOT :found AND :type IS NULL AND folder.unified)" +
" OR (NOT :found AND folder.type = :type)" +
" THEN message.received ELSE 0 END) AS dummy" +
" FROM (SELECT * FROM message" +
" WHERE message.thread IN" +
" (SELECT DISTINCT mm.thread FROM folder ff" +
" JOIN message mm ON mm.folder = ff.id" +
" WHERE ((:found AND mm.ui_found)" +
" OR (NOT :found AND :type IS NULL AND ff.unified)" +
" OR (NOT :found AND :type IS NOT NULL AND ff.type = :type))" +
" AND (NOT mm.ui_hide OR :debug))" +
" ORDER BY received DESC) AS message" + // group_concat
" FROM message" +
" JOIN account_view AS account ON account.id = message.account" +
" LEFT JOIN identity_view AS identity ON identity.id = message.identity" +
" JOIN folder_view AS folder ON folder.id = message.folder" +
@ -97,6 +87,7 @@ public interface DaoMessage {
" HAVING (SUM((:found AND message.ui_found)" +
" OR (NOT :found AND :type IS NULL AND folder.unified)" +
" OR (NOT :found AND :type IS NOT NULL AND folder.type = :type)) > 0)" + // thread can be the same in different accounts
" AND SUM(NOT message.ui_hide OR :debug) > 0" +
" AND (NOT :filter_seen OR SUM(1 - message.ui_seen) > 0)" +
" AND (NOT :filter_unflagged OR COUNT(message.id) - SUM(1 - message.ui_flagged) > 0)" +
" AND (NOT :filter_unknown OR SUM(message.avatar IS NOT NULL AND message.sender <> identity.email) > 0)" +
@ -114,6 +105,7 @@ public interface DaoMessage {
" WHEN 'size' = :sort1 THEN -SUM(message.total)" +
" WHEN 'attachments' = :sort1 THEN -SUM(message.attachments)" +
" WHEN 'snoozed' = :sort1 THEN SUM(CASE WHEN message.ui_snoozed IS NULL THEN 0 ELSE 1 END) = 0" +
" WHEN 'touched' = :sort1 THEN IFNULL(-message.last_touched, 0)" +
" ELSE 0" +
" END" +
", CASE" +
@ -160,13 +152,7 @@ public interface DaoMessage {
" (:found AND folder.type <> '" + EntityFolder.ARCHIVE + "' AND NOT (" + is_outgoing + "))" +
" OR (NOT :found AND folder.id = :folder)" +
" THEN message.received ELSE 0 END) AS dummy" +
" FROM (SELECT * FROM message" +
" WHERE message.thread IN" +
" (SELECT DISTINCT mm.thread FROM message mm" +
" WHERE mm.folder = :folder" +
" AND (NOT mm.ui_hide OR :debug)" +
" AND (NOT :found OR mm.ui_found))" +
" ORDER BY received DESC) AS message" + // group_concat
" FROM message" +
" JOIN account_view AS account ON account.id = message.account" +
" LEFT JOIN identity_view AS identity ON identity.id = message.identity" +
" JOIN folder_view AS folder ON folder.id = message.folder" +
@ -176,7 +162,10 @@ public interface DaoMessage {
" AND (NOT message.ui_hide OR :debug)" +
" AND (NOT :found OR message.ui_found = :found)" +
" GROUP BY CASE WHEN message.thread IS NULL OR NOT :threading THEN message.id ELSE message.thread END" +
" HAVING (NOT :filter_seen OR SUM(1 - message.ui_seen) > 0 OR " + is_outbox + ")" +
" HAVING (SUM((:found AND message.ui_found)" +
" OR (NOT :found AND message.folder = :folder)) > 0)" +
" AND SUM(NOT message.ui_hide OR :debug) > 0" +
" AND (NOT :filter_seen OR SUM(1 - message.ui_seen) > 0 OR " + is_outbox + ")" +
" AND (NOT :filter_unflagged OR COUNT(message.id) - SUM(1 - message.ui_flagged) > 0 OR " + is_outbox + ")" +
" AND (NOT :filter_unknown OR SUM(message.avatar IS NOT NULL AND message.sender <> identity.email) > 0" +
" OR " + is_outbox + " OR " + is_drafts + " OR " + is_sent + ")" +
@ -193,6 +182,7 @@ public interface DaoMessage {
" WHEN 'size' = :sort1 THEN -SUM(message.total)" +
" WHEN 'attachments' = :sort1 THEN -SUM(message.attachments)" +
" WHEN 'snoozed' = :sort1 THEN SUM(CASE WHEN message.ui_snoozed IS NULL THEN 0 ELSE 1 END) = 0" +
" WHEN 'touched' = :sort1 THEN IFNULL(-message.last_touched, 0)" +
" ELSE 0" +
" END" +
", CASE" +
@ -345,6 +335,12 @@ public interface DaoMessage {
" ORDER BY message.received DESC")
List<Long> getMessageIdsByFolder(Long folder);
@Query("SELECT identity, COUNT(*) AS count" +
" FROM message" +
" WHERE folder = :folder" +
" GROUP BY identity")
List<TupleIdentityCount> getIdentitiesByFolder(long folder);
@Transaction
@Query("SELECT message.id FROM message" +
" JOIN folder_view AS folder ON folder.id = message.folder" +
@ -383,9 +379,10 @@ public interface DaoMessage {
" AND (:size IS NULL OR total > :size)" +
" AND (:after IS NULL OR received > :after)" +
" AND (:before IS NULL OR received < :before)" +
" AND (:touched IS NULL OR last_touched > :touched)" +
" AND NOT message.folder IN (:exclude)" +
" GROUP BY message.id" +
" ORDER BY received DESC" +
" ORDER BY CASE WHEN :touched IS NULL THEN received ELSE last_touched END DESC" +
" LIMIT :limit OFFSET :offset")
List<TupleMatch> matchMessages(
Long account, Long folder, long[] exclude, String find,
@ -393,7 +390,7 @@ public interface DaoMessage {
boolean unseen, boolean flagged, boolean hidden, boolean encrypted, boolean with_attachments, boolean with_notes,
int type_count, String[] types,
Integer size,
Long after, Long before,
Long after, Long before, Long touched,
int limit, int offset);
@Query("SELECT id" +
@ -926,7 +923,10 @@ public interface DaoMessage {
int setMessageVerified(long id, boolean verified);
@Query("UPDATE message SET last_attempt = :last_attempt WHERE id = :id AND NOT (last_attempt IS :last_attempt)")
int setMessageLastAttempt(long id, long last_attempt);
int setMessageLastAttempt(long id, Long last_attempt);
@Query("UPDATE message SET last_touched = :last_touched WHERE id = :id AND NOT (last_touched IS :last_touched)")
int setMessageLastTouched(long id, Long last_touched);
@Query("UPDATE message SET ui_ignored = 1" +
" WHERE NOT ui_ignored" +
@ -1068,4 +1068,172 @@ public interface DaoMessage {
" ORDER BY received DESC" +
" LIMIT :keep)")
int deleteMessagesKeep(long folder, int keep);
@Transaction
@SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
@Query("SELECT message.*" +
", account.pop AS accountProtocol, account.name AS accountName, account.category AS accountCategory, COALESCE(identity.color, folder.color, account.color) AS accountColor" +
", account.notify AS accountNotify, account.summary AS accountSummary, account.leave_deleted AS accountLeaveDeleted, account.auto_seen AS accountAutoSeen" +
", folder.name AS folderName, folder.color AS folderColor, folder.display AS folderDisplay, folder.type AS folderType, NULL AS folderInheritedType, folder.unified AS folderUnified, folder.read_only AS folderReadOnly" +
", IFNULL(identity.display, identity.name) AS identityName, identity.email AS identityEmail, identity.color AS identityColor, identity.synchronize AS identitySynchronize" +
", '[' || substr(group_concat(message.`from`, ','), 0, 2048) || ']' AS senders" +
", '[' || substr(group_concat(message.`to`, ','), 0, 2048) || ']' AS recipients" +
", COUNT(message.id) AS count" +
", SUM(1 - message.ui_seen) AS unseen" +
", SUM(1 - message.ui_flagged) AS unflagged" +
", SUM(folder.type = '" + EntityFolder.DRAFTS + "') AS drafts" +
", COUNT(DISTINCT" +
" CASE WHEN NOT message.hash IS NULL THEN message.hash" +
" WHEN NOT message.msgid IS NULL THEN message.msgid" +
" ELSE message.id END) AS visible" +
", COUNT(DISTINCT" +
" CASE WHEN message.ui_seen THEN NULL" +
" WHEN NOT message.hash IS NULL THEN message.hash" +
" WHEN NOT message.msgid IS NULL THEN message.msgid" +
" ELSE message.id END) AS visible_unseen" +
", SUM(message.attachments) AS totalAttachments" +
", SUM(message.total) AS totalSize" +
", message.priority AS ui_priority" +
", message.importance AS ui_importance" +
", MAX(CASE WHEN" +
" (:found AND folder.type <> '" + EntityFolder.ARCHIVE + "' AND NOT (" + is_outgoing + "))" +
" OR (NOT :found AND :type IS NULL AND folder.unified)" +
" OR (NOT :found AND folder.type = :type)" +
" THEN message.received ELSE 0 END) AS dummy" +
" FROM (SELECT * FROM message" +
" WHERE message.thread IN" +
" (SELECT DISTINCT mm.thread FROM folder ff" +
" JOIN message mm ON mm.folder = ff.id" +
" WHERE ((:found AND mm.ui_found)" +
" OR (NOT :found AND :type IS NULL AND ff.unified)" +
" OR (NOT :found AND :type IS NOT NULL AND ff.type = :type))" +
" AND (NOT mm.ui_hide OR :debug))" +
" ORDER BY received DESC) AS message" + // group_concat
" JOIN account_view AS account ON account.id = message.account" +
" LEFT JOIN identity_view AS identity ON identity.id = message.identity" +
" JOIN folder_view AS folder ON folder.id = message.folder" +
" WHERE account.`synchronize`" +
" AND (:threading OR (:type IS NULL AND (folder.unified OR :found)) OR (:type IS NOT NULL AND folder.type = :type))" +
" AND (NOT message.ui_hide OR :debug)" +
" AND (NOT :found OR message.ui_found = :found)" +
" GROUP BY account.id, CASE WHEN message.thread IS NULL OR NOT :threading THEN message.id ELSE message.thread END" +
" HAVING (SUM((:found AND message.ui_found)" +
" OR (NOT :found AND :type IS NULL AND folder.unified)" +
" OR (NOT :found AND :type IS NOT NULL AND folder.type = :type)) > 0)" + // thread can be the same in different accounts
" AND SUM(NOT message.ui_hide OR :debug) > 0" +
" AND (NOT :filter_seen OR SUM(1 - message.ui_seen) > 0)" +
" AND (NOT :filter_unflagged OR COUNT(message.id) - SUM(1 - message.ui_flagged) > 0)" +
" AND (NOT :filter_unknown OR SUM(message.avatar IS NOT NULL AND message.sender <> identity.email) > 0)" +
" AND (NOT :filter_snoozed OR message.ui_snoozed IS NULL OR " + is_drafts + ")" +
" AND (NOT :filter_deleted OR NOT message.ui_deleted)" +
" AND (:filter_language IS NULL OR SUM(message.language = :filter_language) > 0)" +
" ORDER BY CASE WHEN :found THEN 0 ELSE -IFNULL(message.importance, 1) END" +
", CASE WHEN :group_category THEN account.category ELSE '' END COLLATE NOCASE" +
", CASE" +
" WHEN 'unread' = :sort1 THEN SUM(1 - message.ui_seen) = 0" +
" WHEN 'starred' = :sort1 THEN COUNT(message.id) - SUM(1 - message.ui_flagged) = 0" +
" WHEN 'priority' = :sort1 THEN -IFNULL(message.priority, 1)" +
" WHEN 'sender' = :sort1 THEN LOWER(message.sender)" +
" WHEN 'subject' = :sort1 THEN LOWER(message.subject)" +
" WHEN 'size' = :sort1 THEN -SUM(message.total)" +
" WHEN 'attachments' = :sort1 THEN -SUM(message.attachments)" +
" WHEN 'snoozed' = :sort1 THEN SUM(CASE WHEN message.ui_snoozed IS NULL THEN 0 ELSE 1 END) = 0" +
" WHEN 'touched' = :sort1 THEN IFNULL(-message.last_touched, 0)" +
" ELSE 0" +
" END" +
", CASE" +
" WHEN 'unread' = :sort2 THEN SUM(1 - message.ui_seen) = 0" +
" WHEN 'starred' = :sort2 THEN COUNT(message.id) - SUM(1 - message.ui_flagged) = 0" +
" ELSE 0" +
" END" +
", CASE WHEN :ascending THEN message.received ELSE -message.received END")
DataSource.Factory<Integer, TupleMessageEx> pagedUnifiedLegacy(
String type,
boolean threading, boolean group_category,
String sort1, String sort2, boolean ascending,
boolean filter_seen, boolean filter_unflagged, boolean filter_unknown, boolean filter_snoozed, boolean filter_deleted, String filter_language,
boolean found,
boolean debug);
@Transaction
@SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
@Query("SELECT message.*" +
", account.pop AS accountProtocol, account.name AS accountName, account.category AS accountCategory, COALESCE(identity.color, folder.color, account.color) AS accountColor" +
", account.notify AS accountNotify, account.summary AS accountSummary, account.leave_deleted AS accountLeaveDeleted, account.auto_seen AS accountAutoSeen" +
", folder.name AS folderName, folder.color AS folderColor, folder.display AS folderDisplay, folder.type AS folderType, f.inherited_type AS folderInheritedType, folder.unified AS folderUnified, folder.read_only AS folderReadOnly" +
", IFNULL(identity.display, identity.name) AS identityName, identity.email AS identityEmail, identity.color AS identityColor, identity.synchronize AS identitySynchronize" +
", '[' || substr(group_concat(message.`from`, ','), 0, 2048) || ']' AS senders" +
", '[' || substr(group_concat(message.`to`, ','), 0, 2048) || ']' AS recipients" +
", COUNT(message.id) AS count" +
", SUM(1 - message.ui_seen) AS unseen" +
", SUM(1 - message.ui_flagged) AS unflagged" +
", SUM(folder.type = '" + EntityFolder.DRAFTS + "') AS drafts" +
", COUNT(DISTINCT" +
" CASE WHEN NOT message.hash IS NULL THEN message.hash" +
" WHEN NOT message.msgid IS NULL THEN message.msgid" +
" ELSE message.id END) AS visible" +
", COUNT(DISTINCT" +
" CASE WHEN message.ui_seen THEN NULL" +
" WHEN NOT message.hash IS NULL THEN message.hash" +
" WHEN NOT message.msgid IS NULL THEN message.msgid" +
" ELSE message.id END) AS visible_unseen" +
", SUM(message.attachments) AS totalAttachments" +
", SUM(message.total) AS totalSize" +
", message.priority AS ui_priority" +
", message.importance AS ui_importance" +
", MAX(CASE WHEN" +
" (:found AND folder.type <> '" + EntityFolder.ARCHIVE + "' AND NOT (" + is_outgoing + "))" +
" OR (NOT :found AND folder.id = :folder)" +
" THEN message.received ELSE 0 END) AS dummy" +
" FROM (SELECT * FROM message" +
" WHERE message.thread IN" +
" (SELECT DISTINCT mm.thread FROM message mm" +
" WHERE mm.folder = :folder" +
" AND (NOT mm.ui_hide OR :debug)" +
" AND (NOT :found OR mm.ui_found))" +
" ORDER BY received DESC) AS message" + // group_concat
" JOIN account_view AS account ON account.id = message.account" +
" LEFT JOIN identity_view AS identity ON identity.id = message.identity" +
" JOIN folder_view AS folder ON folder.id = message.folder" +
" JOIN folder_view AS f ON f.id = :folder" +
" WHERE (message.account = f.account OR message.account = identity.account OR " + is_outbox + ")" +
" AND (:threading OR folder.id = :folder)" +
" AND (NOT message.ui_hide OR :debug)" +
" AND (NOT :found OR message.ui_found = :found)" +
" GROUP BY CASE WHEN message.thread IS NULL OR NOT :threading THEN message.id ELSE message.thread END" +
" HAVING (SUM((:found AND message.ui_found)" +
" OR (NOT :found AND message.folder = :folder)) > 0)" +
" AND SUM(NOT message.ui_hide OR :debug) > 0" +
" AND (NOT :filter_seen OR SUM(1 - message.ui_seen) > 0 OR " + is_outbox + ")" +
" AND (NOT :filter_unflagged OR COUNT(message.id) - SUM(1 - message.ui_flagged) > 0 OR " + is_outbox + ")" +
" AND (NOT :filter_unknown OR SUM(message.avatar IS NOT NULL AND message.sender <> identity.email) > 0" +
" OR " + is_outbox + " OR " + is_drafts + " OR " + is_sent + ")" +
" AND (NOT :filter_snoozed OR message.ui_snoozed IS NULL OR " + is_outbox + " OR " + is_drafts + ")" +
" AND (NOT :filter_deleted OR NOT message.ui_deleted)" +
" AND (:filter_language IS NULL OR SUM(message.language = :filter_language) > 0 OR " + is_outbox + ")" +
" ORDER BY CASE WHEN :found THEN 0 ELSE -IFNULL(message.importance, 1) END" +
", CASE" +
" WHEN 'unread' = :sort1 THEN SUM(1 - message.ui_seen) = 0" +
" WHEN 'starred' = :sort1 THEN COUNT(message.id) - SUM(1 - message.ui_flagged) = 0" +
" WHEN 'priority' = :sort1 THEN -IFNULL(message.priority, 1)" +
" WHEN 'sender' = :sort1 THEN LOWER(message.sender)" +
" WHEN 'subject' = :sort1 THEN LOWER(message.subject)" +
" WHEN 'size' = :sort1 THEN -SUM(message.total)" +
" WHEN 'attachments' = :sort1 THEN -SUM(message.attachments)" +
" WHEN 'snoozed' = :sort1 THEN SUM(CASE WHEN message.ui_snoozed IS NULL THEN 0 ELSE 1 END) = 0" +
" WHEN 'touched' = :sort1 THEN IFNULL(-message.last_touched, 0)" +
" ELSE 0" +
" END" +
", CASE" +
" WHEN 'unread' = :sort2 THEN SUM(1 - message.ui_seen) = 0" +
" WHEN 'starred' = :sort2 THEN COUNT(message.id) - SUM(1 - message.ui_flagged) = 0" +
" ELSE 0" +
" END" +
", CASE WHEN :ascending THEN message.received ELSE -message.received END")
DataSource.Factory<Integer, TupleMessageEx> pagedFolderLegacy(
long folder, boolean threading,
String sort1, String sort2, boolean ascending,
boolean filter_seen, boolean filter_unflagged, boolean filter_unknown, boolean filter_snoozed, boolean filter_deleted, String filter_language,
boolean found,
boolean debug);
}

View File

@ -43,6 +43,7 @@ public interface DaoOperation {
" WHEN operation.name = '" + EntityOperation.DOWNLOAD + "' THEN 3" +
" WHEN operation.name = '" + EntityOperation.EXISTS + "' THEN 3" +
" WHEN operation.name = '" + EntityOperation.REPORT + "' THEN 3" +
" WHEN operation.name = '" + EntityOperation.SUBJECT + "' THEN 3" +
" WHEN operation.name = '" + EntityOperation.COPY + "' THEN 4" +
" WHEN operation.name = '" + EntityOperation.MOVE + "' THEN 5" +
" WHEN operation.name = '" + EntityOperation.PURGE + "' THEN 6" +

View File

@ -891,6 +891,7 @@ public class DebugHelper {
identity.display + " " + identity.email +
(identity.self ? "" : " !self") +
" [" + (identity.provider == null ? "" : identity.provider) +
":" + identity.user +
":" + ServiceAuthenticator.getAuthTypeName(identity.auth_type) + "]" +
(TextUtils.isEmpty(identity.sender_extra_regex) ? "" : " regex=" + identity.sender_extra_regex) +
(!identity.sender_extra ? "" : " edit" +
@ -1627,7 +1628,7 @@ public class DebugHelper {
sb.append(scheme);
}
if (tabs && BuildConfig.DEBUG)
if (tabs && BuildConfig.DEBUG && false)
try {
boolean bindable = context.bindService(serviceIntent, new CustomTabsServiceConnection() {
@Override

View File

@ -79,9 +79,7 @@ public class DeepL {
static final String PRIVACY_URI = "https://www.deepl.com/privacy/";
// curl https://api-free.deepl.com/v2/languages \
// -d auth_key=... \
// -d type=target
// curl https://api-free.deepl.com/v2/languages -d auth_key=... -d type=target
public static boolean isAvailable(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
@ -117,7 +115,9 @@ public class DeepL {
int frequency = prefs.getInt("translated_" + target, 0);
String flag;
if ("CS".equals(target))
if ("AR".equals(target))
flag = "SA";
else if ("CS".equals(target))
flag = "CZ";
else if ("DA".equals(target))
flag = "DK";

View File

@ -37,6 +37,7 @@ import java.io.IOException;
import java.io.OutputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
@ -132,6 +133,9 @@ public class DisconnectBlacklist {
try (OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) {
Helper.copy(connection.getInputStream(), os);
}
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
prefs.edit().putLong("disconnect_last", new Date().getTime()).apply();
} finally {
connection.disconnect();
}

View File

@ -62,7 +62,6 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
@ -182,7 +181,7 @@ public class DnsHelper {
throw new IllegalArgumentException(type);
}
ResolverApi resolver = DnssecResolverApi.INSTANCE;
ResolverApi resolver = (dnssec ? DnssecResolverApi.INSTANCE : ResolverApi.INSTANCE);
AbstractDnsClient client = resolver.getClient();
if (false) {
@ -566,7 +565,7 @@ public class DnsHelper {
request.connect();
int status = request.getResponseCode();
if (status != HttpURLConnection.HTTP_OK)
if (status != HttpsURLConnection.HTTP_OK)
throw new IOException("Error " + status + ": " + request.getResponseMessage());
ByteArrayOutputStream bos = new ByteArrayOutputStream();

View File

@ -332,6 +332,10 @@ public class EmailService implements AutoCloseable {
properties.put("mail." + protocol + ".ignorebodystructuresize", Boolean.toString(enabled));
}
void setMailFrom(String address) {
properties.put("mail." + protocol + ".from", address);
}
void setSendPartial(boolean enabled) {
properties.put("mail." + protocol + ".sendpartial", Boolean.toString(enabled));
}
@ -516,7 +520,7 @@ public class EmailService implements AutoCloseable {
if (auth == AUTH_TYPE_GMAIL || auth == AUTH_TYPE_OAUTH) {
try {
EntityLog.log(context, EntityLog.Type.Debug,
EntityLog.log(context, EntityLog.Type.Debug1,
ex + "\n" + android.util.Log.getStackTraceString(ex));
authenticator.refreshToken(true);
connect(dnssec, host, port, auth, user, factory);

View File

@ -39,7 +39,6 @@ import org.json.JSONObject;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
@ -156,21 +155,32 @@ public class EntityContact implements Serializable {
if (type == TYPE_FROM) {
if (message.reply == null || message.reply.length == 0) {
if (message.from != null)
addresses.addAll(Arrays.asList(message.from));
addresses.addAll(filterAddresses(message.from));
} else
addresses.addAll(Arrays.asList(message.reply));
addresses.addAll(filterAddresses(message.reply));
} else if (type == TYPE_TO) {
if (message.to != null)
addresses.addAll(Arrays.asList(message.to));
addresses.addAll(filterAddresses(message.to));
if (message.cc != null)
addresses.addAll(Arrays.asList(message.cc));
addresses.addAll(filterAddresses(message.cc));
if (message.bcc != null)
addresses.addAll(Arrays.asList(message.bcc));
addresses.addAll(filterAddresses(message.bcc));
}
update(context, folder.account, message.identity, addresses.toArray(new Address[0]), type, message.received);
}
private static List<Address> filterAddresses(Address[] addresses) {
List<Address> result = new ArrayList<>();
if (addresses != null)
for (Address address : addresses)
if (!MessageHelper.isNoReply(address))
result.add(address);
return result;
}
public static void update(Context context, long account, Long identity, Address[] addresses, int type, long time) {
update(context, account, identity, addresses, null, type, time);
}

View File

@ -415,6 +415,16 @@ public class EntityFolder extends EntityOrder implements Serializable {
return outbox;
}
static List<EntityFolder> getChildFolders(Context context, long id) {
DB db = DB.getInstance(context);
List<EntityFolder> children = db.folder().getChildFolders(id);
if (children == null)
children = new ArrayList<>();
for (EntityFolder child : new ArrayList<>(children))
children.addAll(getChildFolders(context, child.id));
return children;
}
static String getNotificationChannelId(long id) {
return "notification.folder." + id;
}

View File

@ -111,6 +111,7 @@ public class EntityIdentity {
public String replyto;
public String cc;
public String bcc;
public String envelopeFrom;
public String internal;
public String uri; // linked contact
@NonNull

View File

@ -49,7 +49,7 @@ public class EntityLog {
private static Long last_cleanup = null;
private static final long LOG_CLEANUP_INTERVAL = 3600 * 1000L; // milliseconds
private static final long LOG_KEEP_DURATION = 12 * 3600 * 1000L; // milliseconds
private static final long LOG_KEEP_DURATION = (BuildConfig.DEBUG ? 24 : 12) * 3600 * 1000L; // milliseconds
private static final int LOG_DELETE_BATCH_SIZE = 50;
@PrimaryKey(autoGenerate = true)
@ -65,7 +65,7 @@ public class EntityLog {
@NonNull
public String data;
public enum Type {General, Statistics, Scheduling, Network, Account, Protocol, Classification, Notification, Rules, Cloud, Debug}
public enum Type {General, Statistics, Scheduling, Network, Account, Protocol, Classification, Notification, Rules, Cloud, Debug1, Debug2, Debug3}
public static void log(final Context context, String data) {
log(context, Type.General, data);
@ -109,7 +109,7 @@ public class EntityLog {
if (context == null)
return;
if (type == Type.Debug &&
if ((type == Type.Debug1 || type == Type.Debug2 || type == Type.Debug3) &&
!(BuildConfig.DEBUG || Log.isTestRelease()))
return;
@ -233,8 +233,12 @@ public class EntityLog {
return ContextCompat.getColor(context, R.color.solarizedCyan);
case Cloud:
return ContextCompat.getColor(context, R.color.solarizedRed);
case Debug:
return Helper.resolveColor(context, R.attr.colorWarning);
case Debug1:
return ContextCompat.getColor(context, R.color.solarizedRed);
case Debug2:
return ContextCompat.getColor(context, R.color.solarizedGreen);
case Debug3:
return ContextCompat.getColor(context, R.color.solarizedBlue);
default:
return null;
}

View File

@ -127,6 +127,7 @@ public class EntityMessage implements Serializable {
static final Long SWIPE_ACTION_JUNK = -8L;
static final Long SWIPE_ACTION_REPLY = -9L;
static final Long SWIPE_ACTION_IMPORTANCE = -10L;
static final Long SWIPE_ACTION_SUMMARIZE = -11L;
private static final int MAX_SNOOZED = 300;
@ -262,6 +263,7 @@ public class EntityMessage implements Serializable {
public String warning; // persistent
public String error; // volatile
public Long last_attempt; // send
public Long last_touched;
static String generateMessageId() {
return generateMessageId("localhost");
@ -753,6 +755,8 @@ public class EntityMessage implements Serializable {
return "junk";
if (SWIPE_ACTION_REPLY.equals(type))
return "reply";
if (SWIPE_ACTION_SUMMARIZE.equals(type))
return "summarize";
return "???";
}

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