1
0
mirror of https://github.com/moparisthebest/mail synced 2024-10-31 23:35:03 -04:00

Compare commits

...

211 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
Tankred Hase
da639b5a69 bump mobile manifest version 2015-02-10 16:04:42 +01:00
Felix Hammerl
0d17701ebd Merge pull request #266 from whiteout-io/dev/WO-889
Dev/wo 889
2015-02-10 15:36:19 +01:00
Tankred Hase
5bf0890c02 Ignore keys from HKP server in keychain.getUserKeyPair
Remove unused getPublicKeys function
2015-02-10 15:21:04 +01:00
Tankred Hase
f8e5ea6d89 Revert "[WO-878] Do not force import of private key for HKP keys"
This reverts commit 4fe3ceaea2.
2015-02-10 14:36:30 +01:00
Tankred Hase
7ffb7ca148 Remove spellcheck in keychain code input fields 2015-02-09 17:28:54 +01:00
Tankred Hase
5121347640 Update icons in Firefox App manifest 2015-02-08 12:31:29 +01:00
Tankred Hase
2c092b0240 Add native scrolling to contacts on iOS 2015-02-05 09:50:40 +01:00
Tankred Hase
2875228359 Bump android version code 2015-02-05 09:19:00 +01:00
Tankred Hase
274c23ea4c Merge pull request #261 from whiteout-io/dev/WO-878
[WO-878] Do not force import of private key for HKP keys
2015-02-04 11:27:11 +01:00
Felix Hammerl
4fe3ceaea2 [WO-878] Do not force import of private key for HKP keys 2015-02-03 22:48:11 +01:00
Tankred Hase
ff587672d9 Bump android version code 2015-02-03 13:49:10 +01:00
Tankred Hase
fef264248d Merge pull request #259 from whiteout-io/dev/WO-862
Dev/wo 862
2015-02-03 13:38:39 +01:00
Tankred Hase
9a9b0d4cea Change search to filter in contacts lightbox 2015-02-03 13:38:13 +01:00
Tankred Hase
c93eaf17f3 Implement HKP server support
* Add public key import lightbox for copy and paste
* Fix refresh of pgp key change in keychain
* Display additional contacts info
* Filter by name and email addres in autocomplete
* Accept all file endings for key import

Accept all types of files for key import
2015-02-02 20:15:10 +01:00
Tankred Hase
038437595e Merge pull request #255 from whiteout-io/dev/WO-851
[WO-851] Request OAuth token on each connection request
2015-02-02 16:50:12 +01:00
Felix Hammerl
0446f8219b [WO-851] Request OAuth token on each connection request 2015-02-02 16:08:27 +01:00
Tankred Hase
86653e8700 Merge pull request #257 from whiteout-io/dev/WO-857
[WO-857] improved error reporting after XHR
2015-02-02 15:43:56 +01:00
Felix Hammerl
900294a13d [WO-857] improved error reporting after XHR 2015-01-27 12:26:07 +01:00
Felix Hammerl
990950bc48 Merge pull request #254 from whiteout-io/dev/WO-852
Use realname in PGP keygen
2015-01-22 12:14:17 +01:00
Tankred Hase
978822ae55 Merge pull request #250 from whiteout-io/dev/WO-765
[WO-765] do not multiplex sockets through single websocket
2015-01-21 12:18:52 +01:00
Tankred Hase
75a382190a Update package.json 2015-01-21 12:18:08 +01:00
Tankred Hase
6ad3b7402e Use realname in PGP keygen 2015-01-20 13:15:03 +01:00
Tankred Hase
6a525ae643 Add comment in setup for SMS validation 2015-01-20 12:44:45 +01:00
Felix Hammerl
55ab661582 bump dependencies 2015-01-19 12:04:30 +01:00
Felix Hammerl
7c9e8e6a4e Merge pull request #253 from whiteout-io/dev/WO-788
Improve readability of text in desktop mail-list
2015-01-19 11:39:25 +01:00
Tankred Hase
295c781b62 Improve readability of text in desktop mail-list 2015-01-19 10:56:16 +01:00
Felix Hammerl
b7072648b7 Merge pull request #252 from whiteout-io/dev/WO-790
Hide unread count for sent folder
2015-01-19 10:44:24 +01:00
Felix Hammerl
753cd1a4d7 Merge pull request #251 from whiteout-io/dev/WO-773
Dev/wo 773
2015-01-19 10:34:47 +01:00
Tankred Hase
281f4a94cd Hide unread count for sent folder 2015-01-19 10:12:01 +01:00
Tankred Hase
c271dc91dc Remove redundant directives in write view 2015-01-19 10:05:05 +01:00
Tankred Hase
fc6b21e63a Fix spinner in key sync 2015-01-19 10:03:57 +01:00
Felix Hammerl
2be7beb3a1 [WO-765] do not multiplex sockets through single websocket 2015-01-15 17:42:53 +01:00
Tankred Hase
d3b54187cb Seperate PGP fingerprint w/ spaces to make it more readable 2015-01-15 16:19:22 +01:00
Felix Hammerl
c165ced523 Merge pull request #249 from whiteout-io/dev/WO-690
Fetch message contents when leaving read mode
2015-01-15 11:36:56 +01:00
Tankred Hase
d7a4058644 Fetch message contents when leaving read mode 2015-01-15 10:23:15 +01:00
Tankred Hase
7d266e6a79 Delete unrequired underscore-min.js 2015-01-14 18:36:01 +01:00
Tankred Hase
a07ee38fdb bump android version code 2015-01-13 18:52:02 +01:00
180 changed files with 8467 additions and 6761 deletions

View File

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

View File

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

View File

