Compare commits

...

174 Commits

Author SHA1 Message Date
Tankred Hase 43729833a5 Merge pull request #412 from tanx/master
Minor fixes to master
2015-12-17 16:13:36 +07:00
Tankred Hase b05aeea342 Remove gitter link 2015-12-17 16:02:55 +07:00
Tankred Hase 137c8c7c24 Update links in readme 2015-12-17 16:00:49 +07:00
Tankred Hase 853be194d9 Fix dompurify build and upgrade to new version 2015-12-17 12:12:38 +07:00
Tankred Hase 8b36a719c3 Use faster container based travis builds, remove whiteout build email notification from travis 2015-12-14 10:30:25 +07:00
Tankred Hase 8165416c5d Rename repo to mail 2015-12-14 10:25:58 +07:00
Tankred Hase b375d81635 Update git repo name in readme 2015-12-11 19:10:10 +07:00
Felix Hammerl e56f8c2c28 Merge pull request #394 from whiteout-io/dev/WO-1026
[WO-1026] Fix broken key upload after mail server error
2015-08-19 12:36:59 +02:00
Felix Hammerl ad3691fae9 [WO-1026] Fix broken key upload after mail server error 2015-08-19 12:10:16 +02:00
Tankred Hase e0663ab8d8 Remove winstore-jscompat (not necessary in Windows 10 anymore) 2015-08-05 14:12:13 +02:00
Tankred Hase 263b5c13b0 Upgrade to grunt-mocha-phantomjs 0.7.0 2015-08-05 14:11:25 +02:00
Tankred Hase 8636ba201b Properly mock publickey requests 2015-08-05 14:10:29 +02:00
Tankred Hase aa24881efc Fix JS build by fixating transitive browserify dep 2015-07-10 12:39:47 +02:00
Tankred Hase b7b5c1bdf5 Update README.md 2015-07-08 17:55:05 +02:00
Tankred Hase 59d38f9b14 Bump mobile version code 2015-06-26 11:28:25 +02:00
Tankred Hase c44984b2f3 Bump mobile version code 2015-06-18 11:58:57 +02:00
Felix Hammerl c31c320e83 Merge pull request #381 from whiteout-io/WO-1000
Remove invite code from app
2015-06-18 11:52:27 +02:00
Tankred Hase 7f49e691db Remove invite code from app 2015-06-18 10:45:18 +02:00
Tankred Hase a346f1612e bump mobile version 2015-06-15 13:43:28 +02:00
Tankred Hase 6b0b71d4ff Merge pull request #377 from whiteout-io/dev/WO-997
[WO-997] Select multiple messages at once
2015-06-15 13:07:06 +02:00
Felix Hammerl 4683583a0a [WO-997] Select multiple messages at once 2015-06-15 13:02:28 +02:00
Tankred Hase b038ac2c16 Merge pull request #376 from whiteout-io/dev/WO-996
Add error handling for invalid PGP key user id
2015-06-15 12:59:59 +02:00
Tankred Hase f32863dc54 Add error handling for invalid PGP key user id 2015-06-12 16:50:03 +02:00
Felix Hammerl 39d19df187 Order folders by path instead of name to keep subfolder order 2015-06-12 15:15:44 +02:00
Tankred Hase 9bf8c758ec Merge pull request #356 from whiteout-io/dev/WO-927
[WO-927] Clear file input after key import
2015-06-12 12:20:10 +02:00
Tankred Hase 76f770a12b Merge pull request #357 from whiteout-io/audit/WO-03-014
[WO-03-014] Avoid unsinged content spoofing attack
2015-05-21 17:25:35 +02:00
Felix Hammerl 3af376b419 [WO-03-014] Avoid unsinged content spoofing attack 2015-05-21 16:25:50 +02:00
Felix Hammerl e9a8702b39 [WO-927] Clear file input after key import 2015-05-20 17:21:33 +02:00
Tankred Hase 25b9141a5f Merge pull request #353 from whiteout-io/dev/WO-891
[WO-891] Add logout to passphrase dialog
2015-05-19 18:31:16 +02:00
Tankred Hase 1d0efc02a2 Review text 2015-05-19 18:30:31 +02:00
Felix Hammerl c3362c193d Add missing mocks 2015-05-19 17:39:30 +02:00
Felix Hammerl 7f0235c9b2 [WO-981] Set style.display instead of style 2015-05-19 16:39:59 +02:00
Felix Hammerl 8e0dfacd51 [WO-891] Add logout to passphrase dialog 2015-05-19 16:21:52 +02:00
Tankred Hase 467d001483 Bump mobile version code 2015-05-18 17:05:30 +02:00
Tankred Hase e7fb3bcf6d Merge pull request #345 from whiteout-io/dev/WO-986
[WO-986] Use proper path for key download
2015-05-18 16:59:43 +02:00
Tankred Hase ce740b2109 Remove empty lines 2015-05-18 16:57:28 +02:00
Felix Hammerl 0bfaba3bd9 Merge pull request #352 from whiteout-io/dev/WO-944
[WO-03-011] Fix no Reliable Sender Indication is implemented (Medium)
2015-05-18 16:49:08 +02:00
Tankred Hase e7cbf9ed86 Fix browserify build 2015-05-18 16:14:12 +02:00
Tankred Hase c76a392abf [WO-03-011] Fix no Reliable Sender Indication is implemented (Medium)
Display sender email address next to full name
2015-05-18 15:56:31 +02:00
Felix Hammerl ca8c2d9a4f [WO-986] Use proper path for key download 2015-05-13 15:55:55 +02:00
Tankred Hase f287c4cddf Merge pull request #344 from whiteout-io/dev/WO-982
[WO-982] Add placeholder to writer
2015-05-13 11:46:26 +02:00
Felix Hammerl 59006a98d7 Remove anonymized from bug report body 2015-05-12 12:04:48 +02:00
Felix Hammerl 73fcfba2a9 [WO-982] Add placeholder to writer 2015-05-12 11:06:27 +02:00
Tankred Hase b473d2b7fe Bump mobile version code 2015-05-11 15:54:07 +02:00
Tankred Hase 4519ab65c6 Remove empty line 2015-05-11 15:53:28 +02:00
Tankred Hase 19bdf0aa49 Fix iframe resize issue on iOS 2015-05-11 15:23:10 +02:00
Tankred Hase b8f21ac7eb Merge pull request #341 from whiteout-io/dev/WO-726
[WO-726] Nuke user DB on logout
2015-05-11 14:22:13 +02:00
Tankred Hase ff2a7c3e4a Update navigation.js 2015-05-11 14:20:34 +02:00
Tankred Hase 7834f79045 Update app-config.js 2015-05-11 14:20:09 +02:00
Felix Hammerl b35d993ff1 [WO-726] Nuke user DB on logout 2015-05-09 18:58:31 +02:00
Felix Hammerl 4e0388b349 Merge pull request #338 from whiteout-io/audit/WO-03-025
[WO-03-025] Unescape dashes in signed cleartext
2015-05-09 18:15:49 +02:00
Tankred Hase add1cd3919 bump mobile manifest 2015-05-09 16:23:31 +02:00
Tankred Hase d87449c57f Revert "Reset width before decription"
This reverts commit 4cf1ef3107.
2015-05-09 13:18:53 +02:00
Tankred Hase 4cf1ef3107 Reset width before decription 2015-05-09 11:53:33 +02:00
Tankred Hase e5e1c118be Merge pull request #337 from whiteout-io/dev/WO-964
[WO-964] Do not wait for next watch timer for ui update and general clea...
2015-05-09 11:14:25 +02:00
Tankred Hase 8e03b6a2ff Merge pull request #334 from whiteout-io/dev/WO-976
[WO-976] Fix UI when trying to display a message with empty body
2015-05-09 10:52:28 +02:00
Andris Reinman 6963ea33e9 [WO-03-025] Unescape dashes in signed cleartext 2015-05-07 12:28:11 +03:00
Felix Hammerl 0074e0ca90 [WO-964] Do not wait for next watch timer for ui update and general cleanup 2015-05-06 15:42:55 +02:00
Felix Hammerl 9f5daa12b1 Merge pull request #336 from whiteout-io/dev/WO-978
[WO-978] Fix PGP key folder prefix ignored at down and upload
2015-05-05 15:49:07 +02:00
Felix Hammerl 44591cc099 Bump imap-client version 2015-05-05 15:18:56 +02:00
Felix Hammerl ecf16b028b [WO-978] Fix PGP key folder prefix ignored at down and upload 2015-05-04 18:47:12 +02:00
Tankred Hase bcaef5c330 Revert "[WO-03-008] Fix no Origin Checks for postMessage Communication (High)"
This reverts commit 2aa166ac19.
2015-05-04 14:56:31 +02:00
Tankred Hase 9454739818 Revert "Hide wmail account creation in iOS app"
This reverts commit 168eaf0086.

Conflicts:
	src/js/controller/login/add-account.js
2015-04-29 08:48:59 +02:00
Felix Hammerl 991f61d271 [WO-976] Fix UI when trying to display a message with empty body 2015-04-28 19:25:32 +02:00
Tankred Hase f62257a595 Bump mobile version code 2015-04-28 14:37:18 +02:00
Tankred Hase 4f1ead0394 Merge pull request #333 from whiteout-io/dev/WO-975
Dev/wo 975
2015-04-28 14:32:47 +02:00
Tankred Hase bd910df187 Remove redundant case for SW state 2015-04-28 14:31:18 +02:00
Tankred Hase 504e8ffd50 Add simple prefetch service-worker 2015-04-28 13:35:17 +02:00
Felix Hammerl 7c1d68ec6e Merge pull request #332 from whiteout-io/dev/WO-971
Catch notification error on Chrome for Android
2015-04-28 11:22:43 +02:00
Tankred Hase 4293031f1f Catch notification error on Chrome for Android 2015-04-28 10:13:13 +02:00
Tankred Hase 44dac729aa Merge pull request #330 from whiteout-io/dev/WO-939
[WO-939] Fix TLS cert rejection bug
2015-04-28 10:01:34 +02:00
Felix Hammerl 55add6a6d3 [WO-939] Fix TLS cert rejection bug 2015-04-27 18:15:36 +02:00
Tankred Hase d36ddcef7f Fix platform checking in Chrome Web App 2015-04-27 16:52:15 +02:00
Tankred Hase 94188be7b3 Merge pull request #327 from whiteout-io/dev/WO-950
[WO-03-018] Fix weak Passwords & Misleading Passphrase Strength Check (L...
2015-04-27 13:37:38 +02:00
Tankred Hase 215a53e2a9 Merge pull request #329 from whiteout-io/dev/WO-973
Hide wmail account creation in iOS app
2015-04-27 13:23:20 +02:00
Tankred Hase 168eaf0086 Hide wmail account creation in iOS app 2015-04-27 13:19:49 +02:00
Felix Hammerl 9f39b67812 Merge pull request #326 from whiteout-io/dev/WO-961
[WO-03-008] Fix no Origin Checks for postMessage Communication (High)
2015-04-27 11:25:44 +02:00
Felix Hammerl 7540ffcab5 Merge pull request #328 from whiteout-io/dev/WO-952
[WO-03-020] Fix insecure Default in Implementation of BCC Feature (Low)
2015-04-27 11:20:37 +02:00
Tankred Hase 15ff8c85db [WO-03-020] Fix insecure Default in Implementation of BCC Feature (Low)
Display warning to user when using BCC feature
2015-04-24 17:14:13 +02:00
Tankred Hase 4efab0daf0 Upgrade to angular.js v1.3.15 2015-04-24 15:56:51 +02:00
Tankred Hase 701eb36b19 [WO-03-018] Fix weak Passwords & Misleading Passphrase Strength Check (Low)
Remove unsafe password strength indicator
2015-04-24 15:37:06 +02:00
Tankred Hase b687986980 [WO-03-005] Fix off-by-one Error in Prime Worker Code of Forge library (Low)
Upgrade to forge v0.6.26
2015-04-23 19:17:48 +02:00
Tankred Hase 281e53a887 [WO-03-013] Set stricter X-Frame-Options HTTP headers 2015-04-23 17:44:30 +02:00
Tankred Hase 6216fe2f1a [WO-03-024] Fix links can be opened in the message frame in MSIE11 (High)
Add X-Content-Security-Policy HTTP header for IE10+
2015-04-23 17:34:41 +02:00
Tankred Hase 2aa166ac19 [WO-03-008] Fix no Origin Checks for postMessage Communication (High) 2015-04-23 17:09:10 +02:00
Tankred Hase 4a681a73d2 Merge pull request #325 from whiteout-io/dev/WO-947
[WO-03-014] Fix Spoofing of Signed Messages and general UI Concerns (High)
2015-04-22 18:59:11 +02:00
Tankred Hase 56bd5222d2 Merge pull request #322 from whiteout-io/dev/WO-955
[WO-955] WO-03-023 STARTTLS Setting leads to opportunistic STARTSSL
2015-04-22 18:57:13 +02:00
Tankred Hase 1032a1eb06 Update package.json 2015-04-22 18:56:12 +02:00
Tankred Hase e6d109d42d [WO-03-013] Fix lack of X-Frame-Options Header on Whiteout Server (Medium) 2015-04-22 18:35:59 +02:00
Tankred Hase 0dc04e659f Add <math> html tag to DOMpurity backlist to prevent HTTP leaks 2015-04-22 18:01:53 +02:00
Tankred Hase c8779caef2 [WO-03-014] Fix Spoofing of Signed Messages and general UI Concerns (High)
Added signature 'verified' and 'invalid' icons from ModernPGP spec
See https://github.com/ModernPGP/icons#signatures
2015-04-22 17:48:38 +02:00
Felix Hammerl 898e19e3ea Merge pull request #321 from whiteout-io/audit/WO-03-002
[WO-03-002] Fix Insecure Regex Usage on DOMPurify Sanitizer Output (Medi...
2015-04-22 17:38:32 +02:00
Tankred Hase 246d19b76e [WO-03-002] Fix Insecure Regex Usage on DOMPurify Sanitizer Output (Medium) 2015-04-22 14:45:52 +02:00
Tankred Hase 55406cf7e8 Merge pull request #323 from eldios/eldios-patch-1
Change Git cloning command to use HTTP
2015-04-22 11:11:19 +02:00
Felix Hammerl b5c917f95a [WO-955] WO-03-023 STARTTLS Setting leads to opportunistic STARTSSL 2015-04-21 18:16:21 +02:00
Lele a0378cc0b9 Change Git cloning command to use HTTP
..or it will give a permission denied error to everyone but the developers :)
2015-04-21 02:14:34 +02:00
Tankred Hase 8b42f83b35 Merge pull request #319 from whiteout-io/dev/WO-905
[WO-905] Add IMAP compression
2015-04-17 13:18:36 +02:00
Tankred Hase 9f94467ad1 Upgrade to imap-client v0.14.0 2015-04-17 13:16:20 +02:00
Tankred Hase 321b6a9710 Bump mobile version code 2015-04-17 12:17:56 +02:00
Tankred Hase cf85fbd2ee Merge pull request #269 from whiteout-io/dev/wo-630
[WO-630] Added winstore-jscompat.js for Windows Apps compatibility
2015-04-16 16:56:28 +02:00
Andris Reinman 9518cb69fa [WO-630] Added winstore-jscompat.js for Windows Apps compatibility 2015-04-15 12:45:05 +02:00
Tankred Hase ec7e8cbd68 Add rubber band scrolling in dropdown on iOS 2015-04-15 12:03:46 +02:00
Tankred Hase d0f002bfd1 Hide error msg if public key verification does nto work 2015-04-14 18:46:08 +02:00
Tankred Hase 9841a59a9e Improve error message in public key verifier 2015-04-14 17:59:26 +02:00
Tankred Hase 1ffcaf5487 Added node.js 0.12 to travis.yml 2015-04-13 14:38:34 +02:00
Felix Hammerl 150a3c7888 [WO-905] Add IMAP compression 2015-04-12 23:43:13 +02:00
Tankred Hase 1af3eac566 Merge pull request #318 from whiteout-io/dev/WO-914
[WO-914] Limit dropdown folder list in width and size
2015-04-09 16:41:20 +02:00
Felix Hammerl e437a95baa [WO-914] Limit dropdown folder list in width and size 2015-04-09 16:33:59 +02:00
Tankred Hase 2828771c2b Merge pull request #256 from whiteout-io/dev/styleguide
Dev/styleguide
2015-04-09 15:12:06 +02:00
Felix Hammerl 2148d06d49 Copy all iOS icons in post-build step 2015-04-09 14:58:12 +02:00
Mario Volke dca2f4ead9 Add styleguide for basic components 2015-04-09 14:58:08 +02:00
Mario Volke 8807830122 Add basic layout and styling to styleguide 2015-04-09 14:58:08 +02:00
Mario Volke 69ed386765 Add styleguide to grunt dist and watch 2015-04-09 14:58:08 +02:00
Mario Volke e6147e1fdc Add basic styleguide workflow with assemble 2015-04-09 14:58:08 +02:00
Tankred Hase 1d64c2dfb0 Fix visible outbox on startup 2015-04-09 14:46:33 +02:00
Tankred Hase e5f281c124 Merge branch 'master' of github.com:whiteout-io/mail-html5 2015-04-09 14:34:36 +02:00
Tankred Hase 06d772159b [WO-919] Fix chrome rendering error in writer subject box 2015-04-09 14:34:10 +02:00
Tankred Hase ff72822e36 Update README.md 2015-04-08 16:05:23 +02:00
Tankred Hase 09ff51f329 Update README.md 2015-04-08 16:04:38 +02:00
Tankred Hase a2442554ad Bump mobile manifest version code 2015-04-07 18:07:48 +02:00
Tankred Hase 153d0626d2 Remove meta tag CSP directive from index.html since cca inject it by default now 2015-04-07 18:05:55 +02:00
Tankred Hase 222b7d35e3 Merge pull request #316 from whiteout-io/dev/WO-895
Dev/wo 895
2015-04-07 17:16:31 +02:00
Tankred Hase 07e0f39b55 Add update script for DB version 6 2015-04-07 17:15:38 +02:00
Tankred Hase 73febe287e Simplify spinner code 2015-04-07 16:45:51 +02:00
Tankred Hase abbd893438 Merge branch 'dev/WO-895' of github.com:whiteout-io/mail-html5 into dev/WO-895 2015-04-07 16:14:48 +02:00
Tankred Hase d67e0531d5 Prevent selection of unloaded message 2015-04-07 16:14:40 +02:00
Tankred Hase 210ab61ba1 Upgrade to imap-client v0.13.0 2015-04-07 16:13:51 +02:00
Tankred Hase b752269c68 Show spinner for loading messages 2015-04-07 15:59:41 +02:00
Felix Hammerl d8fb06cb08 [WO-895] Add paging and prefetching 2015-04-07 15:18:20 +02:00
Tankred Hase 88e83b6511 Improve error message upon private key import 2015-04-07 12:51:36 +02:00
Tankred Hase 1b8c6b6b8d [WO-906] Show confirm dialog for mobile phone number in account creation 2015-04-01 18:29:23 +02:00
Tankred Hase 8295806b1f Simplify public key verification screen 2015-04-01 17:42:45 +02:00
Tankred Hase c30fbe8b6b Hide 'openpgp_keys' imap folder 2015-04-01 17:36:06 +02:00
Tankred Hase 5a8d7d8324 Remove word 'beta' in signup screens 2015-04-01 17:10:13 +02:00
Tankred Hase c9981239c8 Merge pull request #317 from whiteout-io/dev/WO-885
Implement encrypted private key imap sync
2015-04-01 14:39:50 +02:00
Tankred Hase c8f13511c1 Review imap key-sync 2015-04-01 14:24:46 +02:00
Tankred Hase f41e6e12b9 Upgrade to imap-client v0.12.0 2015-04-01 12:43:54 +02:00
Tankred Hase 809de91354 [WO-895] Implement encrypted private key imap sync
* Add copy and paste private key import during setup
* Simplify key setup screen (login-initial) import option
* Make checkbox background color white
2015-04-01 00:48:16 +02:00
Tankred Hase 220b8af509 Link architecture image to in README 2015-03-18 14:23:13 +01:00
Tankred Hase 1d57b004d1 Move openpgp dependency to devDependencies in package.json 2015-03-18 14:11:24 +01:00
Tankred Hase 518ceec0ef Merge pull request #313 from whiteout-io/dev/WO-860
[WO-860] Introduce publickey-verifier
2015-03-18 13:23:35 +01:00
Felix Hammerl 1d4a9414bb [WO-860] Introduce publickey-verifier 2015-03-18 13:15:22 +01:00
Tankred Hase 0304bbf8fe Upgrade to OpenPGP.js v1.0.0 2015-03-15 11:46:56 +01:00
Tankred Hase 6ceb877472 Dont try to connect to imap when navigator is offline 2015-03-04 14:16:44 +01:00
Tankred Hase 1806f78ef3 Fix tabindexes for input fields 2015-03-01 19:11:16 +01:00
Tankred Hase 881b05df91 Switch name and password fields 2015-03-01 19:06:43 +01:00
Tankred Hase 369ad58134 Fix read-control position 2015-02-27 16:03:00 +01:00
Felix Hammerl 5f19bbeff0 Bump Android version 2015-02-27 12:23:37 +01:00
Tankred Hase 1c361e9c85 Clean up text in login html templates 2015-02-24 14:10:21 +01:00
Felix Hammerl 888204e1b9 Put read controls before reader content 2015-02-20 19:10:57 +01:00
Tankred Hase 427dee8214 Fix aws release script 2015-02-20 16:41:38 +01:00
Tankred Hase e089139474 bump adroid version code 2015-02-20 16:13:07 +01:00
Tankred Hase 6873fd7f3d Merge pull request #299 from whiteout-io/dev/WO-854
Show invite dialog in writer when recipient has no public key
2015-02-20 16:01:38 +01:00
Tankred Hase 9bc2bc7912 Show invite dialog in writer when recipient has no public key 2015-02-20 16:00:04 +01:00
Felix Hammerl 9aebecd45f Merge pull request #298 from whiteout-io/dev/wo-897
Use iframe-resizer
2015-02-19 12:45:40 +01:00
Tankred Hase 9d68b6475c Use iframe-resizer
* dynamically resize iframe based on content
* scroll on complete read-view instead of just iframe-content
* open writer when email address is clicked in iframe
* convert tabs to spaces in read-directive file
* Scale html mails to viewport only from the outside
* Delete release branch before each release in aws_release.sh script
* Make read-controls in read view always visible
2015-02-18 23:01:02 +01:00
Felix Hammerl 2c1e1f669e Merge pull request #296 from whiteout-io/dev/WO-865
Dev/wo 865
2015-02-18 17:00:08 +01:00
Tankred Hase 91693c62ad Refactor config.cloudUrl to config.keyServerUrl 2015-02-16 23:17:29 +01:00
Tankred Hase 93dc53f7b7 Add login to wmail on singup screen
Remove red border on required text input on Firefox
2015-02-16 23:17:01 +01:00
Tankred Hase 59dc2008a4 Fix regex for creating anchors from urls in reader 2015-02-16 21:53:41 +01:00
Tankred Hase 10cd2795f3 Upgrade to angular.js 1.3.13 2015-02-16 15:22:41 +01:00
Tankred Hase 9c6d618ddc Merge pull request #295 from whiteout-io/dev/WO-795
Dev/wo 795
2015-02-16 15:07:48 +01:00
Tankred Hase 163ad5db79 Bump email.js libs to promisified versions 2015-02-16 13:35:15 +01:00
Felix Hammerl 54d495d8d9 [WO-804] Fix bug where message cannot be parsed when deleted from IMAP 2015-02-16 13:33:02 +01:00
Felix Hammerl 0faa5b3743 Track emails by uid instead hashKey attribute 2015-02-16 13:33:02 +01:00
Felix Hammerl 2e3e07aa1d [WO-855] Fix trying to set a flag when email is deleted 2015-02-16 13:33:02 +01:00
Felix Hammerl 3da5a55251 [WO-795] Port to promises 2015-02-16 13:33:01 +01:00
Tankred Hase f28c7854c3 [WO-880] Use WebCrypto for native RSA keygen on iOS 2015-02-16 12:26:26 +01:00
Tankred Hase 57918bbd67 Use npm build of openpgp.js 2015-02-13 10:29:40 +01:00
Tankred Hase c26a51f83d Bump android versioncode 2015-02-11 15:25:23 +01:00
Tankred Hase 6938750803 Upgrade to OpenPGP.js v0.10.0 2015-02-11 14:37:14 +01:00
Felix Hammerl c94c419b38 Merge pull request #267 from whiteout-io/dev/WO-893
Dev/wo 893
2015-02-11 10:45:57 +01:00
Tankred Hase 5e246ee921 Fix invalid user id in PGP keys when realname is left empty 2015-02-10 18:37:41 +01:00
Tankred Hase 443cc3b59b Use keyserver.ubuntu.com for manual lookups in the app 2015-02-10 18:37:12 +01:00
167 changed files with 7750 additions and 6234 deletions

View File

@ -1,10 +1,10 @@
branch-defaults:
release/prod:
environment: mail-html5-prod
environment: mail-prod
release/test:
environment: mail-html5-test
environment: mail-test
global:
application_name: mail-html5
application_name: mail
default_ec2_keyname: null
default_platform: Node.js
default_region: eu-central-1

View File

@ -46,7 +46,8 @@
"Lawnchair",
"_",
"openpgp",
"PhoneNumber"
"PhoneNumber",
"DOMPurify"
],
"globals": {}