@ -1,9 +1,7 @@
sudo: false
language: node_js language: node_js
node_js: node_js:
- "0.10" - "0.12"
before_install: before_install:
- gem install sass - gem install sass
- npm install -g grunt-cli - 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/sinon/pkg/sinon.js',
'node_modules/browsercrow/src/*.js', 'node_modules/browsercrow/src/*.js',
'node_modules/browsersmtp/src/*.js', 'node_modules/browsersmtp/src/*.js',
'src/lib/openpgp/openpgp.min.js', 'node_modules/openpgp/dist/openpgp.min.js',
'src/lib/openpgp/openpgp.worker.min.js', 'node_modules/openpgp/dist/openpgp.worker.min.js',
'src/lib/forge/forge.min.js', 'src/lib/forge/forge.min.js',
'dist/js/pbkdf2-worker.min.js' 'dist/js/pbkdf2-worker.min.js'
], ],
@ -55,8 +55,12 @@ module.exports = function(grunt) {
lib: { lib: {
expand: true, expand: true,
flatten: true, flatten: true,
cwd: 'src/lib/', cwd: './',
src: ['openpgp/openpgp.min.js', 'openpgp/openpgp.worker.min.js', 'forge/forge.min.js'], 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/' dest: 'dist/js/'
}, },
font: { font: {
@ -93,6 +97,11 @@ module.exports = function(grunt) {
'src/css/read-sandbox.css': 'src/sass/read-sandbox.scss', 'src/css/read-sandbox.css': 'src/sass/read-sandbox.scss',
'src/css/all.css': 'src/sass/all.scss' 'src/css/all.css': 'src/sass/all.scss'
} }
},
styleguide: {
files: {
'src/css/styleguide.css': 'src/sass/styleguide.scss'
}
} }
}, },
autoprefixer: { autoprefixer: {
@ -104,6 +113,11 @@ module.exports = function(grunt) {
'src/css/read-sandbox.css': 'src/css/read-sandbox.css', 'src/css/read-sandbox.css': 'src/css/read-sandbox.css',
'src/css/all.css': 'src/css/all.css' 'src/css/all.css': 'src/css/all.css'
} }
},
styleguide: {
files: {
'src/css/styleguide.css': 'src/css/styleguide.css'
}
} }
}, },
csso: { csso: {
@ -115,6 +129,11 @@ module.exports = function(grunt) {
'dist/css/read-sandbox.min.css': 'src/css/read-sandbox.css', 'dist/css/read-sandbox.min.css': 'src/css/read-sandbox.css',
'dist/css/all.min.css': 'src/css/all.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 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: { unitTest: {
files: { files: {
'test/unit/index.browserified.js': [ 'test/unit/index.browserified.js': [
@ -174,6 +199,7 @@ module.exports = function(grunt) {
'test/unit/service/newsletter-service-test.js', 'test/unit/service/newsletter-service-test.js',
'test/unit/service/mail-config-service-test.js', 'test/unit/service/mail-config-service-test.js',
'test/unit/service/invitation-dao-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/outbox-bo-test.js',
'test/unit/email/email-dao-test.js', 'test/unit/email/email-dao-test.js',
'test/unit/email/account-test.js', 'test/unit/email/account-test.js',
@ -185,10 +211,12 @@ module.exports = function(grunt) {
'test/unit/controller/login/login-initial-ctrl-test.js', 'test/unit/controller/login/login-initial-ctrl-test.js',
'test/unit/controller/login/login-new-device-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-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-set-credentials-ctrl-test.js',
'test/unit/controller/login/login-ctrl-test.js', 'test/unit/controller/login/login-ctrl-test.js',
'test/unit/controller/app/dialog-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/account-ctrl-test.js',
'test/unit/controller/app/set-passphrase-ctrl-test.js', 'test/unit/controller/app/set-passphrase-ctrl-test.js',
'test/unit/controller/app/contacts-ctrl-test.js', 'test/unit/controller/app/contacts-ctrl-test.js',
@ -205,7 +233,8 @@ module.exports = function(grunt) {
files: { files: {
'test/integration/index.browserified.js': [ 'test/integration/index.browserified.js': [
'test/main.js', 'test/main.js',
'test/integration/email-dao-test.js' 'test/integration/email-dao-test.js',
'test/integration/publickey-verifier-test.js'
] ]
}, },
options: browserifyOpt options: browserifyOpt
@ -260,6 +289,7 @@ module.exports = function(grunt) {
'src/lib/angular/angular-animate.js', 'src/lib/angular/angular-animate.js',
'src/lib/ngtagsinput/ng-tags-input.min.js', 'src/lib/ngtagsinput/ng-tags-input.min.js',
'node_modules/ng-infinite-scroll/build/ng-infinite-scroll.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/fastclick/fastclick.js',
'src/lib/lawnchair/lawnchair-git.js', 'src/lib/lawnchair/lawnchair-git.js',
'src/lib/lawnchair/lawnchair-adapter-webkit-sqlite-git.js', 'src/lib/lawnchair/lawnchair-adapter-webkit-sqlite-git.js',
@ -277,7 +307,8 @@ module.exports = function(grunt) {
}, },
readSandbox: { readSandbox: {
src: [ 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' 'src/js/controller/app/read-sandbox.js'
], ],
dest: 'dist/js/read-sandbox.min.js' dest: 'dist/js/read-sandbox.min.js'
@ -294,6 +325,10 @@ module.exports = function(grunt) {
src: ['dist/js/tcp-socket-tls-worker.browserified.js'], src: ['dist/js/tcp-socket-tls-worker.browserified.js'],
dest: 'dist/js/tcp-socket-tls-worker.min.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: { unitTest: {
src: [ src: [
'src/lib/underscore/underscore.js', 'src/lib/underscore/underscore.js',
@ -377,6 +412,15 @@ module.exports = function(grunt) {
sourceMapName: 'dist/js/tcp-socket-tls-worker.min.js.map' 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: { options: {
banner: '/*! Copyright © <%= grunt.template.today("yyyy") %>, Whiteout Networks GmbH.*/\n' banner: '/*! Copyright © <%= grunt.template.today("yyyy") %>, Whiteout Networks GmbH.*/\n'
} }
@ -443,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 // Development
connect: { connect: {
@ -466,10 +534,14 @@ module.exports = function(grunt) {
watch: { watch: {
css: { css: {
files: ['src/sass/**/*.scss'], 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: { jsApp: {
files: ['src/js/**/*.js', 'src/**/*.html'], files: ['src/js/**/*.js', 'src/*.html', 'src/tpl/**/*.html'],
tasks: ['dist-js-app'] tasks: ['dist-js-app']
}, },
jsUnitTest: { jsUnitTest: {
@ -482,15 +554,15 @@ module.exports = function(grunt) {
}, },
icons: { icons: {
files: ['src/index.html', 'src/img/icons/*.svg', '!src/img/icons/all.svg'], 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: { lib: {
files: ['src/lib/**/*.js'], files: ['src/lib/**/*.js'],
tasks: ['copy:lib', 'manifest'] tasks: ['copy:lib', 'offline-cache']
}, },
app: { app: {
files: ['src/*.js', 'src/**/*.html', 'src/**/*.json', 'src/manifest.*', 'src/img/**/*', 'src/font/**/*'], 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', 'manifest'] tasks: ['copy:app', 'copy:tpl', 'copy:img', 'copy:font', 'manifest-dev', 'offline-cache']
} }
}, },
@ -509,6 +581,15 @@ module.exports = function(grunt) {
} }
}, },
// Offline caching
swPrecache: {
prod: {
handleFetch: true,
rootDir: 'dist'
}
},
manifest: { manifest: {
generate: { generate: {
options: { options: {
@ -521,6 +602,9 @@ module.exports = function(grunt) {
'manifest.webapp', 'manifest.webapp',
'manifest.mobile.json', 'manifest.mobile.json',
'background.js', 'background.js',
'service-worker.js',
'styleguide/css/styleguide.min.css',
'styleguide/index.html',
'js/app.templates.js', 'js/app.templates.js',
'js/app.js.map', 'js/app.js.map',
'js/app.min.js.map', 'js/app.min.js.map',
@ -534,6 +618,8 @@ module.exports = function(grunt) {
'js/mailreader-parser-worker.min.js.map', 'js/mailreader-parser-worker.min.js.map',
'js/tcp-socket-tls-worker.browserified.js', 'js/tcp-socket-tls-worker.browserified.js',
'js/tcp-socket-tls-worker.min.js.map', '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-100-ios.png',
'img/icon-114-ios.png', 'img/icon-114-ios.png',
'img/icon-120-ios.png', 'img/icon-120-ios.png',
@ -579,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) // Load the plugin(s)
grunt.loadNpmTasks('grunt-browserify'); grunt.loadNpmTasks('grunt-browserify');
grunt.loadNpmTasks('grunt-contrib-concat'); grunt.loadNpmTasks('grunt-contrib-concat');
@ -600,23 +739,20 @@ module.exports = function(grunt) {
grunt.loadNpmTasks('grunt-svgstore'); grunt.loadNpmTasks('grunt-svgstore');
grunt.loadNpmTasks('grunt-shell'); grunt.loadNpmTasks('grunt-shell');
grunt.loadNpmTasks('grunt-angular-templates'); grunt.loadNpmTasks('grunt-angular-templates');
grunt.loadNpmTasks('assemble');
// Build tasks // 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', ['browserify', 'exorcise', 'ngtemplates', 'concat', 'uglify']);
grunt.registerTask('dist-js-app', [ grunt.registerTask('dist-js-app', [
'browserify:app', 'browserify:app',
'browserify:pbkdf2Worker', 'browserify:pbkdf2Worker',
'browserify:mailreaderWorker',
'browserify:tlsWorker',
'exorcise:app', 'exorcise:app',
'ngtemplates', 'ngtemplates',
'concat:app', 'concat:app',
'concat:readSandbox', 'concat:readSandbox',
'concat:pbkdf2Worker', 'concat:pbkdf2Worker',
'concat:mailreaderWorker', 'offline-cache'
'concat:tlsWorker',
'manifest'
]); ]);
grunt.registerTask('dist-js-unitTest', [ grunt.registerTask('dist-js-unitTest', [
'browserify:unitTest', 'browserify:unitTest',
@ -630,7 +766,11 @@ module.exports = function(grunt) {
]); ]);
grunt.registerTask('dist-copy', ['copy']); grunt.registerTask('dist-copy', ['copy']);
grunt.registerTask('dist-assets', ['svgmin', 'svgstore', 'string-replace']); 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 // Test/Dev tasks
grunt.registerTask('dev', ['connect:dev']); grunt.registerTask('dev', ['connect:dev']);
@ -667,8 +807,7 @@ module.exports = function(grunt) {
patchManifest({ patchManifest({
version: version, version: version,
deleteKey: true, deleteKey: true,
keyServer: 'https://keys.whiteout.io/', keyServer: 'https://keys.whiteout.io/'
keychainServer: 'https://keychain.whiteout.io/'
}); });
}); });
@ -690,10 +829,6 @@ module.exports = function(grunt) {
var ksIndex = manifest.permissions.indexOf('https://keys-test.whiteout.io/'); var ksIndex = manifest.permissions.indexOf('https://keys-test.whiteout.io/');
manifest.permissions[ksIndex] = options.keyServer; 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) { if (options.deleteKey) {
delete manifest.key; delete manifest.key;
} }
@ -701,9 +836,9 @@ module.exports = function(grunt) {
fs.writeFileSync(path, JSON.stringify(manifest, null, 2)); fs.writeFileSync(path, JSON.stringify(manifest, null, 2));
} }
grunt.registerTask('release-dev', ['dist', 'manifest-dev', 'compress']); grunt.registerTask('release-dev', ['dist', 'manifest-dev', 'swPrecache:prod', 'compress']);
grunt.registerTask('release-test', ['dist', 'manifest-test', 'clean:release', 'compress']); grunt.registerTask('release-test', ['dist', 'manifest-test', 'clean:release', 'swPrecache:prod', 'compress']);
grunt.registerTask('release-prod', ['dist', 'manifest-prod', 'clean:release', 'compress']); grunt.registerTask('release-prod', ['dist', 'manifest-prod', 'clean:release', 'swPrecache:prod', 'compress']);
grunt.registerTask('default', ['release-dev']); 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). 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 ### 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 ### Privacy and Security
We take the privacy of your data very seriously. Here are some of the technical details: 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). * 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. * [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. * 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.** * 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 ### 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). * 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 ### 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 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 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: Build and generate the `dist/` directory:

View File

@ -3,21 +3,6 @@
"description": "Mail App with integrated OpenPGP encryption.", "description": "Mail App with integrated OpenPGP encryption.",
"author": "Whiteout Networks", "author": "Whiteout Networks",
"homepage": "https://whiteout.io", "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": { "engines": {
"node": ">=0.10" "node": ">=0.10"
}, },
@ -34,16 +19,18 @@
"socket.io": "^1.0.6" "socket.io": "^1.0.6"
}, },
"devDependencies": { "devDependencies": {
"assemble": "~0.4.42",
"axe-logger": "~0.0.2", "axe-logger": "~0.0.2",
"browsercrow": "https://github.com/whiteout-io/browsercrow/tarball/master", "browsercrow": "https://github.com/whiteout-io/browsercrow/tarball/master",
"browsersmtp": "https://github.com/whiteout-io/browsersmtp/tarball/master", "browsersmtp": "https://github.com/whiteout-io/browsersmtp/tarball/master",
"chai": "~1.9.2", "chai": "~1.9.2",
"crypto-lib": "~0.2.1", "crypto-lib": "~0.2.1",
"dompurify": "~0.4.2", "dompurify": "~0.7.3",
"grunt": "~0.4.1", "grunt": "~0.4.1",
"grunt-angular-templates": "~0.5.7", "grunt-angular-templates": "~0.5.7",
"grunt-autoprefixer": "~0.7.2", "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-clean": "~0.5.0",
"grunt-contrib-compress": "~0.5.2", "grunt-contrib-compress": "~0.5.2",
"grunt-contrib-concat": "^0.5.0", "grunt-contrib-concat": "^0.5.0",
@ -56,21 +43,26 @@
"grunt-csso": "~0.6.1", "grunt-csso": "~0.6.1",
"grunt-exorcise": "^0.2.0", "grunt-exorcise": "^0.2.0",
"grunt-manifest": "^0.4.0", "grunt-manifest": "^0.4.0",
"grunt-mocha-phantomjs": "^0.6.0", "grunt-mocha-phantomjs": "^0.7.0",
"grunt-shell": "~1.1.1", "grunt-shell": "~1.1.1",
"grunt-string-replace": "~1.0.0", "grunt-string-replace": "~1.0.0",
"grunt-svgmin": "~1.0.0", "grunt-svgmin": "~1.0.0",
"grunt-svgstore": "~0.3.4", "grunt-svgstore": "~0.3.4",
"imap-client": "~0.9.0", "handlebars-helper-compose": "~0.2.12",
"iframe-resizer": "^2.8.3",
"imap-client": "~0.14.2",
"jquery": "~2.1.1", "jquery": "~2.1.1",
"mailbuild": "^0.3.7",
"mailreader": "~0.4.0", "mailreader": "~0.4.0",
"mocha": "^1.21.4", "mocha": "^1.21.4",
"ng-infinite-scroll": "~1.1.2", "ng-infinite-scroll": "~1.1.2",
"pgpbuilder": "~0.5.0", "openpgp": "^1.0.0",
"pgpmailer": "~0.7.0", "pgpbuilder": "~0.6.0",
"pgpmailer": "~0.9.1",
"sinon": "~1.7.3", "sinon": "~1.7.3",
"tcp-socket": "~0.4.0", "sw-precache": "^1.3.0",
"tcp-socket": "~0.5.0",
"time-grunt": "^1.0.0", "time-grunt": "^1.0.0",
"wo-smtpclient": "~0.5.0" "wo-smtpclient": "~0.6.0"
} }
} }

View File

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

View File

@ -48,10 +48,23 @@ cp ../../../src/img/Default* "platforms/ios/$PROJNAME/Resources/splash"
# fixing missing/wrong icons # fixing missing/wrong icons
echo "Fixing wrong/missing iOS 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-40-ios.png "platforms/ios/$PROJNAME/Resources/icons/icon-40.png"
cp ../../../src/img/icon-180-ios.png "platforms/ios/$PROJNAME/Resources/icons/icon-60@3x.png" cp ../../../src/img/icon-80-ios.png "platforms/ios/$PROJNAME/Resources/icons/icon-40@2x.png"
cp ../../../src/img/icon-87-ios.png "platforms/ios/$PROJNAME/Resources/icons/icon-small@3x.png"
cp ../../../src/img/icon-120-ios.png "platforms/ios/$PROJNAME/Resources/icons/icon-40@3x.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 # print reminder for manual work in xcode
echo "" echo ""

View File

@ -75,19 +75,29 @@ var development = (process.argv[2] === '--dev');
// set HTTP headers // set HTTP headers
app.use(function(req, res, next) { app.use(function(req, res, next) {
// prevent rendering website in foreign iframe (Clickjacking)
res.set('X-Frame-Options', 'DENY');
// HSTS // HSTS
res.set('Strict-Transport-Security', 'max-age=16070400; includeSubDomains'); res.set('Strict-Transport-Security', 'max-age=16070400; includeSubDomains');
// CSP // CSP
var iframe = development ? "http://" + req.hostname + ":" + config.server.port : "https://" + req.hostname; // allow iframe to load assets 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) // set Cache-control Header (for AppCache)
res.set('Cache-control', 'public, max-age=0'); res.set('Cache-control', 'public, max-age=0');
next(); 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'); res.set('Cache-control', 'no-cache');
next(); next();
}
app.use('/tpl/read-sandbox.html', function(req, res, next) {
res.set('X-Frame-Options', 'SAMEORIGIN');
next();
}); });
// redirect all http traffic to https // redirect all http traffic to https
@ -109,60 +119,50 @@ app.use(express.static(__dirname + '/dist'));
// Socket.io proxy // Socket.io proxy
// //
// TODO:test origin constraint
//io.origins(config.server.inboundOrigins.join(' '));
io.on('connection', function(socket) { io.on('connection', function(socket) {
log.info('io', 'New connection [%s] from %s', socket.conn.id, socket.conn.remoteAddress);
log.info('io', 'New connection [%s]', socket.conn.id);
var idCounter = 0;
socket.on('open', function(data, fn) { socket.on('open', function(data, fn) {
var socketId = ++idCounter;
var tcp;
if (!development && config.server.outboundPorts.indexOf(data.port) < 0) { if (!development && config.server.outboundPorts.indexOf(data.port) < 0) {
log.warn('io', 'Open request to %s:%s was rejected, closing [%s:%s]', data.host, data.port, socket.conn.id, socketId); log.info('io', 'Open request to %s:%s was rejected, closing [%s]', data.host, data.port, socket.conn.id);
socket.disconnect(); socket.disconnect();
return; return;
} }
log.verbose('io', 'Open request to %s:%s [%s:%s]', data.host, data.port, socket.conn.id, socketId); log.verbose('io', 'Open request to %s:%s [%s]', data.host, data.port, socket.conn.id);
var tcp = net.connect(data.port, data.host, function() {
tcp = net.connect(data.port, data.host, function() { log.verbose('io', 'Opened tcp connection to %s:%s [%s]', data.host, data.port, socket.conn.id);
log.verbose('io', 'Opened tcp connection to %s:%s [%s:%s]', data.host, data.port, socket.conn.id, socketId);
tcp.on('data', function(chunk) { tcp.on('data', function(chunk) {
log.silly('io', 'Received %s bytes from %s:%s [%s:%s]', chunk.length, data.host, data.port, socket.conn.id, socketId); log.silly('io', 'Received %s bytes from %s:%s [%s]', chunk.length, data.host, data.port, socket.conn.id);
socket.emit('data-' + socketId, chunk); socket.emit('data', chunk);
}); });
tcp.on('error', function(err) { tcp.on('error', function(err) {
log.verbose('io', 'Error for %s:%s [%s:%s]: %s', data.host, data.port, socket.conn.id, socketId, err.message); log.verbose('io', 'Error for %s:%s [%s]: %s', data.host, data.port, socket.conn.id, err.message);
socket.emit('error-' + socketId, err.message); socket.emit('error', err.message);
}); });
tcp.on('end', function() { tcp.on('end', function() {
socket.emit('end-' + socketId); socket.emit('end');
}); });
tcp.on('close', function() { tcp.on('close', function() {
log.verbose('io', 'Closed tcp connection to %s:%s [%s:%s]', data.host, data.port, socket.conn.id, socketId); log.verbose('io', 'Closed tcp connection to %s:%s [%s]', data.host, data.port, socket.conn.id);
socket.emit('close-' + socketId); socket.emit('close');
socket.removeAllListeners('data-' + socketId); socket.removeAllListeners('data');
socket.removeAllListeners('end-' + socketId); socket.removeAllListeners('end');
}); });
socket.on('data-' + socketId, function(chunk, fn) { socket.on('data', function(chunk, fn) {
if (!chunk || !chunk.length) { if (!chunk || !chunk.length) {
if (typeof fn === 'function') { if (typeof fn === 'function') {
fn(); fn();
} }
return; return;
} }
log.silly('io', 'Sending %s bytes to %s:%s [%s:%s]', chunk.length, data.host, data.port, socket.conn.id, socketId); log.silly('io', 'Sending %s bytes to %s:%s [%s]', chunk.length, data.host, data.port, socket.conn.id);
tcp.write(chunk, function() { tcp.write(chunk, function() {
if (typeof fn === 'function') { if (typeof fn === 'function') {
fn(); fn();
@ -170,24 +170,21 @@ io.on('connection', function(socket) {
}); });
}); });
socket.on('end-' + socketId, function() { socket.on('end', function() {
log.verbose('io', 'Received request to close connection to %s:%s [%s:%s]', data.host, data.port, socket.conn.id, socketId); log.verbose('io', 'Received request to close connection to %s:%s [%s]', data.host, data.port, socket.conn.id);
tcp.end(); tcp.end();
}); });
if (typeof fn === 'function') { if (typeof fn === 'function') {
fn(socketId); fn(os.hostname());
} }
});
});
socket.on('disconnect', function() { socket.on('disconnect', function() {
log.info('io', 'Closed connection [%s]', socket.conn.id); log.verbose('io', 'Closed connection [%s], closing connection to %s:%s ', socket.conn.id, data.host, data.port);
tcp.end();
socket.removeAllListeners(); socket.removeAllListeners();
}); });
});
socket.on('hostname', function(fn) {
fn(os.hostname());
}); });
}); });

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"> <meta charset="utf-8">
<title>Whiteout Mail</title> <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 --> <!-- iOS homescreen link -->
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<!-- iOS iPad icon (retina) --> <!-- iOS iPad icon (retina) -->

View File

@ -12,11 +12,24 @@ module.exports = appCfg;
* Global app configurations * Global app configurations
*/ */
appCfg.config = { appCfg.config = {
cloudUrl: 'https://keys.whiteout.io', pgpComment: 'Whiteout Mail - https://whiteout.io',
privkeyServerUrl: 'https://keychain.whiteout.io', keyServerUrl: 'https://keys.whiteout.io',
hkpUrl: 'http://keyserver.ubuntu.com',
adminUrl: 'https://admin-node.whiteout.io', adminUrl: 'https://admin-node.whiteout.io',
settingsUrl: 'https://settings.whiteout.io/autodiscovery/', 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$/], oauthDomains: [/\.gmail\.com$/, /\.googlemail\.com$/],
ignoreUploadOnSentDomains: [/\.gmail\.com$/, /\.googlemail\.com$/], ignoreUploadOnSentDomains: [/\.gmail\.com$/, /\.googlemail\.com$/],
serverPrivateKeyId: 'EE342F0DDBB0F3BE', serverPrivateKeyId: 'EE342F0DDBB0F3BE',
@ -29,7 +42,7 @@ appCfg.config = {
iconPath: '/img/icon-128-chrome.png', iconPath: '/img/icon-128-chrome.png',
verificationUrl: '/verify/', verificationUrl: '/verify/',
verificationUuidLength: 36, verificationUuidLength: 36,
dbVersion: 5, dbVersion: 6,
appVersion: undefined, appVersion: undefined,
outboxMailboxPath: 'OUTBOX', outboxMailboxPath: 'OUTBOX',
outboxMailboxName: 'Outbox', outboxMailboxName: 'Outbox',
@ -55,9 +68,7 @@ function setConfigParams(manifest) {
} }
// get key server base url // get key server base url
cfg.cloudUrl = getUrl('https://keys'); cfg.keyServerUrl = getUrl('https://keys');
// get keychain server base url
cfg.privkeyServerUrl = getUrl('https://keychain');
// get the app version // get the app version
cfg.appVersion = manifest.version; cfg.appVersion = manifest.version;
} }
@ -88,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', 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', bugReportTitle: 'Report a bug',
bugReportSubject: '[Bug] I want to 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', supportAddress: 'mail.support@whiteout.io',
connDocOffline: 'It appears that you are offline. Please retry when you are online.', 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.', 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.',
@ -98,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.', 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}', connDocGenericError: 'There was an error connecting to {0}: {1}',
logoutTitle: 'Logout', 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'; 'use strict';
// // use service-worker or app-cache for offline caching
// AppCache require('./offline-cache');
//
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();
}
}
};
};
}
// //
// Angular app config // Angular app config
@ -68,6 +53,14 @@ app.config(function($routeProvider, $animateProvider) {
templateUrl: 'tpl/login-set-credentials.html', templateUrl: 'tpl/login-set-credentials.html',
controller: require('./controller/login/login-set-credentials') 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', { $routeProvider.when('/login-existing', {
templateUrl: 'tpl/login-existing.html', templateUrl: 'tpl/login-existing.html',
controller: require('./controller/login/login-existing') controller: require('./controller/login/login-existing')
@ -110,7 +103,7 @@ app.controller('WriteCtrl', require('./controller/app/write'));
app.controller('MailListCtrl', require('./controller/app/mail-list')); app.controller('MailListCtrl', require('./controller/app/mail-list'));
app.controller('AccountCtrl', require('./controller/app/account')); app.controller('AccountCtrl', require('./controller/app/account'));
app.controller('SetPassphraseCtrl', require('./controller/app/set-passphrase')); 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('ContactsCtrl', require('./controller/app/contacts'));
app.controller('AboutCtrl', require('./controller/app/about')); app.controller('AboutCtrl', require('./controller/app/about'));
app.controller('DialogCtrl', require('./controller/app/dialog')); app.controller('DialogCtrl', require('./controller/app/dialog'));

View File

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

View File

@ -27,7 +27,7 @@ var AccountCtrl = function($scope, $q, auth, keychain, pgp, appConfig, download,
var fpr = keyParams.fingerprint; 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.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.keysize = keyParams.bitSize;
$scope.publicKeyUrl = appConfig.config.cloudUrl + '/' + userId; $scope.publicKeyUrl = appConfig.config.keyServerUrl + '/' + userId;
// //
// scope functions // scope functions

View File

@ -8,6 +8,42 @@ var ActionBarCtrl = function($scope, $q, email, dialog, status) {
// scope functions // 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 * Move a single message from the currently selected folder to another folder
* @param {Object} message The message that is to be moved * @param {Object} message The message that is to be moved

View File

@ -4,7 +4,7 @@
// Controller // Controller
// //
var ContactsCtrl = function($scope, $q, keychain, pgp, dialog) { var ContactsCtrl = function($scope, $q, keychain, pgp, dialog, appConfig) {
// //
// scope state // scope state
@ -13,10 +13,13 @@ var ContactsCtrl = function($scope, $q, keychain, pgp, dialog) {
$scope.state.contacts = { $scope.state.contacts = {
toggle: function(to) { toggle: function(to) {
$scope.state.lightbox = (to) ? 'contacts' : undefined; $scope.state.lightbox = (to) ? 'contacts' : undefined;
$scope.searchText = undefined;
return $scope.listKeys(); return $scope.listKeys();
} }
}; };
$scope.whiteoutKeyServer = appConfig.config.keyServerUrl.replace(/http[s]?:\/\//, ''); // display key server hostname
// //
// scope functions // scope functions
// //
@ -33,6 +36,7 @@ var ContactsCtrl = function($scope, $q, keychain, pgp, dialog) {
keys.forEach(function(key) { keys.forEach(function(key) {
var params = pgp.getKeyParams(key.publicKey); var params = pgp.getKeyParams(key.publicKey);
_.extend(key, params); _.extend(key, params);
key.fullUserId = key.userIds[0].name + ' <' + key.userIds[0].emailAddress + '>';
}); });
$scope.keys = keys; $scope.keys = keys;
@ -46,39 +50,6 @@ var ContactsCtrl = function($scope, $q, keychain, pgp, dialog) {
$scope.fingerprint = formatted; $scope.fingerprint = formatted;
}; };
$scope.importKey = function(publicKeyArmored) {
var keyParams, pubkey;
// verifiy public key string
if (publicKeyArmored.indexOf('-----BEGIN PGP PUBLIC KEY BLOCK-----') < 0) {
dialog.error({
showBugReporter: false,
message: 'Invalid public key!'
});
return;
}
try {
keyParams = pgp.getKeyParams(publicKeyArmored);
} catch (e) {
dialog.error(new Error('Error reading public key params!'));
return;
}
pubkey = {
_id: keyParams._id,
userId: keyParams.userId,
userIds: keyParams.userIds,
publicKey: publicKeyArmored,
imported: true // mark manually imported keys
};
return keychain.saveLocalPublicKey(pubkey).then(function() {
// update displayed keys
return $scope.listKeys();
}).catch(dialog.error);
};
$scope.removeKey = function(key) { $scope.removeKey = function(key) {
return keychain.removeLocalPublicKey(key._id).then(function() { return keychain.removeLocalPublicKey(key._id).then(function() {
// update displayed keys // update displayed keys

View File

@ -37,6 +37,7 @@ var DialogCtrl = function($scope, dialog) {
$scope.title = options.title; $scope.title = options.title;
$scope.message = options.errMsg || options.message; $scope.message = options.errMsg || options.message;
$scope.faqLink = options.faqLink; $scope.faqLink = options.faqLink;
$scope.faqLinkTitle = options.faqLinkTitle || 'Learn more';
$scope.positiveBtnStr = options.positiveBtnStr || 'Ok'; $scope.positiveBtnStr = options.positiveBtnStr || 'Ok';
$scope.negativeBtnStr = options.negativeBtnStr || 'Cancel'; $scope.negativeBtnStr = options.negativeBtnStr || 'Cancel';
$scope.showNegativeBtn = options.showNegativeBtn || false; $scope.showNegativeBtn = options.showNegativeBtn || false;

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 * Set the route to a message which will go to read mode
*/ */
$scope.navigate = function(message) { $scope.navigate = function(message) {
if (!message || !message.from) {
// early return if message has not finished loading yet
return;
}
$location.search('uid', message.uid); $location.search('uid', message.uid);
}; };
@ -54,24 +58,16 @@ var MailListCtrl = function($scope, $timeout, $location, $filter, $q, status, no
// scope functions // scope functions
// //
$scope.getBody = function(message) { $scope.getBody = function(messages) {
return $q(function(resolve) { return $q(function(resolve) {
resolve(); resolve();
}).then(function() { }).then(function() {
return email.getBody({ return email.getBody({
folder: currentFolder(), 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) { }).catch(function(err) {
if (err.code !== 42) { if (err.code !== 42) {
dialog.error(err); dialog.error(err);
@ -136,6 +132,10 @@ var MailListCtrl = function($scope, $timeout, $location, $filter, $q, status, no
* Date formatting * Date formatting
*/ */
$scope.formatDate = function(date) { $scope.formatDate = function(date) {
if (!date) {
return;
}
if (typeof date === 'string') { if (typeof date === 'string') {
date = new Date(date); date = new Date(date);
} }
@ -294,6 +294,13 @@ var MailListCtrl = function($scope, $timeout, $location, $filter, $q, status, no
}).catch(dialog.error); }).catch(dialog.error);
} }
$scope.$on('read', function(e, state) {
if (!state) {
// load bodies after closing read mode
$scope.loadVisibleBodies();
}
});
function currentFolder() { function currentFolder() {
return $scope.state.nav && $scope.state.nav.currentFolder; return $scope.state.nav && $scope.state.nav.currentFolder;
} }

View File

@ -11,7 +11,7 @@ var NOTIFICATION_SENT_TIMEOUT = 2000;
// Controller // 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()) { if (!$location.search().dev && !account.isLoggedIn()) {
$location.path('/'); // init app $location.path('/'); // init app
return; return;
@ -90,10 +90,10 @@ var NavigationCtrl = function($scope, $location, $q, account, email, outbox, not
// scope functions // scope functions
// //
$scope.onOutboxUpdate = function(err, count) { $scope.onOutboxUpdate = function(err) {
if (err) { if (err) {
dialog.error(err); axe.error('Sending from outbox failed: ' + err.message);
return; status.update('Error sending messages...');
} }
// update the outbox mail count // update the outbox mail count
@ -106,15 +106,11 @@ var NavigationCtrl = function($scope, $location, $q, account, email, outbox, not
return; return;
} }
ob.count = count;
return $q(function(resolve) { return $q(function(resolve) {
resolve(); resolve();
}).then(function() { }).then(function() {
return email.refreshFolder({ return email.refreshOutbox();
folder: ob
});
}).catch(dialog.error); }).catch(dialog.error);
}; };
@ -149,6 +145,9 @@ var NavigationCtrl = function($scope, $location, $q, account, email, outbox, not
if (!$scope.state.nav.currentFolder) { if (!$scope.state.nav.currentFolder) {
$scope.navigate(0); $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 // start checking outbox periodically
outbox.startChecking($scope.onOutboxUpdate); 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

@ -0,0 +1,78 @@
'use strict';
//
// Controller
//
var PublickeyImportCtrl = function($scope, $q, keychain, pgp, hkp, dialog, appConfig) {
//
// scope state
//
$scope.state.publickeyImport = {
toggle: function(to) {
$scope.state.lightbox = (to) ? 'publickey-import' : undefined;
}
};
//
// scope variables
//
$scope.hkpUrl = appConfig.config.hkpUrl.replace(/http[s]?:\/\//, '');
//
// scope functions
//
$scope.importKey = function(publicKeyArmored) {
var keyParams, pubkey;
// verifiy public key string
if (publicKeyArmored.indexOf('-----BEGIN PGP PUBLIC KEY BLOCK-----') < 0) {
dialog.error({
showBugReporter: false,
message: 'Invalid public key!'
});
return;
}
try {
keyParams = pgp.getKeyParams(publicKeyArmored);
} catch (e) {
dialog.error(new Error('Error reading public key params!'));
return;
}
pubkey = {
_id: keyParams._id,
userId: keyParams.userId,
userIds: keyParams.userIds,
publicKey: publicKeyArmored,
imported: true // mark manually imported keys
};
return keychain.saveLocalPublicKey(pubkey).then(function() {
$scope.pastedKey = '';
// display success message
return dialog.info({
title: 'Success',
message: 'Public key ' + keyParams._id + ' for ' + keyParams.userId + ' imported successfully!'
});
}).catch(dialog.error);
};
$scope.lookupKey = function(query) {
var keyUrl = hkp.getIndexUrl(query);
return dialog.info({
title: 'Link',
message: 'Follow this link and paste the PGP key block above...',
faqLink: keyUrl,
faqLinkTitle: keyUrl
});
};
};
module.exports = PublickeyImportCtrl;

View File

@ -1,35 +1,64 @@
'use strict'; '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 // set listener for event from main window
window.onmessage = function(e) { window.onmessage = function(e) {
var html = ''; var html = '';
if (e.data.html) { if (e.data.html) {
// display html mail body // display html mail body
html = '<div class="scale-body">' + e.data.html + '</div>'; html = e.data.html;
} else if (e.data.text) { } else if (e.data.text) {
// diplay text mail body by with colored conversation nodes // diplay text mail body by with colored conversation nodes
html = renderNodes(parseConversation(e.data.text)); html = renderNodes(parseConversation(e.data.text));
} }
// sanitize HTML content: https://github.com/cure53/DOMPurify // 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) { if (e.data.removeImages) {
html = html.replace(/(<img[^>]+\b)src=['"][^'">]+['"]/ig, function(match, prefix) { // remove http leaks
return prefix; 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; attachClickHandlers();
scaleToFit();
}; };
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 * Parse email body and generate conversation nodes
@ -156,7 +185,7 @@ function renderNodes(root) {
var lines = node.split('\n'); var lines = node.split('\n');
for (i = 0; i < lines.length; i++) { for (i = 0; i < lines.length; i++) {
// replace all urls with anchors // 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 // wrap line into an element for easier styling
html += '<div class="line'; html += '<div class="line';
if (isLineEmpty(lines[i])) { if (isLineEmpty(lines[i])) {
@ -189,27 +218,3 @@ function renderNodes(root) {
return '<div class="view-read-body">' + body + '</div>'; 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

@ -4,9 +4,7 @@
// Controller // Controller
// //
var ReadCtrl = function($scope, $location, $q, email, invitation, outbox, pgp, keychain, appConfig, download, auth, dialog) { var ReadCtrl = function($scope, $location, $q, email, invitation, outbox, pgp, keychain, appConfig, download, auth, dialog, status) {
var str = appConfig.string;
// //
// scope state // scope state
@ -47,6 +45,13 @@ var ReadCtrl = function($scope, $location, $q, email, invitation, outbox, pgp, k
// scope functions // scope functions
// //
/**
* Close read mode and return to mail-list
*/
$scope.close = function() {
status.setReading(false);
};
$scope.getKeyId = function(address) { $scope.getKeyId = function(address) {
if ($location.search().dev || !address) { if ($location.search().dev || !address) {
return; return;
@ -151,18 +156,10 @@ var ReadCtrl = function($scope, $location, $q, email, invitation, outbox, pgp, k
}); });
}).then(function() { }).then(function() {
var invitationMail = { var invitationMail = invitation.createMail({
from: [{ sender: sender,
address: sender recipient: recipient
}], });
to: [{
address: recipient
}],
cc: [],
bcc: [],
subject: str.invitationSubject,
body: str.invitationMessage
};
// send invitation mail // send invitation mail
return outbox.put(invitationMail); return outbox.put(invitationMail);

View File

@ -21,59 +21,6 @@ var SetPassphraseCtrl = function($scope, $q, pgp, keychain, dialog) {
// scope functions // 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() { $scope.setPassphrase = function() {
var keyId = pgp.getKeyParams()._id; var keyId = pgp.getKeyParams()._id;

View File

@ -6,7 +6,7 @@ var util = require('crypto-lib').util;
// Controller // 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 str = appConfig.string;
var cfg = appConfig.config; var cfg = appConfig.config;
@ -52,6 +52,8 @@ var WriteCtrl = function($scope, $window, $filter, $q, appConfig, auth, keychain
$scope.body = ''; $scope.body = '';
$scope.attachments = []; $scope.attachments = [];
$scope.addressBookCache = undefined; $scope.addressBookCache = undefined;
$scope.showInvite = undefined;
$scope.invited = [];
} }
function reportBug() { function reportBug() {
@ -158,7 +160,7 @@ var WriteCtrl = function($scope, $window, $filter, $q, appConfig, auth, keychain
if (forward) { if (forward) {
$scope.subject = 'Fwd: ' + re.subject; $scope.subject = 'Fwd: ' + re.subject;
} else { } else {
$scope.subject = 'Re: ' + ((re.subject) ? re.subject.replace('Re: ', '') : ''); $scope.subject = re.subject ? 'Re: ' + re.subject.replace('Re: ', '') : '';
} }
// fill text body // fill text body
@ -198,6 +200,17 @@ var WriteCtrl = function($scope, $window, $filter, $q, appConfig, auth, keychain
// Editing headers // 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 * Verify email address and fetch its public key
*/ */
@ -206,6 +219,14 @@ var WriteCtrl = function($scope, $window, $filter, $q, appConfig, auth, keychain
return; return;
} }
if (recipient.address) {
// display only email address after autocomplete
recipient.displayId = recipient.address;
} else {
// set address after manual input
recipient.address = recipient.displayId;
}
// set display to insecure while fetching keys // set display to insecure while fetching keys
recipient.key = undefined; recipient.key = undefined;
recipient.secure = false; recipient.secure = false;
@ -231,14 +252,18 @@ var WriteCtrl = function($scope, $window, $filter, $q, appConfig, auth, keychain
}).then(function(key) { }).then(function(key) {
if (key) { if (key) {
// compare again since model could have changed during the roundtrip // compare again since model could have changed during the roundtrip
var matchingUserId = _.findWhere(key.userIds, { var userIds = pgp.getKeyParams(key.publicKey).userIds;
var matchingUserId = _.findWhere(userIds, {
emailAddress: recipient.address emailAddress: recipient.address
}); });
// compare either primary userId or (if available) multiple IDs // compare either primary userId or (if available) multiple IDs
if (key.userId === recipient.address || matchingUserId) { if (matchingUserId) {
recipient.key = key; recipient.key = key;
recipient.secure = true; recipient.secure = true;
} }
} else {
// show invite dialog if no key found
$scope.showInvite = true;
} }
$scope.checkSendStatus(); $scope.checkSendStatus();
@ -264,7 +289,10 @@ var WriteCtrl = function($scope, $window, $filter, $q, appConfig, auth, keychain
function check(recipient) { function check(recipient) {
// validate address // validate address
if (!util.validateEmailAddress(recipient.address)) { if (!util.validateEmailAddress(recipient.address)) {
return; return dialog.info({
title: 'Warning',
message: 'Invalid recipient address!'
});
} }
numReceivers++; numReceivers++;
if (!recipient.secure) { if (!recipient.secure) {
@ -274,6 +302,7 @@ var WriteCtrl = function($scope, $window, $filter, $q, appConfig, auth, keychain
// only allow sending if receviers exist // only allow sending if receviers exist
if (numReceivers < 1) { if (numReceivers < 1) {
$scope.showInvite = false;
return; return;
} }
@ -287,6 +316,7 @@ var WriteCtrl = function($scope, $window, $filter, $q, appConfig, auth, keychain
$scope.okToSend = true; $scope.okToSend = true;
$scope.sendBtnText = str.sendBtnSecure; $scope.sendBtnText = str.sendBtnSecure;
$scope.sendBtnSecure = true; $scope.sendBtnSecure = true;
$scope.showInvite = false;
} else { } else {
// send plaintext // send plaintext
$scope.okToSend = true; $scope.okToSend = true;
@ -303,6 +333,56 @@ var WriteCtrl = function($scope, $window, $filter, $q, appConfig, auth, keychain
$scope.attachments.splice($scope.attachments.indexOf(attachment), 1); $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 // Editing email body
// //
@ -393,8 +473,10 @@ var WriteCtrl = function($scope, $window, $filter, $q, appConfig, auth, keychain
// populate address book cache // populate address book cache
return keychain.listLocalPublicKeys().then(function(keys) { return keychain.listLocalPublicKeys().then(function(keys) {
$scope.addressBookCache = keys.map(function(key) { $scope.addressBookCache = keys.map(function(key) {
var name = pgp.getKeyParams(key.publicKey).userIds[0].name;
return { return {
address: key.userId address: key.userId,
displayId: name + ' - ' + key.userId
}; };
}); });
}); });
@ -402,7 +484,7 @@ var WriteCtrl = function($scope, $window, $filter, $q, appConfig, auth, keychain
}).then(function() { }).then(function() {
// filter the address book cache // filter the address book cache
return $scope.addressBookCache.filter(function(i) { return $scope.addressBookCache.filter(function(i) {
return i.address.indexOf(query) !== -1; return i.displayId.toLowerCase().indexOf(query.toLowerCase()) !== -1;
}); });
}).catch(dialog.error); }).catch(dialog.error);

View File

@ -1,17 +1,33 @@
'use strict'; '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 !$routeParams.dev && !auth.isInitialized() && $location.path('/'); // init app
// init phone region // init phone region
$scope.region = 'DE'; $scope.region = 'DE';
$scope.domain = '@' + appConfig.config.mailServer.domain;
$scope.createWhiteoutAccount = function() { $scope.showConfirm = function() {
if ($scope.form.$invalid) { if ($scope.form.$invalid) {
$scope.errMsg = 'Please fill out all required fields!'; $scope.errMsg = 'Please fill out all required fields!';
return; 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) { return $q(function(resolve) {
$scope.busy = true; $scope.busy = true;
$scope.errMsg = undefined; // reset error msg $scope.errMsg = undefined; // reset error msg
@ -19,7 +35,7 @@ var CreateAccountCtrl = function($scope, $location, $routeParams, $q, auth, admi
}).then(function() { }).then(function() {
// read form values // read form values
var emailAddress = $scope.user + '@' + appConfig.config.wmailDomain; var emailAddress = $scope.user + $scope.domain;
var phone = PhoneNumber.Parse($scope.dial, $scope.region); var phone = PhoneNumber.Parse($scope.dial, $scope.region);
if (!phone || !phone.internationalNumber) { if (!phone || !phone.internationalNumber) {
throw new Error('Invalid phone number!'); throw new Error('Invalid phone number!');
@ -36,8 +52,7 @@ var CreateAccountCtrl = function($scope, $location, $routeParams, $q, auth, admi
return admin.createUser({ return admin.createUser({
emailAddress: emailAddress, emailAddress: emailAddress,
password: $scope.pass, password: $scope.pass,
phone: phone.internationalNumber, phone: phone.internationalNumber
betaCode: $scope.betaCode.toUpperCase()
}); });
}).then(function() { }).then(function() {
@ -50,6 +65,15 @@ var CreateAccountCtrl = function($scope, $location, $routeParams, $q, auth, admi
$scope.errMsg = err.errMsg || err.message; $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; module.exports = CreateAccountCtrl;

View File

@ -1,8 +1,10 @@
'use strict'; '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 !$routeParams.dev && !auth.isInitialized() && $location.path('/'); // init app
var str = appConfig.string;
$scope.confirmPassphrase = function() { $scope.confirmPassphrase = function() {
if ($scope.form.$invalid) { if ($scope.form.$invalid) {
$scope.errMsg = 'Please fill out all required fields!'; $scope.errMsg = 'Please fill out all required fields!';
@ -38,6 +40,18 @@ var LoginExistingCtrl = function($scope, $location, $routeParams, $q, email, aut
}).catch(displayError); }).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) { function displayError(err) {
$scope.busy = false; $scope.busy = false;
$scope.incorrect = true; $scope.incorrect = true;

View File

@ -1,6 +1,6 @@
'use strict'; '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 !$routeParams.dev && !auth.isInitialized() && $location.path('/'); // init app
var emailAddress = auth.emailAddress; var emailAddress = auth.emailAddress;
@ -56,16 +56,14 @@ var LoginInitialCtrl = function($scope, $location, $routeParams, $q, newsletter,
}).then(function() { }).then(function() {
// generate key without passphrase // generate key without passphrase
return email.unlock({ return email.unlock({
realname: auth.realname,
passphrase: undefined passphrase: undefined
}); });
}).then(function() { }).then(function(keypair) {
// persist credentials locally // remember keypair for storing after public key verification
return auth.storeCredentials(); publickeyVerifier.keypair = keypair;
$location.path('/login-privatekey-upload');
}).then(function() {
// go to main account screen
$location.path('/account');
}).catch(displayError); }).catch(displayError);
}; };

View File

@ -1,17 +1,36 @@
'use strict'; '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 !$routeParams.dev && !auth.isInitialized() && $location.path('/'); // init app
$scope.incorrect = false; $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() { $scope.confirmPassphrase = function() {
if ($scope.form.$invalid || !$scope.key) { if ($scope.form.$invalid || !$scope.key) {
$scope.errMsg = 'Please fill out all required fields!'; $scope.errMsg = PRIV_ERR_MSG;
return; return;
} }
var userId = auth.emailAddress, var userId = auth.emailAddress,
pubKeyNeedsVerification = false,
keypair; keypair;
return $q(function(resolve) { return $q(function(resolve) {
@ -28,11 +47,11 @@ var LoginExistingCtrl = function($scope, $location, $routeParams, $q, email, aut
keypair = keys || {}; keypair = keys || {};
// extract public key from private key block if missing in key file // 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 { try {
$scope.key.publicKeyArmored = pgp.extractPublicKey($scope.key.privateKeyArmored); $scope.key.publicKeyArmored = pgp.extractPublicKey($scope.key.privateKeyArmored);
} catch (e) { } 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, userIds: pubKeyParams.userIds,
publicKey: $scope.key.publicKeyArmored publicKey: $scope.key.publicKeyArmored
}; };
pubKeyNeedsVerification = true; // this public key needs to be authenticated
} }
// import and validate keypair // import and validate keypair
@ -72,17 +92,20 @@ var LoginExistingCtrl = function($scope, $location, $routeParams, $q, email, aut
throw err; throw err;
}); });
}).then(function() { }).then(function(keypair) {
// perist keys locally if (!pubKeyNeedsVerification) {
return keychain.putUserKeyPair(keypair); // persist credentials and key and go to main account screen
return keychain.putUserKeyPair(keypair).then(function() {
}).then(function() {
// persist credentials locally
return auth.storeCredentials(); return auth.storeCredentials();
}).then(function() { }).then(function() {
// go to main account screen
$location.path('/account'); $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); }).catch(displayError);
}; };

View File

@ -1,20 +1,19 @@
'use strict'; '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 !$routeParams.dev && !auth.isInitialized() && $location.path('/'); // init app
$scope.step = 1;
// //
// Token // scope functions
// //
$scope.checkToken = function() { $scope.checkCode = function() {
if ($scope.tokenForm.$invalid) { if ($scope.form.$invalid) {
$scope.errMsg = 'Please enter a valid recovery token!'; $scope.errMsg = 'Please fill out all required fields!';
return; return;
} }
var cachedKeypair;
var userId = auth.emailAddress; var userId = auth.emailAddress;
return $q(function(resolve) { return $q(function(resolve) {
@ -22,54 +21,38 @@ var LoginPrivateKeyDownloadCtrl = function($scope, $location, $routeParams, $q,
$scope.errMsg = undefined; $scope.errMsg = undefined;
resolve(); resolve();
}).then(function() {
// login to imap
return privateKey.init();
}).then(function() { }).then(function() {
// get public key id for reference // get public key id for reference
return keychain.getUserKeyPair(userId); return keychain.getUserKeyPair(userId);
}).then(function(keypair) { }).then(function(keypair) {
// remember for storage later // remember for storage later
$scope.cachedKeypair = keypair; cachedKeypair = keypair;
return keychain.downloadPrivateKey({ return privateKey.download({
userId: userId, userId: userId,
keyId: keypair.publicKey._id, keyId: keypair.publicKey._id
recoveryToken: $scope.recoveryToken.toUpperCase()
}); });
}).then(function(encryptedPrivateKey) { }).then(function(encryptedKey) {
$scope.encryptedPrivateKey = encryptedPrivateKey; // set decryption code
$scope.busy = false; encryptedKey.code = $scope.code.toUpperCase();
$scope.step++; // decrypt the downloaded encrypted private key
return privateKey.decrypt(encryptedKey);
}).catch(displayError); }).then(function(privkey) {
}; // add private key to cached keypair object
cachedKeypair.privateKey = privkey;
// // store the decrypted private key locally
// Keychain code return keychain.putUserKeyPair(cachedKeypair);
//
$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() { }).then(function() {
return keychain.decryptAndStorePrivateKeyLocally(options);
}).then(function(privateKey) {
// add private key to cached keypair object
$scope.cachedKeypair.privateKey = privateKey;
// try empty passphrase // try empty passphrase
return email.unlock({ return email.unlock({
keypair: $scope.cachedKeypair, keypair: cachedKeypair,
passphrase: undefined passphrase: undefined
}).catch(function(err) { }).catch(function(err) {
// passphrase incorrct ... go to passphrase login screen // passphrase incorrct ... go to passphrase login screen
@ -81,6 +64,10 @@ var LoginPrivateKeyDownloadCtrl = function($scope, $location, $routeParams, $q,
// passphrase is corrent ... // passphrase is corrent ...
return auth.storeCredentials(); return auth.storeCredentials();
}).then(function() {
// logout of imap
return privateKey.destroy();
}).then(function() { }).then(function() {
// continue to main app // continue to main app
$scope.goTo('/account'); $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(), host: $scope.imapHost.toLowerCase(),
port: $scope.imapPort, port: $scope.imapPort,
secure: imapEncryption === ENCRYPTION_METHOD_TLS, secure: imapEncryption === ENCRYPTION_METHOD_TLS,
requireTLS: imapEncryption === ENCRYPTION_METHOD_STARTTLS,
ignoreTLS: imapEncryption === ENCRYPTION_METHOD_NONE ignoreTLS: imapEncryption === ENCRYPTION_METHOD_NONE
}, },
smtp: { smtp: {
host: $scope.smtpHost.toLowerCase(), host: $scope.smtpHost.toLowerCase(),
port: $scope.smtpPort, port: $scope.smtpPort,
secure: smtpEncryption === ENCRYPTION_METHOD_TLS, secure: smtpEncryption === ENCRYPTION_METHOD_TLS,
requireTLS: smtpEncryption === ENCRYPTION_METHOD_STARTTLS,
ignoreTLS: smtpEncryption === ENCRYPTION_METHOD_NONE 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) { } else if (availableKeys && availableKeys.publicKey && !availableKeys.privateKey) {
// check if private key is synced // proceed to private key download
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'); return $scope.goTo('/login-privatekey-download');
} else {
// no private key, import key file
return $scope.goTo('/login-new-device');
}
});
} else { } else {
// no public key available, start onboarding process // 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 * High level crypto api that handles all calls to OpenPGP.js
*/ */
function PGP() { function PGP() {
openpgp.config.commentstring = config.pgpComment;
openpgp.config.prefer_hash_algorithm = openpgp.enums.hash.sha256; openpgp.config.prefer_hash_algorithm = openpgp.enums.hash.sha256;
openpgp.initWorker(config.workerPath + '/openpgp.worker.min.js'); openpgp.initWorker(config.workerPath + '/openpgp.worker.min.js');
} }
@ -21,14 +22,15 @@ function PGP() {
*/ */
PGP.prototype.generateKeys = function(options) { PGP.prototype.generateKeys = function(options) {
return new Promise(function(resolve) { return new Promise(function(resolve) {
var userId, passphrase; var userId, name, passphrase;
if (!util.emailRegEx.test(options.emailAddress) || !options.keySize) { if (!util.emailRegEx.test(options.emailAddress) || !options.keySize) {
throw new Error('Crypto init failed. Not all options set!'); throw new Error('Crypto init failed. Not all options set!');
} }
// generate keypair // generate keypair
userId = 'Whiteout User <' + options.emailAddress + '>'; name = options.realname ? options.realname.replace(/[&\/\\#,+()$~%.'":*?<>{}]/g, '').trim() : '';
userId = name + ' <' + options.emailAddress + '>';
passphrase = (options.passphrase) ? options.passphrase : undefined; passphrase = (options.passphrase) ? options.passphrase : undefined;
resolve({ resolve({
@ -104,7 +106,7 @@ PGP.prototype.getKeyId = function(keyArmored) {
* Read all relevant params of an armored key. * Read all relevant params of an armored key.
*/ */
PGP.prototype.getKeyParams = function(keyArmored) { PGP.prototype.getKeyParams = function(keyArmored) {
var key, packet, userIds; var key, packet, userIds, emailAddress;
// process armored key input // process armored key input
if (keyArmored) { if (keyArmored) {
@ -120,15 +122,24 @@ PGP.prototype.getKeyParams = function(keyArmored) {
// read user names and email addresses // read user names and email addresses
userIds = []; userIds = [];
key.getUserIds().forEach(function(userId) { key.getUserIds().forEach(function(userId) {
if (!userId || userId.indexOf('<') < 0 || userId.indexOf('>') < 0) {
return;
}
userIds.push({ userIds.push({
name: userId.split('<')[0].trim(), name: userId.split('<')[0].trim(),
emailAddress: userId.split('<')[1].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 { return {
_id: packet.getKeyId().toHex().toUpperCase(), _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 userIds: userIds, // a dictonary of all the key's name/address pairs
fingerprint: packet.getFingerprint().toUpperCase(), fingerprint: packet.getFingerprint().toUpperCase(),
algorithm: packet.algorithm, algorithm: packet.algorithm,

View File

@ -173,6 +173,16 @@ ngModule.directive('woClickFileInput', function() {
}; };
}); });
ngModule.directive('woFingerprint', function($timeout) {
return function(scope, elm) {
return $timeout(function() {
// add space after every fourth char to make pgp fingerprint more readable
var fingerprint = elm.text().replace(/(\w{4})/g, '$1 ').trim();
elm.text(fingerprint);
});
};
});
ngModule.directive('woInputCode', function() { ngModule.directive('woInputCode', function() {
var BLOCK_SIZE = 4; var BLOCK_SIZE = 4;
var NUM_BLOCKS = 6; var NUM_BLOCKS = 6;

View File

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

View File

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

View File

@ -1,8 +1,10 @@
'use strict'; 'use strict';
var PREFETCH_ITEMS = 10;
var ngModule = angular.module('woDirectives'); var ngModule = angular.module('woDirectives');
ngModule.directive('listScroll', function() { ngModule.directive('listScroll', function($timeout) {
return { return {
link: function(scope, elm, attrs) { link: function(scope, elm, attrs) {
var model = attrs.listScroll, var model = attrs.listScroll,
@ -12,7 +14,7 @@ ngModule.directive('listScroll', function() {
/* /*
* iterates over the mails in the mail list and loads their bodies if they are visible in the viewport * iterates over the mails in the mail list and loads their bodies if they are visible in the viewport
*/ */
scope.loadVisibleBodies = function() { function loadVisibleBodies() {
var listBorder = listEl.getBoundingClientRect(), var listBorder = listEl.getBoundingClientRect(),
top = listBorder.top, top = listBorder.top,
bottom = listBorder.bottom, bottom = listBorder.bottom,
@ -20,7 +22,10 @@ ngModule.directive('listScroll', function() {
inViewport = false, inViewport = false,
listItem, message, listItem, message,
isPartiallyVisibleTop, isPartiallyVisibleBottom, isVisible, 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) { if (!top && !bottom) {
// list not visible // list not visible
@ -38,7 +43,6 @@ ngModule.directive('listScroll', function() {
} }
message = displayMessages[i]; message = displayMessages[i];
isPartiallyVisibleTop = listItem.top < top && listItem.bottom > top; // a portion of the list item is visible on the top 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 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 isVisible = (listItem.top || listItem.bottom) && listItem.top >= top && listItem.bottom <= bottom; // the list item is visible as a whole
@ -47,12 +51,34 @@ ngModule.directive('listScroll', function() {
// we are now iterating over visible elements // we are now iterating over visible elements
inViewport = true; inViewport = true;
// load mail body of visible // 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) { } else if (inViewport) {
// we are leaving the viewport, so stop iterating over the items // we are leaving the viewport, so stop iterating over the items
break; 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() {
// wait for next tick so that scope is digested and synced to DOM
$timeout(function() {
loadVisibleBodies();
});
}; };
// load body when scrolling // load body when scrolling

View File

@ -45,7 +45,7 @@ ngModule.directive('replySelection', function() {
}; };
}); });
ngModule.directive('frameLoad', function($timeout, $window) { ngModule.directive('frameLoad', function($window) {
return function(scope, elm) { return function(scope, elm) {
var iframe = elm[0]; var iframe = elm[0];
@ -53,46 +53,58 @@ ngModule.directive('frameLoad', function($timeout, $window) {
if (open) { if (open) {
// trigger rendering of iframe // trigger rendering of iframe
// otherwise scale to fit would not compute correct dimensions on mobile // otherwise scale to fit would not compute correct dimensions on mobile
displayText(scope.state.mailList.selected ? scope.state.mailList.selected.body : undefined); displayContent();
displayHtml(scope.state.mailList.selected ? scope.state.mailList.selected.html : undefined);
} }
}); });
$window.addEventListener('resize', scaleToFit); scope.$on('$destroy', function() {
$window.removeEventListener('resize', resetWidth);
$window.removeEventListener('orientationchange', resetWidth);
});
$window.addEventListener('resize', resetWidth);
$window.addEventListener('orientationchange', resetWidth);
// 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
}]
});
}
}
});
iframe.onload = function() { iframe.onload = function() {
// set listeners // set listeners
scope.$watch('state.mailList.selected.body', displayText); scope.$watch('state.mailList.selected.body', displayContent);
scope.$watch('state.mailList.selected.html', displayHtml); scope.$watch('state.mailList.selected.html', displayContent);
// display initial message body // display initial message body
scope.$apply(); scope.$apply();
}; };
function displayText(body) { function displayContent() {
var mail = scope.state.mailList.selected; var mail = scope.state.mailList.selected;
if ((mail && mail.html) || (mail && mail.encrypted && !mail.decrypted)) {
if (!mail || (mail.encrypted && !mail.decrypted)) {
return; return;
} }
// send text body for rendering in iframe resetWidth();
iframe.contentWindow.postMessage({
text: body
}, '*');
$timeout(scaleToFit, 0);
}
function displayHtml(html) {
if (!html) {
return;
}
if (mail.html) {
// if there are image tags in the html? // if there are image tags in the html?
var hasImages = /<img[^>]+\bsrc=['"][^'">]+['"]/ig.test(html); var hasImages = /<img[^>]+\bsrc=['"][^'">]+['"]/ig.test(mail.html);
scope.showImageButton = hasImages; scope.showImageButton = hasImages;
iframe.contentWindow.postMessage({ iframe.contentWindow.postMessage({
html: html, html: mail.html,
removeImages: hasImages // avoids doing unnecessary work on the html removeImages: hasImages // avoids doing unnecessary work on the html
}, '*'); }, '*');
@ -102,30 +114,46 @@ ngModule.directive('frameLoad', function($timeout, $window) {
scope.displayImages = function() { scope.displayImages = function() {
scope.showImageButton = false; scope.showImageButton = false;
iframe.contentWindow.postMessage({ iframe.contentWindow.postMessage({
html: html, html: mail.html,
removeImages: false removeImages: false
}, '*'); }, '*');
}; };
} }
} else if (mail.body) {
$timeout(scaleToFit, 0); iframe.contentWindow.postMessage({
text: mail.body
}, '*');
}
} }
// transform scale iframe (necessary on iOS) to fit container width // reset the iframe width to the original (min-width:100%)
// usually required before a new scaleToFit event
function resetWidth() {
elm.css('width', '');
}
// transform scale iframe to fit container width
// necessary if iframe is wider than container
function scaleToFit() { function scaleToFit() {
var parentWidth = elm.parent().width(); var parentWidth = elm.parent().width();
var w = elm.width(); var w = elm.width();
var scale = ''; var scale = 'none';
if (w > parentWidth) { // only scale html mails
var mail = scope.state.mailList.selected;
if (mail && mail.html && (w > parentWidth)) {
scale = parentWidth / w; scale = parentWidth / w;
scale = 'scale(' + scale + ',' + scale + ')'; scale = 'scale(' + scale + ',' + scale + ')';
} }
elm.css({ elm.css({
'-webkit-transform-origin': '0 0', '-webkit-transform-origin': '0 0',
'-moz-transform-origin': '0 0',
'-ms-transform-origin': '0 0',
'transform-origin': '0 0', 'transform-origin': '0 0',
'-webkit-transform': scale, '-webkit-transform': scale,
'-moz-transform': scale,
'-ms-transform': scale,
'transform': scale 'transform': scale
}); });
} }

View File

@ -4,12 +4,9 @@ var ngModule = angular.module('woEmail');
ngModule.service('account', Account); ngModule.service('account', Account);
module.exports = Account; module.exports = Account;
var axe = require('axe-logger'), var util = require('crypto-lib').util;
util = require('crypto-lib').util,
PgpMailer = require('pgpmailer'),
ImapClient = require('imap-client');
function Account(appConfig, auth, accountStore, email, outbox, keychain, updateHandler, pgpbuilder, dialog) { function Account(appConfig, auth, accountStore, email, outbox, keychain, updateHandler, dialog) {
this._appConfig = appConfig; this._appConfig = appConfig;
this._auth = auth; this._auth = auth;
this._accountStore = accountStore; this._accountStore = accountStore;
@ -17,7 +14,6 @@ function Account(appConfig, auth, accountStore, email, outbox, keychain, updateH
this._outbox = outbox; this._outbox = outbox;
this._keychain = keychain; this._keychain = keychain;
this._updateHandler = updateHandler; this._updateHandler = updateHandler;
this._pgpbuilder = pgpbuilder;
this._dialog = dialog; this._dialog = dialog;
this._accounts = []; // init accounts list this._accounts = []; // init accounts list
} }
@ -102,68 +98,16 @@ Account.prototype.init = function(options) {
}); });
}; };
/**
* Check if the user agent is online.
*/
Account.prototype.isOnline = function() {
return navigator.onLine;
};
/** /**
* Event that is called when the user agent goes online. This create new instances of the imap-client and pgp-mailer and connects to the mail server. * Event that is called when the user agent goes online. This create new instances of the imap-client and pgp-mailer and connects to the mail server.
*/ */
Account.prototype.onConnect = function(callback) { Account.prototype.onConnect = function(callback) {
var self = this; if (!this._emailDao || !this._emailDao._account) {
var config = self._appConfig.config;
callback = callback || self._dialog.error;
if (!self.isOnline() || !self._emailDao || !self._emailDao._account) {
// prevent connection infinite loop // prevent connection infinite loop
return; return;
} }
// init imap/smtp clients this._emailDao.onConnect().then(callback).catch(callback);
self._auth.getCredentials().then(function(credentials) {
// add the maximum update batch size for imap folders to the imap configuration
credentials.imap.maxUpdateSize = config.imapUpdateBatchSize;
// tls socket worker path for multithreaded tls in non-native tls environments
credentials.imap.tlsWorkerPath = credentials.smtp.tlsWorkerPath = config.workerPath + '/tcp-socket-tls-worker.min.js';
var pgpMailer = new PgpMailer(credentials.smtp, self._pgpbuilder);
var imapClient = new ImapClient(credentials.imap);
imapClient.onError = onConnectionError;
pgpMailer.onError = onConnectionError;
// certificate update handling
imapClient.onCert = self._auth.handleCertificateUpdate.bind(self._auth, 'imap', self.onConnect.bind(self), self._dialog.error);
pgpMailer.onCert = self._auth.handleCertificateUpdate.bind(self._auth, 'smtp', self.onConnect.bind(self), self._dialog.error);
// connect to clients
return self._emailDao.onConnect({
imapClient: imapClient,
pgpMailer: pgpMailer,
ignoreUploadOnSent: self._emailDao.checkIgnoreUploadOnSent(credentials.imap.host)
});
}).then(callback).catch(callback);
function onConnectionError(error) {
axe.debug('Connection error. Attempting reconnect in ' + config.reconnectInterval + ' ms. Error: ' + (error.errMsg || error.message) + (error.stack ? ('\n' + error.stack) : ''));
setTimeout(function() {
axe.debug('Reconnecting...');
// re-init client modules on error
self.onConnect(function(err) {
if (err) {
axe.error('Reconnect attempt failed! ' + (err.errMsg || err.message) + (err.stack ? ('\n' + err.stack) : ''));
return;
}
axe.debug('Reconnect attempt complete.');
});
}, config.reconnectInterval);
}
}; };
/** /**
@ -180,6 +124,10 @@ Account.prototype.logout = function() {
var self = this; var self = this;
// clear app config store // clear app config store
return self._auth.logout().then(function() { 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 // delete instance of imap-client and pgp-mailer
return self._emailDao.onDisconnect(); 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('./mailreader');
require('./pgpbuilder'); require('./pgpbuilder');
require('./mailbuild');
require('./email'); require('./email');
require('./outbox'); require('./outbox');
require('./account'); 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, 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 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.publicKeysArmored = []; // gather the public keys
mail.uid = mail.id = util.UUID(); // the mail needs a random id & uid for storage in the database 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. * @param {Function} callback(error, pendingMailsCount) Callback that informs you about the count of pending mails.
*/ */
Outbox.prototype._processOutbox = function(callback) { Outbox.prototype._processOutbox = function(callback) {
var self = this, var self = this;
unsentMails = 0;
// also, if a _processOutbox call is still in progress, ignore it. // also, if a _processOutbox call is still in progress, ignore it.
if (self._outboxBusy) { if (self._outboxBusy) {
@ -129,10 +134,9 @@ Outbox.prototype._processOutbox = function(callback) {
self._outboxBusy = true; self._outboxBusy = true;
// get pending mails from the outbox // 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 we're not online, don't even bother sending mails.
if (!self._emailDao._account.online || _.isEmpty(pendingMails)) { if (!self._emailDao._account.online || _.isEmpty(pendingMails)) {
unsentMails = pendingMails.length;
return; return;
} }
@ -148,7 +152,7 @@ Outbox.prototype._processOutbox = function(callback) {
}).then(function() { }).then(function() {
self._outboxBusy = false; self._outboxBusy = false;
callback(null, unsentMails); callback();
}).catch(function(err) { }).catch(function(err) {
self._outboxBusy = false; 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

@ -30,7 +30,7 @@ Admin.prototype.createUser = function(options) {
throw new Error('User name is already taken!'); throw new Error('User name is already taken!');
} }
throw new Error('Error creating new user!'); throw new Error('Error creating new user! Reason: ' + err.message);
}); });
}; };
@ -57,6 +57,6 @@ Admin.prototype.validateUser = function(options) {
return; return;
} }
throw new Error('Validation failed!'); throw new Error('Validation failed! Reason: ' + err.message);
}); });
}; };

View File

@ -94,6 +94,8 @@ Auth.prototype.getCredentials = function() {
var credentials = { var credentials = {
imap: { imap: {
secure: self.imap.secure, secure: self.imap.secure,
requireTLS: self.imap.requireTLS,
ignoreTLS: self.imap.ignoreTLS,
port: self.imap.port, port: self.imap.port,
host: self.imap.host, host: self.imap.host,
ca: self.imap.ca, ca: self.imap.ca,
@ -105,6 +107,8 @@ Auth.prototype.getCredentials = function() {
}, },
smtp: { smtp: {
secure: self.smtp.secure, secure: self.smtp.secure,
requireTLS: self.smtp.requireTLS,
ignoreTLS: self.smtp.ignoreTLS,
port: self.smtp.port, port: self.smtp.port,
host: self.smtp.host, host: self.smtp.host,
ca: self.smtp.ca, ca: self.smtp.ca,
@ -240,16 +244,8 @@ Auth.prototype.useOAuth = function(hostname) {
Auth.prototype.getOAuthToken = function() { Auth.prototype.getOAuthToken = function() {
var self = this; var self = this;
if (self.oauthToken) {
// removed cached token and get a new one
return self._oauth.refreshToken({
emailAddress: self.emailAddress,
oldToken: self.oauthToken
}).then(onToken);
} else {
// get a fresh oauth token // get a fresh oauth token
return self._oauth.getOAuthToken(self.emailAddress).then(onToken); return self._oauth.getOAuthToken(self.emailAddress).then(onToken);
}
function onToken(oauthToken) { function onToken(oauthToken) {
// shortcut if the email address is already known // shortcut if the email address is already known
@ -305,7 +301,7 @@ Auth.prototype._loadCredentials = function() {
}); });
function loadFromDB(key) { 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]; return cachedItems && cachedItems[0];
}); });
} }
@ -317,7 +313,7 @@ Auth.prototype._loadCredentials = function() {
* @param {Function} callback The error handler * @param {Function} callback The error handler
* @param {[type]} pemEncodedCert The PEM encoded SSL certificate * @param {[type]} pemEncodedCert The PEM encoded SSL certificate
*/ */
Auth.prototype.handleCertificateUpdate = function(component, onConnect, callback, pemEncodedCert) { Auth.prototype.handleCertificateUpdate = function(component, reconnectCallback, callback, pemEncodedCert) {
var self = this; var self = this;
axe.debug('new ssl certificate received: ' + pemEncodedCert); axe.debug('new ssl certificate received: ' + pemEncodedCert);
@ -351,7 +347,7 @@ Auth.prototype.handleCertificateUpdate = function(component, onConnect, callback
self[component].ca = pemEncodedCert; self[component].ca = pemEncodedCert;
self.credentialsDirty = true; self.credentialsDirty = true;
self.storeCredentials().then(function() { self.storeCredentials().then(function() {
onConnect(callback); reconnectCallback(callback);
}).catch(callback); }).catch(callback);
} }
}); });

View File

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

19
src/js/service/hkp.js Normal file
View File

@ -0,0 +1,19 @@
'use strict';
var ngModule = angular.module('woServices');
ngModule.service('hkp', HKP);
module.exports = HKP;
function HKP(appConfig) {
this._appConfig = appConfig;
}
/**
* Return a url of the link to be opened in a new window
* @param {String} query Either the email address or name
* @return {String} The url of the hkp query
*/
HKP.prototype.getIndexUrl = function(query) {
var baseUrl = this._appConfig.config.hkpUrl + '/pks/lookup?op=index&search=';
return baseUrl + encodeURIComponent(query);
};

View File

@ -9,8 +9,10 @@ require('./newsletter');
require('./oauth'); require('./oauth');
require('./privatekey'); require('./privatekey');
require('./publickey'); require('./publickey');
require('./hkp');
require('./admin'); require('./admin');
require('./lawnchair'); require('./lawnchair');
require('./devicestorage'); require('./devicestorage');
require('./auth'); 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. * 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 * @param {Object} restDao The REST Data Access Object abstraction
*/ */
function Invitation(invitationRestDao) { function Invitation(invitationRestDao, appConfig) {
this._restDao = invitationRestDao; 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 * 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); ngModule.service('keychain', Keychain);
module.exports = Keychain; module.exports = Keychain;
var util = require('crypto-lib').util;
var DB_PUBLICKEY = 'publickey', var DB_PUBLICKEY = 'publickey',
DB_PRIVATEKEY = 'privatekey', DB_PRIVATEKEY = 'privatekey';
DB_DEVICENAME = 'devicename',
DB_DEVICE_SECRET = 'devicesecret';
/** /**
* A high-level Data-Access Api for handling Keypair synchronization * A high-level Data-Access Api for handling Keypair synchronization
@ -57,40 +53,6 @@ Keychain.prototype.verifyPublicKey = function(uuid) {
return this._publicKeyDao.verify(uuid); return this._publicKeyDao.verify(uuid);
}; };
/**
* Get an array of public keys by looking in local storage and
* fetching missing keys from the cloud service.
* @param ids [Array] the key ids as [{_id, userId}]
* @return [PublicKeyCollection] The requiested public keys
*/
Keychain.prototype.getPublicKeys = function(ids) {
var self = this,
jobs = [],
pubkeys = [];
ids.forEach(function(i) {
// lookup locally and in storage
var promise = self.lookupPublicKey(i._id).then(function(pubkey) {
if (!pubkey) {
throw new Error('Error looking up public key!');
}
// check if public key with that id has already been fetched
var already = _.findWhere(pubkeys, {
_id: i._id
});
if (!already) {
pubkeys.push(pubkey);
}
});
jobs.push(promise);
});
return Promise.all(jobs).then(function() {
return pubkeys;
});
};
/** /**
* Checks for public key updates of a given user id * Checks for public key updates of a given user id
* @param {String} options.userId The user id (email address) for which to check the key * @param {String} options.userId The user id (email address) for which to check the key
@ -117,13 +79,13 @@ Keychain.prototype.refreshKeyForUserId = function(options) {
// checks if the user's key has been revoked by looking up the key id // checks if the user's key has been revoked by looking up the key id
function checkKeyExists(localKey) { function checkKeyExists(localKey) {
return self._publicKeyDao.get(localKey._id).then(function(cloudKey) { return self._publicKeyDao.getByUserId(userId).then(function(cloudKey) {
if (cloudKey && cloudKey._id === localKey._id) { if (cloudKey && cloudKey._id === localKey._id) {
// the key is present on the server, all is well // the key is present on the server, all is well
return localKey; return localKey;
} }
// the key has changed, update the key // the key has changed, update the key
return updateKey(localKey); return updateKey(localKey, cloudKey);
}).catch(function(err) { }).catch(function(err) {
if (err && err.code === 42) { if (err && err.code === 42) {
@ -134,9 +96,7 @@ Keychain.prototype.refreshKeyForUserId = function(options) {
}); });
} }
function updateKey(localKey) { function updateKey(localKey, newKey) {
// look for an updated key for the user id
return self._publicKeyDao.getByUserId(userId).then(function(newKey) {
// the public key has changed, we need to ask for permission to update the key // the public key has changed, we need to ask for permission to update the key
if (overridePermission) { if (overridePermission) {
// don't query the user, update the public key right away // don't query the user, update the public key right away
@ -144,14 +104,6 @@ Keychain.prototype.refreshKeyForUserId = function(options) {
} else { } else {
return requestPermission(localKey, newKey); return requestPermission(localKey, newKey);
} }
}).catch(function(err) {
// offline?
if (err && err.code === 42) {
return localKey;
}
throw err;
});
} }
function requestPermission(localKey, newKey) { function requestPermission(localKey, newKey) {
@ -195,15 +147,17 @@ Keychain.prototype.getReceiverPublicKey = function(userId) {
var self = this; var self = this;
// search local keyring for public key // 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 // query primary email address
var pubkey = _.findWhere(allPubkeys, { var pubkey = _.findWhere(allPubkeys, {
userId: userId userId: userId
}); });
// query mutliple userIds (for imported public keys) // query mutliple userIds
if (!pubkey) { if (!pubkey) {
for (var i = 0, match; i < allPubkeys.length; i++) { for (var i = 0, match; i < allPubkeys.length; i++) {
match = _.findWhere(allPubkeys[i].userIds, { userIds = self._pgp.getKeyParams(allPubkeys[i].publicKey).userIds;
match = _.findWhere(userIds, {
emailAddress: userId emailAddress: userId
}); });
if (match) { if (match) {
@ -241,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 // Keypair functions
// //
@ -639,12 +209,12 @@ Keychain.prototype.getUserKeyPair = function(userId) {
var self = this; var self = this;
// search for user's public key locally // 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, { var pubkey = _.findWhere(allPubkeys, {
userId: userId userId: userId
}); });
if (pubkey && pubkey._id) { if (pubkey && pubkey._id && !pubkey.source) {
// that user's public key is already in local storage... // that user's public key is already in local storage...
// sync keypair to the cloud // sync keypair to the cloud
return syncKeypair(pubkey._id); return syncKeypair(pubkey._id);
@ -653,13 +223,13 @@ Keychain.prototype.getUserKeyPair = function(userId) {
// no public key by that user id in storage // no public key by that user id in storage
// find from cloud by email address // find from cloud by email address
return self._publicKeyDao.getByUserId(userId).then(function(cloudPubkey) { return self._publicKeyDao.getByUserId(userId).then(function(cloudPubkey) {
if (cloudPubkey && cloudPubkey._id) { if (cloudPubkey && cloudPubkey._id && !cloudPubkey.source) {
// there is a public key for that user already in the cloud... // there is a public key for that user already in the cloud...
// sync keypair to local storage // sync keypair to local storage
return syncKeypair(cloudPubkey._id); return syncKeypair(cloudPubkey._id);
} }
// continue without keypair... generate in crypto.js // continue without keypair... generate or import new keypair
}); });
}); });
@ -668,10 +238,12 @@ Keychain.prototype.getUserKeyPair = function(userId) {
// persist key pair in local storage // persist key pair in local storage
return self.lookupPublicKey(keypairId).then(function(pub) { return self.lookupPublicKey(keypairId).then(function(pub) {
savedPubkey = pub; savedPubkey = pub;
// persist private key in local storage // persist private key in local storage
return self.lookupPrivateKey(keypairId).then(function(priv) { return self.lookupPrivateKey(keypairId);
}).then(function(priv) {
savedPrivkey = priv; savedPrivkey = priv;
});
}).then(function() { }).then(function() {
var keys = {}; var keys = {};
@ -699,7 +271,7 @@ Keychain.prototype.putUserKeyPair = function(keypair) {
// validate input // validate input
if (!keypair || !keypair.publicKey || !keypair.privateKey || !keypair.publicKey.userId || keypair.publicKey.userId !== keypair.privateKey.userId) { if (!keypair || !keypair.publicKey || !keypair.privateKey || !keypair.publicKey.userId || keypair.publicKey.userId !== keypair.privateKey.userId) {
return new Promise(function() { return new Promise(function() {
throw new Error('Incorrect input!'); throw new Error('Cannot put user key pair: Incorrect input!');
}); });
} }
@ -716,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 // Helper functions
// //
@ -752,7 +342,7 @@ Keychain.prototype.lookupPublicKey = function(id) {
*/ */
Keychain.prototype.listLocalPublicKeys = function() { Keychain.prototype.listLocalPublicKeys = function() {
// search local keyring for public key // 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) { Keychain.prototype.removeLocalPublicKey = function(id) {

View File

@ -104,60 +104,49 @@ LawnchairDAO.prototype.read = function(key) {
/** /**
* List all the items of a certain type * List all the items of a certain type
* @param type [String] The type of item e.g. 'email' * @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} * @return {Promise}
*/ */
LawnchairDAO.prototype.list = function(type, offset, num) { LawnchairDAO.prototype.list = function(query, exactMatchOnly) {
var self = this; var self = this;
return new Promise(function(resolve) { return new Promise(function(resolve) {
var i, from, to, var matchingKeys = [];
matchingKeys = [],
intervalKeys = [],
list = [];
// validate input // 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!'); 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 // get all keys
self._db.keys(function(keys) { self._db.keys(function(keys) {
// check if key begins with type // check if there are keys in the db that start with the respective query
keys.forEach(function(key) { matchingKeys = keys.filter(function(key) {
if (key.indexOf(type) === 0) { return query.filter(function(type) {
matchingKeys.push(key); if (exactMatchOnly) {
return key === type;
} else {
return key.indexOf(type) === 0;
} }
}).length > 0;
}); });
// sort keys if (matchingKeys.length === 0) {
matchingKeys.sort(); // no matching keys, resolve
resolve([]);
// 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);
return; return;
} }
// fetch all items from data-store with matching key // fetch all items from data-store with matching keys
self._db.get(intervalKeys, function(intervalList) { self._db.get(matchingKeys, function(intervalList) {
intervalList.forEach(function(item) { var result = intervalList.map(function(item) {
list.push(item.object); return item.object;
}); });
// return only the interval between offset and num resolve(result);
resolve(list);
}); });
}); });
}); });

View File

@ -4,96 +4,89 @@ var ngModule = angular.module('woServices');
ngModule.service('privateKey', PrivateKey); ngModule.service('privateKey', PrivateKey);
module.exports = PrivateKey; module.exports = PrivateKey;
function PrivateKey(privateKeyRestDao) { var ImapClient = require('imap-client');
this._restDao = privateKeyRestDao; var util = require('crypto-lib').util;
}
// var IMAP_KEYS_FOLDER = 'openpgp_keys';
// Device registration functions 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;
}
/** /**
* Request registration of a new device by fetching registration session key. * Configure the local imap client used for key-sync with credentials from the auth module.
* @param {String} options.userId The user's email address
* @param {String} options.deviceName The device's memorable name
* @return {Object} {encryptedRegSessionKey:[base64]}
*/ */
PrivateKey.prototype.requestDeviceRegistration = function(options) { PrivateKey.prototype.init = function() {
var self = this; var self = this;
return new Promise(function(resolve) {
if (!options.userId || !options.deviceName) {
throw new Error('Incomplete arguments!');
}
resolve();
}).then(function() { return self._auth.getCredentials().then(function(credentials) {
var uri = '/device/user/' + options.userId + '/devicename/' + options.deviceName; // tls socket worker path for multithreaded tls in non-native tls environments
return self._restDao.post(undefined, uri); 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. * Cleanup by logging out of the imap client.
* @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
*/ */
PrivateKey.prototype.uploadDeviceSecret = function(options) { PrivateKey.prototype.destroy = function() {
var self = this; this._imap.logout();
// don't wait for logout to complete
return new Promise(function(resolve) { return new Promise(function(resolve) {
if (!options.userId || !options.deviceName || !options.encryptedDeviceSecret || !options.iv) {
throw new Error('Incomplete arguments!');
}
resolve(); 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. * Encrypt and upload the private PGP key to the server.
* @param {String} options.userId The user's email address * @param {String} code The randomly generated or self selected code used to derive the key for the encryption of the private PGP key
* @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
*/ */
PrivateKey.prototype.verifyAuthentication = function(options) { PrivateKey.prototype.encrypt = function(code) {
var self = this; var self = this,
return new Promise(function(resolve) { config = self._appConfig.config,
if (!options.userId || !options.sessionId || !options.encryptedChallenge || !options.encryptedDeviceSecret || !options.iv) { keySize = config.symKeySize,
throw new Error('Incomplete arguments!'); encryptionKey, salt, iv, privkeyId;
}
resolve();
}).then(function() { if (!code) {
var uri = '/auth/user/' + options.userId + '/session/' + options.sessionId; return new Promise(function() {
return self._restDao.put(options, uri); 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._id The hex encoded capital 16 char key id
* @param {String} options.userId The user's email address * @param {String} options.userId The user's email address
* @param {String} options.encryptedPrivateKey The base64 encoded encrypted private PGP key * @param {String} options.encryptedPrivateKey The base64 encoded encrypted private PGP key
* @param {String} options.sessionId The session id
*/ */
PrivateKey.prototype.upload = function(options) { PrivateKey.prototype.upload = function(options) {
var self = this; var self = this,
path;
return new Promise(function(resolve) { return new Promise(function(resolve) {
if (!options._id || !options.userId || !options.encryptedPrivateKey || !options.sessionId || !options.salt || !options.iv) { if (!options._id || !options.userId || !options.encryptedPrivateKey || !options.salt || !options.iv) {
throw new Error('Incomplete arguments!'); throw new Error('Incomplete arguments for key upload!');
} }
resolve(); resolve();
}).then(function() { }).then(function() {
var uri = '/privatekey/user/' + options.userId + '/session/' + options.sessionId;
return self._restDao.post(options, uri);
});
};
/** // Some servers (Exchange, Cyrus) error when creating an existing IMAP mailbox instead of
* Query if an encrypted private PGP key exists on the server without initializing the recovery procedure. // responding with ALREADYEXISTS. Hence we search for the folder before uploading.
* @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();
}).then(function() { self._axe.debug('Searching imap folder for key upload...');
return self._restDao.get({
uri: '/privatekey/user/' + options.userId + '/key/' + options.keyId + '?ignoreRecovery=true',
});
}).then(function() { return self._getFolder().then(function(fullPath) {
return true; 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) { }).catch(function(err) {
// 404: there is no encrypted private key on the server var prettyErr = new Error('Creating imap folder ' + IMAP_KEYS_FOLDER + ' failed: ' + err.message);
if (err.code && err.code !== 200) { self._axe.error(prettyErr);
return false; throw prettyErr;
}
throw err;
}); });
});
}).then(createMessage).then(function(message) {
// upload to imap folder
self._axe.debug('Uploading key...');
return self._imap.uploadMessage({
path: path,
message: message
});
});
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));
// 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);
// 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();
}
}; };
/** /**
* Request download for the encrypted private PGP key. * Check if matching private key is stored in IMAP.
* @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) { PrivateKey.prototype.isSynced = function() {
var self = this; var self = this;
return new Promise(function(resolve) {
if (!options.userId || !options.keyId) {
throw new Error('Incomplete arguments!');
}
resolve();
}).then(function() { return self._getFolder().then(function(path) {
return self._restDao.get({ return self._fetchMessage({
uri: '/privatekey/user/' + options.userId + '/key/' + options.keyId keyId: self._pgp.getKeyId(),
path: path
}); });
}).then(function(msg) {
}).then(function() { return !!msg;
return true; }).catch(function() {
}).catch(function(err) {
// 404: there is no encrypted private key on the server
if (err.code && err.code !== 200) {
return false; return false;
}
throw err;
}); });
}; };
/** /**
* 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.userId The user's email address
* @param {String} options.keyId The private key id * @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]} * @return {Object} {_id:[hex encoded capital 16 char key id], encryptedPrivateKey:[base64 encoded], encryptedUserId: [base64 encoded]}
*/ */
PrivateKey.prototype.download = function(options) { PrivateKey.prototype.download = function(options) {
var self = this; var self = this,
return new Promise(function(resolve) { path, message;
if (!options.userId || !options.keyId || !options.recoveryToken) {
throw new Error('Incomplete arguments!'); 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!');
} }
resolve();
message = msg;
});
}).then(function() {
// get the body for the message
return self._imap.getBodyParts({
path: path,
uid: message.uid,
bodyParts: message.bodyParts
});
}).then(function() { }).then(function() {
return self._restDao.get({ // parse the message
uri: '/privatekey/user/' + options.userId + '/key/' + options.keyId + '/recovery/' + options.recoveryToken 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 // rest dao for use in the public key service
ngModule.factory('publicKeyRestDao', function(appConfig) { ngModule.factory('publicKeyRestDao', function(appConfig) {
var dao = new RestDAO(); var dao = new RestDAO();
dao.setBaseUri(appConfig.config.cloudUrl); dao.setBaseUri(appConfig.config.keyServerUrl);
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);
return dao; return dao;
}); });
// rest dao for use in the invitation service // rest dao for use in the invitation service
ngModule.factory('invitationRestDao', function(appConfig) { ngModule.factory('invitationRestDao', function(appConfig) {
var dao = new RestDAO(); var dao = new RestDAO();
dao.setBaseUri(appConfig.config.cloudUrl); dao.setBaseUri(appConfig.config.keyServerUrl);
return dao; return dao;
}); });
@ -62,11 +55,12 @@ RestDAO.prototype.get = function(options) {
/** /**
* POST (create) request * POST (create) request
*/ */
RestDAO.prototype.post = function(item, uri) { RestDAO.prototype.post = function(item, uri, type) {
return this._processRequest({ return this._processRequest({
method: 'POST', method: 'POST',
payload: item, payload: item,
uri: uri uri: uri,
type: type
}); });
}; };
@ -98,62 +92,65 @@ RestDAO.prototype.remove = function(uri) {
RestDAO.prototype._processRequest = function(options) { RestDAO.prototype._processRequest = function(options) {
var self = this; var self = this;
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
var xhr, format; var xhr, format, accept, payload;
if (typeof options.uri === 'undefined') { if (typeof options.uri === 'undefined') {
throw { throw createError(400, 'Bad Request! URI is a mandatory parameter.');
code: 400,
message: 'Bad Request! URI is a mandatory parameter.'
};
} }
options.type = options.type || 'json'; options.type = options.type || 'json';
payload = options.payload;
if (options.type === 'json') { if (options.type === 'json') {
format = 'application/json'; format = 'application/json';
payload = payload ? JSON.stringify(payload) : undefined;
} else if (options.type === 'xml') { } else if (options.type === 'xml') {
format = 'application/xml'; format = 'application/xml';
} else if (options.type === 'text') { } else if (options.type === 'text') {
format = 'text/plain'; format = 'text/plain';
} else if (options.type === 'form') {
format = 'application/x-www-form-urlencoded; charset=UTF-8';
accept = 'text/html; charset=UTF-8';
} else { } else {
throw { throw createError(400, 'Bad Request! Unhandled data type.');
code: 400,
message: 'Bad Request! Unhandled data type.'
};
} }
xhr = new XMLHttpRequest(); xhr = new XMLHttpRequest();
xhr.open(options.method, self._baseUri + options.uri); xhr.open(options.method, self._baseUri + options.uri);
xhr.setRequestHeader('Accept', format); xhr.setRequestHeader('Accept', accept || format);
xhr.setRequestHeader('Content-Type', format); xhr.setRequestHeader('Content-Type', format);
xhr.onload = function() { xhr.onload = function() {
var res; var res;
if (xhr.readyState === 4 && (xhr.status === 200 || xhr.status === 201 || xhr.status === 304)) {
if (options.type === 'json') { if (options.type === 'json') {
res = xhr.responseText ? JSON.parse(xhr.responseText) : xhr.responseText; try {
res = JSON.parse(xhr.responseText);
} catch (e) {
res = xhr.responseText;
}
} else { } else {
res = xhr.responseText; res = xhr.responseText;
} }
if (xhr.readyState === 4 && (xhr.status === 200 || xhr.status === 201 || xhr.status === 304)) {
resolve(res); resolve(res);
return; return;
} }
reject({ reject(createError(xhr.status, (res && res.error) || xhr.statusText));
code: xhr.status,
message: xhr.statusText
});
}; };
xhr.onerror = function() { xhr.onerror = function() {
reject({ reject(createError(42, 'Error calling ' + options.method + ' on ' + options.uri));
code: 42, };
message: 'Error calling ' + options.method + ' on ' + options.uri
xhr.send(payload);
}); });
}; };
xhr.send(options.payload ? JSON.stringify(options.payload) : undefined); function createError(code, message) {
}); var error = new Error(message);
}; error.code = code;
return error;
}

View File

@ -53,6 +53,7 @@ ConnectionDoctor.prototype.configure = function(credentials) {
port: this.credentials.imap.port, port: this.credentials.imap.port,
secure: this.credentials.imap.secure, secure: this.credentials.imap.secure,
ignoreTLS: this.credentials.imap.ignoreTLS, ignoreTLS: this.credentials.imap.ignoreTLS,
requireTLS: this.credentials.imap.requireTLS,
ca: this.credentials.imap.ca, ca: this.credentials.imap.ca,
tlsWorkerPath: this._workerPath, tlsWorkerPath: this._workerPath,
auth: { auth: {
@ -65,6 +66,7 @@ ConnectionDoctor.prototype.configure = function(credentials) {
this._smtp = new SmtpClient(this.credentials.smtp.host, this.credentials.smtp.port, { this._smtp = new SmtpClient(this.credentials.smtp.host, this.credentials.smtp.port, {
useSecureTransport: this.credentials.smtp.secure, useSecureTransport: this.credentials.smtp.secure,
ignoreTLS: this.credentials.smtp.ignoreTLS, ignoreTLS: this.credentials.smtp.ignoreTLS,
requireTLS: this.credentials.smtp.requireTLS,
ca: this.credentials.smtp.ca, ca: this.credentials.smtp.ca,
tlsWorkerPath: this._workerPath, tlsWorkerPath: this._workerPath,
auth: { auth: {
@ -218,25 +220,21 @@ ConnectionDoctor.prototype._checkImap = function() {
} }
}; };
self._imap.login(function() { self._imap.login().then(function() {
loggedIn = true; loggedIn = true;
return self._imap.listWellKnownFolders();
self._imap.listWellKnownFolders(function(error, wellKnownFolders) { }).then(function(wellKnownFolders) {
if (error) {
reject(createError(GENERIC_ERROR, str.connDocGenericError.replace('{0}', host).replace('{1}', error.message), error));
return;
}
if (wellKnownFolders.Inbox.length === 0) { if (wellKnownFolders.Inbox.length === 0) {
// the client needs at least an inbox folder to work properly // the client needs at least an inbox folder to work properly
reject(createError(NO_INBOX, str.connDocNoInbox.replace('{0}', host))); reject(createError(NO_INBOX, str.connDocNoInbox.replace('{0}', host)));
return; return;
} }
self._imap.logout(function() { return self._imap.logout();
}).then(function(){
resolve(); 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(); supportsBlob = !!new Blob();
} catch (e) {} } catch (e) {}
if (typeof a.download !== "undefined" && supportsBlob) { if (typeof a.download !== 'undefined' && supportsBlob) {
// ff 30+, chrome 27+ (android: 37+) // ff 30+, chrome 27+ (android: 37+)
document.body.appendChild(a); document.body.appendChild(a);
a.style = "display: none"; a.style.display = 'none';
a.href = window.URL.createObjectURL(new Blob([content], { a.href = window.URL.createObjectURL(new Blob([content], {
type: contentType type: contentType
})); }));
@ -52,15 +52,15 @@ Download.prototype.createDownload = function(options) {
var url = window.URL.createObjectURL(new Blob([content], { var url = window.URL.createObjectURL(new Blob([content], {
type: contentType type: contentType
})); }));
var newTab = window.open(url, "_blank"); var newTab = window.open(url, '_blank');
if (!newTab) { if (!newTab) {
window.location.href = url; window.location.href = url;
} }
} else { } else {
// anything else, where anything at all is better than nothing // 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); 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', name: 'Junk',
count: 0, count: 0,
path: 'JUNK' 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; return dummies;
@ -101,9 +122,11 @@ Dummy.prototype.listMails = function() {
'>> from 0.7.0.1\n' + '>> from 0.7.0.1\n' +
'>>\n' + '>>\n' +
'>> God speed!'; // plaintext body '>> 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.encrypted = true;
this.decrypted = true; this.decrypted = true;
this.signed = true;
this.signaturesValid = true;
}; };
var dummies = [], var dummies = [],

View File

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

View File

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

View File

@ -66,7 +66,7 @@ function update(options) {
}); });
function loadFromDB(key) { 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]; return cachedItems && cachedItems[0];
}); });
} }

View File

@ -18,7 +18,7 @@ var POST_UPDATE_DB_VERSION = 5;
*/ */
function update(options) { function update(options) {
// remove the emails // 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] || []; var folders = stored[0] || [];
[FOLDER_TYPE_INBOX, FOLDER_TYPE_SENT, FOLDER_TYPE_DRAFTS, FOLDER_TYPE_TRASH].forEach(function(mbxType) { [FOLDER_TYPE_INBOX, FOLDER_TYPE_SENT, FOLDER_TYPE_DRAFTS, FOLDER_TYPE_TRASH].forEach(function(mbxType) {
var foldersForType = folders.filter(function(mbx) { 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 * (c) 2010-2014 Google, Inc. http://angularjs.org
* License: MIT * License: MIT
*/ */
@ -839,7 +839,8 @@ angular.module('ngAnimate', ['ng'])
* promise that was returned when the animation was started. * promise that was returned when the animation was started.
* *
* ```js * ```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 * //this will still be called even if cancelled
* }); * });
* *
@ -1332,8 +1333,7 @@ angular.module('ngAnimate', ['ng'])
} else if (lastAnimation.event == 'setClass') { } else if (lastAnimation.event == 'setClass') {
animationsToCancel.push(lastAnimation); animationsToCancel.push(lastAnimation);
cleanup(element, className); cleanup(element, className);
} } else if (runningAnimations[className]) {
else if (runningAnimations[className]) {
var current = runningAnimations[className]; var current = runningAnimations[className];
if (current.event == animationEvent) { if (current.event == animationEvent) {
skipAnimation = true; skipAnimation = true;
@ -1874,7 +1874,7 @@ angular.module('ngAnimate', ['ng'])
return; return;
} }
if (!staggerTime && styles) { if (!staggerTime && styles && Object.keys(styles).length > 0) {
if (!timings.transitionDuration) { if (!timings.transitionDuration) {
element.css('transition', timings.animationDuration + 's linear all'); element.css('transition', timings.animationDuration + 's linear all');
appliedStyles.push('transition'); 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 * (c) 2010-2014 Google, Inc. http://angularjs.org
* License: MIT * License: MIT
*/ */
@ -250,31 +250,31 @@ angular.mock.$ExceptionHandlerProvider = function() {
* *
* @param {string} mode Mode of operation, defaults to `rethrow`. * @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` * - `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 * mode stores an array of errors in `$exceptionHandler.errors`, to allow later
* assertion of them. See {@link ngMock.$log#assertEmpty assertEmpty()} and * assertion of them. See {@link ngMock.$log#assertEmpty assertEmpty()} and
* {@link ngMock.$log#reset reset()} * {@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) { 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) { handler = function(e) {
if (arguments.length == 1) { if (arguments.length == 1) {
errors.push(e); errors.push(e);
} else { } else {
errors.push([].slice.call(arguments, 0)); errors.push([].slice.call(arguments, 0));
} }
if (mode === "rethrow") {
throw e;
}
}; };
handler.errors = errors; handler.errors = errors;
break; break;
default: 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 * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
* and returns true if the url match the current definition. * and returns true if the url match the current definition.
* @param {(Object|function(Object))=} headers HTTP headers. * @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 * 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. * 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 * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
* and returns true if the url match the current definition. * and returns true if the url match the current definition.
* @param {(Object|function(Object))=} headers HTTP headers. * @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 * 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. * 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 * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
* and returns true if the url match the current definition. * and returns true if the url match the current definition.
* @param {(Object|function(Object))=} headers HTTP headers. * @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 * 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. * 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 * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives
* data string and returns true if the data is as expected. * data string and returns true if the data is as expected.
* @param {(Object|function(Object))=} headers HTTP headers. * @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 * 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. * 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 * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives
* data string and returns true if the data is as expected. * data string and returns true if the data is as expected.
* @param {(Object|function(Object))=} headers HTTP headers. * @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 * 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. * 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 * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
* and returns true if the url match the current definition. * 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 * 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. * order to change how a matched request is handled.
*/ */
@ -1377,7 +1377,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* is in JSON format. * is in JSON format.
* @param {(Object|function(Object))=} headers HTTP headers or function that receives http header * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
* object and returns true if the headers match the current expectation. * 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 * 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. * 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 * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
* and returns true if the url match the current definition. * and returns true if the url match the current definition.
* @param {Object=} headers HTTP headers. * @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 * 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. * 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 * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
* and returns true if the url match the current definition. * and returns true if the url match the current definition.
* @param {Object=} headers HTTP headers. * @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 * 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. * 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 * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
* and returns true if the url match the current definition. * and returns true if the url match the current definition.
* @param {Object=} headers HTTP headers. * @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 * 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. * 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 * receives data string and returns true if the data is as expected, or Object if request body
* is in JSON format. * is in JSON format.
* @param {Object=} headers HTTP headers. * @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 * 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. * 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 * receives data string and returns true if the data is as expected, or Object if request body
* is in JSON format. * is in JSON format.
* @param {Object=} headers HTTP headers. * @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 * 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. * 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 * receives data string and returns true if the data is as expected, or Object if request body
* is in JSON format. * is in JSON format.
* @param {Object=} headers HTTP headers. * @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 * 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. * 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 * @param {string|RegExp|function(string)} url HTTP url or function that receives the url
* and returns true if the url match the current definition. * 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 * 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. * 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 * @ngdoc module
* @name ngMock * @name ngMock
@ -1837,6 +1908,7 @@ angular.module('ngMock', ['ng']).provider({
$provide.decorator('$$rAF', angular.mock.$RAFDecorator); $provide.decorator('$$rAF', angular.mock.$RAFDecorator);
$provide.decorator('$$asyncCallback', angular.mock.$AsyncCallbackDecorator); $provide.decorator('$$asyncCallback', angular.mock.$AsyncCallbackDecorator);
$provide.decorator('$rootScope', angular.mock.$RootScopeDecorator); $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) { if (window.jasmine || window.mocha) {
var currentSpec = null, var currentSpec = null,
annotatedFunctions = [],
isSpecRunning = function() { isSpecRunning = function() {
return !!currentSpec; 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() { (window.beforeEach || window.setup)(function() {
annotatedFunctions = [];
currentSpec = this; currentSpec = this;
}); });
(window.afterEach || window.teardown)(function() { (window.afterEach || window.teardown)(function() {
var injector = currentSpec.$injector; var injector = currentSpec.$injector;
annotatedFunctions.forEach(function(fn) {
delete fn.$inject;
});
angular.forEach(currentSpec.$modules, function(module) { angular.forEach(currentSpec.$modules, function(module) {
if (module && module.$$hashKey) { if (module && module.$$hashKey) {
module.$$hashKey = undefined; 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 * (c) 2010-2014 Google, Inc. http://angularjs.org
* License: MIT * License: MIT
*/ */
@ -482,21 +482,15 @@ function $RouteProvider() {
* definitions will be interpolated into the location's path, while * definitions will be interpolated into the location's path, while
* remaining properties will be treated as query params. * 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) { updateParams: function(newParams) {
if (this.current && this.current.$$route) { 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); newParams = angular.extend({}, this.current.params, newParams);
$location.path(interpolate(this.current.$$route.originalPath, newParams)); $location.path(interpolate(this.current.$$route.originalPath, newParams));
$location.search(angular.extend({}, $location.search(), searchParams)); // interpolate modifies newParams, only query params are left
} $location.search(newParams);
else { } else {
throw $routeMinErr('norout', 'Tried updating route when with no current route'); 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]);

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

@ -1,10 +1,11 @@
{ {
"name": "Whiteout Mail", "name": "Whiteout Mail",
"description": "Simple and elegant email client with integrated end-to-end encryption. Keeping your emails safe has never been so easy.", "description": "Simple and elegant email client with integrated end-to-end encryption. Keeping your emails safe has never been so easy.",
"version": "0.20.0.0", "version": "0.22.1.0",
"launch_path": "/index.html", "launch_path": "/index.html",
"icons": { "icons": {
"128": "/img/icon-128.png" "128": "/img/icon-128-chrome.png",
"196": "/img/icon-196-universal.png"
}, },
"developer": { "developer": {
"name": "Whiteout Networks GmbH", "name": "Whiteout Networks GmbH",

View File

@ -22,7 +22,7 @@ $color-red-light: #ff878d;
$color-grey: #666; $color-grey: #666;
$color-grey-input: #949494; $color-grey-input: #949494;
$color-grey-dark: #333; $color-grey-dark: #333;
$color-grey-medium: #999; $color-grey-medium: #888;
$color-grey-light: #ccc; $color-grey-light: #ccc;
$color-grey-lighter: #ddd; $color-grey-lighter: #ddd;
$color-grey-lighterer: #f4f4f4; $color-grey-lighterer: #f4f4f4;

View File

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

View File

@ -10,10 +10,6 @@
margin-bottom: 10px; margin-bottom: 10px;
color: $color-error; color: $color-error;
} }
&__password-strong-message {
margin-bottom: 10px;
color: green;
}
&__row { &__row {
margin-bottom: 10px; margin-bottom: 10px;
@ -105,6 +101,7 @@
font-size: $font-size-base; font-size: $font-size-base;
padding: 0.5em 0.7em; padding: 0.5em 0.7em;
outline: none; outline: none;
box-shadow: none;
// ios // ios
border-radius: 0; border-radius: 0;
-webkit-appearance: none; -webkit-appearance: none;
@ -123,6 +120,14 @@
} }
} }
.textarea {
width: 100%;
height: 100px;
border: 1px solid $color-border-light;
resize: none;
outline: none;
}
// Attention: Webkit support only! // Attention: Webkit support only!
.input-select { .input-select {
position: relative; position: relative;
@ -234,6 +239,7 @@
line-height: 1em; line-height: 1em;
border: 1px solid $color-text-light; border: 1px solid $color-text-light;
text-align: center; text-align: center;
background-color: $color-bg;
svg { svg {
display: inline-block; display: inline-block;
fill: $color-main; fill: $color-main;

View File

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

View File

@ -8,6 +8,8 @@
z-index: 1000; z-index: 1000;
background: $color-grey-dark-alpha; background: $color-grey-dark-alpha;
overflow: auto; overflow: auto;
// allow scrolling on iOS
-webkit-overflow-scrolling: touch;
text-align: center; text-align: center;
@include respond-to(md) { @include respond-to(md) {
@ -21,17 +23,16 @@
padding: 15px; padding: 15px;
background: $color-bg; background: $color-bg;
color: $color-text; color: $color-text;
backface-visibility: hidden;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
text-align: left; text-align: left;
margin: 0 auto; margin: 0 auto;
will-change: transform;
@include respond-to(md) { @include respond-to(md) {
width: 90%; width: 90%;
max-width: 762px; max-width: 762px;
min-height: 0; min-height: 0;
backface-visibility: hidden;
} }
} }

View File

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

View File

@ -332,7 +332,7 @@
display: table-row; display: table-row;
background: $color-bg-dark; background: $color-bg-dark;
color: $color-grey; color: $color-text;
cursor: pointer; cursor: pointer;
// Flags // Flags

View File

@ -23,17 +23,53 @@
display: none; 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 components
&__header { &__header {
flex-shrink: 0;
margin-bottom: 1em; margin-bottom: 1em;
padding: $padding-vertical $padding-horizontal 0; padding: $padding-vertical $padding-horizontal 0;
& > .attachments { & > .attachments {
margin-top: 1em; 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 // only visible in stripped version of read view
.mail-addresses__stripped { .mail-addresses__stripped {
@ -41,15 +77,29 @@
} }
&__controls { &__controls {
display: none; display: none;
float: right; position: absolute;
margin-left: 1em; top: 0;
.btn-icon-light { 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; margin-left: 1.4em;
} }
@include respond-to(md) { @include respond-to(md) {
display: block; 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 { &__subject {
font-weight: normal; font-weight: normal;
color: $color-text; color: $color-text;
@ -102,49 +152,18 @@
// Content components // 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 { &__display-images {
flex-shrink: 0;
margin-bottom: 0.5em; margin-bottom: 0.5em;
text-align: center; text-align: center;
padding: 0 $padding-horizontal; 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 { &__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; padding: 0 $padding-horizontal $padding-vertical;
overflow: hidden; // necessary due to iframe scaling via transitions
iframe { iframe {
flex-grow: 1;
border: none; border: none;
width: 100%; min-width: 100%;
} }
} }
@ -172,6 +191,9 @@
.mail-addresses__stripped { .mail-addresses__stripped {
display: inline; display: inline;
} }
.read__sender-address {
display: none;
}
} }
} }
} }

View File

@ -21,6 +21,33 @@
margin-top: 0.5em; 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 { &__subject {
position: relative; position: relative;
margin-top: 1.3em; margin-top: 1.3em;

View File

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

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