View File

@ -1,9 +1,7 @@
sudo: false
language: node_js
node_js:
- "0.10"
- "0.12"
before_install:
- gem install sass
- npm install -g grunt-cli
notifications:
email:
- build@whiteout.io

View File

@ -45,8 +45,8 @@ module.exports = function(grunt) {
'node_modules/sinon/pkg/sinon.js',
'node_modules/browsercrow/src/*.js',
'node_modules/browsersmtp/src/*.js',
'src/lib/openpgp/openpgp.min.js',
'src/lib/openpgp/openpgp.worker.min.js',
'node_modules/openpgp/dist/openpgp.min.js',
'node_modules/openpgp/dist/openpgp.worker.min.js',
'src/lib/forge/forge.min.js',
'dist/js/pbkdf2-worker.min.js'
],
@ -55,8 +55,12 @@ module.exports = function(grunt) {
lib: {
expand: true,
flatten: true,
cwd: 'src/lib/',
src: ['openpgp/openpgp.min.js', 'openpgp/openpgp.worker.min.js', 'forge/forge.min.js'],
cwd: './',
src: [
'node_modules/openpgp/dist/openpgp.min.js',
'node_modules/openpgp/dist/openpgp.worker.min.js',
'src/lib/forge/forge.min.js'
],
dest: 'dist/js/'
},
font: {
@ -93,6 +97,11 @@ module.exports = function(grunt) {
'src/css/read-sandbox.css': 'src/sass/read-sandbox.scss',
'src/css/all.css': 'src/sass/all.scss'
}
},
styleguide: {
files: {
'src/css/styleguide.css': 'src/sass/styleguide.scss'
}
}
},
autoprefixer: {
@ -104,6 +113,11 @@ module.exports = function(grunt) {
'src/css/read-sandbox.css': 'src/css/read-sandbox.css',
'src/css/all.css': 'src/css/all.css'
}
},
styleguide: {
files: {
'src/css/styleguide.css': 'src/css/styleguide.css'
}
}
},
csso: {
@ -115,6 +129,11 @@ module.exports = function(grunt) {
'dist/css/read-sandbox.min.css': 'src/css/read-sandbox.css',
'dist/css/all.min.css': 'src/css/all.css'
}
},
styleguide: {
files: {
'dist/styleguide/css/styleguide.min.css': 'src/css/styleguide.css'
}
}
},
@ -152,6 +171,12 @@ module.exports = function(grunt) {
},
options: browserifyOpt
},
compressionWorker: {
files: {
'dist/js/browserbox-compression-worker.browserified.js': ['node_modules/imap-client/node_modules/browserbox/src/browserbox-compression-worker.js']
},
options: browserifyOpt
},
unitTest: {
files: {
'test/unit/index.browserified.js': [
@ -174,6 +199,7 @@ module.exports = function(grunt) {
'test/unit/service/newsletter-service-test.js',
'test/unit/service/mail-config-service-test.js',
'test/unit/service/invitation-dao-test.js',
'test/unit/service/publickey-verifier-test.js',
'test/unit/email/outbox-bo-test.js',
'test/unit/email/email-dao-test.js',
'test/unit/email/account-test.js',
@ -185,10 +211,11 @@ module.exports = function(grunt) {
'test/unit/controller/login/login-initial-ctrl-test.js',
'test/unit/controller/login/login-new-device-ctrl-test.js',
'test/unit/controller/login/login-privatekey-download-ctrl-test.js',
'test/unit/controller/login/login-privatekey-upload-ctrl-test.js',
'test/unit/controller/login/login-verify-public-key-ctrl-test.js',
'test/unit/controller/login/login-set-credentials-ctrl-test.js',
'test/unit/controller/login/login-ctrl-test.js',
'test/unit/controller/app/dialog-ctrl-test.js',
'test/unit/controller/app/privatekey-upload-ctrl-test.js',
'test/unit/controller/app/publickey-import-ctrl-test.js',
'test/unit/controller/app/account-ctrl-test.js',
'test/unit/controller/app/set-passphrase-ctrl-test.js',
@ -206,7 +233,8 @@ module.exports = function(grunt) {
files: {
'test/integration/index.browserified.js': [
'test/main.js',
'test/integration/email-dao-test.js'
'test/integration/email-dao-test.js',
'test/integration/publickey-verifier-test.js'
]
},
options: browserifyOpt
@ -261,6 +289,7 @@ module.exports = function(grunt) {
'src/lib/angular/angular-animate.js',
'src/lib/ngtagsinput/ng-tags-input.min.js',
'node_modules/ng-infinite-scroll/build/ng-infinite-scroll.min.js',
'node_modules/iframe-resizer/js/iframeResizer.min.js',
'src/lib/fastclick/fastclick.js',
'src/lib/lawnchair/lawnchair-git.js',
'src/lib/lawnchair/lawnchair-adapter-webkit-sqlite-git.js',
@ -278,7 +307,8 @@ module.exports = function(grunt) {
},
readSandbox: {
src: [
'node_modules/dompurify/purify.js',
'node_modules/dompurify/src/purify.js',
'node_modules/iframe-resizer/js/iframeResizer.contentWindow.min.js',
'src/js/controller/app/read-sandbox.js'
],
dest: 'dist/js/read-sandbox.min.js'
@ -295,6 +325,10 @@ module.exports = function(grunt) {
src: ['dist/js/tcp-socket-tls-worker.browserified.js'],
dest: 'dist/js/tcp-socket-tls-worker.min.js'
},
compressionWorker: {
src: ['dist/js/browserbox-compression-worker.browserified.js'],
dest: 'dist/js/browserbox-compression-worker.min.js'
},
unitTest: {
src: [
'src/lib/underscore/underscore.js',
@ -378,6 +412,15 @@ module.exports = function(grunt) {
sourceMapName: 'dist/js/tcp-socket-tls-worker.min.js.map'
}
},
compressionWorker: {
files: {
'dist/js/browserbox-compression-worker.min.js': ['dist/js/browserbox-compression-worker.min.js']
},
options: {
sourceMap: true,
sourceMapName: 'dist/js/browserbox-compression-worker.min.js.map'
}
},
options: {
banner: '/*! Copyright © <%= grunt.template.today("yyyy") %>, Whiteout Networks GmbH.*/\n'
}
@ -444,6 +487,30 @@ module.exports = function(grunt) {
}
},
// Styleguide
assemble: {
options: {
assets: 'dist',
layoutdir: 'src/styleguide/layouts',
layout: 'default.hbs',
partials: ['src/styleguide/blocks/**/*.hbs'],
helpers: [
'handlebars-helper-compose',
'src/styleguide/helpers/**/*.js'
],
data: [
'dist/manifest.json'
],
flatten: true
},
styleguide: {
files: [{
'dist/styleguide/': ['src/styleguide/*.hbs']
}]
}
},
// Development
connect: {
@ -467,10 +534,14 @@ module.exports = function(grunt) {
watch: {
css: {
files: ['src/sass/**/*.scss'],
tasks: ['dist-css', 'manifest']
tasks: ['dist-css', 'offline-cache', 'dist-styleguide']
},
styleguide: {
files: ['src/styleguide/**/*.hbs', 'src/styleguide/**/*.js'],
tasks: ['dist-styleguide']
},
jsApp: {
files: ['src/js/**/*.js', 'src/**/*.html'],
files: ['src/js/**/*.js', 'src/*.html', 'src/tpl/**/*.html'],
tasks: ['dist-js-app']
},
jsUnitTest: {
@ -483,15 +554,15 @@ module.exports = function(grunt) {
},
icons: {
files: ['src/index.html', 'src/img/icons/*.svg', '!src/img/icons/all.svg'],
tasks: ['svgmin', 'svgstore', 'string-replace', 'manifest']
tasks: ['svgmin', 'svgstore', 'string-replace', 'dist-styleguide', 'offline-cache']
},
lib: {
files: ['src/lib/**/*.js'],
tasks: ['copy:lib', 'manifest']
tasks: ['copy:lib', 'offline-cache']
},
app: {
files: ['src/*.js', 'src/**/*.html', 'src/**/*.json', 'src/manifest.*', 'src/img/**/*', 'src/font/**/*'],
tasks: ['copy:app', 'copy:tpl', 'copy:img', 'copy:font', 'manifest-dev', 'manifest']
files: ['src/*.js', 'src/*.html', 'src/tpl/**/*.html', 'src/**/*.json', 'src/manifest.*', 'src/img/**/*', 'src/font/**/*'],
tasks: ['copy:app', 'copy:tpl', 'copy:img', 'copy:font', 'manifest-dev', 'offline-cache']
}
},
@ -510,6 +581,15 @@ module.exports = function(grunt) {
}
},
// Offline caching
swPrecache: {
prod: {
handleFetch: true,
rootDir: 'dist'
}
},
manifest: {
generate: {
options: {
@ -522,6 +602,9 @@ module.exports = function(grunt) {
'manifest.webapp',
'manifest.mobile.json',
'background.js',
'service-worker.js',
'styleguide/css/styleguide.min.css',
'styleguide/index.html',
'js/app.templates.js',
'js/app.js.map',
'js/app.min.js.map',
@ -535,6 +618,8 @@ module.exports = function(grunt) {
'js/mailreader-parser-worker.min.js.map',
'js/tcp-socket-tls-worker.browserified.js',
'js/tcp-socket-tls-worker.min.js.map',
'js/browserbox-compression-worker.browserified.js',
'js/browserbox-compression-worker.min.js.map',
'img/icon-100-ios.png',
'img/icon-114-ios.png',
'img/icon-120-ios.png',
@ -580,6 +665,59 @@ module.exports = function(grunt) {
});
// generate service-worker stasks
grunt.registerMultiTask('swPrecache', function() {
var fs = require('fs');
var path = require('path');
var swPrecache = require('sw-precache');
var packageJson = require('./package.json');
var done = this.async();
var rootDir = this.data.rootDir;
var handleFetch = this.data.handleFetch;
generateServiceWorkerFileContents(rootDir, handleFetch, function(error, serviceWorkerFileContents) {
if (error) {
grunt.fail.warn(error);
}
fs.writeFile(path.join(rootDir, 'service-worker.js'), serviceWorkerFileContents, function(error) {
if (error) {
grunt.fail.warn(error);
}
done();
});
});
function generateServiceWorkerFileContents(rootDir, handleFetch, callback) {
var config = {
cacheId: packageJson.name,
// If handleFetch is false (i.e. because this is called from swPrecache:dev), then
// the service worker will precache resources but won't actually serve them.
// This allows you to test precaching behavior without worry about the cache preventing your
// local changes from being picked up during the development cycle.
handleFetch: handleFetch,
logger: grunt.log.writeln,
dynamicUrlToDependencies: {
'socket.io/socket.io.js': ['node_modules/socket.io/node_modules/socket.io-client/socket.io.js'],
},
staticFileGlobs: [
rootDir + '/*.html',
rootDir + '/tpl/*.html',
rootDir + '/js/**/*.min.js',
rootDir + '/css/**/*.css',
rootDir + '/img/**/*.svg',
rootDir + '/img/*-universal.png',
rootDir + '/font/**.*',
rootDir + '/*.json'
],
maximumFileSizeToCacheInBytes: 100 * 1024 * 1024,
stripPrefix: path.join(rootDir, path.sep)
};
swPrecache(config, callback);
}
});
// Load the plugin(s)
grunt.loadNpmTasks('grunt-browserify');
grunt.loadNpmTasks('grunt-contrib-concat');
@ -601,23 +739,20 @@ module.exports = function(grunt) {
grunt.loadNpmTasks('grunt-svgstore');
grunt.loadNpmTasks('grunt-shell');
grunt.loadNpmTasks('grunt-angular-templates');
grunt.loadNpmTasks('assemble');
// Build tasks
grunt.registerTask('dist-css', ['sass', 'autoprefixer', 'csso']);
grunt.registerTask('dist-css', ['sass:dist', 'autoprefixer:dist', 'csso:dist']);
grunt.registerTask('dist-js', ['browserify', 'exorcise', 'ngtemplates', 'concat', 'uglify']);
grunt.registerTask('dist-js-app', [
'browserify:app',
'browserify:pbkdf2Worker',
'browserify:mailreaderWorker',
'browserify:tlsWorker',
'exorcise:app',
'ngtemplates',
'concat:app',
'concat:readSandbox',
'concat:pbkdf2Worker',
'concat:mailreaderWorker',
'concat:tlsWorker',
'manifest'
'offline-cache'
]);
grunt.registerTask('dist-js-unitTest', [
'browserify:unitTest',
@ -631,7 +766,11 @@ module.exports = function(grunt) {
]);
grunt.registerTask('dist-copy', ['copy']);
grunt.registerTask('dist-assets', ['svgmin', 'svgstore', 'string-replace']);
grunt.registerTask('dist', ['clean:dist', 'shell', 'dist-css', 'dist-js', 'dist-assets', 'dist-copy', 'manifest']);
grunt.registerTask('dist-styleguide', ['sass:styleguide', 'autoprefixer:styleguide', 'csso:styleguide', 'assemble:styleguide']);
// generate styleguide after manifest to forward version number to styleguide
grunt.registerTask('dist', ['clean:dist', 'shell', 'dist-css', 'dist-js', 'dist-assets', 'dist-copy', 'manifest', 'dist-styleguide']);
grunt.registerTask('offline-cache', ['manifest', 'swPrecache:prod']);
// Test/Dev tasks
grunt.registerTask('dev', ['connect:dev']);
@ -668,8 +807,7 @@ module.exports = function(grunt) {
patchManifest({
version: version,
deleteKey: true,
keyServer: 'https://keys.whiteout.io/',
keychainServer: 'https://keychain.whiteout.io/'
keyServer: 'https://keys.whiteout.io/'
});
});
@ -691,10 +829,6 @@ module.exports = function(grunt) {
var ksIndex = manifest.permissions.indexOf('https://keys-test.whiteout.io/');
manifest.permissions[ksIndex] = options.keyServer;
}
if (options.keychainServer) {
var kcsIndex = manifest.permissions.indexOf('https://keychain-test.whiteout.io/');
manifest.permissions[kcsIndex] = options.keychainServer;
}
if (options.deleteKey) {
delete manifest.key;
}
@ -702,9 +836,9 @@ module.exports = function(grunt) {
fs.writeFileSync(path, JSON.stringify(manifest, null, 2));
}
grunt.registerTask('release-dev', ['dist', 'manifest-dev', 'compress']);
grunt.registerTask('release-test', ['dist', 'manifest-test', 'clean:release', 'compress']);
grunt.registerTask('release-prod', ['dist', 'manifest-prod', 'clean:release', 'compress']);
grunt.registerTask('release-dev', ['dist', 'manifest-dev', 'swPrecache:prod', 'compress']);
grunt.registerTask('release-test', ['dist', 'manifest-test', 'clean:release', 'swPrecache:prod', 'compress']);
grunt.registerTask('release-prod', ['dist', 'manifest-prod', 'clean:release', 'swPrecache:prod', 'compress']);
grunt.registerTask('default', ['release-dev']);
};

View File

@ -1,4 +1,4 @@
Whiteout Mail [![Build Status](https://travis-ci.org/whiteout-io/mail-html5.svg?branch=master)](https://travis-ci.org/whiteout-io/mail-html5) [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/whiteout-io/mail-html5?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
Whiteout Mail [![Build Status](https://travis-ci.org/whiteout-io/mail.svg?branch=master)](https://travis-ci.org/whiteout-io/mail)
==========
Whiteout Mail is an easy to use email client with integrated OpenPGP encryption written in pure JavaScript. Download the official version under [whiteout.io](https://whiteout.io/#product).
@ -7,15 +7,17 @@ Whiteout Mail is an easy to use email client with integrated OpenPGP encryption
### Features
You can read about product features and our future roadmap in our [FAQ](https://github.com/whiteout-io/mail-html5/wiki/FAQ).
You can read about product features and our future roadmap in our [FAQ](https://github.com/whiteout-io/mail/wiki/FAQ).
### Privacy and Security
We take the privacy of your data very seriously. Here are some of the technical details:
* The code has undergone a [full security audit](https://blog.whiteout.io/2015/06/11/whiteout-mail-1-0-and-security-audit-by-cure53/) by [Cure53](https://cure53.de).
* Messages are [encrypted end-to-end ](http://en.wikipedia.org/wiki/End-to-end_encryption) using the [OpenPGP](http://en.wikipedia.org/wiki/Pretty_Good_Privacy) standard. This means that only you and the recipient can read your mail. Your messages and private PGP key are stored only on your computer (in IndexedDB).
* Users have the option to use [encrypted private key sync](https://blog.whiteout.io/2014/07/07/secure-pgp-key-sync-a-proposal/) if they want to use Whiteout on multiple devices.
* Users have the option to use [encrypted private key sync](https://github.com/whiteout-io/mail/wiki/Secure-OpenPGP-Key-Pair-Synchronization-via-IMAP) if they want to use Whiteout on multiple devices.
* [Content Security Policy (CSP)](http://www.html5rocks.com/en/tutorials/security/content-security-policy/) is enforced to prevent injection attacks.
@ -25,19 +27,23 @@ We take the privacy of your data very seriously. Here are some of the technical
* Like most native email clients, whiteout mail uses raw [TCP sockets](http://developer.chrome.com/apps/socket.html) to communicate directly with your mail server via IMAP/SMTP. TLS is used to protect your password and message data in transit.
* The app is deployed as a signed [Chrome Packaged App](https://developer.chrome.com/apps/about_apps.html) with [auditable static versions](https://github.com/whiteout-io/mail-html5/releases) in order to prevent [problems with host-based security](https://blog.whiteout.io/2014/04/13/heartbleed-and-javascript-crypto/).
* The app is deployed as a signed [Chrome Packaged App](https://developer.chrome.com/apps/about_apps.html) with [auditable static versions](https://github.com/whiteout-io/mail/releases) in order to prevent [problems with host-based security](https://blog.whiteout.io/2014/04/13/heartbleed-and-javascript-crypto/).
* The app can also be used from any modern web browser in environments where installing an app is not possible (e.g. a locked down corporate desktop). The IMAP/SMTP TLS sessions are still terminated in the user's browser using JS crypto ([Forge](https://github.com/digitalbazaar/forge)), but the encrypted TLS payload is proxied via [socket.io](http://socket.io/), due to the lack of raw sockets in the browser. **Please keep in mind that this mode of operation is not as secure as using the signed packaged app, since users must trust the webserver to deliver the correct code. This mode will still protect user against passive attacks like wiretapping (since PGP and TLS are still applied in the user's browser), but not against active attacks from the webserver. So it's best to decide which threat model applies to you.**
### Architecture
![client architecture](https://whiteout.io/img/app_layers.png)
### Reporting bugs and feature requests
* We will launch a bug bounty program later on for independent security researchers. If you find any security vulnerabilities, don't hesitate to contact us [security@whiteout.io](mailto:security@whiteout.io).
* You can also just create an [issue](https://github.com/whiteout-io/mail-html5/issues) on GitHub if you're missing a feature or just want to give us feedback. It would be much appreciated!
* You can also just create an [issue](https://github.com/whiteout-io/mail/issues) on GitHub if you're missing a feature or just want to give us feedback. It would be much appreciated!
### Testing
You can download a prebuilt bundle under [releases](https://github.com/whiteout-io/mail-html5/releases) or build your own from source (requires [node.js](http://nodejs.org/download/), [grunt](http://gruntjs.com/getting-started#installing-the-cli) and [sass](http://sass-lang.com/install)):
You can download a prebuilt bundle under [releases](https://github.com/whiteout-io/mail/releases) or build your own from source (requires [node.js](http://nodejs.org/download/), [grunt](http://gruntjs.com/getting-started#installing-the-cli) and [sass](http://sass-lang.com/install)):
npm install && npm test
@ -65,7 +71,7 @@ The App can be used either as a Chrome Packaged App or just by hosting it on you
Clone the git repository
git clone git@github.com:whiteout-io/mail-html5.git
git clone https://github.com/whiteout-io/mail.git
Build and generate the `dist/` directory:

View File

@ -3,21 +3,6 @@
"description": "Mail App with integrated OpenPGP encryption.",
"author": "Whiteout Networks",
"homepage": "https://whiteout.io",
"repository": {
"type": "git",
"url": "https://github.com/whiteout-io/mail-html5.git"
},
"keywords": [
"email",
"mail",
"client",
"app",
"openpgp",
"pgp",
"gpg",
"imap",
"smtp"
],
"engines": {
"node": ">=0.10"
},
@ -34,16 +19,18 @@
"socket.io": "^1.0.6"
},
"devDependencies": {
"assemble": "~0.4.42",
"axe-logger": "~0.0.2",
"browsercrow": "https://github.com/whiteout-io/browsercrow/tarball/master",
"browsersmtp": "https://github.com/whiteout-io/browsersmtp/tarball/master",
"chai": "~1.9.2",
"crypto-lib": "~0.2.1",
"dompurify": "~0.4.2",
"dompurify": "~0.7.3",
"grunt": "~0.4.1",
"grunt-angular-templates": "~0.5.7",
"grunt-autoprefixer": "~0.7.2",
"grunt-browserify": "^3.0.1",
"grunt-browserify": "3.7.0",
"insert-module-globals": "6.5.0",
"grunt-contrib-clean": "~0.5.0",
"grunt-contrib-compress": "~0.5.2",
"grunt-contrib-concat": "^0.5.0",
@ -56,19 +43,24 @@
"grunt-csso": "~0.6.1",
"grunt-exorcise": "^0.2.0",
"grunt-manifest": "^0.4.0",
"grunt-mocha-phantomjs": "^0.6.0",
"grunt-mocha-phantomjs": "^0.7.0",
"grunt-shell": "~1.1.1",
"grunt-string-replace": "~1.0.0",
"grunt-svgmin": "~1.0.0",
"grunt-svgstore": "~0.3.4",
"imap-client": "~0.10.0",
"handlebars-helper-compose": "~0.2.12",
"iframe-resizer": "^2.8.3",
"imap-client": "~0.14.2",
"jquery": "~2.1.1",
"mailbuild": "^0.3.7",
"mailreader": "~0.4.0",
"mocha": "^1.21.4",
"ng-infinite-scroll": "~1.1.2",
"pgpbuilder": "~0.5.0",
"pgpmailer": "~0.8.0",
"openpgp": "^1.0.0",
"pgpbuilder": "~0.6.0",
"pgpmailer": "~0.9.1",
"sinon": "~1.7.3",
"sw-precache": "^1.3.0",
"tcp-socket": "~0.5.0",
"time-grunt": "^1.0.0",
"wo-smtpclient": "~0.6.0"

View File

@ -11,14 +11,18 @@ fi
# switch branch
git checkout $2
git branch release/$1
git checkout release/$1
git branch -D release/$1
git checkout -b release/$1
git merge $2 --no-edit
# abort if tests fail
set -e
# build and test
rm -rf node_modules/
npm cache clear
npm install
npm test
grunt release-$1 --release=$3
# install only production dependencies

View File

@ -48,10 +48,23 @@ cp ../../../src/img/Default* "platforms/ios/$PROJNAME/Resources/splash"
# fixing missing/wrong icons
echo "Fixing wrong/missing iOS icons"
cp ../../../src/img/icon-60-ios.png "platforms/ios/$PROJNAME/Resources/icons/icon-60.png"
cp ../../../src/img/icon-180-ios.png "platforms/ios/$PROJNAME/Resources/icons/icon-60@3x.png"
cp ../../../src/img/icon-87-ios.png "platforms/ios/$PROJNAME/Resources/icons/icon-small@3x.png"
cp ../../../src/img/icon-40-ios.png "platforms/ios/$PROJNAME/Resources/icons/icon-40.png"
cp ../../../src/img/icon-80-ios.png "platforms/ios/$PROJNAME/Resources/icons/icon-40@2x.png"
cp ../../../src/img/icon-120-ios.png "platforms/ios/$PROJNAME/Resources/icons/icon-40@3x.png"
cp ../../../src/img/icon-50-ios.png "platforms/ios/$PROJNAME/Resources/icons/icon-50.png"
cp ../../../src/img/icon-100-ios.png "platforms/ios/$PROJNAME/Resources/icons/icon-50@2x.png"
cp ../../../src/img/icon-60-ios.png "platforms/ios/$PROJNAME/Resources/icons/icon-60.png"
cp ../../../src/img/icon-120-ios.png "platforms/ios/$PROJNAME/Resources/icons/icon-60@2x.png"
cp ../../../src/img/icon-180-ios.png "platforms/ios/$PROJNAME/Resources/icons/icon-60@3x.png"
cp ../../../src/img/icon-72-ios.png "platforms/ios/$PROJNAME/Resources/icons/icon-72.png"
cp ../../../src/img/icon-144-ios.png "platforms/ios/$PROJNAME/Resources/icons/icon-72@2x.png"
cp ../../../src/img/icon-76-ios.png "platforms/ios/$PROJNAME/Resources/icons/icon-76.png"
cp ../../../src/img/icon-152-ios.png "platforms/ios/$PROJNAME/Resources/icons/icon-76@2x.png"
cp ../../../src/img/icon-29-ios.png "platforms/ios/$PROJNAME/Resources/icons/icon-small.png"
cp ../../../src/img/icon-58-ios.png "platforms/ios/$PROJNAME/Resources/icons/icon-small@2x.png"
cp ../../../src/img/icon-87-ios.png "platforms/ios/$PROJNAME/Resources/icons/icon-small@3x.png"
cp ../../../src/img/icon-57-ios.png "platforms/ios/$PROJNAME/Resources/icons/icon.png"
cp ../../../src/img/icon-114-ios.png "platforms/ios/$PROJNAME/Resources/icons/icon@2x.png"
# print reminder for manual work in xcode
echo ""

View File

@ -75,19 +75,29 @@ var development = (process.argv[2] === '--dev');
// set HTTP headers
app.use(function(req, res, next) {
// prevent rendering website in foreign iframe (Clickjacking)
res.set('X-Frame-Options', 'DENY');
// HSTS
res.set('Strict-Transport-Security', 'max-age=16070400; includeSubDomains');
// CSP
var iframe = development ? "http://" + req.hostname + ":" + config.server.port : "https://" + req.hostname; // allow iframe to load assets
res.set('Content-Security-Policy', "default-src 'self' " + iframe + "; object-src 'none'; connect-src *; style-src 'self' 'unsafe-inline' " + iframe + "; img-src *");
var csp = "default-src 'self' " + iframe + "; object-src 'none'; connect-src *; style-src 'self' 'unsafe-inline' " + iframe + "; img-src *";
res.set('Content-Security-Policy', csp);
res.set('X-Content-Security-Policy', csp);
// set Cache-control Header (for AppCache)
res.set('Cache-control', 'public, max-age=0');
next();
});
app.use('/service-worker.js', noCache);
app.use('/appcache.manifest', noCache);
app.use('/appcache.manifest', function(req, res, next) {
function noCache(req, res, next) {
res.set('Cache-control', 'no-cache');
next();
}
app.use('/tpl/read-sandbox.html', function(req, res, next) {
res.set('X-Frame-Options', 'SAMEORIGIN');
next();
});
// redirect all http traffic to https

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="100px" height="100px" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<!-- Generator: Sketch 3.0.4 (8053) - http://www.bohemiancoding.com/sketch -->
<title>signature-invalid-cutout</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="signature-invalid-cutout" sketch:type="MSArtboardGroup" transform="translate(0.110156, 0.000000)" fill="#000000">
<path d="M77.3119658,92 L50,64.6787909 L22.6865385,92 L8.00299145,77.3054987 L35.3149573,49.9977557 L8,22.6870202 L22.6850427,8.00149623 L50,35.3137279 L77.3149573,8 L92,22.6825315 L64.6850427,49.9977557 L91.9970085,77.3054987 L77.3119658,92 Z" sketch:type="MSShapeGroup"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 995 B

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="100px" height="100px" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<!-- Generator: Sketch 3.0.4 (8053) - http://www.bohemiancoding.com/sketch -->
<title>signature-verified-cutout</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="signature-verified-cutout" sketch:type="MSArtboardGroup" transform="translate(0.110156, 0.000000)" fill="#000000">
<path d="M50,97 C75.9573832,97 97,75.9573832 97,50 C97,24.0426168 75.9573832,3 50,3 C24.0426168,3 3,24.0426168 3,50 C3,75.9573832 24.0426168,97 50,97 Z M46.2732912,77.5085 L20,57.830916 L27.9184401,47.6349702 L43.3096859,59.5152262 L70.31112,23 L80.867825,30.7782191 L46.2732912,77.5085 Z" sketch:type="MSShapeGroup"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -4,9 +4,6 @@
<meta charset="utf-8">
<title>Whiteout Mail</title>
<!-- Theses CSP rules are used as a fallback in runtimes such as Cordova -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self' chrome-extension: file: gap:; object-src 'none'; script-src 'self' 'unsafe-eval' chrome-extension: file: gap:; connect-src *; style-src 'self' 'unsafe-inline' chrome-extension: file: gap:; img-src *">
<!-- iOS homescreen link -->
<meta name="apple-mobile-web-app-capable" content="yes">
<!-- iOS iPad icon (retina) -->

View File

@ -12,12 +12,24 @@ module.exports = appCfg;
* Global app configurations
*/
appCfg.config = {
cloudUrl: 'https://keys.whiteout.io',
hkpUrl: 'https://pgp.mit.edu',
privkeyServerUrl: 'https://keychain.whiteout.io',
pgpComment: 'Whiteout Mail - https://whiteout.io',
keyServerUrl: 'https://keys.whiteout.io',
hkpUrl: 'http://keyserver.ubuntu.com',
adminUrl: 'https://admin-node.whiteout.io',
settingsUrl: 'https://settings.whiteout.io/autodiscovery/',
wmailDomain: 'wmail.io',
mailServer: {
domain: 'wmail.io',
imap: {
hostname: 'imap.wmail.io',
port: 993,
secure: true
},
smtp: {
hostname: 'smtp.wmail.io',
port: 465,
secure: true
}
},
oauthDomains: [/\.gmail\.com$/, /\.googlemail\.com$/],
ignoreUploadOnSentDomains: [/\.gmail\.com$/, /\.googlemail\.com$/],
serverPrivateKeyId: 'EE342F0DDBB0F3BE',
@ -30,7 +42,7 @@ appCfg.config = {
iconPath: '/img/icon-128-chrome.png',
verificationUrl: '/verify/',
verificationUuidLength: 36,
dbVersion: 5,
dbVersion: 6,
appVersion: undefined,
outboxMailboxPath: 'OUTBOX',
outboxMailboxName: 'Outbox',
@ -56,9 +68,7 @@ function setConfigParams(manifest) {
}
// get key server base url
cfg.cloudUrl = getUrl('https://keys');
// get keychain server base url
cfg.privkeyServerUrl = getUrl('https://keychain');
cfg.keyServerUrl = getUrl('https://keys');
// get the app version
cfg.appVersion = manifest.version;
}
@ -89,7 +99,7 @@ appCfg.string = {
certificateFaqLink: 'https://github.com/whiteout-io/mail-html5/wiki/FAQ#what-does-the-ssl-certificate-for-the-mail-server--changed-mean',
bugReportTitle: 'Report a bug',
bugReportSubject: '[Bug] I want to report a bug',
bugReportBody: 'Steps to reproduce\n1. \n2. \n3. \n\nWhat happens?\n\n\nWhat do you expect to happen instead?\n\n\n\n== PLEASE DONT PUT ANY KEYS HERE! ==\n\n\n## Log\n\nBelow is the log. It includes your interactions with your email provider in an anonymized way from the point where you started the app for the last time. Any information provided by you will be used for the porpose of locating and fixing the bug you reported. It will be deleted subsequently. However, you can edit this log and/or remove log data in the event that something would show up.\n\nUser-Agent: {0}\nVersion: {1}\n\n',
bugReportBody: 'Steps to reproduce\n1. \n2. \n3. \n\nWhat happens?\n\n\nWhat do you expect to happen instead?\n\n\n\n== PLEASE DONT PUT ANY KEYS HERE! ==\n\n\n## Log\n\nBelow is the log. It includes your interactions with your email provider from the point where you started the app for the last time. Login data and email content has been stripped. Any information provided by you will be used for the purpose of locating and fixing the bug you reported. It will be deleted subsequently. However, you can edit this log and/or remove log data in the event that something would show up.\n\nUser-Agent: {0}\nVersion: {1}\n\n',
supportAddress: 'mail.support@whiteout.io',
connDocOffline: 'It appears that you are offline. Please retry when you are online.',
connDocTlsWrongCert: 'A connection to {0} was rejected because the TLS certificate is invalid. Please have a look at the FAQ for information on how to fix this error.',
@ -99,5 +109,7 @@ appCfg.string = {
connDocNoInbox: 'We could not detect an IMAP inbox folder on {0}. Please have a look at the FAQ for information on how to fix this error.',
connDocGenericError: 'There was an error connecting to {0}: {1}',
logoutTitle: 'Logout',
logoutMessage: 'Are you sure you want to logout?'
logoutMessage: 'Are you sure you want to log out? Please back up your encryption key before proceeding!',
removePreAuthAccountTitle: 'Remove account',
removePreAuthAccountMessage: 'Are you sure you want to remove your account from this device?'
};

View File

@ -1,22 +1,7 @@
'use strict';
//
// AppCache
//
if (typeof window.applicationCache !== 'undefined') {
window.onload = function() {
// Check if a new AppCache is available on page load.
window.applicationCache.onupdateready = function() {
if (window.applicationCache.status === window.applicationCache.UPDATEREADY) {
// Browser downloaded a new app cache
if (window.confirm('A new version of Whiteout Mail is available. Restart the app to update?')) {
window.location.reload();
}
}
};
};
}
// use service-worker or app-cache for offline caching
require('./offline-cache');
//
// Angular app config
@ -68,6 +53,14 @@ app.config(function($routeProvider, $animateProvider) {
templateUrl: 'tpl/login-set-credentials.html',
controller: require('./controller/login/login-set-credentials')
});
$routeProvider.when('/login-privatekey-upload', {
templateUrl: 'tpl/login-privatekey-upload.html',
controller: require('./controller/login/login-privatekey-upload')
});
$routeProvider.when('/login-verify-public-key', {
templateUrl: 'tpl/login-verify-public-key.html',
controller: require('./controller/login/login-verify-public-key')
});
$routeProvider.when('/login-existing', {
templateUrl: 'tpl/login-existing.html',
controller: require('./controller/login/login-existing')
@ -110,7 +103,6 @@ app.controller('WriteCtrl', require('./controller/app/write'));
app.controller('MailListCtrl', require('./controller/app/mail-list'));
app.controller('AccountCtrl', require('./controller/app/account'));
app.controller('SetPassphraseCtrl', require('./controller/app/set-passphrase'));
app.controller('PrivateKeyUploadCtrl', require('./controller/app/privatekey-upload'));
app.controller('PublicKeyImportCtrl', require('./controller/app/publickey-import'));
app.controller('ContactsCtrl', require('./controller/app/contacts'));
app.controller('AboutCtrl', require('./controller/app/about'));

View File

@ -16,7 +16,7 @@ var AboutCtrl = function($scope, appConfig) {
// scope variables
//
$scope.version = appConfig.config.appVersion + ' (beta)';
$scope.version = appConfig.config.appVersion;
$scope.date = new Date();
};

View File

@ -27,7 +27,7 @@ var AccountCtrl = function($scope, $q, auth, keychain, pgp, appConfig, download,
var fpr = keyParams.fingerprint;
$scope.fingerprint = fpr.slice(0, 4) + ' ' + fpr.slice(4, 8) + ' ' + fpr.slice(8, 12) + ' ' + fpr.slice(12, 16) + ' ' + fpr.slice(16, 20) + ' ' + fpr.slice(20, 24) + ' ' + fpr.slice(24, 28) + ' ' + fpr.slice(28, 32) + ' ' + fpr.slice(32, 36) + ' ' + fpr.slice(36);
$scope.keysize = keyParams.bitSize;
$scope.publicKeyUrl = appConfig.config.cloudUrl + '/' + userId;
$scope.publicKeyUrl = appConfig.config.keyServerUrl + '/' + userId;
//
// scope functions

View File

@ -8,6 +8,42 @@ var ActionBarCtrl = function($scope, $q, email, dialog, status) {
// scope functions
//
$scope.CHECKNONE = 0;
$scope.CHECKALL = 1;
$scope.CHECKUNREAD = 2;
$scope.CHECKREAD = 3;
$scope.CHECKFLAGGED = 4;
$scope.CHECKUNFLAGGED = 5;
$scope.CHECKENCRYPTED = 6;
$scope.CHECKUNENCRYPTED = 7;
$scope.check = function(option) {
currentFolder().messages.forEach(function(email) {
if (!email.from) {
// only mark loaded messages, not the dummy messages
return;
}
if (option === $scope.CHECKNONE) {
email.checked = false;
} else if (option === $scope.CHECKALL) {
email.checked = true;
} else if (option === $scope.CHECKUNREAD) {
email.checked = !!email.unread;
} else if (option === $scope.CHECKREAD) {
email.checked = !email.unread;
} else if (option === $scope.CHECKFLAGGED) {
email.checked = !!email.flagged;
} else if (option === $scope.CHECKUNFLAGGED) {
email.checked = !email.flagged;
} else if (option === $scope.CHECKENCRYPTED) {
email.checked = !!email.encrypted;
} else if (option === $scope.CHECKUNENCRYPTED) {
email.checked = !email.encrypted;
}
});
};
/**
* Move a single message from the currently selected folder to another folder
* @param {Object} message The message that is to be moved

View File

@ -18,7 +18,7 @@ var ContactsCtrl = function($scope, $q, keychain, pgp, dialog, appConfig) {
}
};
$scope.whiteoutKeyServer = appConfig.config.cloudUrl.replace(/http[s]?:\/\//, ''); // display key server hostname
$scope.whiteoutKeyServer = appConfig.config.keyServerUrl.replace(/http[s]?:\/\//, ''); // display key server hostname
//
// scope functions

View File

@ -32,6 +32,10 @@ var MailListCtrl = function($scope, $timeout, $location, $filter, $q, status, no
* Set the route to a message which will go to read mode
*/
$scope.navigate = function(message) {
if (!message || !message.from) {
// early return if message has not finished loading yet
return;
}
$location.search('uid', message.uid);
};
@ -54,24 +58,16 @@ var MailListCtrl = function($scope, $timeout, $location, $filter, $q, status, no
// scope functions
//
$scope.getBody = function(message) {
$scope.getBody = function(messages) {
return $q(function(resolve) {
resolve();
}).then(function() {
return email.getBody({
folder: currentFolder(),
message: message
messages: messages
});
}).then(function() {
// automatically decrypt if it's the selected message
if (message === currentMessage()) {
return email.decryptBody({
message: message
});
}
}).catch(function(err) {
if (err.code !== 42) {
dialog.error(err);
@ -136,6 +132,10 @@ var MailListCtrl = function($scope, $timeout, $location, $filter, $q, status, no
* Date formatting
*/
$scope.formatDate = function(date) {
if (!date) {
return;
}
if (typeof date === 'string') {
date = new Date(date);
}

View File

@ -11,7 +11,7 @@ var NOTIFICATION_SENT_TIMEOUT = 2000;
// Controller
//
var NavigationCtrl = function($scope, $location, $q, account, email, outbox, notification, appConfig, dialog, dummy) {
var NavigationCtrl = function($scope, $location, $q, $timeout, account, email, outbox, notification, status, appConfig, dialog, dummy, privateKey, axe) {
if (!$location.search().dev && !account.isLoggedIn()) {
$location.path('/'); // init app
return;
@ -90,10 +90,10 @@ var NavigationCtrl = function($scope, $location, $q, account, email, outbox, not
// scope functions
//
$scope.onOutboxUpdate = function(err, count) {
$scope.onOutboxUpdate = function(err) {
if (err) {
dialog.error(err);
return;
axe.error('Sending from outbox failed: ' + err.message);
status.update('Error sending messages...');
}
// update the outbox mail count
@ -106,15 +106,11 @@ var NavigationCtrl = function($scope, $location, $q, account, email, outbox, not
return;
}
ob.count = count;
return $q(function(resolve) {
resolve();
}).then(function() {
return email.refreshFolder({
folder: ob
});
return email.refreshOutbox();
}).catch(dialog.error);
};
@ -149,6 +145,9 @@ var NavigationCtrl = function($scope, $location, $q, account, email, outbox, not
if (!$scope.state.nav.currentFolder) {
$scope.navigate(0);
}
// check if the private PGP key is synced
$scope.checkKeySyncStatus();
});
//
@ -178,6 +177,45 @@ var NavigationCtrl = function($scope, $location, $q, account, email, outbox, not
// start checking outbox periodically
outbox.startChecking($scope.onOutboxUpdate);
}
$scope.checkKeySyncStatus = function() {
return $q(function(resolve) {
resolve();
}).then(function() {
// login to imap
return privateKey.init();
}).then(function() {
// check key sync status
return privateKey.isSynced();
}).then(function(synced) {
if (!synced) {
dialog.confirm({
title: 'Key backup',
message: 'Your encryption key is not backed up. Back up now?',
positiveBtnStr: 'Backup',
negativeBtnStr: 'Not now',
showNegativeBtn: true,
callback: function(granted) {
if (granted) {
// logout of the current session
email.onDisconnect().then(function() {
// send to key upload screen
$timeout(function() {
$location.path('/login-privatekey-upload');
});
});
}
}
});
}
// logout of imap
return privateKey.destroy();
}).catch(axe.error);
};
};
module.exports = NavigationCtrl;
module.exports = NavigationCtrl;

View File

@ -1,155 +0,0 @@
'use strict';
var util = require('crypto-lib').util;
var PrivateKeyUploadCtrl = function($scope, $q, keychain, pgp, dialog, auth) {
//
// scope state
//
$scope.state.privateKeyUpload = {
toggle: function(to) {
// open lightbox
$scope.state.lightbox = (to) ? 'privatekey-upload' : undefined;
if (!to) {
return;
}
// show syncing status
$scope.step = 4;
// check if key is already synced
return $scope.checkServerForKey().then(function(privateKeySynced) {
if (privateKeySynced) {
// close lightbox
$scope.state.lightbox = undefined;
// show message
return dialog.info({
title: 'Info',
message: 'Your PGP key has already been synced.'
});
}
// show sync ui if key is not synced
$scope.displayUploadUi();
});
}
};
//
// scope functions
//
$scope.checkServerForKey = function() {
var keyParams = pgp.getKeyParams();
return $q(function(resolve) {
resolve();
}).then(function() {
return keychain.hasPrivateKey({
userId: keyParams.userId,
keyId: keyParams._id
});
}).then(function(privateKeySynced) {
return privateKeySynced ? privateKeySynced : undefined;
}).catch(dialog.error);
};
$scope.displayUploadUi = function() {
// go to step 1
$scope.step = 1;
// generate new code for the user
$scope.code = util.randomString(24);
$scope.displayedCode = $scope.code.slice(0, 4) + '-' + $scope.code.slice(4, 8) + '-' + $scope.code.slice(8, 12) + '-' + $scope.code.slice(12, 16) + '-' + $scope.code.slice(16, 20) + '-' + $scope.code.slice(20, 24);
// clear input field of any previous artifacts
$scope.inputCode = '';
};
$scope.verifyCode = function() {
if ($scope.inputCode.toUpperCase() !== $scope.code) {
var err = new Error('The code does not match. Please go back and check the generated code.');
dialog.error(err);
return false;
}
return true;
};
$scope.setDeviceName = function() {
return $q(function(resolve) {
resolve();
}).then(function() {
return keychain.setDeviceName($scope.deviceName);
});
};
$scope.encryptAndUploadKey = function() {
var userId = auth.emailAddress;
var code = $scope.code;
// register device to keychain service
return $q(function(resolve) {
resolve();
}).then(function() {
// register the device
return keychain.registerDevice({
userId: userId
});
}).then(function() {
// encrypt private PGP key using code and upload
return keychain.uploadPrivateKey({
userId: userId,
code: code
});
}).catch(dialog.error);
};
$scope.goBack = function() {
if ($scope.step > 1) {
$scope.step--;
}
};
$scope.goForward = function() {
if ($scope.step < 2) {
$scope.step++;
return;
}
if ($scope.step === 2 && $scope.verifyCode()) {
$scope.step++;
return;
}
if ($scope.step === 3) {
// set device name to local storage
return $scope.setDeviceName().then(function() {
// show spinner
$scope.step++;
// init key sync
return $scope.encryptAndUploadKey();
}).then(function() {
// close sync dialog
$scope.state.privateKeyUpload.toggle(false);
// show success message
dialog.info({
title: 'Success',
message: 'Whiteout Keychain setup successful!'
});
}).catch(dialog.error);
}
};
};
module.exports = PrivateKeyUploadCtrl;

View File

@ -20,7 +20,7 @@ var PublickeyImportCtrl = function($scope, $q, keychain, pgp, hkp, dialog, appCo
// scope variables
//
$scope.hkpUrl = appConfig.config.hkpUrl.replace('https://', '');
$scope.hkpUrl = appConfig.config.hkpUrl.replace(/http[s]?:\/\//, '');
//
// scope functions

View File

@ -1,35 +1,64 @@
'use strict';
// add DOMPurify hook to sanitze attributes
DOMPurify.addHook('afterSanitizeAttributes', function(node) {
// open all links in a new window
if ('target' in node) {
node.setAttribute('target', '_blank');
}
});
// set listener for event from main window
window.onmessage = function(e) {
var html = '';
if (e.data.html) {
// display html mail body
html = '<div class="scale-body">' + e.data.html + '</div>';
html = e.data.html;
} else if (e.data.text) {
// diplay text mail body by with colored conversation nodes
html = renderNodes(parseConversation(e.data.text));
}
// sanitize HTML content: https://github.com/cure53/DOMPurify
html = window.DOMPurify.sanitize(html);
// make links open in a new window
html = html.replace(/<a /g, '<a target="_blank" ');
// remove sources where necessary
if (e.data.removeImages) {
html = html.replace(/(<img[^>]+\b)src=['"][^'">]+['"]/ig, function(match, prefix) {
return prefix;
// remove http leaks
document.body.innerHTML = DOMPurify.sanitize(html, {
FORBID_TAGS: ['style', 'svg', 'audio', 'video', 'math'],
FORBID_ATTR: ['src']
});
} else {
document.body.innerHTML = DOMPurify.sanitize(html);
}
document.body.innerHTML = html;
scaleToFit();
attachClickHandlers();
};
window.addEventListener('resize', scaleToFit);
/**
* Send a message to the main window when email address is clicked
*/
function attachClickHandlers() {
var elements = document.getElementsByTagName('a');
for (var i = 0, len = elements.length; i < len; i++) {
elements[i].onclick = handle;
}
function handle(e) {
var text = e.target.textContent || e.target.innerText;
if (checkEmailAddress(text)) {
e.preventDefault();
window.parentIFrame.sendMessage({
type: 'email',
address: text
});
}
}
function checkEmailAddress(text) {
var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(text);
}
}
/**
* Parse email body and generate conversation nodes
@ -156,7 +185,7 @@ function renderNodes(root) {
var lines = node.split('\n');
for (i = 0; i < lines.length; i++) {
// replace all urls with anchors
lines[i] = lines[i].replace(/(https?:\/\/[^\s]+)/g, createArchor);
lines[i] = lines[i].replace(/(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&\/\/=]*)/g, createArchor);
// wrap line into an element for easier styling
html += '<div class="line';
if (isLineEmpty(lines[i])) {
@ -188,28 +217,4 @@ function renderNodes(root) {
}
return '<div class="view-read-body">' + body + '</div>';
}
/**
* Transform scale content to fit iframe width
*/
function scaleToFit() {
var view = document.getElementsByClassName('scale-body').item(0);
if (!view) {
return;
}
var parentWidth = view.parentNode.offsetWidth;
var w = view.offsetWidth;
var scale = '';
if (w > parentWidth) {
scale = parentWidth / w;
scale = 'scale(' + scale + ',' + scale + ')';
}
view.style['-webkit-transform-origin'] = '0 0';
view.style.transformOrigin = '0 0';
view.style['-webkit-transform'] = scale;
view.style.transform = scale;
}

View File

@ -6,8 +6,6 @@
var ReadCtrl = function($scope, $location, $q, email, invitation, outbox, pgp, keychain, appConfig, download, auth, dialog, status) {
var str = appConfig.string;
//
// scope state
//
@ -158,18 +156,10 @@ var ReadCtrl = function($scope, $location, $q, email, invitation, outbox, pgp, k
});
}).then(function() {
var invitationMail = {
from: [{
address: sender
}],
to: [{
address: recipient
}],
cc: [],
bcc: [],
subject: str.invitationSubject,
body: str.invitationMessage
};
var invitationMail = invitation.createMail({
sender: sender,
recipient: recipient
});
// send invitation mail
return outbox.put(invitationMail);

View File

@ -21,59 +21,6 @@ var SetPassphraseCtrl = function($scope, $q, pgp, keychain, dialog) {
// scope functions
//
/*
* Taken from jQuery validate.password plug-in 1.0
* http://bassistance.de/jquery-plugins/jquery-plugin-validate.password/
*
* Copyright (c) 2009 Jörn Zaefferer
*
* Licensed under the MIT
* http://www.opensource.org/licenses/mit-license.php
*/
$scope.checkPassphraseQuality = function() {
var passphrase = $scope.newPassphrase;
$scope.passphraseRating = 0;
var LOWER = /[a-z]/,
UPPER = /[A-Z]/,
DIGIT = /[0-9]/,
DIGITS = /[0-9].*[0-9]/,
SPECIAL = /[^a-zA-Z0-9]/,
SAME = /^(.)\1+$/;
function uncapitalize(str) {
return str.substring(0, 1).toLowerCase() + str.substring(1);
}
if (!passphrase) {
// no rating for empty passphrase
$scope.passphraseMsg = '';
return;
}
if (passphrase.length < 8 || SAME.test(passphrase)) {
$scope.passphraseMsg = 'Very weak';
return;
}
var lower = LOWER.test(passphrase),
upper = UPPER.test(uncapitalize(passphrase)),
digit = DIGIT.test(passphrase),
digits = DIGITS.test(passphrase),
special = SPECIAL.test(passphrase);
if (lower && upper && digit || lower && digits || upper && digits || special) {
$scope.passphraseMsg = 'Strong';
$scope.passphraseRating = 3;
} else if (lower && upper || lower && digit || upper && digit) {
$scope.passphraseMsg = 'Good';
$scope.passphraseRating = 2;
} else {
$scope.passphraseMsg = 'Weak';
$scope.passphraseRating = 1;
}
};
$scope.setPassphrase = function() {
var keyId = pgp.getKeyParams()._id;

View File

@ -6,7 +6,7 @@ var util = require('crypto-lib').util;
// Controller
//
var WriteCtrl = function($scope, $window, $filter, $q, appConfig, auth, keychain, pgp, email, outbox, dialog, axe, status) {
var WriteCtrl = function($scope, $window, $filter, $q, appConfig, auth, keychain, pgp, email, outbox, dialog, axe, status, invitation) {
var str = appConfig.string;
var cfg = appConfig.config;
@ -52,6 +52,8 @@ var WriteCtrl = function($scope, $window, $filter, $q, appConfig, auth, keychain
$scope.body = '';
$scope.attachments = [];
$scope.addressBookCache = undefined;
$scope.showInvite = undefined;
$scope.invited = [];
}
function reportBug() {
@ -158,7 +160,7 @@ var WriteCtrl = function($scope, $window, $filter, $q, appConfig, auth, keychain
if (forward) {
$scope.subject = 'Fwd: ' + re.subject;
} else {
$scope.subject = 'Re: ' + ((re.subject) ? re.subject.replace('Re: ', '') : '');
$scope.subject = re.subject ? 'Re: ' + re.subject.replace('Re: ', '') : '';
}
// fill text body
@ -198,6 +200,17 @@ var WriteCtrl = function($scope, $window, $filter, $q, appConfig, auth, keychain
// Editing headers
//
/**
* Warn users when using BCC
*/
$scope.toggleShowBCC = function() {
$scope.showBCC = true;
return dialog.info({
title: 'Warning',
message: 'Cannot send encrypted messages with BCC!'
});
};
/**
* Verify email address and fetch its public key
*/
@ -248,6 +261,9 @@ var WriteCtrl = function($scope, $window, $filter, $q, appConfig, auth, keychain
recipient.key = key;
recipient.secure = true;
}
} else {
// show invite dialog if no key found
$scope.showInvite = true;
}
$scope.checkSendStatus();
@ -286,6 +302,7 @@ var WriteCtrl = function($scope, $window, $filter, $q, appConfig, auth, keychain
// only allow sending if receviers exist
if (numReceivers < 1) {
$scope.showInvite = false;
return;
}
@ -299,6 +316,7 @@ var WriteCtrl = function($scope, $window, $filter, $q, appConfig, auth, keychain
$scope.okToSend = true;
$scope.sendBtnText = str.sendBtnSecure;
$scope.sendBtnSecure = true;
$scope.showInvite = false;
} else {
// send plaintext
$scope.okToSend = true;
@ -315,6 +333,56 @@ var WriteCtrl = function($scope, $window, $filter, $q, appConfig, auth, keychain
$scope.attachments.splice($scope.attachments.indexOf(attachment), 1);
};
/**
* Invite all users without a public key
*/
$scope.invite = function() {
var sender = auth.emailAddress,
sendJobs = [],
invitees = [];
$scope.showInvite = false;
// get recipients with no keys
$scope.to.forEach(check);
$scope.cc.forEach(check);
$scope.bcc.forEach(check);
function check(recipient) {
if (util.validateEmailAddress(recipient.address) && !recipient.secure && $scope.invited.indexOf(recipient.address) === -1) {
invitees.push(recipient.address);
}
}
return $q(function(resolve) {
resolve();
}).then(function() {
invitees.forEach(function(recipientAddress) {
var invitationMail = invitation.createMail({
sender: sender,
recipient: recipientAddress
});
// send invitation mail
var promise = outbox.put(invitationMail).then(function() {
return invitation.invite({
recipient: recipientAddress,
sender: sender
});
});
sendJobs.push(promise);
// remember already invited users to prevent spamming
$scope.invited.push(recipientAddress);
});
return Promise.all(sendJobs);
}).catch(function(err) {
$scope.showInvite = true;
return dialog.error(err);
});
};
//
// Editing email body
//

View File

@ -1,17 +1,33 @@
'use strict';
var CreateAccountCtrl = function($scope, $location, $routeParams, $q, auth, admin, appConfig) {
var CreateAccountCtrl = function($scope, $location, $routeParams, $q, auth, admin, appConfig, dialog) {
!$routeParams.dev && !auth.isInitialized() && $location.path('/'); // init app
// init phone region
$scope.region = 'DE';
$scope.domain = '@' + appConfig.config.mailServer.domain;
$scope.createWhiteoutAccount = function() {
$scope.showConfirm = function() {
if ($scope.form.$invalid) {
$scope.errMsg = 'Please fill out all required fields!';
return;
}
return dialog.confirm({
title: 'SMS validation',
message: 'Your mobile phone number will be validated via SMS. Are you sure it\'s correct?',
positiveBtnStr: 'Yes',
negativeBtnStr: 'Check again',
showNegativeBtn: true,
callback: function(granted) {
if (granted) {
$scope.createWhiteoutAccount();
}
}
});
};
$scope.createWhiteoutAccount = function() {
return $q(function(resolve) {
$scope.busy = true;
$scope.errMsg = undefined; // reset error msg
@ -19,7 +35,7 @@ var CreateAccountCtrl = function($scope, $location, $routeParams, $q, auth, admi
}).then(function() {
// read form values
var emailAddress = $scope.user + '@' + appConfig.config.wmailDomain;
var emailAddress = $scope.user + $scope.domain;
var phone = PhoneNumber.Parse($scope.dial, $scope.region);
if (!phone || !phone.internationalNumber) {
throw new Error('Invalid phone number!');
@ -36,8 +52,7 @@ var CreateAccountCtrl = function($scope, $location, $routeParams, $q, auth, admi
return admin.createUser({
emailAddress: emailAddress,
password: $scope.pass,
phone: phone.internationalNumber,
betaCode: $scope.betaCode.toUpperCase()
phone: phone.internationalNumber
});
}).then(function() {
@ -50,6 +65,15 @@ var CreateAccountCtrl = function($scope, $location, $routeParams, $q, auth, admi
$scope.errMsg = err.errMsg || err.message;
});
};
$scope.loginToExisting = function() {
// set server config
$scope.state.login = {
mailConfig: appConfig.config.mailServer
};
// proceed to login
$location.path('/login-set-credentials');
};
};
module.exports = CreateAccountCtrl;

View File

@ -1,8 +1,10 @@
'use strict';
var LoginExistingCtrl = function($scope, $location, $routeParams, $q, email, auth, keychain) {
var LoginExistingCtrl = function($scope, $location, $routeParams, $q, email, auth, keychain, account, dialog, appConfig) {
!$routeParams.dev && !auth.isInitialized() && $location.path('/'); // init app
var str = appConfig.string;
$scope.confirmPassphrase = function() {
if ($scope.form.$invalid) {
$scope.errMsg = 'Please fill out all required fields!';
@ -38,6 +40,18 @@ var LoginExistingCtrl = function($scope, $location, $routeParams, $q, email, aut
}).catch(displayError);
};
$scope.logout = function() {
return dialog.confirm({
title: str.removePreAuthAccountTitle,
message: str.removePreAuthAccountMessage,
callback: function(confirm) {
if (confirm) {
account.logout().catch(dialog.error);
}
}
});
};
function displayError(err) {
$scope.busy = false;
$scope.incorrect = true;

View File

@ -1,6 +1,6 @@
'use strict';
var LoginInitialCtrl = function($scope, $location, $routeParams, $q, newsletter, email, auth) {
var LoginInitialCtrl = function($scope, $location, $routeParams, $q, newsletter, email, auth, publickeyVerifier) {
!$routeParams.dev && !auth.isInitialized() && $location.path('/'); // init app
var emailAddress = auth.emailAddress;
@ -60,13 +60,10 @@ var LoginInitialCtrl = function($scope, $location, $routeParams, $q, newsletter,
passphrase: undefined
});
}).then(function() {
// persist credentials locally
return auth.storeCredentials();
}).then(function() {
// go to main account screen
$location.path('/account');
}).then(function(keypair) {
// remember keypair for storing after public key verification
publickeyVerifier.keypair = keypair;
$location.path('/login-privatekey-upload');
}).catch(displayError);
};

View File

@ -1,17 +1,36 @@
'use strict';
var LoginExistingCtrl = function($scope, $location, $routeParams, $q, email, auth, pgp, keychain) {
var LoginExistingCtrl = function($scope, $location, $routeParams, $q, email, auth, pgp, keychain, publickeyVerifier) {
!$routeParams.dev && !auth.isInitialized() && $location.path('/'); // init app
$scope.incorrect = false;
var PRIV_KEY_PREFIX = '-----BEGIN PGP PRIVATE KEY BLOCK-----';
var PUB_KEY_PREFIX = '-----BEGIN PGP PUBLIC KEY BLOCK-----';
var PRIV_ERR_MSG = 'Cannot find private PGP key block!';
$scope.pasteKey = function(pasted) {
var index = pasted.indexOf(PRIV_KEY_PREFIX);
if (index === -1) {
$scope.errMsg = PRIV_ERR_MSG;
return;
}
$scope.errMsg = undefined; // reset error msg
$scope.key = {
privateKeyArmored: pasted.substring(index, pasted.length).trim()
};
};
$scope.confirmPassphrase = function() {
if ($scope.form.$invalid || !$scope.key) {
$scope.errMsg = 'Please fill out all required fields!';
$scope.errMsg = PRIV_ERR_MSG;
return;
}
var userId = auth.emailAddress,
pubKeyNeedsVerification = false,
keypair;
return $q(function(resolve) {
@ -28,11 +47,11 @@ var LoginExistingCtrl = function($scope, $location, $routeParams, $q, email, aut
keypair = keys || {};
// extract public key from private key block if missing in key file
if (!$scope.key.publicKeyArmored || $scope.key.publicKeyArmored.indexOf('-----BEGIN PGP PUBLIC KEY BLOCK-----') < 0) {
if (!$scope.key.publicKeyArmored || $scope.key.publicKeyArmored.indexOf(PUB_KEY_PREFIX) < 0) {
try {
$scope.key.publicKeyArmored = pgp.extractPublicKey($scope.key.privateKeyArmored);
} catch (e) {
throw new Error('Error reading PGP key!');
throw new Error('Cannot find public PGP key!');
}
}
@ -61,6 +80,7 @@ var LoginExistingCtrl = function($scope, $location, $routeParams, $q, email, aut
userIds: pubKeyParams.userIds,
publicKey: $scope.key.publicKeyArmored
};
pubKeyNeedsVerification = true; // this public key needs to be authenticated
}
// import and validate keypair
@ -72,17 +92,20 @@ var LoginExistingCtrl = function($scope, $location, $routeParams, $q, email, aut
throw err;
});
}).then(function() {
// perist keys locally
return keychain.putUserKeyPair(keypair);
}).then(function(keypair) {
if (!pubKeyNeedsVerification) {
// persist credentials and key and go to main account screen
return keychain.putUserKeyPair(keypair).then(function() {
return auth.storeCredentials();
}).then(function() {
$location.path('/account');
});
}
}).then(function() {
// persist credentials locally
return auth.storeCredentials();
}).then(function() {
// go to main account screen
$location.path('/account');
// remember keypair for public key verification
publickeyVerifier.keypair = keypair;
// upload private key and then go to public key verification
$location.path('/login-privatekey-upload');
}).catch(displayError);
};

View File

@ -1,20 +1,19 @@
'use strict';
var LoginPrivateKeyDownloadCtrl = function($scope, $location, $routeParams, $q, auth, email, keychain) {
var LoginPrivateKeyDownloadCtrl = function($scope, $location, $routeParams, $q, auth, email, privateKey, keychain) {
!$routeParams.dev && !auth.isInitialized() && $location.path('/'); // init app
$scope.step = 1;
//
// Token
// scope functions
//
$scope.checkToken = function() {
if ($scope.tokenForm.$invalid) {
$scope.errMsg = 'Please enter a valid recovery token!';
$scope.checkCode = function() {
if ($scope.form.$invalid) {
$scope.errMsg = 'Please fill out all required fields!';
return;
}
var cachedKeypair;
var userId = auth.emailAddress;
return $q(function(resolve) {
@ -22,54 +21,38 @@ var LoginPrivateKeyDownloadCtrl = function($scope, $location, $routeParams, $q,
$scope.errMsg = undefined;
resolve();
}).then(function() {
// login to imap
return privateKey.init();
}).then(function() {
// get public key id for reference
return keychain.getUserKeyPair(userId);
}).then(function(keypair) {
// remember for storage later
$scope.cachedKeypair = keypair;
return keychain.downloadPrivateKey({
cachedKeypair = keypair;
return privateKey.download({
userId: userId,
keyId: keypair.publicKey._id,
recoveryToken: $scope.recoveryToken.toUpperCase()
keyId: keypair.publicKey._id
});
}).then(function(encryptedPrivateKey) {
$scope.encryptedPrivateKey = encryptedPrivateKey;
$scope.busy = false;
$scope.step++;
}).then(function(encryptedKey) {
// set decryption code
encryptedKey.code = $scope.code.toUpperCase();
// decrypt the downloaded encrypted private key
return privateKey.decrypt(encryptedKey);
}).catch(displayError);
};
//
// Keychain code
//
$scope.checkCode = function() {
if ($scope.codeForm.$invalid) {
$scope.errMsg = 'Please fill out all required fields!';
return;
}
var options = $scope.encryptedPrivateKey;
options.code = $scope.code.toUpperCase();
return $q(function(resolve) {
$scope.busy = true;
$scope.errMsg = undefined;
resolve();
}).then(function(privkey) {
// add private key to cached keypair object
cachedKeypair.privateKey = privkey;
// store the decrypted private key locally
return keychain.putUserKeyPair(cachedKeypair);
}).then(function() {
return keychain.decryptAndStorePrivateKeyLocally(options);
}).then(function(privateKey) {
// add private key to cached keypair object
$scope.cachedKeypair.privateKey = privateKey;
// try empty passphrase
return email.unlock({
keypair: $scope.cachedKeypair,
keypair: cachedKeypair,
passphrase: undefined
}).catch(function(err) {
// passphrase incorrct ... go to passphrase login screen
@ -81,6 +64,10 @@ var LoginPrivateKeyDownloadCtrl = function($scope, $location, $routeParams, $q,
// passphrase is corrent ...
return auth.storeCredentials();
}).then(function() {
// logout of imap
return privateKey.destroy();
}).then(function() {
// continue to main app
$scope.goTo('/account');

View File

@ -0,0 +1,83 @@
'use strict';
var util = require('crypto-lib').util;
var LoginPrivateKeyUploadCtrl = function($scope, $location, $routeParams, $q, auth, privateKey) {
!$routeParams.dev && !auth.isInitialized() && $location.path('/'); // init app
//
// scope state
//
// go to step 1
$scope.step = 1;
// generate new code for the user
$scope.code = util.randomString(24);
$scope.displayedCode = $scope.code.replace(/.{4}/g, "$&-").replace(/-$/, '');
// clear input field of any previous artifacts
$scope.inputCode = '';
//
// scope functions
//
$scope.encryptAndUploadKey = function() {
return $q(function(resolve) {
$scope.busy = true;
$scope.errMsg = undefined;
$scope.incorrect = false;
resolve();
}).then(function() {
if ($scope.inputCode.toUpperCase() !== $scope.code) {
throw new Error('The code does not match. Please go back and check the generated code.');
}
}).then(function() {
// login to imap
return privateKey.init();
}).then(function() {
// encrypt the private key
return privateKey.encrypt($scope.code);
}).then(function(encryptedPayload) {
// set user id to encrypted payload
encryptedPayload.userId = auth.emailAddress;
// encrypt private PGP key using code and upload
return privateKey.upload(encryptedPayload);
}).then(function() {
// logout of imap
return privateKey.destroy();
}).then(function() {
// continue to public key verification
$location.path('/login-verify-public-key');
}).catch(displayError);
};
$scope.goForward = function() {
$scope.step++;
};
$scope.goBack = function() {
if ($scope.step > 1) {
$scope.step--;
}
};
//
// helper functions
//
function displayError(err) {
$scope.busy = false;
$scope.incorrect = true;
$scope.errMsg = err.errMsg || err.message;
}
};
module.exports = LoginPrivateKeyUploadCtrl;

View File

@ -68,12 +68,14 @@ var SetCredentialsCtrl = function($scope, $location, $routeParams, $q, auth, con
host: $scope.imapHost.toLowerCase(),
port: $scope.imapPort,
secure: imapEncryption === ENCRYPTION_METHOD_TLS,
requireTLS: imapEncryption === ENCRYPTION_METHOD_STARTTLS,
ignoreTLS: imapEncryption === ENCRYPTION_METHOD_NONE
},
smtp: {
host: $scope.smtpHost.toLowerCase(),
port: $scope.smtpPort,
secure: smtpEncryption === ENCRYPTION_METHOD_TLS,
requireTLS: smtpEncryption === ENCRYPTION_METHOD_STARTTLS,
ignoreTLS: smtpEncryption === ENCRYPTION_METHOD_NONE
}
};

View File

@ -0,0 +1,96 @@
'use strict';
var RETRY_INTERVAL = 5000;
var PublicKeyVerifierCtrl = function($scope, $location, $q, $timeout, $interval, auth, publickeyVerifier, publicKey) {
$scope.retries = 0;
/**
* Runs a verification attempt
*/
$scope.verify = function() {
disarmTimeouts();
return $q(function(resolve) {
// updates the GUI
$scope.errMsg = undefined;
resolve();
}).then(function() {
// pre-flight check: is there already a public key for the user?
return publicKey.getByUserId(auth.emailAddress);
}).then(function(cloudPubkey) {
if (!cloudPubkey || (cloudPubkey && cloudPubkey.source)) {
// no pubkey, need to do the roundtrip
return verifyImap();
}
// pubkey has already been verified, we're done here
return success();
}).catch(function(error) {
$scope.errMsg = error.message; // display error
scheduleVerification(); // schedule next verification attempt
});
function verifyImap() {
// retrieve the credentials
return auth.getCredentials().then(function(credentials) {
return publickeyVerifier.configure(credentials); // configure imap
}).then(function() {
return publickeyVerifier.verify(); // connect to imap to look for the message
}).then(function() {
return success();
});
}
};
function success() {
return $q(function(resolve) {
resolve();
}).then(function() {
// persist keypair
return publickeyVerifier.persistKeypair();
}).then(function() {
// persist credentials locally (needs private key to encrypt imap password)
return auth.storeCredentials();
}).then(function() {
$location.path('/account'); // go to main account screen
});
}
/**
* schedules next verification attempt in RETRY_INTERVAL ms (scope.timeout)
* and sets up a countdown timer for the ui (scope.countdown)
*/
function scheduleVerification() {
$scope.timeout = setTimeout($scope.verify, RETRY_INTERVAL);
// shows the countdown timer, decrements each second
$scope.countdown = RETRY_INTERVAL / 1000;
$scope.countdownDecrement = setInterval(function() {
if ($scope.countdown > 0) {
$timeout(function() {
$scope.countdown--;
}, 0);
}
}, 1000);
}
function disarmTimeouts() {
clearTimeout($scope.timeout);
clearInterval($scope.countdownDecrement);
}
// upload public key and then schedule verifcation
publickeyVerifier.uploadPublicKey().then(scheduleVerification);
};
module.exports = PublicKeyVerifierCtrl;

View File

@ -52,19 +52,8 @@ var LoginCtrl = function($scope, $timeout, $location, updateHandler, account, au
});
} else if (availableKeys && availableKeys.publicKey && !availableKeys.privateKey) {
// check if private key is synced
return keychain.requestPrivateKeyDownload({
userId: availableKeys.publicKey.userId,
keyId: availableKeys.publicKey._id,
}).then(function(privateKeySynced) {
if (privateKeySynced) {
// private key is synced, proceed to download
return $scope.goTo('/login-privatekey-download');
} else {
// no private key, import key file
return $scope.goTo('/login-new-device');
}
});
// proceed to private key download
return $scope.goTo('/login-privatekey-download');
} else {
// no public key available, start onboarding process

View File

@ -11,6 +11,7 @@ var util = openpgp.util,
* High level crypto api that handles all calls to OpenPGP.js
*/
function PGP() {
openpgp.config.commentstring = config.pgpComment;
openpgp.config.prefer_hash_algorithm = openpgp.enums.hash.sha256;
openpgp.initWorker(config.workerPath + '/openpgp.worker.min.js');
}
@ -28,8 +29,8 @@ PGP.prototype.generateKeys = function(options) {
}
// generate keypair
name = options.realname ? options.realname.replace(/[&\/\\#,+()$~%.'":*?<>{}]/g, '').trim() + ' ' : '';
userId = name + '<' + options.emailAddress + '>';
name = options.realname ? options.realname.replace(/[&\/\\#,+()$~%.'":*?<>{}]/g, '').trim() : '';
userId = name + ' <' + options.emailAddress + '>';
passphrase = (options.passphrase) ? options.passphrase : undefined;
resolve({
@ -105,7 +106,7 @@ PGP.prototype.getKeyId = function(keyArmored) {
* Read all relevant params of an armored key.
*/
PGP.prototype.getKeyParams = function(keyArmored) {
var key, packet, userIds;
var key, packet, userIds, emailAddress;
// process armored key input
if (keyArmored) {
@ -121,15 +122,24 @@ PGP.prototype.getKeyParams = function(keyArmored) {
// read user names and email addresses
userIds = [];
key.getUserIds().forEach(function(userId) {
if (!userId || userId.indexOf('<') < 0 || userId.indexOf('>') < 0) {
return;
}
userIds.push({
name: userId.split('<')[0].trim(),
emailAddress: userId.split('<')[1].split('>')[0].trim()
});
});
// check user ID
emailAddress = userIds[0] && userIds[0].emailAddress;
if (!emailAddress) {
throw new Error('Cannot parse PGP key user ID!');
}
return {
_id: packet.getKeyId().toHex().toUpperCase(),
userId: userIds[0].emailAddress, // the primary (first) email address of the key
userId: emailAddress, // the primary (first) email address of the key
userIds: userIds, // a dictonary of all the key's name/address pairs
fingerprint: packet.getFingerprint().toUpperCase(),
algorithm: packet.algorithm,

View File

@ -8,6 +8,7 @@ ngModule.directive('keyfileInput', function() {
for (var i = 0; i < e.target.files.length; i++) {
importKey(e.target.files.item(i));
}
elm.val(null); // clear input
});
function importKey(file) {

View File

@ -18,7 +18,6 @@ ngModule.directive('fileReader', function() {
keyParts;
if (index === -1) {
scope.displayError(new Error('Error parsing private PGP key block!'));
return;
}

View File

@ -1,5 +1,7 @@
'use strict';
var PREFETCH_ITEMS = 10;
var ngModule = angular.module('woDirectives');
ngModule.directive('listScroll', function($timeout) {
@ -20,7 +22,10 @@ ngModule.directive('listScroll', function($timeout) {
inViewport = false,
listItem, message,
isPartiallyVisibleTop, isPartiallyVisibleBottom, isVisible,
displayMessages = scope[model];
displayMessages = scope[model],
visible = [],
prefetchLowerBound = displayMessages.length, // lowest index where we need to start prefetching
prefetchUpperBound = 0; // highest index where we need to start prefetching
if (!top && !bottom) {
// list not visible
@ -38,7 +43,6 @@ ngModule.directive('listScroll', function($timeout) {
}
message = displayMessages[i];
isPartiallyVisibleTop = listItem.top < top && listItem.bottom > top; // a portion of the list item is visible on the top
isPartiallyVisibleBottom = listItem.top < bottom && listItem.bottom > bottom; // a portion of the list item is visible on the bottom
isVisible = (listItem.top || listItem.bottom) && listItem.top >= top && listItem.bottom <= bottom; // the list item is visible as a whole
@ -47,12 +51,27 @@ ngModule.directive('listScroll', function($timeout) {
// we are now iterating over visible elements
inViewport = true;
// load mail body of visible
scope.getBody(message);
visible.push(message);
prefetchLowerBound = Math.max(Math.min(prefetchLowerBound, i - 1), 0);
prefetchUpperBound = Math.max(prefetchUpperBound, i + 1);
} else if (inViewport) {
// we are leaving the viewport, so stop iterating over the items
break;
}
}
//
// prefetch: [prefetchLowerBound - 20 ; prefetchLowerBound] and [prefetchUpperBound; prefetchUpperBound + 20]
//
// normalize lowest index to 0, slice interprets values <0 as "start from end"
var prefetchLower = displayMessages.slice(Math.max(prefetchLowerBound - PREFETCH_ITEMS, 0), prefetchLowerBound);
var prefetchUpper = displayMessages.slice(prefetchUpperBound, prefetchUpperBound + PREFETCH_ITEMS);
visible.concat(prefetchLower).concat(prefetchUpper).forEach(function(email) {
scope.getBody([email]);
});
}
scope.loadVisibleBodies = function() {

View File

@ -3,131 +3,159 @@
var ngModule = angular.module('woDirectives');
ngModule.directive('replySelection', function() {
return function(scope, elm) {
var popover, visible;
return function(scope, elm) {
var popover, visible;
popover = angular.element(document.querySelector('.reply-selection'));
visible = false;
popover = angular.element(document.querySelector('.reply-selection'));
visible = false;
elm.on('touchstart click', appear);
elm.parent().parent().on('touchstart click', disappear);
popover.on('touchstart click', disappear);
elm.on('touchstart click', appear);
elm.parent().parent().on('touchstart click', disappear);
popover.on('touchstart click', disappear);
function appear(e) {
e.preventDefault();
e.stopPropagation();
function appear(e) {
e.preventDefault();
e.stopPropagation();
visible = true;
visible = true;
// set popover position
var top = elm[0].offsetTop;
var left = elm[0].offsetLeft;
var width = elm[0].offsetWidth;
var height = elm[0].offsetHeight;
// set popover position
var top = elm[0].offsetTop;
var left = elm[0].offsetLeft;
var width = elm[0].offsetWidth;
var height = elm[0].offsetHeight;
popover[0].style.transition = 'opacity 0.1s linear';
popover[0].style.top = (top + height) + 'px';
popover[0].style.left = (left + width / 2 - popover[0].offsetWidth / 2) + 'px';
popover[0].style.opacity = '1';
}
popover[0].style.transition = 'opacity 0.1s linear';
popover[0].style.top = (top + height) + 'px';
popover[0].style.left = (left + width / 2 - popover[0].offsetWidth / 2) + 'px';
popover[0].style.opacity = '1';
}
function disappear() {
if (!visible) {
return;
}
function disappear() {
if (!visible) {
return;
}
popover[0].style.transition = 'opacity 0.25s linear, top 0.25s step-end, left 0.25s step-end';
popover[0].style.opacity = '0';
popover[0].style.top = '-9999px';
popover[0].style.left = '-9999px';
visible = false;
}
};
popover[0].style.transition = 'opacity 0.25s linear, top 0.25s step-end, left 0.25s step-end';
popover[0].style.opacity = '0';
popover[0].style.top = '-9999px';
popover[0].style.left = '-9999px';
visible = false;
}
};
});
ngModule.directive('frameLoad', function($timeout, $window) {
return function(scope, elm) {
var iframe = elm[0];
ngModule.directive('frameLoad', function($window) {
return function(scope, elm) {
var iframe = elm[0];
scope.$watch('state.read.open', function(open) {
if (open) {
// trigger rendering of iframe
// otherwise scale to fit would not compute correct dimensions on mobile
displayText(scope.state.mailList.selected ? scope.state.mailList.selected.body : undefined);
displayHtml(scope.state.mailList.selected ? scope.state.mailList.selected.html : undefined);
}
});
scope.$watch('state.read.open', function(open) {
if (open) {
// trigger rendering of iframe
// otherwise scale to fit would not compute correct dimensions on mobile
displayContent();
}
});
$window.addEventListener('resize', scaleToFit);
scope.$on('$destroy', function() {
$window.removeEventListener('resize', resetWidth);
$window.removeEventListener('orientationchange', resetWidth);
});
iframe.onload = function() {
// set listeners
scope.$watch('state.mailList.selected.body', displayText);
scope.$watch('state.mailList.selected.html', displayHtml);
// display initial message body
scope.$apply();
};
$window.addEventListener('resize', resetWidth);
$window.addEventListener('orientationchange', resetWidth);
function displayText(body) {
var mail = scope.state.mailList.selected;
if ((mail && mail.html) || (mail && mail.encrypted && !mail.decrypted)) {
return;
}
// use iframe-resizer to dynamically adapt iframe size to its content
elm.iFrameResize({
enablePublicMethods: true,
sizeWidth: true,
resizedCallback: scaleToFit,
messageCallback: function(e) {
if (e.message.type === 'email') {
scope.state.writer.write({
from: [{
address: e.message.address
}]
});
}
}
});
// send text body for rendering in iframe
iframe.contentWindow.postMessage({
text: body
}, '*');
iframe.onload = function() {
// set listeners
scope.$watch('state.mailList.selected.body', displayContent);
scope.$watch('state.mailList.selected.html', displayContent);
// display initial message body
scope.$apply();
};
$timeout(scaleToFit, 0);
}
function displayContent() {
var mail = scope.state.mailList.selected;
function displayHtml(html) {
if (!html) {
return;
}
if (!mail || (mail.encrypted && !mail.decrypted)) {
return;
}
// if there are image tags in the html?
var hasImages = /<img[^>]+\bsrc=['"][^'">]+['"]/ig.test(html);
scope.showImageButton = hasImages;
resetWidth();
iframe.contentWindow.postMessage({
html: html,
removeImages: hasImages // avoids doing unnecessary work on the html
}, '*');
if (mail.html) {
// if there are image tags in the html?
var hasImages = /<img[^>]+\bsrc=['"][^'">]+['"]/ig.test(mail.html);
scope.showImageButton = hasImages;
// only add a scope function to reload the html if there are images
if (hasImages) {
// reload WITH images
scope.displayImages = function() {
scope.showImageButton = false;
iframe.contentWindow.postMessage({
html: html,
removeImages: false
}, '*');
};
}
iframe.contentWindow.postMessage({
html: mail.html,
removeImages: hasImages // avoids doing unnecessary work on the html
}, '*');
$timeout(scaleToFit, 0);
}
// only add a scope function to reload the html if there are images
if (hasImages) {
// reload WITH images
scope.displayImages = function() {
scope.showImageButton = false;
iframe.contentWindow.postMessage({
html: mail.html,
removeImages: false
}, '*');
};
}
} else if (mail.body) {
iframe.contentWindow.postMessage({
text: mail.body
}, '*');
}
}
// transform scale iframe (necessary on iOS) to fit container width
function scaleToFit() {
var parentWidth = elm.parent().width();
var w = elm.width();
var scale = '';
// reset the iframe width to the original (min-width:100%)
// usually required before a new scaleToFit event
function resetWidth() {
elm.css('width', '');
}
if (w > parentWidth) {
scale = parentWidth / w;
scale = 'scale(' + scale + ',' + scale + ')';
}
// transform scale iframe to fit container width
// necessary if iframe is wider than container
function scaleToFit() {
var parentWidth = elm.parent().width();
var w = elm.width();
var scale = 'none';
elm.css({
'-webkit-transform-origin': '0 0',
'transform-origin': '0 0',
'-webkit-transform': scale,
'transform': scale
});
}
};
// only scale html mails
var mail = scope.state.mailList.selected;
if (mail && mail.html && (w > parentWidth)) {
scale = parentWidth / w;
scale = 'scale(' + scale + ',' + scale + ')';
}
elm.css({
'-webkit-transform-origin': '0 0',
'-moz-transform-origin': '0 0',
'-ms-transform-origin': '0 0',
'transform-origin': '0 0',
'-webkit-transform': scale,
'-moz-transform': scale,
'-ms-transform': scale,
'transform': scale
});
}
};
});

View File

@ -124,6 +124,10 @@ Account.prototype.logout = function() {
var self = this;
// clear app config store
return self._auth.logout().then(function() {
// clear the account DB, including keys and messages
return self._accountStore.clear();
}).then(function() {
// delete instance of imap-client and pgp-mailer
return self._emailDao.onDisconnect();

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,7 @@ angular.module('woEmail', ['woAppConfig', 'woUtil', 'woServices', 'woCrypto']);
require('./mailreader');
require('./pgpbuilder');
require('./mailbuild');
require('./email');
require('./outbox');
require('./account');

View File

@ -0,0 +1,8 @@
'use strict';
var Mailbuild = require('mailbuild');
var ngModule = angular.module('woEmail');
ngModule.factory('mailbuild', function() {
return Mailbuild;
});

View File

@ -62,6 +62,12 @@ Outbox.prototype.put = function(mail) {
var self = this,
allReaders = mail.from.concat(mail.to.concat(mail.cc.concat(mail.bcc))); // all the users that should be able to read the mail
if (mail.to.concat(mail.cc.concat(mail.bcc)).length === 0) {
return new Promise(function() {
throw new Error('Message has no recipients!');
});
}
mail.publicKeysArmored = []; // gather the public keys
mail.uid = mail.id = util.UUID(); // the mail needs a random id & uid for storage in the database
@ -118,8 +124,7 @@ Outbox.prototype.put = function(mail) {
* @param {Function} callback(error, pendingMailsCount) Callback that informs you about the count of pending mails.
*/
Outbox.prototype._processOutbox = function(callback) {
var self = this,
unsentMails = 0;
var self = this;
// also, if a _processOutbox call is still in progress, ignore it.
if (self._outboxBusy) {
@ -129,10 +134,9 @@ Outbox.prototype._processOutbox = function(callback) {
self._outboxBusy = true;
// get pending mails from the outbox
self._devicestorage.listItems(outboxDb, 0, null).then(function(pendingMails) {
self._devicestorage.listItems(outboxDb).then(function(pendingMails) {
// if we're not online, don't even bother sending mails.
if (!self._emailDao._account.online || _.isEmpty(pendingMails)) {
unsentMails = pendingMails.length;
return;
}
@ -148,7 +152,7 @@ Outbox.prototype._processOutbox = function(callback) {
}).then(function() {
self._outboxBusy = false;
callback(null, unsentMails);
callback();
}).catch(function(err) {
self._outboxBusy = false;

91
src/js/offline-cache.js Normal file
View File

@ -0,0 +1,91 @@
/**
* Copyright 2015 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
var UPDATE_MSG = 'A new version of Whiteout Mail is available. Restart the app to update?';
if ('serviceWorker' in navigator &&
// See http://www.chromium.org/Home/chromium-security/prefer-secure-origins-for-powerful-new-features
(window.location.protocol === 'https:' ||
window.location.hostname === 'localhost' ||
window.location.hostname.indexOf('127.') === 0)) {
// prefer new service worker cache
useServiceWorker();
} else if ('applicationCache' in window) {
// Fall back to app cache
useAppCache();
}
function useServiceWorker() {
// Your service-worker.js *must* be located at the top-level directory relative to your site.
// It won't be able to control pages unless it's located at the same level or higher than them.
// *Don't* register service worker file in, e.g., a scripts/ sub-directory!
// See https://github.com/slightlyoff/ServiceWorker/issues/468
navigator.serviceWorker.register('service-worker.js', {
scope: './'
}).then(function(registration) {
// Check to see if there's an updated version of service-worker.js with new files to cache:
// https://slightlyoff.github.io/ServiceWorker/spec/service_worker/index.html#service-worker-registration-update-method
if (typeof registration.update === 'function') {
registration.update();
}
// updatefound is fired if service-worker.js changes.
registration.onupdatefound = function() {
// The updatefound event implies that registration.installing is set; see
// https://slightlyoff.github.io/ServiceWorker/spec/service_worker/index.html#service-worker-container-updatefound-event
var installingWorker = registration.installing;
installingWorker.onstatechange = function() {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the old content will have been purged and the fresh content will
// have been added to the cache.
// It's the perfect time to display a "New content is available; please refresh."
// message in the page's interface.
if (window.confirm(UPDATE_MSG)) {
window.location.reload();
}
} else {
// At this point, everything has been precached, but the service worker is not
// controlling the page. The service worker will not take control until the next
// reload or navigation to a page under the registered scope.
// It's the perfect time to display a "Content is cached for offline use." message.
console.log('Content is cached, and will be available for offline use the next time the page is loaded.');
}
}
};
};
}).catch(function(e) {
console.error('Error during service worker registration:', e);
});
}
function useAppCache() {
window.onload = function() {
// Check if a new AppCache is available on page load.
window.applicationCache.onupdateready = function() {
if (window.applicationCache.status === window.applicationCache.UPDATEREADY) {
// Browser downloaded a new app cache
if (window.confirm(UPDATE_MSG)) {
window.location.reload();
}
}
};
};
}

View File

@ -94,6 +94,8 @@ Auth.prototype.getCredentials = function() {
var credentials = {
imap: {
secure: self.imap.secure,
requireTLS: self.imap.requireTLS,
ignoreTLS: self.imap.ignoreTLS,
port: self.imap.port,
host: self.imap.host,
ca: self.imap.ca,
@ -105,6 +107,8 @@ Auth.prototype.getCredentials = function() {
},
smtp: {
secure: self.smtp.secure,
requireTLS: self.smtp.requireTLS,
ignoreTLS: self.smtp.ignoreTLS,
port: self.smtp.port,
host: self.smtp.host,
ca: self.smtp.ca,
@ -297,7 +301,7 @@ Auth.prototype._loadCredentials = function() {
});
function loadFromDB(key) {
return self._appConfigStore.listItems(key, 0, null).then(function(cachedItems) {
return self._appConfigStore.listItems(key).then(function(cachedItems) {
return cachedItems && cachedItems[0];
});
}

View File

@ -83,14 +83,13 @@ DeviceStorage.prototype.removeList = function(type) {
/**
* List stored items of a given type
* @param type [String] The type of item e.g. 'email'
* @param offset [Number] The offset of items to fetch (0 is the last stored item)
* @param num [Number] The number of items to fetch (null means fetch all)
* @param {String/Array} query The type of item e.g. 'email'
* @param {Boolean} exactMatchOnly Specifies if only exact matches are extracted from the DB as opposed to keys that start with the query
* @return {Promise}
*/
DeviceStorage.prototype.listItems = function(type, offset, num) {
// fetch all items of a certain type from the data-store
return this._lawnchairDAO.list(type, offset, num);
DeviceStorage.prototype.listItems = function(query, exactMatchOnly) {
// fetch all items of a certain query from the data-store
return this._lawnchairDAO.list(query, exactMatchOnly);
};
/**

View File

@ -14,4 +14,5 @@ require('./admin');
require('./lawnchair');
require('./devicestorage');
require('./auth');
require('./keychain');
require('./keychain');
require('./publickey-verifier');

View File

@ -8,13 +8,33 @@ module.exports = Invitation;
* The Invitation is a high level Data Access Object that access the invitation service REST endpoint.
* @param {Object} restDao The REST Data Access Object abstraction
*/
function Invitation(invitationRestDao) {
function Invitation(invitationRestDao, appConfig) {
this._restDao = invitationRestDao;
this._appConfig = appConfig;
}
//
// API
//
/**
* Create the invitation mail object
* @param {String} options.sender The sender's email address
* @param {String} options.recipient The recipient's email address
* @return {Object} The mail object
*/
Invitation.prototype.createMail = function(options) {
var str = this._appConfig.string;
return {
from: [{
address: options.sender
}],
to: [{
address: options.recipient
}],
cc: [],
bcc: [],
subject: str.invitationSubject,
body: str.invitationMessage
};
};
/**
* Notes an invite for the recipient by the sender in the invitation web service

View File

@ -4,12 +4,8 @@ var ngModule = angular.module('woServices');
ngModule.service('keychain', Keychain);
module.exports = Keychain;
var util = require('crypto-lib').util;
var DB_PUBLICKEY = 'publickey',
DB_PRIVATEKEY = 'privatekey',
DB_DEVICENAME = 'devicename',
DB_DEVICE_SECRET = 'devicesecret';
DB_PRIVATEKEY = 'privatekey';
/**
* A high-level Data-Access Api for handling Keypair synchronization
@ -151,7 +147,7 @@ Keychain.prototype.getReceiverPublicKey = function(userId) {
var self = this;
// search local keyring for public key
return self._lawnchairDAO.list(DB_PUBLICKEY, 0, null).then(function(allPubkeys) {
return self._lawnchairDAO.list(DB_PUBLICKEY).then(function(allPubkeys) {
var userIds;
// query primary email address
var pubkey = _.findWhere(allPubkeys, {
@ -199,390 +195,6 @@ Keychain.prototype.getReceiverPublicKey = function(userId) {
}
};
//
// Device registration functions
//
/**
* Set the device's memorable name e.g 'iPhone Work'
* @param {String} deviceName The device name
*/
Keychain.prototype.setDeviceName = function(deviceName) {
if (!deviceName) {
return new Promise(function() {
throw new Error('Please set a device name!');
});
}
return this._lawnchairDAO.persist(DB_DEVICENAME, deviceName);
};
/**
* Get the device' memorable name from local storage. Throws an error if not set
* @return {String} The device name
*/
Keychain.prototype.getDeviceName = function() {
// check if deviceName is already persisted in storage
return this._lawnchairDAO.read(DB_DEVICENAME).then(function(deviceName) {
if (!deviceName) {
throw new Error('Device name not set!');
}
return deviceName;
});
};
/**
* Geneate a device specific key and secret to authenticate to the private key service.
*/
Keychain.prototype.getDeviceSecret = function() {
var self = this,
config = self._appConfig.config;
// generate random deviceSecret or get from storage
return self._lawnchairDAO.read(DB_DEVICE_SECRET).then(function(storedDevSecret) {
if (storedDevSecret) {
// a device key is already available locally
return storedDevSecret;
}
// generate random deviceSecret
var deviceSecret = util.random(config.symKeySize);
// persist deviceSecret to local storage (in plaintext)
return self._lawnchairDAO.persist(DB_DEVICE_SECRET, deviceSecret).then(function() {
return deviceSecret;
});
});
};
/**
* Register the device on the private key server. This will give the device access to upload an encrypted private key.
* @param {String} options.userId The user's email address
*/
Keychain.prototype.registerDevice = function(options) {
var self = this,
devName,
config = self._appConfig.config;
// check if deviceName is already persisted in storage
return self.getDeviceName().then(function(deviceName) {
return requestDeviceRegistration(deviceName);
});
function requestDeviceRegistration(deviceName) {
devName = deviceName;
// request device registration session key
return self._privateKeyDao.requestDeviceRegistration({
userId: options.userId,
deviceName: deviceName
}).then(function(regSessionKey) {
if (!regSessionKey.encryptedRegSessionKey) {
throw new Error('Invalid format for session key!');
}
return decryptSessionKey(regSessionKey);
});
}
function decryptSessionKey(regSessionKey) {
return self.lookupPublicKey(config.serverPrivateKeyId).then(function(serverPubkey) {
if (!serverPubkey || !serverPubkey.publicKey) {
throw new Error('Server public key for device registration not found!');
}
// decrypt the session key
var ct = regSessionKey.encryptedRegSessionKey;
return self._pgp.decrypt(ct, serverPubkey.publicKey).then(function(pt) {
if (!pt.signaturesValid) {
throw new Error('Verifying PGP signature failed!');
}
return uploadDeviceSecret(pt.decrypted);
});
});
}
function uploadDeviceSecret(regSessionKey) {
// generate iv
var iv = util.random(config.symIvSize);
// read device secret from local storage
return self.getDeviceSecret().then(function(deviceSecret) {
// encrypt deviceSecret
return self._crypto.encrypt(deviceSecret, regSessionKey, iv);
}).then(function(encryptedDeviceSecret) {
// upload encryptedDeviceSecret
return self._privateKeyDao.uploadDeviceSecret({
userId: options.userId,
deviceName: devName,
encryptedDeviceSecret: encryptedDeviceSecret,
iv: iv
});
});
}
};
//
// Private key functions
//
/**
* Authenticate to the private key server (required before private PGP key upload).
* @param {String} userId The user's email address
* @return {Object} {sessionId:String, sessionKey:[base64 encoded]}
*/
Keychain.prototype._authenticateToPrivateKeyServer = function(userId) {
var self = this,
sessionId,
config = self._appConfig.config;
// request auth session key required for upload
return self._privateKeyDao.requestAuthSessionKey({
userId: userId
}).then(function(authSessionKey) {
if (!authSessionKey.encryptedAuthSessionKey || !authSessionKey.encryptedChallenge || !authSessionKey.sessionId) {
throw new Error('Invalid format for session key!');
}
// remember session id for verification
sessionId = authSessionKey.sessionId;
return decryptSessionKey(authSessionKey);
});
function decryptSessionKey(authSessionKey) {
var ptSessionKey, ptChallenge, serverPubkey;
return self.lookupPublicKey(config.serverPrivateKeyId).then(function(pubkey) {
if (!pubkey || !pubkey.publicKey) {
throw new Error('Server public key for authentication not found!');
}
serverPubkey = pubkey;
// decrypt the session key
var ct1 = authSessionKey.encryptedAuthSessionKey;
return self._pgp.decrypt(ct1, serverPubkey.publicKey);
}).then(function(pt) {
if (!pt.signaturesValid) {
throw new Error('Verifying PGP signature failed!');
}
ptSessionKey = pt.decrypted;
// decrypt the challenge
var ct2 = authSessionKey.encryptedChallenge;
return self._pgp.decrypt(ct2, serverPubkey.publicKey);
}).then(function(pt) {
if (!pt.signaturesValid) {
throw new Error('Verifying PGP signature failed!');
}
ptChallenge = pt.decrypted;
return encryptChallenge(ptSessionKey, ptChallenge);
});
}
function encryptChallenge(sessionKey, challenge) {
var deviceSecret, encryptedChallenge;
var iv = util.random(config.symIvSize);
// get device secret
return self.getDeviceSecret().then(function(secret) {
deviceSecret = secret;
// encrypt the challenge
return self._crypto.encrypt(challenge, sessionKey, iv);
}).then(function(ct) {
encryptedChallenge = ct;
// encrypt the device secret
return self._crypto.encrypt(deviceSecret, sessionKey, iv);
}).then(function(encryptedDeviceSecret) {
return replyChallenge({
encryptedChallenge: encryptedChallenge,
encryptedDeviceSecret: encryptedDeviceSecret,
iv: iv
}, sessionKey);
});
}
function replyChallenge(response, sessionKey) {
// respond to challenge by uploading the with the session key encrypted challenge
return self._privateKeyDao.verifyAuthentication({
userId: userId,
sessionId: sessionId,
encryptedChallenge: response.encryptedChallenge,
encryptedDeviceSecret: response.encryptedDeviceSecret,
iv: response.iv
}).then(function() {
return {
sessionId: sessionId,
sessionKey: sessionKey
};
});
}
};
/**
* Encrypt and upload the private PGP key to the server.
* @param {String} options.userId The user's email address
* @param {String} options.code The randomly generated or self selected code used to derive the key for the encryption of the private PGP key
*/
Keychain.prototype.uploadPrivateKey = function(options) {
var self = this,
config = self._appConfig.config,
keySize = config.symKeySize,
salt;
if (!options.userId || !options.code) {
return new Promise(function() {
throw new Error('Incomplete arguments!');
});
}
return deriveKey(options.code);
function deriveKey(code) {
// generate random salt
salt = util.random(keySize);
// derive key from the code using PBKDF2
return self._crypto.deriveKey(code, salt, keySize).then(function(key) {
return encryptPrivateKey(key);
});
}
function encryptPrivateKey(encryptionKey) {
var privkeyId, pgpBlock,
iv = util.random(config.symIvSize);
// get private key from local storage
return self.getUserKeyPair(options.userId).then(function(keypair) {
privkeyId = keypair.privateKey._id;
pgpBlock = keypair.privateKey.encryptedKey;
// encrypt the private key with the derived key
return self._crypto.encrypt(pgpBlock, encryptionKey, iv);
}).then(function(ct) {
return uploadPrivateKey({
_id: privkeyId,
userId: options.userId,
encryptedPrivateKey: ct,
salt: salt,
iv: iv
});
});
}
function uploadPrivateKey(payload) {
var pt = payload.encryptedPrivateKey,
iv = payload.iv;
// authenticate to server for upload
return self._authenticateToPrivateKeyServer(options.userId).then(function(authSessionKey) {
// set sessionId
payload.sessionId = authSessionKey.sessionId;
// encrypt encryptedPrivateKey again using authSessionKey
var key = authSessionKey.sessionKey;
return self._crypto.encrypt(pt, key, iv);
}).then(function(ct) {
// replace the encryptedPrivateKey with the double wrapped ciphertext
payload.encryptedPrivateKey = ct;
// upload the encrypted priavet key
return self._privateKeyDao.upload(payload);
});
}
};
/**
* Request downloading the user's encrypted private key. This will initiate the server to send the recovery token via email/sms to the user.
* @param {String} options.userId The user's email address
* @param {String} options.keyId The private PGP key id
*/
Keychain.prototype.requestPrivateKeyDownload = function(options) {
return this._privateKeyDao.requestDownload(options);
};
/**
* Query if an encrypted private PGP key exists on the server without initializing the recovery procedure
* @param {String} options.userId The user's email address
* @param {String} options.keyId The private PGP key id
*/
Keychain.prototype.hasPrivateKey = function(options) {
return this._privateKeyDao.hasPrivateKey(options);
};
/**
* Download the encrypted private PGP key from the server using the recovery token.
* @param {String} options.userId The user's email address
* @param {String} options.keyId The user's email address
* @param {String} options.recoveryToken The recovery token acquired via email/sms from the key server
*/
Keychain.prototype.downloadPrivateKey = function(options) {
return this._privateKeyDao.download(options);
};
/**
* This is called after the encrypted private key has successfully been downloaded and it's ready to be decrypted and stored in localstorage.
* @param {String} options._id The private PGP key id
* @param {String} options.userId The user's email address
* @param {String} options.code The randomly generated or self selected code used to derive the key for the decryption of the private PGP key
* @param {String} options.encryptedPrivateKey The encrypted private PGP key
* @param {String} options.salt The salt required to derive the code derived key
* @param {String} options.iv The iv used to encrypt the private PGP key
*/
Keychain.prototype.decryptAndStorePrivateKeyLocally = function(options) {
var self = this,
code = options.code,
salt = options.salt,
config = self._appConfig.config,
keySize = config.symKeySize;
if (!options._id || !options.userId || !options.code || !options.salt || !options.encryptedPrivateKey || !options.iv) {
return new Promise(function() {
throw new Error('Incomplete arguments!');
});
}
// derive key from the code and the salt using PBKDF2
return self._crypto.deriveKey(code, salt, keySize).then(function(key) {
return decryptAndStore(key);
});
function decryptAndStore(derivedKey) {
// decrypt the private key with the derived key
var ct = options.encryptedPrivateKey,
iv = options.iv;
return self._crypto.decrypt(ct, derivedKey, iv).then(function(privateKeyArmored) {
// validate pgp key
var keyParams;
try {
keyParams = self._pgp.getKeyParams(privateKeyArmored);
} catch (e) {
throw new Error('Error parsing private PGP key!');
}
if (keyParams._id !== options._id || keyParams.userId !== options.userId) {
throw new Error('Private key parameters don\'t match with public key\'s!');
}
var keyObject = {
_id: options._id,
userId: options.userId,
encryptedKey: privateKeyArmored
};
// store private key locally
return self.saveLocalPrivateKey(keyObject).then(function() {
return keyObject;
});
}).catch(function() {
throw new Error('Invalid keychain code!');
});
}
};
//
// Keypair functions
//
@ -597,7 +209,7 @@ Keychain.prototype.getUserKeyPair = function(userId) {
var self = this;
// search for user's public key locally
return self._lawnchairDAO.list(DB_PUBLICKEY, 0, null).then(function(allPubkeys) {
return self._lawnchairDAO.list(DB_PUBLICKEY).then(function(allPubkeys) {
var pubkey = _.findWhere(allPubkeys, {
userId: userId
});
@ -659,7 +271,7 @@ Keychain.prototype.putUserKeyPair = function(keypair) {
// validate input
if (!keypair || !keypair.publicKey || !keypair.privateKey || !keypair.publicKey.userId || keypair.publicKey.userId !== keypair.privateKey.userId) {
return new Promise(function() {
throw new Error('Incorrect input!');
throw new Error('Cannot put user key pair: Incorrect input!');
});
}
@ -676,6 +288,24 @@ Keychain.prototype.putUserKeyPair = function(keypair) {
});
};
/**
* Uploads the public key
* @param {Object} publicKey The user's public key
* @return {Promise}
*/
Keychain.prototype.uploadPublicKey = function(publicKey) {
var self = this;
// validate input
if (!publicKey || !publicKey.userId || !publicKey.publicKey) {
return new Promise(function() {
throw new Error('Cannot upload user key pair: Incorrect input!');
});
}
return self._publicKeyDao.put(publicKey);
};
//
// Helper functions
//
@ -712,7 +342,7 @@ Keychain.prototype.lookupPublicKey = function(id) {
*/
Keychain.prototype.listLocalPublicKeys = function() {
// search local keyring for public key
return this._lawnchairDAO.list(DB_PUBLICKEY, 0, null);
return this._lawnchairDAO.list(DB_PUBLICKEY);
};
Keychain.prototype.removeLocalPublicKey = function(id) {

View File

@ -104,60 +104,49 @@ LawnchairDAO.prototype.read = function(key) {
/**
* List all the items of a certain type
* @param type [String] The type of item e.g. 'email'
* @param offset [Number] The offset of items to fetch (0 is the last stored item)
* @param num [Number] The number of items to fetch (null means fetch all)
* @return {Promise}
*/
LawnchairDAO.prototype.list = function(type, offset, num) {
LawnchairDAO.prototype.list = function(query, exactMatchOnly) {
var self = this;
return new Promise(function(resolve) {
var i, from, to,
matchingKeys = [],
intervalKeys = [],
list = [];
var matchingKeys = [];
// validate input
if (!type || typeof offset === 'undefined' || typeof num === 'undefined') {
if ((Array.isArray(query) && query.length === 0) || (!Array.isArray(query) && !query)) {
throw new Error('Args not is not set!');
}
// this method operates on arrays of keys, so normalize input 'key' -> ['key']
if (!Array.isArray(query)) {
query = [query];
}
// get all keys
self._db.keys(function(keys) {
// check if key begins with type
keys.forEach(function(key) {
if (key.indexOf(type) === 0) {
matchingKeys.push(key);
}
// check if there are keys in the db that start with the respective query
matchingKeys = keys.filter(function(key) {
return query.filter(function(type) {
if (exactMatchOnly) {
return key === type;
} else {
return key.indexOf(type) === 0;
}
}).length > 0;
});
// sort keys
matchingKeys.sort();
// set window of items to fetch
// if num is null, list all items
from = (num) ? matchingKeys.length - offset - num : 0;
to = matchingKeys.length - 1 - offset;
// filter items within requested interval
for (i = 0; i < matchingKeys.length; i++) {
if (i >= from && i <= to) {
intervalKeys.push(matchingKeys[i]);
}
}
// return if there are no matching keys
if (intervalKeys.length === 0) {
resolve(list);
if (matchingKeys.length === 0) {
// no matching keys, resolve
resolve([]);
return;
}
// fetch all items from data-store with matching key
self._db.get(intervalKeys, function(intervalList) {
intervalList.forEach(function(item) {
list.push(item.object);
// fetch all items from data-store with matching keys
self._db.get(matchingKeys, function(intervalList) {
var result = intervalList.map(function(item) {
return item.object;
});
// return only the interval between offset and num
resolve(list);
resolve(result);
});
});
});

View File

@ -4,96 +4,89 @@ var ngModule = angular.module('woServices');
ngModule.service('privateKey', PrivateKey);
module.exports = PrivateKey;
function PrivateKey(privateKeyRestDao) {
this._restDao = privateKeyRestDao;
var ImapClient = require('imap-client');
var util = require('crypto-lib').util;
var IMAP_KEYS_FOLDER = 'openpgp_keys';
var MIME_TYPE = 'application/x.encrypted-pgp-key';
var MSG_PART_TYPE_ATTACHMENT = 'attachment';
function PrivateKey(auth, mailbuild, mailreader, appConfig, pgp, crypto, axe) {
this._auth = auth;
this._Mailbuild = mailbuild;
this._mailreader = mailreader;
this._appConfig = appConfig;
this._pgp = pgp;
this._crypto = crypto;
this._axe = axe;
}
//
// Device registration functions
//
/**
* Request registration of a new device by fetching registration session key.
* @param {String} options.userId The user's email address
* @param {String} options.deviceName The device's memorable name
* @return {Object} {encryptedRegSessionKey:[base64]}
* Configure the local imap client used for key-sync with credentials from the auth module.
*/
PrivateKey.prototype.requestDeviceRegistration = function(options) {
PrivateKey.prototype.init = function() {
var self = this;
return new Promise(function(resolve) {
if (!options.userId || !options.deviceName) {
throw new Error('Incomplete arguments!');
}
resolve();
}).then(function() {
var uri = '/device/user/' + options.userId + '/devicename/' + options.deviceName;
return self._restDao.post(undefined, uri);
return self._auth.getCredentials().then(function(credentials) {
// tls socket worker path for multithreaded tls in non-native tls environments
credentials.imap.tlsWorkerPath = self._appConfig.config.workerPath + '/tcp-socket-tls-worker.min.js';
self._imap = new ImapClient(credentials.imap);
self._imap.onError = self._axe.error;
// login to the imap server
return self._imap.login();
});
};
/**
* Authenticate device registration by uploading the deviceSecret encrypted with the regSessionKeys.
* @param {String} options.userId The user's email address
* @param {String} options.deviceName The device's memorable name
* @param {String} options.encryptedDeviceSecret The base64 encoded encrypted device secret
* @param {String} options.iv The iv used for encryption
* Cleanup by logging out of the imap client.
*/
PrivateKey.prototype.uploadDeviceSecret = function(options) {
var self = this;
PrivateKey.prototype.destroy = function() {
this._imap.logout();
// don't wait for logout to complete
return new Promise(function(resolve) {
if (!options.userId || !options.deviceName || !options.encryptedDeviceSecret || !options.iv) {
throw new Error('Incomplete arguments!');
}
resolve();
}).then(function() {
var uri = '/device/user/' + options.userId + '/devicename/' + options.deviceName;
return self._restDao.put(options, uri);
});
};
//
// Private key functions
//
/**
* Request authSessionKeys required for upload the encrypted private PGP key.
* @param {String} options.userId The user's email address
* @return {Object} {sessionId, encryptedAuthSessionKey:[base64 encoded], encryptedChallenge:[base64 encoded]}
*/
PrivateKey.prototype.requestAuthSessionKey = function(options) {
var self = this;
return new Promise(function(resolve) {
if (!options.userId) {
throw new Error('Incomplete arguments!');
}
resolve();
}).then(function() {
var uri = '/auth/user/' + options.userId;
return self._restDao.post(undefined, uri);
});
};
/**
* Verifiy authentication by uploading the challenge and deviceSecret encrypted with the authSessionKeys as a response.
* @param {String} options.userId The user's email address
* @param {String} options.encryptedChallenge The server's base64 encoded challenge encrypted using the authSessionKey
* @param {String} options.encryptedDeviceSecret The server's base64 encoded deviceSecret encrypted using the authSessionKey
* @param {String} options.iv The iv used for encryption
* Encrypt and upload the private PGP key to the server.
* @param {String} code The randomly generated or self selected code used to derive the key for the encryption of the private PGP key
*/
PrivateKey.prototype.verifyAuthentication = function(options) {
var self = this;
return new Promise(function(resolve) {
if (!options.userId || !options.sessionId || !options.encryptedChallenge || !options.encryptedDeviceSecret || !options.iv) {
throw new Error('Incomplete arguments!');
}
resolve();
PrivateKey.prototype.encrypt = function(code) {
var self = this,
config = self._appConfig.config,
keySize = config.symKeySize,
encryptionKey, salt, iv, privkeyId;
}).then(function() {
var uri = '/auth/user/' + options.userId + '/session/' + options.sessionId;
return self._restDao.put(options, uri);
if (!code) {
return new Promise(function() {
throw new Error('Incomplete arguments!');
});
}
// generate random salt and iv
salt = util.random(keySize);
iv = util.random(config.symIvSize);
// derive key from the code using PBKDF2
return self._crypto.deriveKey(code, salt, keySize).then(function(key) {
encryptionKey = key;
// get private key from local storage
return self._pgp.exportKeys();
}).then(function(keypair) {
privkeyId = keypair.keyId;
// encrypt the private key with the derived key
return self._crypto.encrypt(keypair.privateKeyArmored, encryptionKey, iv);
}).then(function(ct) {
return {
_id: privkeyId,
encryptedPrivateKey: ct,
salt: salt,
iv: iv
};
});
};
@ -102,104 +95,304 @@ PrivateKey.prototype.verifyAuthentication = function(options) {
* @param {String} options._id The hex encoded capital 16 char key id
* @param {String} options.userId The user's email address
* @param {String} options.encryptedPrivateKey The base64 encoded encrypted private PGP key
* @param {String} options.sessionId The session id
*/
PrivateKey.prototype.upload = function(options) {
var self = this;
var self = this,
path;
return new Promise(function(resolve) {
if (!options._id || !options.userId || !options.encryptedPrivateKey || !options.sessionId || !options.salt || !options.iv) {
throw new Error('Incomplete arguments!');
if (!options._id || !options.userId || !options.encryptedPrivateKey || !options.salt || !options.iv) {
throw new Error('Incomplete arguments for key upload!');
}
resolve();
}).then(function() {
var uri = '/privatekey/user/' + options.userId + '/session/' + options.sessionId;
return self._restDao.post(options, uri);
});
};
/**
* Query if an encrypted private PGP key exists on the server without initializing the recovery procedure.
* @param {String} options.userId The user's email address
* @param {String} options.keyId The private PGP key id
* @return {Boolean} whether the key was found on the server or not.
*/
PrivateKey.prototype.hasPrivateKey = function(options) {
var self = this;
return new Promise(function(resolve) {
if (!options.userId || !options.keyId) {
throw new Error('Incomplete arguments!');
}
resolve();
// Some servers (Exchange, Cyrus) error when creating an existing IMAP mailbox instead of
// responding with ALREADYEXISTS. Hence we search for the folder before uploading.
}).then(function() {
return self._restDao.get({
uri: '/privatekey/user/' + options.userId + '/key/' + options.keyId + '?ignoreRecovery=true',
self._axe.debug('Searching imap folder for key upload...');
return self._getFolder().then(function(fullPath) {
path = fullPath;
}).catch(function() {
// create imap folder
self._axe.debug('Folder not found, creating imap folder.');
return self._imap.createFolder({
path: IMAP_KEYS_FOLDER
}).then(function(fullPath) {
path = fullPath;
self._axe.debug('Successfully created imap folder ' + path);
}).catch(function(err) {
var prettyErr = new Error('Creating imap folder ' + IMAP_KEYS_FOLDER + ' failed: ' + err.message);
self._axe.error(prettyErr);
throw prettyErr;
});
});
}).then(function() {
return true;
}).then(createMessage).then(function(message) {
}).catch(function(err) {
// 404: there is no encrypted private key on the server
if (err.code && err.code !== 200) {
return false;
}
throw err;
});
};
/**
* Request download for the encrypted private PGP key.
* @param {String} options.userId The user's email address
* @param {String} options.keyId The private PGP key id
* @return {Boolean} whether the key was found on the server or not.
*/
PrivateKey.prototype.requestDownload = function(options) {
var self = this;
return new Promise(function(resolve) {
if (!options.userId || !options.keyId) {
throw new Error('Incomplete arguments!');
}
resolve();
}).then(function() {
return self._restDao.get({
uri: '/privatekey/user/' + options.userId + '/key/' + options.keyId
// upload to imap folder
self._axe.debug('Uploading key...');
return self._imap.uploadMessage({
path: path,
message: message
});
});
}).then(function() {
return true;
function createMessage() {
var encryptedKeyBuf = util.binStr2Uint8Arr(util.base642Str(options.encryptedPrivateKey));
var saltBuf = util.binStr2Uint8Arr(util.base642Str(options.salt));
var ivBuf = util.binStr2Uint8Arr(util.base642Str(options.iv));
}).catch(function(err) {
// 404: there is no encrypted private key on the server
if (err.code && err.code !== 200) {
return false;
}
// allocate payload buffer for sync
var payloadBuf = new Uint8Array(1 + saltBuf.length + ivBuf.length + encryptedKeyBuf.length);
var offset = 0;
// set version byte
payloadBuf[offset] = 0x01; // version 1 of the key-sync protocol
offset++;
// copy salt bytes
payloadBuf.set(saltBuf, offset);
offset += saltBuf.length;
// copy iv bytes
payloadBuf.set(ivBuf, offset);
offset += ivBuf.length;
// copy encrypted key bytes
payloadBuf.set(encryptedKeyBuf, offset);
throw err;
// create MIME tree
var rootNode = options.rootNode || new self._Mailbuild();
rootNode.setHeader({
subject: options._id,
from: options.userId,
to: options.userId,
'content-type': MIME_TYPE + '; charset=us-ascii',
'content-transfer-encoding': 'base64'
});
rootNode.setContent(payloadBuf);
return rootNode.build();
}
};
/**
* Check if matching private key is stored in IMAP.
*/
PrivateKey.prototype.isSynced = function() {
var self = this;
return self._getFolder().then(function(path) {
return self._fetchMessage({
keyId: self._pgp.getKeyId(),
path: path
});
}).then(function(msg) {
return !!msg;
}).catch(function() {
return false;
});
};
/**
* Verify the download request for the private PGP key using the recovery token sent via email. This downloads the actual encrypted private key.
* Verify the download request for the private PGP key.
* @param {String} options.userId The user's email address
* @param {String} options.keyId The private key id
* @param {String} options.recoveryToken The token proving the user own the email account
* @return {Object} {_id:[hex encoded capital 16 char key id], encryptedPrivateKey:[base64 encoded], encryptedUserId: [base64 encoded]}
*/
PrivateKey.prototype.download = function(options) {
var self = this;
return new Promise(function(resolve) {
if (!options.userId || !options.keyId || !options.recoveryToken) {
throw new Error('Incomplete arguments!');
}
resolve();
var self = this,
path, message;
return self._getFolder().then(function(fullPath) {
path = fullPath;
return self._fetchMessage({
keyId: options.keyId,
path: path
}).then(function(msg) {
if (!msg) {
throw new Error('Private key not synced!');
}
message = msg;
});
}).then(function() {
// get the body for the message
return self._imap.getBodyParts({
path: path,
uid: message.uid,
bodyParts: message.bodyParts
});
}).then(function() {
return self._restDao.get({
uri: '/privatekey/user/' + options.userId + '/key/' + options.keyId + '/recovery/' + options.recoveryToken
// parse the message
return self._parse(message);
}).then(function(root) {
var payloadBuf = filterBodyParts(root, MSG_PART_TYPE_ATTACHMENT)[0].content;
var offset = 0;
var SALT_LEN = 32;
var IV_LEN = 12;
// check version
var version = payloadBuf[offset];
offset++;
if (version !== 1) {
throw new Error('Unsupported key sync protocol version!');
}
// salt
var saltBuf = payloadBuf.subarray(offset, offset + SALT_LEN);
offset += SALT_LEN;
// iv
var ivBuf = payloadBuf.subarray(offset, offset + IV_LEN);
offset += IV_LEN;
// encrypted private key
var encryptedKeyBuf = payloadBuf.subarray(offset, payloadBuf.length);
return {
_id: options.keyId,
userId: options.userId,
encryptedPrivateKey: util.str2Base64(util.uint8Arr2BinStr(encryptedKeyBuf)),
salt: util.str2Base64(util.uint8Arr2BinStr(saltBuf)),
iv: util.str2Base64(util.uint8Arr2BinStr(ivBuf))
};
});
};
/**
* This is called after the encrypted private key has successfully been downloaded and it's ready to be decrypted and stored in localstorage.
* @param {String} options._id The private PGP key id
* @param {String} options.userId The user's email address
* @param {String} options.code The randomly generated or self selected code used to derive the key for the decryption of the private PGP key
* @param {String} options.encryptedPrivateKey The encrypted private PGP key
* @param {String} options.salt The salt required to derive the code derived key
* @param {String} options.iv The iv used to encrypt the private PGP key
*/
PrivateKey.prototype.decrypt = function(options) {
var self = this,
config = self._appConfig.config,
keySize = config.symKeySize;
if (!options._id || !options.userId || !options.code || !options.salt || !options.encryptedPrivateKey || !options.iv) {
return new Promise(function() {
throw new Error('Incomplete arguments!');
});
}
// derive key from the code and the salt using PBKDF2
return self._crypto.deriveKey(options.code, options.salt, keySize).then(function(derivedKey) {
// decrypt the private key with the derived key
return self._crypto.decrypt(options.encryptedPrivateKey, derivedKey, options.iv).catch(function() {
throw new Error('Invalid backup code!');
});
}).then(function(privateKeyArmored) {
// validate pgp key
var keyParams;
try {
keyParams = self._pgp.getKeyParams(privateKeyArmored);
} catch (e) {
throw new Error('Error parsing private PGP key!');
}
if (keyParams._id !== options._id || keyParams.userId !== options.userId) {
throw new Error('Private key parameters don\'t match with public key\'s!');
}
return {
_id: options._id,
userId: options.userId,
encryptedKey: privateKeyArmored
};
});
};
PrivateKey.prototype._getFolder = function() {
var self = this;
return self._imap.listWellKnownFolders().then(function(wellKnownFolders) {
var paths = []; // gathers paths
// extract the paths from the folder arrays
for (var folderType in wellKnownFolders) {
if (wellKnownFolders.hasOwnProperty(folderType) && Array.isArray(wellKnownFolders[folderType])) {
paths = paths.concat(_.pluck(wellKnownFolders[folderType], 'path'));
}
}
paths = paths.filter(function(path) {
// find a folder that ends with IMAP_KEYS_FOLDER
var lastIndex = path.lastIndexOf(IMAP_KEYS_FOLDER);
return (lastIndex !== -1) && (lastIndex + IMAP_KEYS_FOLDER.length === path.length);
});
if (paths.length > 1) {
self._axe.warn('Multiple folders matching path ' + IMAP_KEYS_FOLDER + ' found, PGP key target folder unclear. Picking first one: ' + paths.join(', '));
}
if (paths.length === 0) {
throw new Error('Imap folder ' + IMAP_KEYS_FOLDER + ' does not exist for key sync!');
}
return paths[0];
});
};
PrivateKey.prototype._fetchMessage = function(options) {
var self = this;
if (!options.keyId) {
return new Promise(function() {
throw new Error('Incomplete arguments!');
});
}
// get the metadata for the message
return self._imap.listMessages({
path: options.path
}).then(function(messages) {
if (!messages.length) {
// message has been deleted in the meantime
return;
}
// get matching private key if multiple keys uloaded
return _.findWhere(messages, {
subject: options.keyId
});
}).catch(function(e) {
throw new Error('Failed to retrieve PGP key message from IMAP! Reason: ' + e.message);
});
};
PrivateKey.prototype._parse = function(options) {
var self = this;
return new Promise(function(resolve, reject) {
self._mailreader.parse(options, function(err, root) {
if (err) {
reject(err);
} else {
resolve(root);
}
});
});
};
};
/**
* Helper function that recursively traverses the body parts tree. Looks for bodyParts that match the provided type and aggregates them
*
* @param {Array} bodyParts The bodyParts array
* @param {String} type The type to look up
* @param {undefined} result Leave undefined, only used for recursion
*/
function filterBodyParts(bodyParts, type, result) {
result = result || [];
bodyParts.forEach(function(part) {
if (part.type === type) {
result.push(part);
} else if (Array.isArray(part.content)) {
filterBodyParts(part.content, type, result);
}
});
return result;
}

View File

@ -0,0 +1,247 @@
'use strict';
var MSG_PART_ATTR_CONTENT = 'content';
var MSG_PART_TYPE_TEXT = 'text';
var ngModule = angular.module('woServices');
ngModule.service('publickeyVerifier', PublickeyVerifier);
module.exports = PublickeyVerifier;
var ImapClient = require('imap-client');
function PublickeyVerifier(auth, appConfig, mailreader, keychain) {
this._appConfig = appConfig;
this._mailreader = mailreader;
this._keychain = keychain;
this._auth = auth;
this._workerPath = appConfig.config.workerPath + '/tcp-socket-tls-worker.min.js';
this._keyServerUrl = this._appConfig.config.keyServerUrl;
}
//
// Public API
//
PublickeyVerifier.prototype.configure = function() {
var self = this;
return self._auth.getCredentials().then(function(credentials) {
// tls socket worker path for multithreaded tls in non-native tls environments
credentials.imap.tlsWorkerPath = self._appConfig.config.workerPath + '/tcp-socket-tls-worker.min.js';
self._imap = new ImapClient(credentials.imap);
});
};
PublickeyVerifier.prototype.uploadPublicKey = function() {
if (this.keypair) {
return this._keychain.uploadPublicKey(this.keypair.publicKey);
}
return new Promise(function(resolve) {
resolve();
});
};
PublickeyVerifier.prototype.persistKeypair = function() {
if (this.keypair) {
return this._keychain.putUserKeyPair(this.keypair);
}
return new Promise(function(resolve) {
resolve();
});
};
PublickeyVerifier.prototype.verify = function() {
var self = this,
verificationSuccessful = false;
// have to wrap it in a promise to catch .onError of imap-client
return new Promise(function(resolve, reject) {
self._imap.onError = reject;
// login
self._imap.login().then(function() {
// list folders
return self._imap.listWellKnownFolders();
}).then(function(wellKnownFolders) {
var paths = []; // gathers paths
// extract the paths from the folder arrays
for (var folderType in wellKnownFolders) {
if (wellKnownFolders.hasOwnProperty(folderType) && Array.isArray(wellKnownFolders[folderType])) {
paths = paths.concat(_.pluck(wellKnownFolders[folderType], 'path'));
}
}
return paths;
}).then(function(paths) {
return self._searchAll(paths); // search
}).then(function(candidates) {
if (!candidates.length) {
// nothing here to potentially verify
verificationSuccessful = false;
return;
}
// verify everything that looks like a verification mail
return self._verifyAll(candidates).then(function(success) {
verificationSuccessful = success;
});
}).then(function() {
// at this point, we don't care about errors anymore
self._imap.onError = function() {};
self._imap.logout();
if (!verificationSuccessful) {
// nothing unexpected went wrong, but no public key could be verified
throw new Error();
}
resolve(); // we're done
}).catch(reject);
});
};
PublickeyVerifier.prototype._searchAll = function(paths) {
var self = this,
candidates = []; // gather matching uids
// async for-loop inside a then-able
return new Promise(next);
// search each path for the relevant email
function next(resolve) {
if (!paths.length) {
resolve(candidates);
return;
}
var path = paths.shift();
self._imap.search({
path: path,
header: ['Subject', self._appConfig.string.verificationSubject]
}).then(function(uids) {
uids.forEach(function(uid) {
candidates.push({
path: path,
uid: uid
});
});
next(resolve); // keep on searching
}).catch(function() {
next(resolve); // if there's an error, just search the next inbox
});
}
};
PublickeyVerifier.prototype._verifyAll = function(candidates) {
var self = this;
// async for-loop inside a then-able
return new Promise(next);
function next(resolve) {
if (!candidates.length) {
resolve(false);
return;
}
var candidate = candidates.shift();
self._verify(candidate.path, candidate.uid).then(function(success) {
if (success) {
resolve(success); // we're done here
} else {
next(resolve);
}
}).catch(function() {
next(resolve); // ignore
});
}
};
PublickeyVerifier.prototype._verify = function(path, uid) {
var self = this,
message;
// get the metadata for the message
return self._imap.listMessages({
path: path,
firstUid: uid,
lastUid: uid
}).then(function(messages) {
if (!messages.length) {
// message has been deleted in the meantime
throw new Error('Message has already been deleted');
}
// remember in scope
message = messages[0];
}).then(function() {
// get the body for the message
return self._imap.getBodyParts({
path: path,
uid: uid,
bodyParts: message.bodyParts
});
}).then(function() {
// parse the message
return new Promise(function(resolve, reject) {
self._mailreader.parse(message, function(err, root) {
if (err) {
reject(err);
} else {
resolve(root);
}
});
});
}).then(function(root) {
// extract the nonce
var body = _.pluck(filterBodyParts(root, MSG_PART_TYPE_TEXT), MSG_PART_ATTR_CONTENT).join('\n'),
verificationUrlPrefix = self._keyServerUrl + self._appConfig.config.verificationUrl,
uuid = body.split(verificationUrlPrefix).pop().substr(0, self._appConfig.config.verificationUuidLength),
uuidRegex = /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/;
// there's no valid uuid in the message, so forget about it
if (!uuidRegex.test(uuid)) {
throw new Error('No public key verifier found!');
}
// there's a valid uuid in the message, so try to verify it
return self._keychain.verifyPublicKey(uuid).catch(function(err) {
throw new Error('Verifying your public key failed: ' + err.message);
});
}).then(function() {
return self._imap.deleteMessage({
path: path,
uid: uid
}).catch(function() {}); // ignore error here
}).then(function() {
return true;
});
};
/**
* Helper function that recursively traverses the body parts tree. Looks for bodyParts that match the provided type and aggregates them
*
* @param {Array} bodyParts The bodyParts array
* @param {String} type The type to look up
* @param {undefined} result Leave undefined, only used for recursion
*/
function filterBodyParts(bodyParts, type, result) {
result = result || [];
bodyParts.forEach(function(part) {
if (part.type === type) {
result.push(part);
} else if (Array.isArray(part.content)) {
filterBodyParts(part.content, type, result);
}
});
return result;
}

View File

@ -5,21 +5,14 @@ var ngModule = angular.module('woServices');
// rest dao for use in the public key service
ngModule.factory('publicKeyRestDao', function(appConfig) {
var dao = new RestDAO();
dao.setBaseUri(appConfig.config.cloudUrl);
return dao;
});
// rest dao for use in the private key service
ngModule.factory('privateKeyRestDao', function(appConfig) {
var dao = new RestDAO();
dao.setBaseUri(appConfig.config.privkeyServerUrl);
dao.setBaseUri(appConfig.config.keyServerUrl);
return dao;
});
// rest dao for use in the invitation service
ngModule.factory('invitationRestDao', function(appConfig) {
var dao = new RestDAO();
dao.setBaseUri(appConfig.config.cloudUrl);
dao.setBaseUri(appConfig.config.keyServerUrl);
return dao;
});
@ -133,7 +126,7 @@ RestDAO.prototype._processRequest = function(options) {
if (options.type === 'json') {
try {
res = JSON.parse(xhr.responseText);
} catch(e) {
} catch (e) {
res = xhr.responseText;
}
} else {

View File

@ -53,6 +53,7 @@ ConnectionDoctor.prototype.configure = function(credentials) {
port: this.credentials.imap.port,
secure: this.credentials.imap.secure,
ignoreTLS: this.credentials.imap.ignoreTLS,
requireTLS: this.credentials.imap.requireTLS,
ca: this.credentials.imap.ca,
tlsWorkerPath: this._workerPath,
auth: {
@ -65,6 +66,7 @@ ConnectionDoctor.prototype.configure = function(credentials) {
this._smtp = new SmtpClient(this.credentials.smtp.host, this.credentials.smtp.port, {
useSecureTransport: this.credentials.smtp.secure,
ignoreTLS: this.credentials.smtp.ignoreTLS,
requireTLS: this.credentials.smtp.requireTLS,
ca: this.credentials.smtp.ca,
tlsWorkerPath: this._workerPath,
auth: {
@ -218,25 +220,21 @@ ConnectionDoctor.prototype._checkImap = function() {
}
};
self._imap.login(function() {
self._imap.login().then(function() {
loggedIn = true;
return self._imap.listWellKnownFolders();
}).then(function(wellKnownFolders) {
if (wellKnownFolders.Inbox.length === 0) {
// the client needs at least an inbox folder to work properly
reject(createError(NO_INBOX, str.connDocNoInbox.replace('{0}', host)));
return;
}
self._imap.listWellKnownFolders(function(error, wellKnownFolders) {
if (error) {
reject(createError(GENERIC_ERROR, str.connDocGenericError.replace('{0}', host).replace('{1}', error.message), error));
return;
}
if (wellKnownFolders.Inbox.length === 0) {
// the client needs at least an inbox folder to work properly
reject(createError(NO_INBOX, str.connDocNoInbox.replace('{0}', host)));
return;
}
self._imap.logout(function() {
resolve();
});
});
return self._imap.logout();
}).then(function(){
resolve();
}).catch(function(error) {
reject(createError(GENERIC_ERROR, str.connDocGenericError.replace('{0}', host).replace('{1}', error.message), error));
});
});
};

View File

@ -25,10 +25,10 @@ Download.prototype.createDownload = function(options) {
supportsBlob = !!new Blob();
} catch (e) {}
if (typeof a.download !== "undefined" && supportsBlob) {
if (typeof a.download !== 'undefined' && supportsBlob) {
// ff 30+, chrome 27+ (android: 37+)
document.body.appendChild(a);
a.style = "display: none";
a.style.display = 'none';
a.href = window.URL.createObjectURL(new Blob([content], {
type: contentType
}));
@ -52,15 +52,15 @@ Download.prototype.createDownload = function(options) {
var url = window.URL.createObjectURL(new Blob([content], {
type: contentType
}));
var newTab = window.open(url, "_blank");
var newTab = window.open(url, '_blank');
if (!newTab) {
window.location.href = url;
}
} else {
// anything else, where anything at all is better than nothing
if (typeof content !== "string" && content.buffer) {
if (typeof content !== 'string' && content.buffer) {
content = util.arrBuf2BinStr(content.buffer);
}
window.open('data:' + contentType + ';base64,' + btoa(content), "_blank");
window.open('data:' + contentType + ';base64,' + btoa(content), '_blank');
}
};

View File

@ -45,6 +45,27 @@ Dummy.prototype.listFolders = function() {
name: 'Junk',
count: 0,
path: 'JUNK'
}, {
name: 'Foo',
count: 0,
path: 'FOO'
}, {
name: 'Snafu',
count: 0,
path: 'SNAFU'
}, {
name: 'Tralalalala',
count: 0,
path: 'TRALALALALA'
}, {
name: 'Another one',
count: 0,
path: 'ANOTHERONE'
}, {
name: 'Mucho Folder',
count: 0,
path: 'MUCHOFOLDER'
}];
return dummies;
@ -101,9 +122,11 @@ Dummy.prototype.listMails = function() {
'>> from 0.7.0.1\n' +
'>>\n' +
'>> God speed!'; // plaintext body
//this.html = '<!DOCTYPE html><html><head></head><body><h1 style="border: 1px solid red; width: 500px;">Hello there' + Math.random() + '</h1></body></html>';
//this.html = '<!DOCTYPE html><html><head></head><body><h1 style="border: 1px solid red; width: 500px; margin:0;">Hello there' + Math.random() + '</h1></body></html>';
this.encrypted = true;
this.decrypted = true;
this.signed = true;
this.signaturesValid = true;
};
var dummies = [],

View File

@ -4,8 +4,9 @@ var ngModule = angular.module('woUtil');
ngModule.service('notification', Notif);
module.exports = Notif;
function Notif(appConfig) {
function Notif(appConfig, axe) {
this._appConfig = appConfig;
this._axe = axe;
if (window.Notification) {
this.hasPermission = Notification.permission === "granted";
@ -39,10 +40,17 @@ Notif.prototype.create = function(options) {
});
}
var notification = new Notification(options.title, {
body: options.message,
icon: self._appConfig.config.iconPath
});
var notification;
try {
notification = new Notification(options.title, {
body: options.message,
icon: self._appConfig.config.iconPath
});
} catch (err) {
self._axe.error('Displaying notification failed: ' + err.message);
return;
}
notification.onclick = function() {
window.focus();
options.onClick();

View File

@ -10,7 +10,8 @@ var axe = require('axe-logger'),
updateV2 = require('./update-v2'),
updateV3 = require('./update-v3'),
updateV4 = require('./update-v4'),
updateV5 = require('./update-v5');
updateV5 = require('./update-v5'),
updateV6 = require('./update-v6');
/**
* Handles database migration
@ -18,7 +19,7 @@ var axe = require('axe-logger'),
function UpdateHandler(appConfigStore, accountStore, auth, dialog) {
this._appConfigStorage = appConfigStore;
this._userStorage = accountStore;
this._updateScripts = [updateV1, updateV2, updateV3, updateV4, updateV5];
this._updateScripts = [updateV1, updateV2, updateV3, updateV4, updateV5, updateV6];
this._auth = auth;
this._dialog = dialog;
}
@ -32,7 +33,7 @@ UpdateHandler.prototype.update = function() {
targetVersion = cfg.dbVersion,
versionDbType = 'dbVersion';
return self._appConfigStorage.listItems(versionDbType, 0, null).then(function(items) {
return self._appConfigStorage.listItems(versionDbType).then(function(items) {
// parse the database version number
if (items && items.length > 0) {
currentVersion = parseInt(items[0], 10);

View File

@ -66,7 +66,7 @@ function update(options) {
});
function loadFromDB(key) {
return options.appConfigStorage.listItems(key, 0, null).then(function(cachedItems) {
return options.appConfigStorage.listItems(key).then(function(cachedItems) {
return cachedItems && cachedItems[0];
});
}

View File

@ -18,7 +18,7 @@ var POST_UPDATE_DB_VERSION = 5;
*/
function update(options) {
// remove the emails
return options.userStorage.listItems(FOLDER_DB_TYPE, 0, null).then(function(stored) {
return options.userStorage.listItems(FOLDER_DB_TYPE).then(function(stored) {
var folders = stored[0] || [];
[FOLDER_TYPE_INBOX, FOLDER_TYPE_SENT, FOLDER_TYPE_DRAFTS, FOLDER_TYPE_TRASH].forEach(function(mbxType) {
var foldersForType = folders.filter(function(mbx) {

View File

@ -0,0 +1,18 @@
'use strict';
/**
* Update handler for transition database version 5 -> 6
*/
function update(options) {
var emailDbType = 'email_',
versionDbType = 'dbVersion',
postUpdateDbVersion = 6;
// remove the emails
return options.userStorage.removeList(emailDbType).then(function() {
// update the database version to postUpdateDbVersion
return options.appConfigStorage.storeList([postUpdateDbVersion], versionDbType);
});
}
module.exports = update;

View File

@ -1,5 +1,5 @@
/**
* @license AngularJS v1.3.7
* @license AngularJS v1.3.15
* (c) 2010-2014 Google, Inc. http://angularjs.org
* License: MIT
*/
@ -839,7 +839,8 @@ angular.module('ngAnimate', ['ng'])
* promise that was returned when the animation was started.
*
* ```js
* var promise = $animate.addClass(element, 'super-long-animation').then(function() {
* var promise = $animate.addClass(element, 'super-long-animation');
* promise.then(function() {
* //this will still be called even if cancelled
* });
*
@ -1332,8 +1333,7 @@ angular.module('ngAnimate', ['ng'])
} else if (lastAnimation.event == 'setClass') {
animationsToCancel.push(lastAnimation);
cleanup(element, className);
}
else if (runningAnimations[className]) {
} else if (runningAnimations[className]) {
var current = runningAnimations[className];
if (current.event == animationEvent) {
skipAnimation = true;
@ -1874,7 +1874,7 @@ angular.module('ngAnimate', ['ng'])
return;
}
if (!staggerTime && styles) {
if (!staggerTime && styles && Object.keys(styles).length > 0) {
if (!timings.transitionDuration) {
element.css('transition', timings.animationDuration + 's linear all');
appliedStyles.push('transition');

View File

@ -1,5 +1,5 @@
/**
* @license AngularJS v1.3.7
* @license AngularJS v1.3.15
* (c) 2010-2014 Google, Inc. http://angularjs.org
* License: MIT
*/
@ -250,31 +250,31 @@ angular.mock.$ExceptionHandlerProvider = function() {
*
* @param {string} mode Mode of operation, defaults to `rethrow`.
*
* - `rethrow`: If any errors are passed to the handler in tests, it typically means that there
* is a bug in the application or test, so this mock will make these tests fail.
* - `log`: Sometimes it is desirable to test that an error is thrown, for this case the `log`
* mode stores an array of errors in `$exceptionHandler.errors`, to allow later
* assertion of them. See {@link ngMock.$log#assertEmpty assertEmpty()} and
* {@link ngMock.$log#reset reset()}
* - `rethrow`: If any errors are passed to the handler in tests, it typically means that there
* is a bug in the application or test, so this mock will make these tests fail.
* For any implementations that expect exceptions to be thrown, the `rethrow` mode
* will also maintain a log of thrown errors.
*/
this.mode = function(mode) {
switch (mode) {
case 'rethrow':
handler = function(e) {
throw e;
};
break;
case 'log':
var errors = [];
switch (mode) {
case 'log':
case 'rethrow':
var errors = [];
handler = function(e) {
if (arguments.length == 1) {
errors.push(e);
} else {
errors.push([].slice.call(arguments, 0));
}
if (mode === "rethrow") {
throw e;
}
};
handler.errors = errors;
break;
default:
@ -1283,7 +1283,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* @param {string|RegExp|function(string)} url HTTP url or function that receives the url
* and returns true if the url match the current definition.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
* order to change how a matched request is handled.
*/
@ -1297,7 +1297,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* @param {string|RegExp|function(string)} url HTTP url or function that receives the url
* and returns true if the url match the current definition.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
* order to change how a matched request is handled.
*/
@ -1311,7 +1311,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* @param {string|RegExp|function(string)} url HTTP url or function that receives the url
* and returns true if the url match the current definition.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
* order to change how a matched request is handled.
*/
@ -1327,7 +1327,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* @param {(string|RegExp|function(string))=} data HTTP request body or function that receives
* data string and returns true if the data is as expected.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
* order to change how a matched request is handled.
*/
@ -1343,7 +1343,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* @param {(string|RegExp|function(string))=} data HTTP request body or function that receives
* data string and returns true if the data is as expected.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
* order to change how a matched request is handled.
*/
@ -1356,7 +1356,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
*
* @param {string|RegExp|function(string)} url HTTP url or function that receives the url
* and returns true if the url match the current definition.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
* order to change how a matched request is handled.
*/
@ -1377,7 +1377,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* is in JSON format.
* @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
* object and returns true if the headers match the current expectation.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
* order to change how a matched request is handled.
*
@ -1412,7 +1412,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* @param {string|RegExp|function(string)} url HTTP url or function that receives the url
* and returns true if the url match the current definition.
* @param {Object=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
* order to change how a matched request is handled. See #expect for more info.
*/
@ -1426,7 +1426,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* @param {string|RegExp|function(string)} url HTTP url or function that receives the url
* and returns true if the url match the current definition.
* @param {Object=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
* order to change how a matched request is handled.
*/
@ -1440,7 +1440,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* @param {string|RegExp|function(string)} url HTTP url or function that receives the url
* and returns true if the url match the current definition.
* @param {Object=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
* order to change how a matched request is handled.
*/
@ -1457,7 +1457,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* receives data string and returns true if the data is as expected, or Object if request body
* is in JSON format.
* @param {Object=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
* order to change how a matched request is handled.
*/
@ -1474,7 +1474,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* receives data string and returns true if the data is as expected, or Object if request body
* is in JSON format.
* @param {Object=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
* order to change how a matched request is handled.
*/
@ -1491,7 +1491,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* receives data string and returns true if the data is as expected, or Object if request body
* is in JSON format.
* @param {Object=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
* order to change how a matched request is handled.
*/
@ -1504,7 +1504,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
*
* @param {string|RegExp|function(string)} url HTTP url or function that receives the url
* and returns true if the url match the current definition.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
* order to change how a matched request is handled.
*/
@ -1809,6 +1809,77 @@ angular.mock.$RootElementProvider = function() {
};
};
/**
* @ngdoc service
* @name $controller
* @description
* A decorator for {@link ng.$controller} with additional `bindings` parameter, useful when testing
* controllers of directives that use {@link $compile#-bindtocontroller- `bindToController`}.
*
*
* ## Example
*
* ```js
*
* // Directive definition ...
*
* myMod.directive('myDirective', {
* controller: 'MyDirectiveController',
* bindToController: {
* name: '@'
* }
* });
*
*
* // Controller definition ...
*
* myMod.controller('MyDirectiveController', ['log', function($log) {
* $log.info(this.name);
* })];
*
*
* // In a test ...
*
* describe('myDirectiveController', function() {
* it('should write the bound name to the log', inject(function($controller, $log) {
* var ctrl = $controller('MyDirective', { /* no locals &#42;/ }, { name: 'Clark Kent' });
* expect(ctrl.name).toEqual('Clark Kent');
* expect($log.info.logs).toEqual(['Clark Kent']);
* });
* });
*
* ```
*
* @param {Function|string} constructor If called with a function then it's considered to be the
* controller constructor function. Otherwise it's considered to be a string which is used
* to retrieve the controller constructor using the following steps:
*
* * check if a controller with given name is registered via `$controllerProvider`
* * check if evaluating the string on the current scope returns a constructor
* * if $controllerProvider#allowGlobals, check `window[constructor]` on the global
* `window` object (not recommended)
*
* The string can use the `controller as property` syntax, where the controller instance is published
* as the specified property on the `scope`; the `scope` must be injected into `locals` param for this
* to work correctly.
*
* @param {Object} locals Injection locals for Controller.
* @param {Object=} bindings Properties to add to the controller before invoking the constructor. This is used
* to simulate the `bindToController` feature and simplify certain kinds of tests.
* @return {Object} Instance of given controller.
*/
angular.mock.$ControllerDecorator = ['$delegate', function($delegate) {
return function(expression, locals, later, ident) {
if (later && typeof later === 'object') {
var create = $delegate(expression, locals, true, ident);
angular.extend(create.instance, later);
return create();
}
return $delegate(expression, locals, later, ident);
};
}];
/**
* @ngdoc module
* @name ngMock
@ -1837,6 +1908,7 @@ angular.module('ngMock', ['ng']).provider({
$provide.decorator('$$rAF', angular.mock.$RAFDecorator);
$provide.decorator('$$asyncCallback', angular.mock.$AsyncCallbackDecorator);
$provide.decorator('$rootScope', angular.mock.$RootScopeDecorator);
$provide.decorator('$controller', angular.mock.$ControllerDecorator);
}]);
/**
@ -2134,18 +2206,32 @@ angular.mock.$RootScopeDecorator = ['$delegate', function($delegate) {
if (window.jasmine || window.mocha) {
var currentSpec = null,
annotatedFunctions = [],
isSpecRunning = function() {
return !!currentSpec;
};
angular.mock.$$annotate = angular.injector.$$annotate;
angular.injector.$$annotate = function(fn) {
if (typeof fn === 'function' && !fn.$inject) {
annotatedFunctions.push(fn);
}
return angular.mock.$$annotate.apply(this, arguments);
};
(window.beforeEach || window.setup)(function() {
annotatedFunctions = [];
currentSpec = this;
});
(window.afterEach || window.teardown)(function() {
var injector = currentSpec.$injector;
annotatedFunctions.forEach(function(fn) {
delete fn.$inject;
});
angular.forEach(currentSpec.$modules, function(module) {
if (module && module.$$hashKey) {
module.$$hashKey = undefined;

View File

@ -1,5 +1,5 @@
/**
* @license AngularJS v1.3.7
* @license AngularJS v1.3.15
* (c) 2010-2014 Google, Inc. http://angularjs.org
* License: MIT
*/
@ -482,21 +482,15 @@ function $RouteProvider() {
* definitions will be interpolated into the location's path, while
* remaining properties will be treated as query params.
*
* @param {Object} newParams mapping of URL parameter names to values
* @param {!Object<string, string>} newParams mapping of URL parameter names to values
*/
updateParams: function(newParams) {
if (this.current && this.current.$$route) {
var searchParams = {}, self=this;
angular.forEach(Object.keys(newParams), function(key) {
if (!self.current.pathParams[key]) searchParams[key] = newParams[key];
});
newParams = angular.extend({}, this.current.params, newParams);
$location.path(interpolate(this.current.$$route.originalPath, newParams));
$location.search(angular.extend({}, $location.search(), searchParams));
}
else {
// interpolate modifies newParams, only query params are left
$location.search(newParams);
} else {
throw $routeMinErr('norout', 'Tried updating route when with no current route');
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
/*! OpenPGPjs.org this is LGPL licensed code, see LICENSE/our website for more information.- v0.9.0 - 2014-12-09 */!function a(b,c,d){function e(g,h){if(!c[g]){if(!b[g]){var i="function"==typeof require&&require;if(!h&&i)return i(g,!0);if(f)return f(g,!0);throw new Error("Cannot find module '"+g+"'")}var j=c[g]={exports:{}};b[g][0].call(j.exports,function(a){var c=b[g][1][a];return e(c?c:a)},j,j.exports,a,b,c,d)}return c[g].exports}for(var f="function"==typeof require&&require,g=0;g<d.length;g++)e(d[g]);return e}({1:[function(){function a(a){window.openpgp.crypto.random.randomBuffer.size<d&&postMessage({event:"request-seed"}),postMessage(a)}function b(a){var b=window.openpgp.packet.List.fromStructuredClone(a);return new window.openpgp.key.Key(b)}function c(a){var b=window.openpgp.packet.List.fromStructuredClone(a);return new window.openpgp.message.Message(b)}window={},Function.prototype.bind||(Function.prototype.bind=function(a){if("function"!=typeof this)throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");var b=Array.prototype.slice.call(arguments,1),c=this,d=function(){},e=function(){return c.apply(this instanceof d&&a?this:a,b.concat(Array.prototype.slice.call(arguments)))};return d.prototype=this.prototype,e.prototype=new d,e}),importScripts("openpgp.min.js");var d=4e4,e=6e4;window.openpgp.crypto.random.randomBuffer.init(e),self.onmessage=function(d){var e=null,f=null,g=d.data,h=!1;switch(g.event){case"seed-random":g.buf instanceof Uint8Array||(g.buf=new Uint8Array(g.buf)),window.openpgp.crypto.random.randomBuffer.set(g.buf);break;case"encrypt-message":g.keys.length||(g.keys=[g.keys]),g.keys=g.keys.map(b),window.openpgp.encryptMessage(g.keys,g.text).then(function(b){a({event:"method-return",data:b})})["catch"](function(b){a({event:"method-return",err:b.message})});break;case"sign-and-encrypt-message":g.publicKeys.length||(g.publicKeys=[g.publicKeys]),g.publicKeys=g.publicKeys.map(b),g.privateKey=b(g.privateKey),window.openpgp.signAndEncryptMessage(g.publicKeys,g.privateKey,g.text).then(function(b){a({event:"method-return",data:b})})["catch"](function(b){a({event:"method-return",err:b.message})});break;case"decrypt-message":g.privateKey=b(g.privateKey),g.message=c(g.message.packets),window.openpgp.decryptMessage(g.privateKey,g.message).then(function(b){a({event:"method-return",data:b})})["catch"](function(b){a({event:"method-return",err:b.message})});break;case"decrypt-and-verify-message":g.privateKey=b(g.privateKey),g.publicKeys.length||(g.publicKeys=[g.publicKeys]),g.publicKeys=g.publicKeys.map(b),g.message=c(g.message.packets),window.openpgp.decryptAndVerifyMessage(g.privateKey,g.publicKeys,g.message).then(function(b){a({event:"method-return",data:b})})["catch"](function(b){a({event:"method-return",err:b.message})});break;case"sign-clear-message":g.privateKeys=g.privateKeys.map(b),window.openpgp.signClearMessage(g.privateKeys,g.text).then(function(b){a({event:"method-return",data:b})})["catch"](function(b){a({event:"method-return",err:b.message})});break;case"verify-clear-signed-message":g.publicKeys.length||(g.publicKeys=[g.publicKeys]),g.publicKeys=g.publicKeys.map(b);var i=window.openpgp.packet.List.fromStructuredClone(g.message.packets);g.message=new window.openpgp.cleartext.CleartextMessage(g.message.text,i),window.openpgp.verifyClearSignedMessage(g.publicKeys,g.message).then(function(b){a({event:"method-return",data:b})})["catch"](function(b){a({event:"method-return",err:b.message})});break;case"generate-key-pair":window.openpgp.generateKeyPair(g.options).then(function(b){b.key=b.key.toPacketlist(),a({event:"method-return",data:b})})["catch"](function(b){a({event:"method-return",err:b.message})});break;case"decrypt-key":try{g.privateKey=b(g.privateKey),h=g.privateKey.decrypt(g.password),h?e=g.privateKey.toPacketlist():f="Wrong password"}catch(j){f=j.message}a({event:"method-return",data:e,err:f});break;case"decrypt-key-packet":try{g.privateKey=b(g.privateKey),g.keyIds=g.keyIds.map(window.openpgp.Keyid.fromClone),h=g.privateKey.decryptKeyPacket(g.keyIds,g.password),h?e=g.privateKey.toPacketlist():f="Wrong password"}catch(j){f=j.message}a({event:"method-return",data:e,err:f});break;default:throw new Error("Unknown Worker Event.")}}},{}]},{},[1]);

View File

@ -12,7 +12,6 @@
"unlimitedStorage",
"notifications",
"https://keys-test.whiteout.io/",
"https://keychain-test.whiteout.io/",
"https://settings.whiteout.io/",
"https://admin-node.whiteout.io/",
"https://www.googleapis.com/",

View File

@ -1,6 +1,6 @@
{
"packageId": "io.whiteout.WhiteoutMail",
"versionCode": 16,
"versionCode": 28,
"CFBundleVersion": "1",
"ios": {

View File

@ -4,7 +4,12 @@
left: -9999px;
display: block;
z-index: 9000;
max-height: 400px;
max-width: 300px;
min-width: 150px;
overflow-y: auto;
// allow scrolling on iOS
-webkit-overflow-scrolling: touch;
text-align: left;
font-size: $font-size-base;
background: $color-bg;
@ -39,6 +44,9 @@
padding: 0.5em 15px 0.5em 15px;
color: $color-main;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
& > svg {
display: inline-block;

View File

@ -10,10 +10,6 @@
margin-bottom: 10px;
color: $color-error;
}
&__password-strong-message {
margin-bottom: 10px;
color: green;
}
&__row {
margin-bottom: 10px;
@ -105,6 +101,7 @@
font-size: $font-size-base;
padding: 0.5em 0.7em;
outline: none;
box-shadow: none;
// ios
border-radius: 0;
-webkit-appearance: none;
@ -242,6 +239,7 @@
line-height: 1em;
border: 1px solid $color-text-light;
text-align: center;
background-color: $color-bg;
svg {
display: inline-block;
fill: $color-main;

View File

@ -27,4 +27,5 @@
.typo-code {
font-family: monospace;
font-weight: bold;
user-select: text;
}

View File

@ -23,17 +23,16 @@
padding: 15px;
background: $color-bg;
color: $color-text;
backface-visibility: hidden;
display: flex;
flex-direction: column;
text-align: left;
margin: 0 auto;
will-change: transform;
@include respond-to(md) {
width: 90%;
max-width: 762px;
min-height: 0;
backface-visibility: hidden;
}
}

View File

@ -32,6 +32,14 @@
}
}
.toolbar {
.toolbar__label {
@include respond-to(xs-only) {
padding-left: 0;
}
}
}
&__main {
flex-grow: 1;
margin: 0 auto 20px;

View File

@ -23,17 +23,53 @@
display: none;
}
}
&__working {
position: relative;
flex-grow: 1;
padding: 0 $padding-horizontal;
& > div {
@include scut-vcenter-tt;
width: 100%;
text-align: center;
font-size: $font-size-bigger;
strong {
color: $color-text-light;
vertical-align: middle;
}
}
}
&__content {
flex-grow: 1;
overflow: auto;
// allow scrolling on iOS
-webkit-overflow-scrolling: touch;
}
// Header components
&__header {
flex-shrink: 0;
margin-bottom: 1em;
padding: $padding-vertical $padding-horizontal 0;
& > .attachments {
margin-top: 1em;
}
.signature-status {
& > svg {
vertical-align: middle;
width: 1.5em;
height: 1.5em;
margin-bottom: .2em;
fill: $color-main;
}
}
.signature-status--invalid {
& > svg {
fill: $color-error-area;
}
}
}
// only visible in stripped version of read view
.mail-addresses__stripped {
@ -41,15 +77,29 @@
}
&__controls {
display: none;
float: right;
margin-left: 1em;
.btn-icon-light {
position: absolute;
top: 0;
right: $scrollbar-width; // don't cover scrollbar
padding: $padding-vertical $padding-horizontal;
background-color: $color-white;
z-index: 999; // places the buttons on top of the content
.btn-icon-light + .btn-icon-light {
margin-left: 1.4em;
}
@include respond-to(md) {
display: block;
}
}
&__controls__dummy {
display: none;
float: right;
// the size of the real controls
width: 242px;
height: 39px;
@include respond-to(md) {
display: block;
}
}
&__subject {
font-weight: normal;
color: $color-text;
@ -102,49 +152,18 @@
// Content components
&__signature-status {
flex-shrink: 0;
margin-top: 0;
margin-bottom: 0.5em;
text-align: center;
color: $color-error;
padding: 0 $padding-horizontal;
}
&__display-images {
flex-shrink: 0;
margin-bottom: 0.5em;
text-align: center;
padding: 0 $padding-horizontal;
}
&__working {
position: relative;
flex-grow: 1;
padding: 0 $padding-horizontal;
& > div {
@include scut-vcenter-tt;
width: 100%;
text-align: center;
font-size: $font-size-bigger;
strong {
color: $color-text-light;
vertical-align: middle;
}
}
}
&__body {
flex-grow: 1;
display: flex;
flex-direction: column;
// allow scrolling on iOS
overflow: auto;
-webkit-overflow-scrolling: touch;
padding: 0 $padding-horizontal $padding-vertical;
overflow: hidden; // necessary due to iframe scaling via transitions
iframe {
flex-grow: 1;
border: none;
width: 100%;
min-width: 100%;
}
}
@ -172,6 +191,9 @@
.mail-addresses__stripped {
display: inline;
}
.read__sender-address {
display: none;
}
}
}
}

View File

@ -21,6 +21,33 @@
margin-top: 0.5em;
}
}
&__invite {
position: relative;
margin-top: 1.3em;
border: 1px solid $color-red-light;
p {
color: $color-red-light;
margin: 0.7em 1em;
svg {
width: 1em;
height: 1em;
fill: $color-red-light;
// for better valignment
position: relative;
top: 0.15em;
margin-right: 0.3em;
}
}
.btn {
position: absolute;
top: 5px;
right: 5px;
}
}
&__subject {
position: relative;
margin-top: 1.3em;

View File

@ -5,24 +5,11 @@
// Mixins
@import "mixins/responsive";
@import "mixins/scrollbar";
@include scrollbar();
html {
// use overflow auto and not scroll otherwise IE shows scrollbars always
overflow: auto;
}
body {
margin: 0;
}
.scale-body {
// necessary to compute overflowing content width in JS
float: left;
}
.view-read-body {
font-family: $font-family-base;
font-size: $font-size-base;

18
src/sass/styleguide.scss Executable file
View File

@ -0,0 +1,18 @@
// Third party libs
@import "lib/scut";
// Config
@import "base/config"; // Modify this for custom colors, font-sizes, etc
// Mixins
@import "mixins/responsive";
@import "mixins/scrollbar";
// Styleguide specific blocks
// Namespaced with "sg-"
// (BEM-like Naming, see http://cssguidelin.es/#bem-like-naming)
@import "styleguide/layout";
@import "styleguide/typo";
@import "styleguide/sections";
@import "styleguide/icon-list";

View File

@ -0,0 +1,20 @@
.sg-icon-list {
list-style: none;
padding: 0;
margin: 0;
& > li {
display: inline-block;
padding: 0 10px;
margin-bottom: 20px;
text-align: center;
color: $color-text-light;
width: 10em;
& > svg {
display: block;
margin: 0 auto 10px;
width: 2em;
height: 2em;
fill: $color-text;
}
}
}

View File

@ -0,0 +1,82 @@
// Styleguide layout
.sg-layout {
height: 100%;
overflow-y: auto;
// allow scrolling on iOS
-webkit-overflow-scrolling: touch;
&__canvas {
min-height: 100%;
display: flex;
flex-direction: column;
background: $color-bg;
color: $color-text;
}
&__header {
padding: 50px 20px;
background: $color-bg-dark;
border-bottom: 1px solid darken($color-bg-dark, 10%);
img {
display: block;
width: 100%;
max-width: 300px;
max-height: 4em;
margin: 0 auto 10px;
}
}
&__main {
flex-grow: 1;
width: 100%;
padding: 10px 20px;
margin: 0 auto;
max-width: 1150px;
}
&__footer {
width: 100%;
text-align: center;
font-size: $font-size-small;
color: $color-text-light;
line-height: 1.5;
padding: 10px 20px;
nav {
ul {
list-style: none;
margin: 0;
padding: 0;
}
li {
display: inline;
&:after {
display: inline-block;
content: ' | ';
margin: 0 0.5em;
}
&:last-child:after {
display: none;
}
}
a {
color: inherit;
text-decoration: none;
&:hover,
&:focus {
text-decoration: underline;
}
}
}
@include respond-to(md) {
text-align: left;
nav {
float: right;
}
}
}
}

View File

@ -0,0 +1,33 @@
.sg-section {
margin: 50px 0;
}
.sg-block {
margin: 20px 0;
padding-top: 20px;
border-top: 1px solid $color-border-light;
&:last-of-type {
border-bottom: 1px solid $color-border-light;
}
&__description {
margin-bottom: 20px;
}
&__example {
margin-bottom: 20px;
}
@include respond-to(lg) {
@include scut-clearfix;
&__description {
float: left;
width: 25%;
padding-right: 30px;
}
&__example {
margin-left: 25%;
}
}
}

View File

@ -0,0 +1,42 @@
// Styleguide typography
.sg-typo-title {
text-transform: uppercase;
font-weight: normal;
text-align: center;
font-size: $font-size-bigger;
color: $color-main;
margin: 0;
@include respond-to(md) {
font-size: $font-size-huge;
}
}
.sg-typo-section-title {
font-size: $font-size-bigger;
font-weight: normal;
color: $color-text;
margin: 0 0 20px;
@include respond-to(md) {
font-size: $font-size-huge;
}
}
.sg-typo-description-title {
font-size: $font-size-big;
font-weight: bold;
color: $color-text;
margin: 0;
}
.sg-typo-code {
font-family: monospace;
font-weight: bold;
border-radius: 0.2em;
font-size: $font-size-small;
background: $color-bg-dark;
color: $color-text;
padding: 0.1em 0.3em;
}

View File

@ -0,0 +1,11 @@
'use strict';
module.exports.register = function(Handlebars) {
// Customize this helper
Handlebars.registerHelper('stripFileExtension', function(str) {
var content = str.replace(/\.[^\.]*$/, '');
return new Handlebars.SafeString(content);
});
};

66
src/styleguide/index.hbs Normal file
View File

@ -0,0 +1,66 @@
---
title: Styleguide
sections:
- title: Typography
src: src/styleguide/sections/typo/*.hbs
- title: Buttons
src: src/styleguide/sections/buttons/*.hbs
- title: Forms
src: src/styleguide/sections/form/*.hbs
- title: Labels
src: src/styleguide/sections/labels.hbs
- title: Spinners
src: src/styleguide/sections/spinner/*.hbs
- title: Attachments
src: src/styleguide/sections/attachments.hbs
- title: Dropdowns
src: src/styleguide/sections/dropdown.hbs
- title: Tooltips
src: src/styleguide/sections/Tooltip.hbs
- title: Mail addresses
src: src/styleguide/sections/mail_addresses.hbs
- title: Tags input
src: src/styleguide/sections/tags_input.hbs
- title: Toolbars
src: src/styleguide/sections/toolbars/*.hbs
---
<section class="sg-section">
<h2 class="sg-typo-section-title">Icons</h2>
<div class="sg-block">
<div class="sg-block__description">
<h3 class="sg-typo-description-title">Available icons</h3>
<p class="typo-paragraph">
All icons are available via inline svg and the <code class="sg-typo-code">xlink:href</code>
attribute of the <code class="sg-typo-code">&lt;use&gt;</code> tag.
</p>
</div>
<div class="sg-block__example">
<ul class="sg-icon-list">
{{#compose src="src/img/icons/[!all]*.svg"}}
<li>
<svg role="presentation"><use xlink:href="#icon-{{stripFileExtension @filename}}" /></svg>
{{@filename}}
</li>
{{/compose}}
</ul>
</div>
</div>
</section>
{{#each sections}}
<section class="sg-section">
<h2 class="sg-typo-section-title">{{title}}</h2>
{{#compose src=src}}
<div class="sg-block">
<div class="sg-block__description">
<h3 class="sg-typo-description-title">{{@title}}</h3>
<p class="typo-paragraph">{{{@description}}}</p>
</div>
<div class="sg-block__example">
{{{@content}}}
</div>
</div>
{{/compose}}
</section>
{{/each}}

View File

@ -0,0 +1,47 @@
---
version: foobar
currentDate: <%= new Date() %>
---
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{ title }} | Whiteout Mail</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
<link rel="stylesheet" media="all" href="{{assets}}/css/all.min.css" type="text/css">
<link rel="stylesheet" media="all" href="{{assets}}/styleguide/css/styleguide.min.css" type="text/css">
</head>
<body>
<!-- inline icons have to come first, hide immediately with inline styles -->
<div style="width: 0; height: 0; visibility: hidden;">
{{glob "src/img/icons/all.svg"}}
</div>
<div class="sg-layout">
<header class="sg-layout__header">
<img src="{{assets}}/img/whiteout_logo.svg" alt="whiteout.io">
<h1 class="sg-typo-title">{{ title }}</h1>
</header>
<main class="sg-layout__main">
{{> body}}
</main>
<footer class="sg-layout__footer">
<nav>
<ul>
<li><a href="https://whiteout.io/imprint.html" target="_blank">Imprint</a></li>
<li><a href="https://whiteout.io/privacy-service.html" target="_blank">Privacy</a></li>
<li><a href="https://whiteout.io/terms.html" target="_blank">Terms</a></li>
<li><a href="https://github.com/whiteout-io/mail-html5/blob/master/README.md#license" target="_blank">License</a></li>
<li>Version: {{manifest.version}}</li>
</ul>
</nav>
&copy; {{formatDate currentDate "%Y"}} Whiteout Networks GmbH
</footer>
</div>
</body>
</html>

View File

@ -0,0 +1,21 @@
---
title: List of attachments
description: List of attached files with optional delete button.
---
<ul class="attachments">
<li>
<svg><use xlink:href="#icon-attachment" /></svg>
file1.txt
<button class="attachments__delete">
<svg><use xlink:href="#icon-close_circle" /><title>Delete</title></svg>
</button>
</li>
<li>
<svg><use xlink:href="#icon-attachment" /></svg>
file1.txt
<button class="attachments__delete">
<svg><use xlink:href="#icon-close_circle" /><title>Delete</title></svg>
</button>
</li>
</ul>

View File

@ -0,0 +1,16 @@
---
title: Regular button
description: >
There are various button types. All button types support to be disabled
via attribute <code class="sg-typo-code">disabled</code> or
<code class="sg-typo-code">aria-disabled="true"</code>.
---
<button class="btn">
<svg><use xlink:href="#icon-close" /><title>Delete</title></svg>
Regular
</button>
<button class="btn" disabled>
<svg><use xlink:href="#icon-close" /><title>Delete</title></svg>
Disabled regular
</button>

View File

@ -0,0 +1,9 @@
---
title: Big button
description:
---
<button class="btn btn--big">
<svg><use xlink:href="#icon-close" /><title>Delete</title></svg>
Big
</button>

View File

@ -0,0 +1,8 @@
---
title: Icon button
description:
---
<button class="btn-icon">
<svg><use xlink:href="#icon-write" /><title>New mail</title></svg>
</button>

View File

@ -0,0 +1,8 @@
---
title: Light icon button
description:
---
<button class="btn-icon-light">
<svg><use xlink:href="#icon-write" /><title>New mail</title></svg>
</button>

View File

@ -0,0 +1,8 @@
---
title: Very light icon button
description:
---
<button class="btn-icon-very-light">
<svg><use xlink:href="#icon-write" /><title>New mail</title></svg>
</button>

View File

@ -0,0 +1,10 @@
---
title: Invalid button
description: >
Use to show invalid state of a form.
---
<button class="btn btn--invalid">
<svg><use xlink:href="#icon-close" /><title>Delete</title></svg>
Invalid
</button>

View File

@ -0,0 +1,9 @@
---
title: Light button
description:
---
<button class="btn btn--light">
<svg><use xlink:href="#icon-close" /><title>Delete</title></svg>
Light
</button>

View File

@ -0,0 +1,10 @@
---
title: Light dropbown button
description: >
Use in combination with JavaScript and <code class="sg-typo-code">.dropdown</code>.
---
<button class="btn btn--light-dropdown">
Dropdown
<svg class="btn__dropdown" role="presentation"><use xlink:href="#icon-dropdown" /></svg>
</button>

View File

@ -0,0 +1,9 @@
---
title: Navicon button
description: >
Main menu button
---
<button class="btn-navicon">
<svg><use xlink:href="#icon-navicon" /><title>Toggle Navigation</title></svg>
</button>

View File

@ -0,0 +1,10 @@
---
title: Secondary button
description: >
Use in conjuction with another primary regular button.
---
<button class="btn btn--secondary">
<svg><use xlink:href="#icon-close" /><title>Delete</title></svg>
Secondary
</button>

View File

@ -0,0 +1,10 @@
---
title: Dropdown
description: >
The dropdown list is positioned absolutely within the application via JavaScript.
---
<ul class="dropdown dropdown--show" style="position: static;">
<li><button><svg><use xlink:href="#icon-reply_light" /></svg> Reply</button></li>
<li><button><svg><use xlink:href="#icon-reply_all_light" /></svg> Reply All</button></li>
</ul>

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