mirror of
https://github.com/moparisthebest/mail
synced 2024-10-31 15:25:01 -04:00
Compare commits
211 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
43729833a5 | ||
|
b05aeea342 | ||
|
137c8c7c24 | ||
|
853be194d9 | ||
|
8b36a719c3 | ||
|
8165416c5d | ||
|
b375d81635 | ||
|
e56f8c2c28 | ||
|
ad3691fae9 | ||
|
e0663ab8d8 | ||
|
263b5c13b0 | ||
|
8636ba201b | ||
|
aa24881efc | ||
|
b7b5c1bdf5 | ||
|
59d38f9b14 | ||
|
c44984b2f3 | ||
|
c31c320e83 | ||
|
7f49e691db | ||
|
a346f1612e | ||
|
6b0b71d4ff | ||
|
4683583a0a | ||
|
b038ac2c16 | ||
|
f32863dc54 | ||
|
39d19df187 | ||
|
9bf8c758ec | ||
|
76f770a12b | ||
|
3af376b419 | ||
|
e9a8702b39 | ||
|
25b9141a5f | ||
|
1d0efc02a2 | ||
|
c3362c193d | ||
|
7f0235c9b2 | ||
|
8e0dfacd51 | ||
|
467d001483 | ||
|
e7fb3bcf6d | ||
|
ce740b2109 | ||
|
0bfaba3bd9 | ||
|
e7cbf9ed86 | ||
|
c76a392abf | ||
|
ca8c2d9a4f | ||
|
f287c4cddf | ||
|
59006a98d7 | ||
|
73fcfba2a9 | ||
|
b473d2b7fe | ||
|
4519ab65c6 | ||
|
19bdf0aa49 | ||
|
b8f21ac7eb | ||
|
ff2a7c3e4a | ||
|
7834f79045 | ||
|
b35d993ff1 | ||
|
4e0388b349 | ||
|
add1cd3919 | ||
|
d87449c57f | ||
|
4cf1ef3107 | ||
|
e5e1c118be | ||
|
8e03b6a2ff | ||
|
6963ea33e9 | ||
|
0074e0ca90 | ||
|
9f5daa12b1 | ||
|
44591cc099 | ||
|
ecf16b028b | ||
|
bcaef5c330 | ||
|
9454739818 | ||
|
991f61d271 | ||
|
f62257a595 | ||
|
4f1ead0394 | ||
|
bd910df187 | ||
|
504e8ffd50 | ||
|
7c1d68ec6e | ||
|
4293031f1f | ||
|
44dac729aa | ||
|
55add6a6d3 | ||
|
d36ddcef7f | ||
|
94188be7b3 | ||
|
215a53e2a9 | ||
|
168eaf0086 | ||
|
9f39b67812 | ||
|
7540ffcab5 | ||
|
15ff8c85db | ||
|
4efab0daf0 | ||
|
701eb36b19 | ||
|
b687986980 | ||
|
281e53a887 | ||
|
6216fe2f1a | ||
|
2aa166ac19 | ||
|
4a681a73d2 | ||
|
56bd5222d2 | ||
|
1032a1eb06 | ||
|
e6d109d42d | ||
|
0dc04e659f | ||
|
c8779caef2 | ||
|
898e19e3ea | ||
|
246d19b76e | ||
|
55406cf7e8 | ||
|
b5c917f95a | ||
|
a0378cc0b9 | ||
|
8b42f83b35 | ||
|
9f94467ad1 | ||
|
321b6a9710 | ||
|
cf85fbd2ee | ||
|
9518cb69fa | ||
|
ec7e8cbd68 | ||
|
d0f002bfd1 | ||
|
9841a59a9e | ||
|
1ffcaf5487 | ||
|
150a3c7888 | ||
|
1af3eac566 | ||
|
e437a95baa | ||
|
2828771c2b | ||
|
2148d06d49 | ||
|
dca2f4ead9 | ||
|
8807830122 | ||
|
69ed386765 | ||
|
e6147e1fdc | ||
|
1d64c2dfb0 | ||
|
e5f281c124 | ||
|
06d772159b | ||
|
ff72822e36 | ||
|
09ff51f329 | ||
|
a2442554ad | ||
|
153d0626d2 | ||
|
222b7d35e3 | ||
|
07e0f39b55 | ||
|
73febe287e | ||
|
abbd893438 | ||
|
d67e0531d5 | ||
|
210ab61ba1 | ||
|
b752269c68 | ||
|
d8fb06cb08 | ||
|
88e83b6511 | ||
|
1b8c6b6b8d | ||
|
8295806b1f | ||
|
c30fbe8b6b | ||
|
5a8d7d8324 | ||
|
c9981239c8 | ||
|
c8f13511c1 | ||
|
f41e6e12b9 | ||
|
809de91354 | ||
|
220b8af509 | ||
|
1d57b004d1 | ||
|
518ceec0ef | ||
|
1d4a9414bb | ||
|
0304bbf8fe | ||
|
6ceb877472 | ||
|
1806f78ef3 | ||
|
881b05df91 | ||
|
369ad58134 | ||
|
5f19bbeff0 | ||
|
1c361e9c85 | ||
|
888204e1b9 | ||
|
427dee8214 | ||
|
e089139474 | ||
|
6873fd7f3d | ||
|
9bc2bc7912 | ||
|
9aebecd45f | ||
|
9d68b6475c | ||
|
2c1e1f669e | ||
|
91693c62ad | ||
|
93dc53f7b7 | ||
|
59dc2008a4 | ||
|
10cd2795f3 | ||
|
9c6d618ddc | ||
|
163ad5db79 | ||
|
54d495d8d9 | ||
|
0faa5b3743 | ||
|
2e3e07aa1d | ||
|
3da5a55251 | ||
|
f28c7854c3 | ||
|
57918bbd67 | ||
|
c26a51f83d | ||
|
6938750803 | ||
|
c94c419b38 | ||
|
5e246ee921 | ||
|
443cc3b59b | ||
|
da639b5a69 | ||
|
0d17701ebd | ||
|
5bf0890c02 | ||
|
f8e5ea6d89 | ||
|
7ffb7ca148 | ||
|
5121347640 | ||
|
2c092b0240 | ||
|
2875228359 | ||
|
274c23ea4c | ||
|
4fe3ceaea2 | ||
|
ff587672d9 | ||
|
fef264248d | ||
|
9a9b0d4cea | ||
|
c93eaf17f3 | ||
|
038437595e | ||
|
0446f8219b | ||
|
86653e8700 | ||
|
900294a13d | ||
|
990950bc48 | ||
|
978822ae55 | ||
|
75a382190a | ||
|
6ad3b7402e | ||
|
6a525ae643 | ||
|
55ab661582 | ||
|
7c9e8e6a4e | ||
|
295c781b62 | ||
|
b7072648b7 | ||
|
753cd1a4d7 | ||
|
281f4a94cd | ||
|
c271dc91dc | ||
|
fc6b21e63a | ||
|
2be7beb3a1 | ||
|
d3b54187cb | ||
|
c165ced523 | ||
|
d7a4058644 | ||
|
7d266e6a79 | ||
|
a07ee38fdb |
@ -1,10 +1,10 @@
|
||||
branch-defaults:
|
||||
release/prod:
|
||||
environment: mail-html5-prod
|
||||
environment: mail-prod
|
||||
release/test:
|
||||
environment: mail-html5-test
|
||||
environment: mail-test
|
||||
global:
|
||||
application_name: mail-html5
|
||||
application_name: mail
|
||||
default_ec2_keyname: null
|
||||
default_platform: Node.js
|
||||
default_region: eu-central-1
|
||||
|
@ -46,7 +46,8 @@
|
||||
"Lawnchair",
|
||||
"_",
|
||||
"openpgp",
|
||||
"PhoneNumber"
|
||||
"PhoneNumber",
|
||||
"DOMPurify"
|
||||
],
|
||||
|
||||
"globals": {}
|
||||
|
@ -1,9 +1,7 @@
|
||||
sudo: false
|
||||
language: node_js
|
||||
node_js:
|
||||
- "0.10"
|
||||
- "0.12"
|
||||
before_install:
|
||||
- gem install sass
|
||||
- npm install -g grunt-cli
|
||||
notifications:
|
||||
email:
|
||||
- build@whiteout.io
|
||||
|
193
Gruntfile.js
193
Gruntfile.js
@ -45,8 +45,8 @@ module.exports = function(grunt) {
|
||||
'node_modules/sinon/pkg/sinon.js',
|
||||
'node_modules/browsercrow/src/*.js',
|
||||
'node_modules/browsersmtp/src/*.js',
|
||||
'src/lib/openpgp/openpgp.min.js',
|
||||
'src/lib/openpgp/openpgp.worker.min.js',
|
||||
'node_modules/openpgp/dist/openpgp.min.js',
|
||||
'node_modules/openpgp/dist/openpgp.worker.min.js',
|
||||
'src/lib/forge/forge.min.js',
|
||||
'dist/js/pbkdf2-worker.min.js'
|
||||
],
|
||||
@ -55,8 +55,12 @@ module.exports = function(grunt) {
|
||||
lib: {
|
||||
expand: true,
|
||||
flatten: true,
|
||||
cwd: 'src/lib/',
|
||||
src: ['openpgp/openpgp.min.js', 'openpgp/openpgp.worker.min.js', 'forge/forge.min.js'],
|
||||
cwd: './',
|
||||
src: [
|
||||
'node_modules/openpgp/dist/openpgp.min.js',
|
||||
'node_modules/openpgp/dist/openpgp.worker.min.js',
|
||||
'src/lib/forge/forge.min.js'
|
||||
],
|
||||
dest: 'dist/js/'
|
||||
},
|
||||
font: {
|
||||
@ -93,6 +97,11 @@ module.exports = function(grunt) {
|
||||
'src/css/read-sandbox.css': 'src/sass/read-sandbox.scss',
|
||||
'src/css/all.css': 'src/sass/all.scss'
|
||||
}
|
||||
},
|
||||
styleguide: {
|
||||
files: {
|
||||
'src/css/styleguide.css': 'src/sass/styleguide.scss'
|
||||
}
|
||||
}
|
||||
},
|
||||
autoprefixer: {
|
||||
@ -104,6 +113,11 @@ module.exports = function(grunt) {
|
||||
'src/css/read-sandbox.css': 'src/css/read-sandbox.css',
|
||||
'src/css/all.css': 'src/css/all.css'
|
||||
}
|
||||
},
|
||||
styleguide: {
|
||||
files: {
|
||||
'src/css/styleguide.css': 'src/css/styleguide.css'
|
||||
}
|
||||
}
|
||||
},
|
||||
csso: {
|
||||
@ -115,6 +129,11 @@ module.exports = function(grunt) {
|
||||
'dist/css/read-sandbox.min.css': 'src/css/read-sandbox.css',
|
||||
'dist/css/all.min.css': 'src/css/all.css'
|
||||
}
|
||||
},
|
||||
styleguide: {
|
||||
files: {
|
||||
'dist/styleguide/css/styleguide.min.css': 'src/css/styleguide.css'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -152,6 +171,12 @@ module.exports = function(grunt) {
|
||||
},
|
||||
options: browserifyOpt
|
||||
},
|
||||
compressionWorker: {
|
||||
files: {
|
||||
'dist/js/browserbox-compression-worker.browserified.js': ['node_modules/imap-client/node_modules/browserbox/src/browserbox-compression-worker.js']
|
||||
},
|
||||
options: browserifyOpt
|
||||
},
|
||||
unitTest: {
|
||||
files: {
|
||||
'test/unit/index.browserified.js': [
|
||||
@ -174,6 +199,7 @@ module.exports = function(grunt) {
|
||||
'test/unit/service/newsletter-service-test.js',
|
||||
'test/unit/service/mail-config-service-test.js',
|
||||
'test/unit/service/invitation-dao-test.js',
|
||||
'test/unit/service/publickey-verifier-test.js',
|
||||
'test/unit/email/outbox-bo-test.js',
|
||||
'test/unit/email/email-dao-test.js',
|
||||
'test/unit/email/account-test.js',
|
||||
@ -185,10 +211,12 @@ module.exports = function(grunt) {
|
||||
'test/unit/controller/login/login-initial-ctrl-test.js',
|
||||
'test/unit/controller/login/login-new-device-ctrl-test.js',
|
||||
'test/unit/controller/login/login-privatekey-download-ctrl-test.js',
|
||||
'test/unit/controller/login/login-privatekey-upload-ctrl-test.js',
|
||||
'test/unit/controller/login/login-verify-public-key-ctrl-test.js',
|
||||
'test/unit/controller/login/login-set-credentials-ctrl-test.js',
|
||||
'test/unit/controller/login/login-ctrl-test.js',
|
||||
'test/unit/controller/app/dialog-ctrl-test.js',
|
||||
'test/unit/controller/app/privatekey-upload-ctrl-test.js',
|
||||
'test/unit/controller/app/publickey-import-ctrl-test.js',
|
||||
'test/unit/controller/app/account-ctrl-test.js',
|
||||
'test/unit/controller/app/set-passphrase-ctrl-test.js',
|
||||
'test/unit/controller/app/contacts-ctrl-test.js',
|
||||
@ -205,7 +233,8 @@ module.exports = function(grunt) {
|
||||
files: {
|
||||
'test/integration/index.browserified.js': [
|
||||
'test/main.js',
|
||||
'test/integration/email-dao-test.js'
|
||||
'test/integration/email-dao-test.js',
|
||||
'test/integration/publickey-verifier-test.js'
|
||||
]
|
||||
},
|
||||
options: browserifyOpt
|
||||
@ -260,6 +289,7 @@ module.exports = function(grunt) {
|
||||
'src/lib/angular/angular-animate.js',
|
||||
'src/lib/ngtagsinput/ng-tags-input.min.js',
|
||||
'node_modules/ng-infinite-scroll/build/ng-infinite-scroll.min.js',
|
||||
'node_modules/iframe-resizer/js/iframeResizer.min.js',
|
||||
'src/lib/fastclick/fastclick.js',
|
||||
'src/lib/lawnchair/lawnchair-git.js',
|
||||
'src/lib/lawnchair/lawnchair-adapter-webkit-sqlite-git.js',
|
||||
@ -277,7 +307,8 @@ module.exports = function(grunt) {
|
||||
},
|
||||
readSandbox: {
|
||||
src: [
|
||||
'node_modules/dompurify/purify.js',
|
||||
'node_modules/dompurify/src/purify.js',
|
||||
'node_modules/iframe-resizer/js/iframeResizer.contentWindow.min.js',
|
||||
'src/js/controller/app/read-sandbox.js'
|
||||
],
|
||||
dest: 'dist/js/read-sandbox.min.js'
|
||||
@ -294,6 +325,10 @@ module.exports = function(grunt) {
|
||||
src: ['dist/js/tcp-socket-tls-worker.browserified.js'],
|
||||
dest: 'dist/js/tcp-socket-tls-worker.min.js'
|
||||
},
|
||||
compressionWorker: {
|
||||
src: ['dist/js/browserbox-compression-worker.browserified.js'],
|
||||
dest: 'dist/js/browserbox-compression-worker.min.js'
|
||||
},
|
||||
unitTest: {
|
||||
src: [
|
||||
'src/lib/underscore/underscore.js',
|
||||
@ -377,6 +412,15 @@ module.exports = function(grunt) {
|
||||
sourceMapName: 'dist/js/tcp-socket-tls-worker.min.js.map'
|
||||
}
|
||||
},
|
||||
compressionWorker: {
|
||||
files: {
|
||||
'dist/js/browserbox-compression-worker.min.js': ['dist/js/browserbox-compression-worker.min.js']
|
||||
},
|
||||
options: {
|
||||
sourceMap: true,
|
||||
sourceMapName: 'dist/js/browserbox-compression-worker.min.js.map'
|
||||
}
|
||||
},
|
||||
options: {
|
||||
banner: '/*! Copyright © <%= grunt.template.today("yyyy") %>, Whiteout Networks GmbH.*/\n'
|
||||
}
|
||||
@ -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
|
||||
|
||||
connect: {
|
||||
@ -466,10 +534,14 @@ module.exports = function(grunt) {
|
||||
watch: {
|
||||
css: {
|
||||
files: ['src/sass/**/*.scss'],
|
||||
tasks: ['dist-css', 'manifest']
|
||||
tasks: ['dist-css', 'offline-cache', 'dist-styleguide']
|
||||
},
|
||||
styleguide: {
|
||||
files: ['src/styleguide/**/*.hbs', 'src/styleguide/**/*.js'],
|
||||
tasks: ['dist-styleguide']
|
||||
},
|
||||
jsApp: {
|
||||
files: ['src/js/**/*.js', 'src/**/*.html'],
|
||||
files: ['src/js/**/*.js', 'src/*.html', 'src/tpl/**/*.html'],
|
||||
tasks: ['dist-js-app']
|
||||
},
|
||||
jsUnitTest: {
|
||||
@ -482,15 +554,15 @@ module.exports = function(grunt) {
|
||||
},
|
||||
icons: {
|
||||
files: ['src/index.html', 'src/img/icons/*.svg', '!src/img/icons/all.svg'],
|
||||
tasks: ['svgmin', 'svgstore', 'string-replace', 'manifest']
|
||||
tasks: ['svgmin', 'svgstore', 'string-replace', 'dist-styleguide', 'offline-cache']
|
||||
},
|
||||
lib: {
|
||||
files: ['src/lib/**/*.js'],
|
||||
tasks: ['copy:lib', 'manifest']
|
||||
tasks: ['copy:lib', 'offline-cache']
|
||||
},
|
||||
app: {
|
||||
files: ['src/*.js', 'src/**/*.html', 'src/**/*.json', 'src/manifest.*', 'src/img/**/*', 'src/font/**/*'],
|
||||
tasks: ['copy:app', 'copy:tpl', 'copy:img', 'copy:font', 'manifest-dev', 'manifest']
|
||||
files: ['src/*.js', 'src/*.html', 'src/tpl/**/*.html', 'src/**/*.json', 'src/manifest.*', 'src/img/**/*', 'src/font/**/*'],
|
||||
tasks: ['copy:app', 'copy:tpl', 'copy:img', 'copy:font', 'manifest-dev', 'offline-cache']
|
||||
}
|
||||
},
|
||||
|
||||
@ -509,6 +581,15 @@ module.exports = function(grunt) {
|
||||
}
|
||||
},
|
||||
|
||||
// Offline caching
|
||||
|
||||
swPrecache: {
|
||||
prod: {
|
||||
handleFetch: true,
|
||||
rootDir: 'dist'
|
||||
}
|
||||
},
|
||||
|
||||
manifest: {
|
||||
generate: {
|
||||
options: {
|
||||
@ -521,6 +602,9 @@ module.exports = function(grunt) {
|
||||
'manifest.webapp',
|
||||
'manifest.mobile.json',
|
||||
'background.js',
|
||||
'service-worker.js',
|
||||
'styleguide/css/styleguide.min.css',
|
||||
'styleguide/index.html',
|
||||
'js/app.templates.js',
|
||||
'js/app.js.map',
|
||||
'js/app.min.js.map',
|
||||
@ -534,6 +618,8 @@ module.exports = function(grunt) {
|
||||
'js/mailreader-parser-worker.min.js.map',
|
||||
'js/tcp-socket-tls-worker.browserified.js',
|
||||
'js/tcp-socket-tls-worker.min.js.map',
|
||||
'js/browserbox-compression-worker.browserified.js',
|
||||
'js/browserbox-compression-worker.min.js.map',
|
||||
'img/icon-100-ios.png',
|
||||
'img/icon-114-ios.png',
|
||||
'img/icon-120-ios.png',
|
||||
@ -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)
|
||||
grunt.loadNpmTasks('grunt-browserify');
|
||||
grunt.loadNpmTasks('grunt-contrib-concat');
|
||||
@ -600,23 +739,20 @@ module.exports = function(grunt) {
|
||||
grunt.loadNpmTasks('grunt-svgstore');
|
||||
grunt.loadNpmTasks('grunt-shell');
|
||||
grunt.loadNpmTasks('grunt-angular-templates');
|
||||
grunt.loadNpmTasks('assemble');
|
||||
|
||||
// Build tasks
|
||||
grunt.registerTask('dist-css', ['sass', 'autoprefixer', 'csso']);
|
||||
grunt.registerTask('dist-css', ['sass:dist', 'autoprefixer:dist', 'csso:dist']);
|
||||
grunt.registerTask('dist-js', ['browserify', 'exorcise', 'ngtemplates', 'concat', 'uglify']);
|
||||
grunt.registerTask('dist-js-app', [
|
||||
'browserify:app',
|
||||
'browserify:pbkdf2Worker',
|
||||
'browserify:mailreaderWorker',
|
||||
'browserify:tlsWorker',
|
||||
'exorcise:app',
|
||||
'ngtemplates',
|
||||
'concat:app',
|
||||
'concat:readSandbox',
|
||||
'concat:pbkdf2Worker',
|
||||
'concat:mailreaderWorker',
|
||||
'concat:tlsWorker',
|
||||
'manifest'
|
||||
'offline-cache'
|
||||
]);
|
||||
grunt.registerTask('dist-js-unitTest', [
|
||||
'browserify:unitTest',
|
||||
@ -630,7 +766,11 @@ module.exports = function(grunt) {
|
||||
]);
|
||||
grunt.registerTask('dist-copy', ['copy']);
|
||||
grunt.registerTask('dist-assets', ['svgmin', 'svgstore', 'string-replace']);
|
||||
grunt.registerTask('dist', ['clean:dist', 'shell', 'dist-css', 'dist-js', 'dist-assets', 'dist-copy', 'manifest']);
|
||||
grunt.registerTask('dist-styleguide', ['sass:styleguide', 'autoprefixer:styleguide', 'csso:styleguide', 'assemble:styleguide']);
|
||||
// generate styleguide after manifest to forward version number to styleguide
|
||||
grunt.registerTask('dist', ['clean:dist', 'shell', 'dist-css', 'dist-js', 'dist-assets', 'dist-copy', 'manifest', 'dist-styleguide']);
|
||||
|
||||
grunt.registerTask('offline-cache', ['manifest', 'swPrecache:prod']);
|
||||
|
||||
// Test/Dev tasks
|
||||
grunt.registerTask('dev', ['connect:dev']);
|
||||
@ -667,8 +807,7 @@ module.exports = function(grunt) {
|
||||
patchManifest({
|
||||
version: version,
|
||||
deleteKey: true,
|
||||
keyServer: 'https://keys.whiteout.io/',
|
||||
keychainServer: 'https://keychain.whiteout.io/'
|
||||
keyServer: 'https://keys.whiteout.io/'
|
||||
});
|
||||
});
|
||||
|
||||
@ -690,10 +829,6 @@ module.exports = function(grunt) {
|
||||
var ksIndex = manifest.permissions.indexOf('https://keys-test.whiteout.io/');
|
||||
manifest.permissions[ksIndex] = options.keyServer;
|
||||
}
|
||||
if (options.keychainServer) {
|
||||
var kcsIndex = manifest.permissions.indexOf('https://keychain-test.whiteout.io/');
|
||||
manifest.permissions[kcsIndex] = options.keychainServer;
|
||||
}
|
||||
if (options.deleteKey) {
|
||||
delete manifest.key;
|
||||
}
|
||||
@ -701,9 +836,9 @@ module.exports = function(grunt) {
|
||||
fs.writeFileSync(path, JSON.stringify(manifest, null, 2));
|
||||
}
|
||||
|
||||
grunt.registerTask('release-dev', ['dist', 'manifest-dev', 'compress']);
|
||||
grunt.registerTask('release-test', ['dist', 'manifest-test', 'clean:release', 'compress']);
|
||||
grunt.registerTask('release-prod', ['dist', 'manifest-prod', 'clean:release', 'compress']);
|
||||
grunt.registerTask('release-dev', ['dist', 'manifest-dev', 'swPrecache:prod', 'compress']);
|
||||
grunt.registerTask('release-test', ['dist', 'manifest-test', 'clean:release', 'swPrecache:prod', 'compress']);
|
||||
grunt.registerTask('release-prod', ['dist', 'manifest-prod', 'clean:release', 'swPrecache:prod', 'compress']);
|
||||
grunt.registerTask('default', ['release-dev']);
|
||||
|
||||
};
|
20
README.md
20
README.md
@ -1,4 +1,4 @@
|
||||
Whiteout Mail [![Build Status](https://travis-ci.org/whiteout-io/mail-html5.svg?branch=master)](https://travis-ci.org/whiteout-io/mail-html5) [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/whiteout-io/mail-html5?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
Whiteout Mail [![Build Status](https://travis-ci.org/whiteout-io/mail.svg?branch=master)](https://travis-ci.org/whiteout-io/mail)
|
||||
==========
|
||||
|
||||
Whiteout Mail is an easy to use email client with integrated OpenPGP encryption written in pure JavaScript. Download the official version under [whiteout.io](https://whiteout.io/#product).
|
||||
@ -7,15 +7,17 @@ Whiteout Mail is an easy to use email client with integrated OpenPGP encryption
|
||||
|
||||
### Features
|
||||
|
||||
You can read about product features and our future roadmap in our [FAQ](https://github.com/whiteout-io/mail-html5/wiki/FAQ).
|
||||
You can read about product features and our future roadmap in our [FAQ](https://github.com/whiteout-io/mail/wiki/FAQ).
|
||||
|
||||
### Privacy and Security
|
||||
|
||||
We take the privacy of your data very seriously. Here are some of the technical details:
|
||||
|
||||
* The code has undergone a [full security audit](https://blog.whiteout.io/2015/06/11/whiteout-mail-1-0-and-security-audit-by-cure53/) by [Cure53](https://cure53.de).
|
||||
|
||||
* Messages are [encrypted end-to-end ](http://en.wikipedia.org/wiki/End-to-end_encryption) using the [OpenPGP](http://en.wikipedia.org/wiki/Pretty_Good_Privacy) standard. This means that only you and the recipient can read your mail. Your messages and private PGP key are stored only on your computer (in IndexedDB).
|
||||
|
||||
* Users have the option to use [encrypted private key sync](https://blog.whiteout.io/2014/07/07/secure-pgp-key-sync-a-proposal/) if they want to use Whiteout on multiple devices.
|
||||
* Users have the option to use [encrypted private key sync](https://github.com/whiteout-io/mail/wiki/Secure-OpenPGP-Key-Pair-Synchronization-via-IMAP) if they want to use Whiteout on multiple devices.
|
||||
|
||||
* [Content Security Policy (CSP)](http://www.html5rocks.com/en/tutorials/security/content-security-policy/) is enforced to prevent injection attacks.
|
||||
|
||||
@ -25,19 +27,23 @@ We take the privacy of your data very seriously. Here are some of the technical
|
||||
|
||||
* Like most native email clients, whiteout mail uses raw [TCP sockets](http://developer.chrome.com/apps/socket.html) to communicate directly with your mail server via IMAP/SMTP. TLS is used to protect your password and message data in transit.
|
||||
|
||||
* The app is deployed as a signed [Chrome Packaged App](https://developer.chrome.com/apps/about_apps.html) with [auditable static versions](https://github.com/whiteout-io/mail-html5/releases) in order to prevent [problems with host-based security](https://blog.whiteout.io/2014/04/13/heartbleed-and-javascript-crypto/).
|
||||
* The app is deployed as a signed [Chrome Packaged App](https://developer.chrome.com/apps/about_apps.html) with [auditable static versions](https://github.com/whiteout-io/mail/releases) in order to prevent [problems with host-based security](https://blog.whiteout.io/2014/04/13/heartbleed-and-javascript-crypto/).
|
||||
|
||||
* The app can also be used from any modern web browser in environments where installing an app is not possible (e.g. a locked down corporate desktop). The IMAP/SMTP TLS sessions are still terminated in the user's browser using JS crypto ([Forge](https://github.com/digitalbazaar/forge)), but the encrypted TLS payload is proxied via [socket.io](http://socket.io/), due to the lack of raw sockets in the browser. **Please keep in mind that this mode of operation is not as secure as using the signed packaged app, since users must trust the webserver to deliver the correct code. This mode will still protect user against passive attacks like wiretapping (since PGP and TLS are still applied in the user's browser), but not against active attacks from the webserver. So it's best to decide which threat model applies to you.**
|
||||
|
||||
### Architecture
|
||||
|
||||
![client architecture](https://whiteout.io/img/app_layers.png)
|
||||
|
||||
### Reporting bugs and feature requests
|
||||
|
||||
* We will launch a bug bounty program later on for independent security researchers. If you find any security vulnerabilities, don't hesitate to contact us [security@whiteout.io](mailto:security@whiteout.io).
|
||||
|
||||
* You can also just create an [issue](https://github.com/whiteout-io/mail-html5/issues) on GitHub if you're missing a feature or just want to give us feedback. It would be much appreciated!
|
||||
* You can also just create an [issue](https://github.com/whiteout-io/mail/issues) on GitHub if you're missing a feature or just want to give us feedback. It would be much appreciated!
|
||||
|
||||
### Testing
|
||||
|
||||
You can download a prebuilt bundle under [releases](https://github.com/whiteout-io/mail-html5/releases) or build your own from source (requires [node.js](http://nodejs.org/download/), [grunt](http://gruntjs.com/getting-started#installing-the-cli) and [sass](http://sass-lang.com/install)):
|
||||
You can download a prebuilt bundle under [releases](https://github.com/whiteout-io/mail/releases) or build your own from source (requires [node.js](http://nodejs.org/download/), [grunt](http://gruntjs.com/getting-started#installing-the-cli) and [sass](http://sass-lang.com/install)):
|
||||
|
||||
npm install && npm test
|
||||
|
||||
@ -65,7 +71,7 @@ The App can be used either as a Chrome Packaged App or just by hosting it on you
|
||||
|
||||
Clone the git repository
|
||||
|
||||
git clone git@github.com:whiteout-io/mail-html5.git
|
||||
git clone https://github.com/whiteout-io/mail.git
|
||||
|
||||
Build and generate the `dist/` directory:
|
||||
|
||||
|
38
package.json
38
package.json
@ -3,21 +3,6 @@
|
||||
"description": "Mail App with integrated OpenPGP encryption.",
|
||||
"author": "Whiteout Networks",
|
||||
"homepage": "https://whiteout.io",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/whiteout-io/mail-html5.git"
|
||||
},
|
||||
"keywords": [
|
||||
"email",
|
||||
"mail",
|
||||
"client",
|
||||
"app",
|
||||
"openpgp",
|
||||
"pgp",
|
||||
"gpg",
|
||||
"imap",
|
||||
"smtp"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
},
|
||||
@ -34,16 +19,18 @@
|
||||
"socket.io": "^1.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"assemble": "~0.4.42",
|
||||
"axe-logger": "~0.0.2",
|
||||
"browsercrow": "https://github.com/whiteout-io/browsercrow/tarball/master",
|
||||
"browsersmtp": "https://github.com/whiteout-io/browsersmtp/tarball/master",
|
||||
"chai": "~1.9.2",
|
||||
"crypto-lib": "~0.2.1",
|
||||
"dompurify": "~0.4.2",
|
||||
"dompurify": "~0.7.3",
|
||||
"grunt": "~0.4.1",
|
||||
"grunt-angular-templates": "~0.5.7",
|
||||
"grunt-autoprefixer": "~0.7.2",
|
||||
"grunt-browserify": "^3.0.1",
|
||||
"grunt-browserify": "3.7.0",
|
||||
"insert-module-globals": "6.5.0",
|
||||
"grunt-contrib-clean": "~0.5.0",
|
||||
"grunt-contrib-compress": "~0.5.2",
|
||||
"grunt-contrib-concat": "^0.5.0",
|
||||
@ -56,21 +43,26 @@
|
||||
"grunt-csso": "~0.6.1",
|
||||
"grunt-exorcise": "^0.2.0",
|
||||
"grunt-manifest": "^0.4.0",
|
||||
"grunt-mocha-phantomjs": "^0.6.0",
|
||||
"grunt-mocha-phantomjs": "^0.7.0",
|
||||
"grunt-shell": "~1.1.1",
|
||||
"grunt-string-replace": "~1.0.0",
|
||||
"grunt-svgmin": "~1.0.0",
|
||||
"grunt-svgstore": "~0.3.4",
|
||||
"imap-client": "~0.9.0",
|
||||
"handlebars-helper-compose": "~0.2.12",
|
||||
"iframe-resizer": "^2.8.3",
|
||||
"imap-client": "~0.14.2",
|
||||
"jquery": "~2.1.1",
|
||||
"mailbuild": "^0.3.7",
|
||||
"mailreader": "~0.4.0",
|
||||
"mocha": "^1.21.4",
|
||||
"ng-infinite-scroll": "~1.1.2",
|
||||
"pgpbuilder": "~0.5.0",
|
||||
"pgpmailer": "~0.7.0",
|
||||
"openpgp": "^1.0.0",
|
||||
"pgpbuilder": "~0.6.0",
|
||||
"pgpmailer": "~0.9.1",
|
||||
"sinon": "~1.7.3",
|
||||
"tcp-socket": "~0.4.0",
|
||||
"sw-precache": "^1.3.0",
|
||||
"tcp-socket": "~0.5.0",
|
||||
"time-grunt": "^1.0.0",
|
||||
"wo-smtpclient": "~0.5.0"
|
||||
"wo-smtpclient": "~0.6.0"
|
||||
}
|
||||
}
|
||||
|
@ -11,14 +11,18 @@ fi
|
||||
|
||||
# switch branch
|
||||
git checkout $2
|
||||
git branch release/$1
|
||||
git checkout release/$1
|
||||
git branch -D release/$1
|
||||
git checkout -b release/$1
|
||||
git merge $2 --no-edit
|
||||
|
||||
# abort if tests fail
|
||||
set -e
|
||||
|
||||
# build and test
|
||||
rm -rf node_modules/
|
||||
npm cache clear
|
||||
npm install
|
||||
npm test
|
||||
grunt release-$1 --release=$3
|
||||
|
||||
# install only production dependencies
|
||||
|
@ -48,10 +48,23 @@ cp ../../../src/img/Default* "platforms/ios/$PROJNAME/Resources/splash"
|
||||
|
||||
# fixing missing/wrong icons
|
||||
echo "Fixing wrong/missing iOS icons"
|
||||
cp ../../../src/img/icon-60-ios.png "platforms/ios/$PROJNAME/Resources/icons/icon-60.png"
|
||||
cp ../../../src/img/icon-180-ios.png "platforms/ios/$PROJNAME/Resources/icons/icon-60@3x.png"
|
||||
cp ../../../src/img/icon-87-ios.png "platforms/ios/$PROJNAME/Resources/icons/icon-small@3x.png"
|
||||
cp ../../../src/img/icon-40-ios.png "platforms/ios/$PROJNAME/Resources/icons/icon-40.png"
|
||||
cp ../../../src/img/icon-80-ios.png "platforms/ios/$PROJNAME/Resources/icons/icon-40@2x.png"
|
||||
cp ../../../src/img/icon-120-ios.png "platforms/ios/$PROJNAME/Resources/icons/icon-40@3x.png"
|
||||
cp ../../../src/img/icon-50-ios.png "platforms/ios/$PROJNAME/Resources/icons/icon-50.png"
|
||||
cp ../../../src/img/icon-100-ios.png "platforms/ios/$PROJNAME/Resources/icons/icon-50@2x.png"
|
||||
cp ../../../src/img/icon-60-ios.png "platforms/ios/$PROJNAME/Resources/icons/icon-60.png"
|
||||
cp ../../../src/img/icon-120-ios.png "platforms/ios/$PROJNAME/Resources/icons/icon-60@2x.png"
|
||||
cp ../../../src/img/icon-180-ios.png "platforms/ios/$PROJNAME/Resources/icons/icon-60@3x.png"
|
||||
cp ../../../src/img/icon-72-ios.png "platforms/ios/$PROJNAME/Resources/icons/icon-72.png"
|
||||
cp ../../../src/img/icon-144-ios.png "platforms/ios/$PROJNAME/Resources/icons/icon-72@2x.png"
|
||||
cp ../../../src/img/icon-76-ios.png "platforms/ios/$PROJNAME/Resources/icons/icon-76.png"
|
||||
cp ../../../src/img/icon-152-ios.png "platforms/ios/$PROJNAME/Resources/icons/icon-76@2x.png"
|
||||
cp ../../../src/img/icon-29-ios.png "platforms/ios/$PROJNAME/Resources/icons/icon-small.png"
|
||||
cp ../../../src/img/icon-58-ios.png "platforms/ios/$PROJNAME/Resources/icons/icon-small@2x.png"
|
||||
cp ../../../src/img/icon-87-ios.png "platforms/ios/$PROJNAME/Resources/icons/icon-small@3x.png"
|
||||
cp ../../../src/img/icon-57-ios.png "platforms/ios/$PROJNAME/Resources/icons/icon.png"
|
||||
cp ../../../src/img/icon-114-ios.png "platforms/ios/$PROJNAME/Resources/icons/icon@2x.png"
|
||||
|
||||
# print reminder for manual work in xcode
|
||||
echo ""
|
||||
|
71
server.js
71
server.js
@ -75,19 +75,29 @@ var development = (process.argv[2] === '--dev');
|
||||
|
||||
// set HTTP headers
|
||||
app.use(function(req, res, next) {
|
||||
// prevent rendering website in foreign iframe (Clickjacking)
|
||||
res.set('X-Frame-Options', 'DENY');
|
||||
// HSTS
|
||||
res.set('Strict-Transport-Security', 'max-age=16070400; includeSubDomains');
|
||||
// CSP
|
||||
var iframe = development ? "http://" + req.hostname + ":" + config.server.port : "https://" + req.hostname; // allow iframe to load assets
|
||||
res.set('Content-Security-Policy', "default-src 'self' " + iframe + "; object-src 'none'; connect-src *; style-src 'self' 'unsafe-inline' " + iframe + "; img-src *");
|
||||
var csp = "default-src 'self' " + iframe + "; object-src 'none'; connect-src *; style-src 'self' 'unsafe-inline' " + iframe + "; img-src *";
|
||||
res.set('Content-Security-Policy', csp);
|
||||
res.set('X-Content-Security-Policy', csp);
|
||||
// set Cache-control Header (for AppCache)
|
||||
res.set('Cache-control', 'public, max-age=0');
|
||||
next();
|
||||
});
|
||||
app.use('/service-worker.js', noCache);
|
||||
app.use('/appcache.manifest', noCache);
|
||||
|
||||
app.use('/appcache.manifest', function(req, res, next) {
|
||||
function noCache(req, res, next) {
|
||||
res.set('Cache-control', 'no-cache');
|
||||
next();
|
||||
}
|
||||
app.use('/tpl/read-sandbox.html', function(req, res, next) {
|
||||
res.set('X-Frame-Options', 'SAMEORIGIN');
|
||||
next();
|
||||
});
|
||||
|
||||
// redirect all http traffic to https
|
||||
@ -109,60 +119,50 @@ app.use(express.static(__dirname + '/dist'));
|
||||
// Socket.io proxy
|
||||
//
|
||||
|
||||
// TODO:test origin constraint
|
||||
//io.origins(config.server.inboundOrigins.join(' '));
|
||||
|
||||
io.on('connection', function(socket) {
|
||||
|
||||
log.info('io', 'New connection [%s]', socket.conn.id);
|
||||
|
||||
var idCounter = 0;
|
||||
log.info('io', 'New connection [%s] from %s', socket.conn.id, socket.conn.remoteAddress);
|
||||
|
||||
socket.on('open', function(data, fn) {
|
||||
var socketId = ++idCounter;
|
||||
var tcp;
|
||||
|
||||
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();
|
||||
return;
|
||||
}
|
||||
|
||||
log.verbose('io', 'Open request to %s:%s [%s:%s]', data.host, data.port, socket.conn.id, socketId);
|
||||
|
||||
tcp = net.connect(data.port, data.host, function() {
|
||||
log.verbose('io', 'Opened tcp connection 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() {
|
||||
log.verbose('io', 'Opened tcp connection to %s:%s [%s]', data.host, data.port, socket.conn.id);
|
||||
|
||||
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);
|
||||
socket.emit('data-' + socketId, chunk);
|
||||
log.silly('io', 'Received %s bytes from %s:%s [%s]', chunk.length, data.host, data.port, socket.conn.id);
|
||||
socket.emit('data', chunk);
|
||||
});
|
||||
|
||||
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);
|
||||
socket.emit('error-' + socketId, err.message);
|
||||
log.verbose('io', 'Error for %s:%s [%s]: %s', data.host, data.port, socket.conn.id, err.message);
|
||||
socket.emit('error', err.message);
|
||||
});
|
||||
|
||||
tcp.on('end', function() {
|
||||
socket.emit('end-' + socketId);
|
||||
socket.emit('end');
|
||||
});
|
||||
|
||||
tcp.on('close', function() {
|
||||
log.verbose('io', 'Closed tcp connection to %s:%s [%s:%s]', data.host, data.port, socket.conn.id, socketId);
|
||||
socket.emit('close-' + socketId);
|
||||
log.verbose('io', 'Closed tcp connection to %s:%s [%s]', data.host, data.port, socket.conn.id);
|
||||
socket.emit('close');
|
||||
|
||||
socket.removeAllListeners('data-' + socketId);
|
||||
socket.removeAllListeners('end-' + socketId);
|
||||
socket.removeAllListeners('data');
|
||||
socket.removeAllListeners('end');
|
||||
});
|
||||
|
||||
socket.on('data-' + socketId, function(chunk, fn) {
|
||||
socket.on('data', function(chunk, fn) {
|
||||
if (!chunk || !chunk.length) {
|
||||
if (typeof fn === 'function') {
|
||||
fn();
|
||||
}
|
||||
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() {
|
||||
if (typeof fn === 'function') {
|
||||
fn();
|
||||
@ -170,24 +170,21 @@ io.on('connection', function(socket) {
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('end-' + socketId, function() {
|
||||
log.verbose('io', 'Received request to close connection to %s:%s [%s:%s]', data.host, data.port, socket.conn.id, socketId);
|
||||
socket.on('end', function() {
|
||||
log.verbose('io', 'Received request to close connection to %s:%s [%s]', data.host, data.port, socket.conn.id);
|
||||
tcp.end();
|
||||
});
|
||||
|
||||
if (typeof fn === 'function') {
|
||||
fn(socketId);
|
||||
fn(os.hostname());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
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.on('hostname', function(fn) {
|
||||
fn(os.hostname());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
12
src/img/icons/signature-invalid.svg
Executable file
12
src/img/icons/signature-invalid.svg
Executable 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 |
12
src/img/icons/signature-verified.svg
Executable file
12
src/img/icons/signature-verified.svg
Executable 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 |
@ -4,9 +4,6 @@
|
||||
<meta charset="utf-8">
|
||||
<title>Whiteout Mail</title>
|
||||
|
||||
<!-- Theses CSP rules are used as a fallback in runtimes such as Cordova -->
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self' chrome-extension: file: gap:; object-src 'none'; script-src 'self' 'unsafe-eval' chrome-extension: file: gap:; connect-src *; style-src 'self' 'unsafe-inline' chrome-extension: file: gap:; img-src *">
|
||||
|
||||
<!-- iOS homescreen link -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<!-- iOS iPad icon (retina) -->
|
||||
|
@ -12,11 +12,24 @@ module.exports = appCfg;
|
||||
* Global app configurations
|
||||
*/
|
||||
appCfg.config = {
|
||||
cloudUrl: 'https://keys.whiteout.io',
|
||||
privkeyServerUrl: 'https://keychain.whiteout.io',
|
||||
pgpComment: 'Whiteout Mail - https://whiteout.io',
|
||||
keyServerUrl: 'https://keys.whiteout.io',
|
||||
hkpUrl: 'http://keyserver.ubuntu.com',
|
||||
adminUrl: 'https://admin-node.whiteout.io',
|
||||
settingsUrl: 'https://settings.whiteout.io/autodiscovery/',
|
||||
wmailDomain: 'wmail.io',
|
||||
mailServer: {
|
||||
domain: 'wmail.io',
|
||||
imap: {
|
||||
hostname: 'imap.wmail.io',
|
||||
port: 993,
|
||||
secure: true
|
||||
},
|
||||
smtp: {
|
||||
hostname: 'smtp.wmail.io',
|
||||
port: 465,
|
||||
secure: true
|
||||
}
|
||||
},
|
||||
oauthDomains: [/\.gmail\.com$/, /\.googlemail\.com$/],
|
||||
ignoreUploadOnSentDomains: [/\.gmail\.com$/, /\.googlemail\.com$/],
|
||||
serverPrivateKeyId: 'EE342F0DDBB0F3BE',
|
||||
@ -29,7 +42,7 @@ appCfg.config = {
|
||||
iconPath: '/img/icon-128-chrome.png',
|
||||
verificationUrl: '/verify/',
|
||||
verificationUuidLength: 36,
|
||||
dbVersion: 5,
|
||||
dbVersion: 6,
|
||||
appVersion: undefined,
|
||||
outboxMailboxPath: 'OUTBOX',
|
||||
outboxMailboxName: 'Outbox',
|
||||
@ -55,9 +68,7 @@ function setConfigParams(manifest) {
|
||||
}
|
||||
|
||||
// get key server base url
|
||||
cfg.cloudUrl = getUrl('https://keys');
|
||||
// get keychain server base url
|
||||
cfg.privkeyServerUrl = getUrl('https://keychain');
|
||||
cfg.keyServerUrl = getUrl('https://keys');
|
||||
// get the app version
|
||||
cfg.appVersion = manifest.version;
|
||||
}
|
||||
@ -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',
|
||||
bugReportTitle: 'Report a bug',
|
||||
bugReportSubject: '[Bug] I want to report a bug',
|
||||
bugReportBody: 'Steps to reproduce\n1. \n2. \n3. \n\nWhat happens?\n\n\nWhat do you expect to happen instead?\n\n\n\n== PLEASE DONT PUT ANY KEYS HERE! ==\n\n\n## Log\n\nBelow is the log. It includes your interactions with your email provider in an anonymized way from the point where you started the app for the last time. Any information provided by you will be used for the porpose of locating and fixing the bug you reported. It will be deleted subsequently. However, you can edit this log and/or remove log data in the event that something would show up.\n\nUser-Agent: {0}\nVersion: {1}\n\n',
|
||||
bugReportBody: 'Steps to reproduce\n1. \n2. \n3. \n\nWhat happens?\n\n\nWhat do you expect to happen instead?\n\n\n\n== PLEASE DONT PUT ANY KEYS HERE! ==\n\n\n## Log\n\nBelow is the log. It includes your interactions with your email provider from the point where you started the app for the last time. Login data and email content has been stripped. Any information provided by you will be used for the purpose of locating and fixing the bug you reported. It will be deleted subsequently. However, you can edit this log and/or remove log data in the event that something would show up.\n\nUser-Agent: {0}\nVersion: {1}\n\n',
|
||||
supportAddress: 'mail.support@whiteout.io',
|
||||
connDocOffline: 'It appears that you are offline. Please retry when you are online.',
|
||||
connDocTlsWrongCert: 'A connection to {0} was rejected because the TLS certificate is invalid. Please have a look at the FAQ for information on how to fix this error.',
|
||||
@ -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.',
|
||||
connDocGenericError: 'There was an error connecting to {0}: {1}',
|
||||
logoutTitle: 'Logout',
|
||||
logoutMessage: 'Are you sure you want to logout?'
|
||||
logoutMessage: 'Are you sure you want to log out? Please back up your encryption key before proceeding!',
|
||||
removePreAuthAccountTitle: 'Remove account',
|
||||
removePreAuthAccountMessage: 'Are you sure you want to remove your account from this device?'
|
||||
};
|
@ -1,22 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
//
|
||||
// AppCache
|
||||
//
|
||||
|
||||
if (typeof window.applicationCache !== 'undefined') {
|
||||
window.onload = function() {
|
||||
// Check if a new AppCache is available on page load.
|
||||
window.applicationCache.onupdateready = function() {
|
||||
if (window.applicationCache.status === window.applicationCache.UPDATEREADY) {
|
||||
// Browser downloaded a new app cache
|
||||
if (window.confirm('A new version of Whiteout Mail is available. Restart the app to update?')) {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
// use service-worker or app-cache for offline caching
|
||||
require('./offline-cache');
|
||||
|
||||
//
|
||||
// Angular app config
|
||||
@ -68,6 +53,14 @@ app.config(function($routeProvider, $animateProvider) {
|
||||
templateUrl: 'tpl/login-set-credentials.html',
|
||||
controller: require('./controller/login/login-set-credentials')
|
||||
});
|
||||
$routeProvider.when('/login-privatekey-upload', {
|
||||
templateUrl: 'tpl/login-privatekey-upload.html',
|
||||
controller: require('./controller/login/login-privatekey-upload')
|
||||
});
|
||||
$routeProvider.when('/login-verify-public-key', {
|
||||
templateUrl: 'tpl/login-verify-public-key.html',
|
||||
controller: require('./controller/login/login-verify-public-key')
|
||||
});
|
||||
$routeProvider.when('/login-existing', {
|
||||
templateUrl: 'tpl/login-existing.html',
|
||||
controller: require('./controller/login/login-existing')
|
||||
@ -110,7 +103,7 @@ app.controller('WriteCtrl', require('./controller/app/write'));
|
||||
app.controller('MailListCtrl', require('./controller/app/mail-list'));
|
||||
app.controller('AccountCtrl', require('./controller/app/account'));
|
||||
app.controller('SetPassphraseCtrl', require('./controller/app/set-passphrase'));
|
||||
app.controller('PrivateKeyUploadCtrl', require('./controller/app/privatekey-upload'));
|
||||
app.controller('PublicKeyImportCtrl', require('./controller/app/publickey-import'));
|
||||
app.controller('ContactsCtrl', require('./controller/app/contacts'));
|
||||
app.controller('AboutCtrl', require('./controller/app/about'));
|
||||
app.controller('DialogCtrl', require('./controller/app/dialog'));
|
||||
|
@ -16,7 +16,7 @@ var AboutCtrl = function($scope, appConfig) {
|
||||
// scope variables
|
||||
//
|
||||
|
||||
$scope.version = appConfig.config.appVersion + ' (beta)';
|
||||
$scope.version = appConfig.config.appVersion;
|
||||
$scope.date = new Date();
|
||||
|
||||
};
|
||||
|
@ -27,7 +27,7 @@ var AccountCtrl = function($scope, $q, auth, keychain, pgp, appConfig, download,
|
||||
var fpr = keyParams.fingerprint;
|
||||
$scope.fingerprint = fpr.slice(0, 4) + ' ' + fpr.slice(4, 8) + ' ' + fpr.slice(8, 12) + ' ' + fpr.slice(12, 16) + ' ' + fpr.slice(16, 20) + ' ' + fpr.slice(20, 24) + ' ' + fpr.slice(24, 28) + ' ' + fpr.slice(28, 32) + ' ' + fpr.slice(32, 36) + ' ' + fpr.slice(36);
|
||||
$scope.keysize = keyParams.bitSize;
|
||||
$scope.publicKeyUrl = appConfig.config.cloudUrl + '/' + userId;
|
||||
$scope.publicKeyUrl = appConfig.config.keyServerUrl + '/' + userId;
|
||||
|
||||
//
|
||||
// scope functions
|
||||
|
@ -8,6 +8,42 @@ var ActionBarCtrl = function($scope, $q, email, dialog, status) {
|
||||
// scope functions
|
||||
//
|
||||
|
||||
$scope.CHECKNONE = 0;
|
||||
$scope.CHECKALL = 1;
|
||||
$scope.CHECKUNREAD = 2;
|
||||
$scope.CHECKREAD = 3;
|
||||
$scope.CHECKFLAGGED = 4;
|
||||
$scope.CHECKUNFLAGGED = 5;
|
||||
$scope.CHECKENCRYPTED = 6;
|
||||
$scope.CHECKUNENCRYPTED = 7;
|
||||
|
||||
$scope.check = function(option) {
|
||||
currentFolder().messages.forEach(function(email) {
|
||||
if (!email.from) {
|
||||
// only mark loaded messages, not the dummy messages
|
||||
return;
|
||||
}
|
||||
|
||||
if (option === $scope.CHECKNONE) {
|
||||
email.checked = false;
|
||||
} else if (option === $scope.CHECKALL) {
|
||||
email.checked = true;
|
||||
} else if (option === $scope.CHECKUNREAD) {
|
||||
email.checked = !!email.unread;
|
||||
} else if (option === $scope.CHECKREAD) {
|
||||
email.checked = !email.unread;
|
||||
} else if (option === $scope.CHECKFLAGGED) {
|
||||
email.checked = !!email.flagged;
|
||||
} else if (option === $scope.CHECKUNFLAGGED) {
|
||||
email.checked = !email.flagged;
|
||||
} else if (option === $scope.CHECKENCRYPTED) {
|
||||
email.checked = !!email.encrypted;
|
||||
} else if (option === $scope.CHECKUNENCRYPTED) {
|
||||
email.checked = !email.encrypted;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Move a single message from the currently selected folder to another folder
|
||||
* @param {Object} message The message that is to be moved
|
||||
|
@ -4,7 +4,7 @@
|
||||
// Controller
|
||||
//
|
||||
|
||||
var ContactsCtrl = function($scope, $q, keychain, pgp, dialog) {
|
||||
var ContactsCtrl = function($scope, $q, keychain, pgp, dialog, appConfig) {
|
||||
|
||||
//
|
||||
// scope state
|
||||
@ -13,10 +13,13 @@ var ContactsCtrl = function($scope, $q, keychain, pgp, dialog) {
|
||||
$scope.state.contacts = {
|
||||
toggle: function(to) {
|
||||
$scope.state.lightbox = (to) ? 'contacts' : undefined;
|
||||
$scope.searchText = undefined;
|
||||
return $scope.listKeys();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.whiteoutKeyServer = appConfig.config.keyServerUrl.replace(/http[s]?:\/\//, ''); // display key server hostname
|
||||
|
||||
//
|
||||
// scope functions
|
||||
//
|
||||
@ -33,6 +36,7 @@ var ContactsCtrl = function($scope, $q, keychain, pgp, dialog) {
|
||||
keys.forEach(function(key) {
|
||||
var params = pgp.getKeyParams(key.publicKey);
|
||||
_.extend(key, params);
|
||||
key.fullUserId = key.userIds[0].name + ' <' + key.userIds[0].emailAddress + '>';
|
||||
});
|
||||
$scope.keys = keys;
|
||||
|
||||
@ -46,39 +50,6 @@ var ContactsCtrl = function($scope, $q, keychain, pgp, dialog) {
|
||||
$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) {
|
||||
return keychain.removeLocalPublicKey(key._id).then(function() {
|
||||
// update displayed keys
|
||||
|
@ -37,6 +37,7 @@ var DialogCtrl = function($scope, dialog) {
|
||||
$scope.title = options.title;
|
||||
$scope.message = options.errMsg || options.message;
|
||||
$scope.faqLink = options.faqLink;
|
||||
$scope.faqLinkTitle = options.faqLinkTitle || 'Learn more';
|
||||
$scope.positiveBtnStr = options.positiveBtnStr || 'Ok';
|
||||
$scope.negativeBtnStr = options.negativeBtnStr || 'Cancel';
|
||||
$scope.showNegativeBtn = options.showNegativeBtn || false;
|
||||
|
@ -32,6 +32,10 @@ var MailListCtrl = function($scope, $timeout, $location, $filter, $q, status, no
|
||||
* Set the route to a message which will go to read mode
|
||||
*/
|
||||
$scope.navigate = function(message) {
|
||||
if (!message || !message.from) {
|
||||
// early return if message has not finished loading yet
|
||||
return;
|
||||
}
|
||||
$location.search('uid', message.uid);
|
||||
};
|
||||
|
||||
@ -54,24 +58,16 @@ var MailListCtrl = function($scope, $timeout, $location, $filter, $q, status, no
|
||||
// scope functions
|
||||
//
|
||||
|
||||
$scope.getBody = function(message) {
|
||||
$scope.getBody = function(messages) {
|
||||
return $q(function(resolve) {
|
||||
resolve();
|
||||
|
||||
}).then(function() {
|
||||
return email.getBody({
|
||||
folder: currentFolder(),
|
||||
message: message
|
||||
messages: messages
|
||||
});
|
||||
|
||||
}).then(function() {
|
||||
// automatically decrypt if it's the selected message
|
||||
if (message === currentMessage()) {
|
||||
return email.decryptBody({
|
||||
message: message
|
||||
});
|
||||
}
|
||||
|
||||
}).catch(function(err) {
|
||||
if (err.code !== 42) {
|
||||
dialog.error(err);
|
||||
@ -136,6 +132,10 @@ var MailListCtrl = function($scope, $timeout, $location, $filter, $q, status, no
|
||||
* Date formatting
|
||||
*/
|
||||
$scope.formatDate = function(date) {
|
||||
if (!date) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof date === 'string') {
|
||||
date = new Date(date);
|
||||
}
|
||||
@ -294,6 +294,13 @@ var MailListCtrl = function($scope, $timeout, $location, $filter, $q, status, no
|
||||
}).catch(dialog.error);
|
||||
}
|
||||
|
||||
$scope.$on('read', function(e, state) {
|
||||
if (!state) {
|
||||
// load bodies after closing read mode
|
||||
$scope.loadVisibleBodies();
|
||||
}
|
||||
});
|
||||
|
||||
function currentFolder() {
|
||||
return $scope.state.nav && $scope.state.nav.currentFolder;
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ var NOTIFICATION_SENT_TIMEOUT = 2000;
|
||||
// Controller
|
||||
//
|
||||
|
||||
var NavigationCtrl = function($scope, $location, $q, account, email, outbox, notification, appConfig, dialog, dummy) {
|
||||
var NavigationCtrl = function($scope, $location, $q, $timeout, account, email, outbox, notification, status, appConfig, dialog, dummy, privateKey, axe) {
|
||||
if (!$location.search().dev && !account.isLoggedIn()) {
|
||||
$location.path('/'); // init app
|
||||
return;
|
||||
@ -90,10 +90,10 @@ var NavigationCtrl = function($scope, $location, $q, account, email, outbox, not
|
||||
// scope functions
|
||||
//
|
||||
|
||||
$scope.onOutboxUpdate = function(err, count) {
|
||||
$scope.onOutboxUpdate = function(err) {
|
||||
if (err) {
|
||||
dialog.error(err);
|
||||
return;
|
||||
axe.error('Sending from outbox failed: ' + err.message);
|
||||
status.update('Error sending messages...');
|
||||
}
|
||||
|
||||
// update the outbox mail count
|
||||
@ -106,15 +106,11 @@ var NavigationCtrl = function($scope, $location, $q, account, email, outbox, not
|
||||
return;
|
||||
}
|
||||
|
||||
ob.count = count;
|
||||
|
||||
return $q(function(resolve) {
|
||||
resolve();
|
||||
|
||||
}).then(function() {
|
||||
return email.refreshFolder({
|
||||
folder: ob
|
||||
});
|
||||
return email.refreshOutbox();
|
||||
|
||||
}).catch(dialog.error);
|
||||
};
|
||||
@ -149,6 +145,9 @@ var NavigationCtrl = function($scope, $location, $q, account, email, outbox, not
|
||||
if (!$scope.state.nav.currentFolder) {
|
||||
$scope.navigate(0);
|
||||
}
|
||||
|
||||
// check if the private PGP key is synced
|
||||
$scope.checkKeySyncStatus();
|
||||
});
|
||||
|
||||
//
|
||||
@ -178,6 +177,45 @@ var NavigationCtrl = function($scope, $location, $q, account, email, outbox, not
|
||||
// start checking outbox periodically
|
||||
outbox.startChecking($scope.onOutboxUpdate);
|
||||
}
|
||||
|
||||
$scope.checkKeySyncStatus = function() {
|
||||
return $q(function(resolve) {
|
||||
resolve();
|
||||
|
||||
}).then(function() {
|
||||
// login to imap
|
||||
return privateKey.init();
|
||||
|
||||
}).then(function() {
|
||||
// check key sync status
|
||||
return privateKey.isSynced();
|
||||
|
||||
}).then(function(synced) {
|
||||
if (!synced) {
|
||||
dialog.confirm({
|
||||
title: 'Key backup',
|
||||
message: 'Your encryption key is not backed up. Back up now?',
|
||||
positiveBtnStr: 'Backup',
|
||||
negativeBtnStr: 'Not now',
|
||||
showNegativeBtn: true,
|
||||
callback: function(granted) {
|
||||
if (granted) {
|
||||
// logout of the current session
|
||||
email.onDisconnect().then(function() {
|
||||
// send to key upload screen
|
||||
$timeout(function() {
|
||||
$location.path('/login-privatekey-upload');
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// logout of imap
|
||||
return privateKey.destroy();
|
||||
|
||||
}).catch(axe.error);
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = NavigationCtrl;
|
@ -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;
|
78
src/js/controller/app/publickey-import.js
Normal file
78
src/js/controller/app/publickey-import.js
Normal 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;
|
@ -1,35 +1,64 @@
|
||||
'use strict';
|
||||
|
||||
// add DOMPurify hook to sanitze attributes
|
||||
DOMPurify.addHook('afterSanitizeAttributes', function(node) {
|
||||
// open all links in a new window
|
||||
if ('target' in node) {
|
||||
node.setAttribute('target', '_blank');
|
||||
}
|
||||
});
|
||||
|
||||
// set listener for event from main window
|
||||
window.onmessage = function(e) {
|
||||
var html = '';
|
||||
|
||||
if (e.data.html) {
|
||||
// display html mail body
|
||||
html = '<div class="scale-body">' + e.data.html + '</div>';
|
||||
html = e.data.html;
|
||||
} else if (e.data.text) {
|
||||
// diplay text mail body by with colored conversation nodes
|
||||
html = renderNodes(parseConversation(e.data.text));
|
||||
}
|
||||
|
||||
// sanitize HTML content: https://github.com/cure53/DOMPurify
|
||||
html = window.DOMPurify.sanitize(html);
|
||||
// make links open in a new window
|
||||
html = html.replace(/<a /g, '<a target="_blank" ');
|
||||
|
||||
// remove sources where necessary
|
||||
if (e.data.removeImages) {
|
||||
html = html.replace(/(<img[^>]+\b)src=['"][^'">]+['"]/ig, function(match, prefix) {
|
||||
return prefix;
|
||||
// remove http leaks
|
||||
document.body.innerHTML = DOMPurify.sanitize(html, {
|
||||
FORBID_TAGS: ['style', 'svg', 'audio', 'video', 'math'],
|
||||
FORBID_ATTR: ['src']
|
||||
});
|
||||
} else {
|
||||
document.body.innerHTML = DOMPurify.sanitize(html);
|
||||
}
|
||||
|
||||
document.body.innerHTML = html;
|
||||
|
||||
scaleToFit();
|
||||
attachClickHandlers();
|
||||
};
|
||||
|
||||
window.addEventListener('resize', scaleToFit);
|
||||
/**
|
||||
* Send a message to the main window when email address is clicked
|
||||
*/
|
||||
function attachClickHandlers() {
|
||||
var elements = document.getElementsByTagName('a');
|
||||
for (var i = 0, len = elements.length; i < len; i++) {
|
||||
elements[i].onclick = handle;
|
||||
}
|
||||
|
||||
function handle(e) {
|
||||
var text = e.target.textContent || e.target.innerText;
|
||||
if (checkEmailAddress(text)) {
|
||||
e.preventDefault();
|
||||
window.parentIFrame.sendMessage({
|
||||
type: 'email',
|
||||
address: text
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function checkEmailAddress(text) {
|
||||
var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
return re.test(text);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse email body and generate conversation nodes
|
||||
@ -156,7 +185,7 @@ function renderNodes(root) {
|
||||
var lines = node.split('\n');
|
||||
for (i = 0; i < lines.length; i++) {
|
||||
// replace all urls with anchors
|
||||
lines[i] = lines[i].replace(/(https?:\/\/[^\s]+)/g, createArchor);
|
||||
lines[i] = lines[i].replace(/(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&\/\/=]*)/g, createArchor);
|
||||
// wrap line into an element for easier styling
|
||||
html += '<div class="line';
|
||||
if (isLineEmpty(lines[i])) {
|
||||
@ -189,27 +218,3 @@ function renderNodes(root) {
|
||||
|
||||
return '<div class="view-read-body">' + body + '</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform scale content to fit iframe width
|
||||
*/
|
||||
function scaleToFit() {
|
||||
var view = document.getElementsByClassName('scale-body').item(0);
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
|
||||
var parentWidth = view.parentNode.offsetWidth;
|
||||
var w = view.offsetWidth;
|
||||
var scale = '';
|
||||
|
||||
if (w > parentWidth) {
|
||||
scale = parentWidth / w;
|
||||
scale = 'scale(' + scale + ',' + scale + ')';
|
||||
}
|
||||
|
||||
view.style['-webkit-transform-origin'] = '0 0';
|
||||
view.style.transformOrigin = '0 0';
|
||||
view.style['-webkit-transform'] = scale;
|
||||
view.style.transform = scale;
|
||||
}
|
@ -4,9 +4,7 @@
|
||||
// Controller
|
||||
//
|
||||
|
||||
var ReadCtrl = function($scope, $location, $q, email, invitation, outbox, pgp, keychain, appConfig, download, auth, dialog) {
|
||||
|
||||
var str = appConfig.string;
|
||||
var ReadCtrl = function($scope, $location, $q, email, invitation, outbox, pgp, keychain, appConfig, download, auth, dialog, status) {
|
||||
|
||||
//
|
||||
// scope state
|
||||
@ -47,6 +45,13 @@ var ReadCtrl = function($scope, $location, $q, email, invitation, outbox, pgp, k
|
||||
// scope functions
|
||||
//
|
||||
|
||||
/**
|
||||
* Close read mode and return to mail-list
|
||||
*/
|
||||
$scope.close = function() {
|
||||
status.setReading(false);
|
||||
};
|
||||
|
||||
$scope.getKeyId = function(address) {
|
||||
if ($location.search().dev || !address) {
|
||||
return;
|
||||
@ -151,18 +156,10 @@ var ReadCtrl = function($scope, $location, $q, email, invitation, outbox, pgp, k
|
||||
});
|
||||
|
||||
}).then(function() {
|
||||
var invitationMail = {
|
||||
from: [{
|
||||
address: sender
|
||||
}],
|
||||
to: [{
|
||||
address: recipient
|
||||
}],
|
||||
cc: [],
|
||||
bcc: [],
|
||||
subject: str.invitationSubject,
|
||||
body: str.invitationMessage
|
||||
};
|
||||
var invitationMail = invitation.createMail({
|
||||
sender: sender,
|
||||
recipient: recipient
|
||||
});
|
||||
// send invitation mail
|
||||
return outbox.put(invitationMail);
|
||||
|
||||
|
@ -21,59 +21,6 @@ var SetPassphraseCtrl = function($scope, $q, pgp, keychain, dialog) {
|
||||
// scope functions
|
||||
//
|
||||
|
||||
/*
|
||||
* Taken from jQuery validate.password plug-in 1.0
|
||||
* http://bassistance.de/jquery-plugins/jquery-plugin-validate.password/
|
||||
*
|
||||
* Copyright (c) 2009 Jörn Zaefferer
|
||||
*
|
||||
* Licensed under the MIT
|
||||
* http://www.opensource.org/licenses/mit-license.php
|
||||
*/
|
||||
$scope.checkPassphraseQuality = function() {
|
||||
var passphrase = $scope.newPassphrase;
|
||||
$scope.passphraseRating = 0;
|
||||
|
||||
var LOWER = /[a-z]/,
|
||||
UPPER = /[A-Z]/,
|
||||
DIGIT = /[0-9]/,
|
||||
DIGITS = /[0-9].*[0-9]/,
|
||||
SPECIAL = /[^a-zA-Z0-9]/,
|
||||
SAME = /^(.)\1+$/;
|
||||
|
||||
function uncapitalize(str) {
|
||||
return str.substring(0, 1).toLowerCase() + str.substring(1);
|
||||
}
|
||||
|
||||
if (!passphrase) {
|
||||
// no rating for empty passphrase
|
||||
$scope.passphraseMsg = '';
|
||||
return;
|
||||
}
|
||||
|
||||
if (passphrase.length < 8 || SAME.test(passphrase)) {
|
||||
$scope.passphraseMsg = 'Very weak';
|
||||
return;
|
||||
}
|
||||
|
||||
var lower = LOWER.test(passphrase),
|
||||
upper = UPPER.test(uncapitalize(passphrase)),
|
||||
digit = DIGIT.test(passphrase),
|
||||
digits = DIGITS.test(passphrase),
|
||||
special = SPECIAL.test(passphrase);
|
||||
|
||||
if (lower && upper && digit || lower && digits || upper && digits || special) {
|
||||
$scope.passphraseMsg = 'Strong';
|
||||
$scope.passphraseRating = 3;
|
||||
} else if (lower && upper || lower && digit || upper && digit) {
|
||||
$scope.passphraseMsg = 'Good';
|
||||
$scope.passphraseRating = 2;
|
||||
} else {
|
||||
$scope.passphraseMsg = 'Weak';
|
||||
$scope.passphraseRating = 1;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.setPassphrase = function() {
|
||||
var keyId = pgp.getKeyParams()._id;
|
||||
|
||||
|
@ -6,7 +6,7 @@ var util = require('crypto-lib').util;
|
||||
// Controller
|
||||
//
|
||||
|
||||
var WriteCtrl = function($scope, $window, $filter, $q, appConfig, auth, keychain, pgp, email, outbox, dialog, axe, status) {
|
||||
var WriteCtrl = function($scope, $window, $filter, $q, appConfig, auth, keychain, pgp, email, outbox, dialog, axe, status, invitation) {
|
||||
|
||||
var str = appConfig.string;
|
||||
var cfg = appConfig.config;
|
||||
@ -52,6 +52,8 @@ var WriteCtrl = function($scope, $window, $filter, $q, appConfig, auth, keychain
|
||||
$scope.body = '';
|
||||
$scope.attachments = [];
|
||||
$scope.addressBookCache = undefined;
|
||||
$scope.showInvite = undefined;
|
||||
$scope.invited = [];
|
||||
}
|
||||
|
||||
function reportBug() {
|
||||
@ -158,7 +160,7 @@ var WriteCtrl = function($scope, $window, $filter, $q, appConfig, auth, keychain
|
||||
if (forward) {
|
||||
$scope.subject = 'Fwd: ' + re.subject;
|
||||
} else {
|
||||
$scope.subject = 'Re: ' + ((re.subject) ? re.subject.replace('Re: ', '') : '');
|
||||
$scope.subject = re.subject ? 'Re: ' + re.subject.replace('Re: ', '') : '';
|
||||
}
|
||||
|
||||
// fill text body
|
||||
@ -198,6 +200,17 @@ var WriteCtrl = function($scope, $window, $filter, $q, appConfig, auth, keychain
|
||||
// Editing headers
|
||||
//
|
||||
|
||||
/**
|
||||
* Warn users when using BCC
|
||||
*/
|
||||
$scope.toggleShowBCC = function() {
|
||||
$scope.showBCC = true;
|
||||
return dialog.info({
|
||||
title: 'Warning',
|
||||
message: 'Cannot send encrypted messages with BCC!'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Verify email address and fetch its public key
|
||||
*/
|
||||
@ -206,6 +219,14 @@ var WriteCtrl = function($scope, $window, $filter, $q, appConfig, auth, keychain
|
||||
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
|
||||
recipient.key = undefined;
|
||||
recipient.secure = false;
|
||||
@ -231,14 +252,18 @@ var WriteCtrl = function($scope, $window, $filter, $q, appConfig, auth, keychain
|
||||
}).then(function(key) {
|
||||
if (key) {
|
||||
// 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
|
||||
});
|
||||
// compare either primary userId or (if available) multiple IDs
|
||||
if (key.userId === recipient.address || matchingUserId) {
|
||||
if (matchingUserId) {
|
||||
recipient.key = key;
|
||||
recipient.secure = true;
|
||||
}
|
||||
} else {
|
||||
// show invite dialog if no key found
|
||||
$scope.showInvite = true;
|
||||
}
|
||||
$scope.checkSendStatus();
|
||||
|
||||
@ -264,7 +289,10 @@ var WriteCtrl = function($scope, $window, $filter, $q, appConfig, auth, keychain
|
||||
function check(recipient) {
|
||||
// validate address
|
||||
if (!util.validateEmailAddress(recipient.address)) {
|
||||
return;
|
||||
return dialog.info({
|
||||
title: 'Warning',
|
||||
message: 'Invalid recipient address!'
|
||||
});
|
||||
}
|
||||
numReceivers++;
|
||||
if (!recipient.secure) {
|
||||
@ -274,6 +302,7 @@ var WriteCtrl = function($scope, $window, $filter, $q, appConfig, auth, keychain
|
||||
|
||||
// only allow sending if receviers exist
|
||||
if (numReceivers < 1) {
|
||||
$scope.showInvite = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@ -287,6 +316,7 @@ var WriteCtrl = function($scope, $window, $filter, $q, appConfig, auth, keychain
|
||||
$scope.okToSend = true;
|
||||
$scope.sendBtnText = str.sendBtnSecure;
|
||||
$scope.sendBtnSecure = true;
|
||||
$scope.showInvite = false;
|
||||
} else {
|
||||
// send plaintext
|
||||
$scope.okToSend = true;
|
||||
@ -303,6 +333,56 @@ var WriteCtrl = function($scope, $window, $filter, $q, appConfig, auth, keychain
|
||||
$scope.attachments.splice($scope.attachments.indexOf(attachment), 1);
|
||||
};
|
||||
|
||||
/**
|
||||
* Invite all users without a public key
|
||||
*/
|
||||
$scope.invite = function() {
|
||||
var sender = auth.emailAddress,
|
||||
sendJobs = [],
|
||||
invitees = [];
|
||||
|
||||
$scope.showInvite = false;
|
||||
|
||||
// get recipients with no keys
|
||||
$scope.to.forEach(check);
|
||||
$scope.cc.forEach(check);
|
||||
$scope.bcc.forEach(check);
|
||||
|
||||
function check(recipient) {
|
||||
if (util.validateEmailAddress(recipient.address) && !recipient.secure && $scope.invited.indexOf(recipient.address) === -1) {
|
||||
invitees.push(recipient.address);
|
||||
}
|
||||
}
|
||||
|
||||
return $q(function(resolve) {
|
||||
resolve();
|
||||
|
||||
}).then(function() {
|
||||
invitees.forEach(function(recipientAddress) {
|
||||
var invitationMail = invitation.createMail({
|
||||
sender: sender,
|
||||
recipient: recipientAddress
|
||||
});
|
||||
// send invitation mail
|
||||
var promise = outbox.put(invitationMail).then(function() {
|
||||
return invitation.invite({
|
||||
recipient: recipientAddress,
|
||||
sender: sender
|
||||
});
|
||||
});
|
||||
sendJobs.push(promise);
|
||||
// remember already invited users to prevent spamming
|
||||
$scope.invited.push(recipientAddress);
|
||||
});
|
||||
|
||||
return Promise.all(sendJobs);
|
||||
|
||||
}).catch(function(err) {
|
||||
$scope.showInvite = true;
|
||||
return dialog.error(err);
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Editing email body
|
||||
//
|
||||
@ -393,8 +473,10 @@ var WriteCtrl = function($scope, $window, $filter, $q, appConfig, auth, keychain
|
||||
// populate address book cache
|
||||
return keychain.listLocalPublicKeys().then(function(keys) {
|
||||
$scope.addressBookCache = keys.map(function(key) {
|
||||
var name = pgp.getKeyParams(key.publicKey).userIds[0].name;
|
||||
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() {
|
||||
// filter the address book cache
|
||||
return $scope.addressBookCache.filter(function(i) {
|
||||
return i.address.indexOf(query) !== -1;
|
||||
return i.displayId.toLowerCase().indexOf(query.toLowerCase()) !== -1;
|
||||
});
|
||||
|
||||
}).catch(dialog.error);
|
||||
|
@ -1,17 +1,33 @@
|
||||
'use strict';
|
||||
|
||||
var CreateAccountCtrl = function($scope, $location, $routeParams, $q, auth, admin, appConfig) {
|
||||
var CreateAccountCtrl = function($scope, $location, $routeParams, $q, auth, admin, appConfig, dialog) {
|
||||
!$routeParams.dev && !auth.isInitialized() && $location.path('/'); // init app
|
||||
|
||||
// init phone region
|
||||
$scope.region = 'DE';
|
||||
$scope.domain = '@' + appConfig.config.mailServer.domain;
|
||||
|
||||
$scope.createWhiteoutAccount = function() {
|
||||
$scope.showConfirm = function() {
|
||||
if ($scope.form.$invalid) {
|
||||
$scope.errMsg = 'Please fill out all required fields!';
|
||||
return;
|
||||
}
|
||||
|
||||
return dialog.confirm({
|
||||
title: 'SMS validation',
|
||||
message: 'Your mobile phone number will be validated via SMS. Are you sure it\'s correct?',
|
||||
positiveBtnStr: 'Yes',
|
||||
negativeBtnStr: 'Check again',
|
||||
showNegativeBtn: true,
|
||||
callback: function(granted) {
|
||||
if (granted) {
|
||||
$scope.createWhiteoutAccount();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.createWhiteoutAccount = function() {
|
||||
return $q(function(resolve) {
|
||||
$scope.busy = true;
|
||||
$scope.errMsg = undefined; // reset error msg
|
||||
@ -19,7 +35,7 @@ var CreateAccountCtrl = function($scope, $location, $routeParams, $q, auth, admi
|
||||
|
||||
}).then(function() {
|
||||
// read form values
|
||||
var emailAddress = $scope.user + '@' + appConfig.config.wmailDomain;
|
||||
var emailAddress = $scope.user + $scope.domain;
|
||||
var phone = PhoneNumber.Parse($scope.dial, $scope.region);
|
||||
if (!phone || !phone.internationalNumber) {
|
||||
throw new Error('Invalid phone number!');
|
||||
@ -36,8 +52,7 @@ var CreateAccountCtrl = function($scope, $location, $routeParams, $q, auth, admi
|
||||
return admin.createUser({
|
||||
emailAddress: emailAddress,
|
||||
password: $scope.pass,
|
||||
phone: phone.internationalNumber,
|
||||
betaCode: $scope.betaCode.toUpperCase()
|
||||
phone: phone.internationalNumber
|
||||
});
|
||||
|
||||
}).then(function() {
|
||||
@ -50,6 +65,15 @@ var CreateAccountCtrl = function($scope, $location, $routeParams, $q, auth, admi
|
||||
$scope.errMsg = err.errMsg || err.message;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.loginToExisting = function() {
|
||||
// set server config
|
||||
$scope.state.login = {
|
||||
mailConfig: appConfig.config.mailServer
|
||||
};
|
||||
// proceed to login
|
||||
$location.path('/login-set-credentials');
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = CreateAccountCtrl;
|
@ -1,8 +1,10 @@
|
||||
'use strict';
|
||||
|
||||
var LoginExistingCtrl = function($scope, $location, $routeParams, $q, email, auth, keychain) {
|
||||
var LoginExistingCtrl = function($scope, $location, $routeParams, $q, email, auth, keychain, account, dialog, appConfig) {
|
||||
!$routeParams.dev && !auth.isInitialized() && $location.path('/'); // init app
|
||||
|
||||
var str = appConfig.string;
|
||||
|
||||
$scope.confirmPassphrase = function() {
|
||||
if ($scope.form.$invalid) {
|
||||
$scope.errMsg = 'Please fill out all required fields!';
|
||||
@ -38,6 +40,18 @@ var LoginExistingCtrl = function($scope, $location, $routeParams, $q, email, aut
|
||||
}).catch(displayError);
|
||||
};
|
||||
|
||||
$scope.logout = function() {
|
||||
return dialog.confirm({
|
||||
title: str.removePreAuthAccountTitle,
|
||||
message: str.removePreAuthAccountMessage,
|
||||
callback: function(confirm) {
|
||||
if (confirm) {
|
||||
account.logout().catch(dialog.error);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function displayError(err) {
|
||||
$scope.busy = false;
|
||||
$scope.incorrect = true;
|
||||
|
@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
var LoginInitialCtrl = function($scope, $location, $routeParams, $q, newsletter, email, auth) {
|
||||
var LoginInitialCtrl = function($scope, $location, $routeParams, $q, newsletter, email, auth, publickeyVerifier) {
|
||||
!$routeParams.dev && !auth.isInitialized() && $location.path('/'); // init app
|
||||
|
||||
var emailAddress = auth.emailAddress;
|
||||
@ -56,16 +56,14 @@ var LoginInitialCtrl = function($scope, $location, $routeParams, $q, newsletter,
|
||||
}).then(function() {
|
||||
// generate key without passphrase
|
||||
return email.unlock({
|
||||
realname: auth.realname,
|
||||
passphrase: undefined
|
||||
});
|
||||
|
||||
}).then(function() {
|
||||
// persist credentials locally
|
||||
return auth.storeCredentials();
|
||||
|
||||
}).then(function() {
|
||||
// go to main account screen
|
||||
$location.path('/account');
|
||||
}).then(function(keypair) {
|
||||
// remember keypair for storing after public key verification
|
||||
publickeyVerifier.keypair = keypair;
|
||||
$location.path('/login-privatekey-upload');
|
||||
|
||||
}).catch(displayError);
|
||||
};
|
||||
|
@ -1,17 +1,36 @@
|
||||
'use strict';
|
||||
|
||||
var LoginExistingCtrl = function($scope, $location, $routeParams, $q, email, auth, pgp, keychain) {
|
||||
var LoginExistingCtrl = function($scope, $location, $routeParams, $q, email, auth, pgp, keychain, publickeyVerifier) {
|
||||
!$routeParams.dev && !auth.isInitialized() && $location.path('/'); // init app
|
||||
|
||||
$scope.incorrect = false;
|
||||
|
||||
var PRIV_KEY_PREFIX = '-----BEGIN PGP PRIVATE KEY BLOCK-----';
|
||||
var PUB_KEY_PREFIX = '-----BEGIN PGP PUBLIC KEY BLOCK-----';
|
||||
var PRIV_ERR_MSG = 'Cannot find private PGP key block!';
|
||||
|
||||
$scope.pasteKey = function(pasted) {
|
||||
var index = pasted.indexOf(PRIV_KEY_PREFIX);
|
||||
if (index === -1) {
|
||||
$scope.errMsg = PRIV_ERR_MSG;
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.errMsg = undefined; // reset error msg
|
||||
|
||||
$scope.key = {
|
||||
privateKeyArmored: pasted.substring(index, pasted.length).trim()
|
||||
};
|
||||
};
|
||||
|
||||
$scope.confirmPassphrase = function() {
|
||||
if ($scope.form.$invalid || !$scope.key) {
|
||||
$scope.errMsg = 'Please fill out all required fields!';
|
||||
$scope.errMsg = PRIV_ERR_MSG;
|
||||
return;
|
||||
}
|
||||
|
||||
var userId = auth.emailAddress,
|
||||
pubKeyNeedsVerification = false,
|
||||
keypair;
|
||||
|
||||
return $q(function(resolve) {
|
||||
@ -28,11 +47,11 @@ var LoginExistingCtrl = function($scope, $location, $routeParams, $q, email, aut
|
||||
keypair = keys || {};
|
||||
|
||||
// extract public key from private key block if missing in key file
|
||||
if (!$scope.key.publicKeyArmored || $scope.key.publicKeyArmored.indexOf('-----BEGIN PGP PUBLIC KEY BLOCK-----') < 0) {
|
||||
if (!$scope.key.publicKeyArmored || $scope.key.publicKeyArmored.indexOf(PUB_KEY_PREFIX) < 0) {
|
||||
try {
|
||||
$scope.key.publicKeyArmored = pgp.extractPublicKey($scope.key.privateKeyArmored);
|
||||
} catch (e) {
|
||||
throw new Error('Error reading PGP key!');
|
||||
throw new Error('Cannot find public PGP key!');
|
||||
}
|
||||
}
|
||||
|
||||
@ -61,6 +80,7 @@ var LoginExistingCtrl = function($scope, $location, $routeParams, $q, email, aut
|
||||
userIds: pubKeyParams.userIds,
|
||||
publicKey: $scope.key.publicKeyArmored
|
||||
};
|
||||
pubKeyNeedsVerification = true; // this public key needs to be authenticated
|
||||
}
|
||||
|
||||
// import and validate keypair
|
||||
@ -72,17 +92,20 @@ var LoginExistingCtrl = function($scope, $location, $routeParams, $q, email, aut
|
||||
throw err;
|
||||
});
|
||||
|
||||
}).then(function() {
|
||||
// perist keys locally
|
||||
return keychain.putUserKeyPair(keypair);
|
||||
|
||||
}).then(function() {
|
||||
// persist credentials locally
|
||||
}).then(function(keypair) {
|
||||
if (!pubKeyNeedsVerification) {
|
||||
// persist credentials and key and go to main account screen
|
||||
return keychain.putUserKeyPair(keypair).then(function() {
|
||||
return auth.storeCredentials();
|
||||
|
||||
}).then(function() {
|
||||
// go to main account screen
|
||||
$location.path('/account');
|
||||
});
|
||||
}
|
||||
|
||||
// remember keypair for public key verification
|
||||
publickeyVerifier.keypair = keypair;
|
||||
// upload private key and then go to public key verification
|
||||
$location.path('/login-privatekey-upload');
|
||||
|
||||
}).catch(displayError);
|
||||
};
|
||||
|
@ -1,20 +1,19 @@
|
||||
'use strict';
|
||||
|
||||
var LoginPrivateKeyDownloadCtrl = function($scope, $location, $routeParams, $q, auth, email, keychain) {
|
||||
var LoginPrivateKeyDownloadCtrl = function($scope, $location, $routeParams, $q, auth, email, privateKey, keychain) {
|
||||
!$routeParams.dev && !auth.isInitialized() && $location.path('/'); // init app
|
||||
|
||||
$scope.step = 1;
|
||||
|
||||
//
|
||||
// Token
|
||||
// scope functions
|
||||
//
|
||||
|
||||
$scope.checkToken = function() {
|
||||
if ($scope.tokenForm.$invalid) {
|
||||
$scope.errMsg = 'Please enter a valid recovery token!';
|
||||
$scope.checkCode = function() {
|
||||
if ($scope.form.$invalid) {
|
||||
$scope.errMsg = 'Please fill out all required fields!';
|
||||
return;
|
||||
}
|
||||
|
||||
var cachedKeypair;
|
||||
var userId = auth.emailAddress;
|
||||
|
||||
return $q(function(resolve) {
|
||||
@ -22,54 +21,38 @@ var LoginPrivateKeyDownloadCtrl = function($scope, $location, $routeParams, $q,
|
||||
$scope.errMsg = undefined;
|
||||
resolve();
|
||||
|
||||
}).then(function() {
|
||||
// login to imap
|
||||
return privateKey.init();
|
||||
|
||||
}).then(function() {
|
||||
// get public key id for reference
|
||||
return keychain.getUserKeyPair(userId);
|
||||
|
||||
}).then(function(keypair) {
|
||||
// remember for storage later
|
||||
$scope.cachedKeypair = keypair;
|
||||
return keychain.downloadPrivateKey({
|
||||
cachedKeypair = keypair;
|
||||
return privateKey.download({
|
||||
userId: userId,
|
||||
keyId: keypair.publicKey._id,
|
||||
recoveryToken: $scope.recoveryToken.toUpperCase()
|
||||
keyId: keypair.publicKey._id
|
||||
});
|
||||
|
||||
}).then(function(encryptedPrivateKey) {
|
||||
$scope.encryptedPrivateKey = encryptedPrivateKey;
|
||||
$scope.busy = false;
|
||||
$scope.step++;
|
||||
}).then(function(encryptedKey) {
|
||||
// set decryption code
|
||||
encryptedKey.code = $scope.code.toUpperCase();
|
||||
// decrypt the downloaded encrypted private key
|
||||
return privateKey.decrypt(encryptedKey);
|
||||
|
||||
}).catch(displayError);
|
||||
};
|
||||
|
||||
//
|
||||
// Keychain code
|
||||
//
|
||||
|
||||
$scope.checkCode = function() {
|
||||
if ($scope.codeForm.$invalid) {
|
||||
$scope.errMsg = 'Please fill out all required fields!';
|
||||
return;
|
||||
}
|
||||
|
||||
var options = $scope.encryptedPrivateKey;
|
||||
options.code = $scope.code.toUpperCase();
|
||||
|
||||
return $q(function(resolve) {
|
||||
$scope.busy = true;
|
||||
$scope.errMsg = undefined;
|
||||
resolve();
|
||||
}).then(function(privkey) {
|
||||
// add private key to cached keypair object
|
||||
cachedKeypair.privateKey = privkey;
|
||||
// store the decrypted private key locally
|
||||
return keychain.putUserKeyPair(cachedKeypair);
|
||||
|
||||
}).then(function() {
|
||||
return keychain.decryptAndStorePrivateKeyLocally(options);
|
||||
|
||||
}).then(function(privateKey) {
|
||||
// add private key to cached keypair object
|
||||
$scope.cachedKeypair.privateKey = privateKey;
|
||||
// try empty passphrase
|
||||
return email.unlock({
|
||||
keypair: $scope.cachedKeypair,
|
||||
keypair: cachedKeypair,
|
||||
passphrase: undefined
|
||||
}).catch(function(err) {
|
||||
// passphrase incorrct ... go to passphrase login screen
|
||||
@ -81,6 +64,10 @@ var LoginPrivateKeyDownloadCtrl = function($scope, $location, $routeParams, $q,
|
||||
// passphrase is corrent ...
|
||||
return auth.storeCredentials();
|
||||
|
||||
}).then(function() {
|
||||
// logout of imap
|
||||
return privateKey.destroy();
|
||||
|
||||
}).then(function() {
|
||||
// continue to main app
|
||||
$scope.goTo('/account');
|
||||
|
83
src/js/controller/login/login-privatekey-upload.js
Normal file
83
src/js/controller/login/login-privatekey-upload.js
Normal 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;
|
@ -68,12 +68,14 @@ var SetCredentialsCtrl = function($scope, $location, $routeParams, $q, auth, con
|
||||
host: $scope.imapHost.toLowerCase(),
|
||||
port: $scope.imapPort,
|
||||
secure: imapEncryption === ENCRYPTION_METHOD_TLS,
|
||||
requireTLS: imapEncryption === ENCRYPTION_METHOD_STARTTLS,
|
||||
ignoreTLS: imapEncryption === ENCRYPTION_METHOD_NONE
|
||||
},
|
||||
smtp: {
|
||||
host: $scope.smtpHost.toLowerCase(),
|
||||
port: $scope.smtpPort,
|
||||
secure: smtpEncryption === ENCRYPTION_METHOD_TLS,
|
||||
requireTLS: smtpEncryption === ENCRYPTION_METHOD_STARTTLS,
|
||||
ignoreTLS: smtpEncryption === ENCRYPTION_METHOD_NONE
|
||||
}
|
||||
};
|
||||
|
96
src/js/controller/login/login-verify-public-key.js
Normal file
96
src/js/controller/login/login-verify-public-key.js
Normal 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;
|
@ -52,19 +52,8 @@ var LoginCtrl = function($scope, $timeout, $location, updateHandler, account, au
|
||||
});
|
||||
|
||||
} else if (availableKeys && availableKeys.publicKey && !availableKeys.privateKey) {
|
||||
// check if private key is synced
|
||||
return keychain.requestPrivateKeyDownload({
|
||||
userId: availableKeys.publicKey.userId,
|
||||
keyId: availableKeys.publicKey._id,
|
||||
}).then(function(privateKeySynced) {
|
||||
if (privateKeySynced) {
|
||||
// private key is synced, proceed to download
|
||||
// proceed to private key download
|
||||
return $scope.goTo('/login-privatekey-download');
|
||||
} else {
|
||||
// no private key, import key file
|
||||
return $scope.goTo('/login-new-device');
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
// no public key available, start onboarding process
|
||||
|
@ -11,6 +11,7 @@ var util = openpgp.util,
|
||||
* High level crypto api that handles all calls to OpenPGP.js
|
||||
*/
|
||||
function PGP() {
|
||||
openpgp.config.commentstring = config.pgpComment;
|
||||
openpgp.config.prefer_hash_algorithm = openpgp.enums.hash.sha256;
|
||||
openpgp.initWorker(config.workerPath + '/openpgp.worker.min.js');
|
||||
}
|
||||
@ -21,14 +22,15 @@ function PGP() {
|
||||
*/
|
||||
PGP.prototype.generateKeys = function(options) {
|
||||
return new Promise(function(resolve) {
|
||||
var userId, passphrase;
|
||||
var userId, name, passphrase;
|
||||
|
||||
if (!util.emailRegEx.test(options.emailAddress) || !options.keySize) {
|
||||
throw new Error('Crypto init failed. Not all options set!');
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
resolve({
|
||||
@ -104,7 +106,7 @@ PGP.prototype.getKeyId = function(keyArmored) {
|
||||
* Read all relevant params of an armored key.
|
||||
*/
|
||||
PGP.prototype.getKeyParams = function(keyArmored) {
|
||||
var key, packet, userIds;
|
||||
var key, packet, userIds, emailAddress;
|
||||
|
||||
// process armored key input
|
||||
if (keyArmored) {
|
||||
@ -120,15 +122,24 @@ PGP.prototype.getKeyParams = function(keyArmored) {
|
||||
// read user names and email addresses
|
||||
userIds = [];
|
||||
key.getUserIds().forEach(function(userId) {
|
||||
if (!userId || userId.indexOf('<') < 0 || userId.indexOf('>') < 0) {
|
||||
return;
|
||||
}
|
||||
userIds.push({
|
||||
name: userId.split('<')[0].trim(),
|
||||
emailAddress: userId.split('<')[1].split('>')[0].trim()
|
||||
});
|
||||
});
|
||||
|
||||
// check user ID
|
||||
emailAddress = userIds[0] && userIds[0].emailAddress;
|
||||
if (!emailAddress) {
|
||||
throw new Error('Cannot parse PGP key user ID!');
|
||||
}
|
||||
|
||||
return {
|
||||
_id: packet.getKeyId().toHex().toUpperCase(),
|
||||
userId: userIds[0].emailAddress, // the primary (first) email address of the key
|
||||
userId: emailAddress, // the primary (first) email address of the key
|
||||
userIds: userIds, // a dictonary of all the key's name/address pairs
|
||||
fingerprint: packet.getFingerprint().toUpperCase(),
|
||||
algorithm: packet.algorithm,
|
||||
|
@ -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() {
|
||||
var BLOCK_SIZE = 4;
|
||||
var NUM_BLOCKS = 6;
|
||||
|
@ -8,6 +8,7 @@ ngModule.directive('keyfileInput', function() {
|
||||
for (var i = 0; i < e.target.files.length; i++) {
|
||||
importKey(e.target.files.item(i));
|
||||
}
|
||||
elm.val(null); // clear input
|
||||
});
|
||||
|
||||
function importKey(file) {
|
||||
|
@ -18,7 +18,6 @@ ngModule.directive('fileReader', function() {
|
||||
keyParts;
|
||||
|
||||
if (index === -1) {
|
||||
scope.displayError(new Error('Error parsing private PGP key block!'));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,10 @@
|
||||
'use strict';
|
||||
|
||||
var PREFETCH_ITEMS = 10;
|
||||
|
||||
var ngModule = angular.module('woDirectives');
|
||||
|
||||
ngModule.directive('listScroll', function() {
|
||||
ngModule.directive('listScroll', function($timeout) {
|
||||
return {
|
||||
link: function(scope, elm, attrs) {
|
||||
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
|
||||
*/
|
||||
scope.loadVisibleBodies = function() {
|
||||
function loadVisibleBodies() {
|
||||
var listBorder = listEl.getBoundingClientRect(),
|
||||
top = listBorder.top,
|
||||
bottom = listBorder.bottom,
|
||||
@ -20,7 +22,10 @@ ngModule.directive('listScroll', function() {
|
||||
inViewport = false,
|
||||
listItem, message,
|
||||
isPartiallyVisibleTop, isPartiallyVisibleBottom, isVisible,
|
||||
displayMessages = scope[model];
|
||||
displayMessages = scope[model],
|
||||
visible = [],
|
||||
prefetchLowerBound = displayMessages.length, // lowest index where we need to start prefetching
|
||||
prefetchUpperBound = 0; // highest index where we need to start prefetching
|
||||
|
||||
if (!top && !bottom) {
|
||||
// list not visible
|
||||
@ -38,7 +43,6 @@ ngModule.directive('listScroll', function() {
|
||||
}
|
||||
message = displayMessages[i];
|
||||
|
||||
|
||||
isPartiallyVisibleTop = listItem.top < top && listItem.bottom > top; // a portion of the list item is visible on the top
|
||||
isPartiallyVisibleBottom = listItem.top < bottom && listItem.bottom > bottom; // a portion of the list item is visible on the bottom
|
||||
isVisible = (listItem.top || listItem.bottom) && listItem.top >= top && listItem.bottom <= bottom; // the list item is visible as a whole
|
||||
@ -47,12 +51,34 @@ ngModule.directive('listScroll', function() {
|
||||
// we are now iterating over visible elements
|
||||
inViewport = true;
|
||||
// load mail body of visible
|
||||
scope.getBody(message);
|
||||
visible.push(message);
|
||||
|
||||
prefetchLowerBound = Math.max(Math.min(prefetchLowerBound, i - 1), 0);
|
||||
prefetchUpperBound = Math.max(prefetchUpperBound, i + 1);
|
||||
} else if (inViewport) {
|
||||
// we are leaving the viewport, so stop iterating over the items
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// prefetch: [prefetchLowerBound - 20 ; prefetchLowerBound] and [prefetchUpperBound; prefetchUpperBound + 20]
|
||||
//
|
||||
|
||||
// normalize lowest index to 0, slice interprets values <0 as "start from end"
|
||||
var prefetchLower = displayMessages.slice(Math.max(prefetchLowerBound - PREFETCH_ITEMS, 0), prefetchLowerBound);
|
||||
var prefetchUpper = displayMessages.slice(prefetchUpperBound, prefetchUpperBound + PREFETCH_ITEMS);
|
||||
|
||||
visible.concat(prefetchLower).concat(prefetchUpper).forEach(function(email) {
|
||||
scope.getBody([email]);
|
||||
});
|
||||
}
|
||||
|
||||
scope.loadVisibleBodies = function() {
|
||||
// wait for next tick so that scope is digested and synced to DOM
|
||||
$timeout(function() {
|
||||
loadVisibleBodies();
|
||||
});
|
||||
};
|
||||
|
||||
// load body when scrolling
|
||||
|
@ -45,7 +45,7 @@ ngModule.directive('replySelection', function() {
|
||||
};
|
||||
});
|
||||
|
||||
ngModule.directive('frameLoad', function($timeout, $window) {
|
||||
ngModule.directive('frameLoad', function($window) {
|
||||
return function(scope, elm) {
|
||||
var iframe = elm[0];
|
||||
|
||||
@ -53,46 +53,58 @@ ngModule.directive('frameLoad', function($timeout, $window) {
|
||||
if (open) {
|
||||
// trigger rendering of iframe
|
||||
// otherwise scale to fit would not compute correct dimensions on mobile
|
||||
displayText(scope.state.mailList.selected ? scope.state.mailList.selected.body : undefined);
|
||||
displayHtml(scope.state.mailList.selected ? scope.state.mailList.selected.html : undefined);
|
||||
displayContent();
|
||||
}
|
||||
});
|
||||
|
||||
$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() {
|
||||
// set listeners
|
||||
scope.$watch('state.mailList.selected.body', displayText);
|
||||
scope.$watch('state.mailList.selected.html', displayHtml);
|
||||
scope.$watch('state.mailList.selected.body', displayContent);
|
||||
scope.$watch('state.mailList.selected.html', displayContent);
|
||||
// display initial message body
|
||||
scope.$apply();
|
||||
};
|
||||
|
||||
function displayText(body) {
|
||||
function displayContent() {
|
||||
var mail = scope.state.mailList.selected;
|
||||
if ((mail && mail.html) || (mail && mail.encrypted && !mail.decrypted)) {
|
||||
|
||||
if (!mail || (mail.encrypted && !mail.decrypted)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// send text body for rendering in iframe
|
||||
iframe.contentWindow.postMessage({
|
||||
text: body
|
||||
}, '*');
|
||||
|
||||
$timeout(scaleToFit, 0);
|
||||
}
|
||||
|
||||
function displayHtml(html) {
|
||||
if (!html) {
|
||||
return;
|
||||
}
|
||||
resetWidth();
|
||||
|
||||
if (mail.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;
|
||||
|
||||
iframe.contentWindow.postMessage({
|
||||
html: html,
|
||||
html: mail.html,
|
||||
removeImages: hasImages // avoids doing unnecessary work on the html
|
||||
}, '*');
|
||||
|
||||
@ -102,30 +114,46 @@ ngModule.directive('frameLoad', function($timeout, $window) {
|
||||
scope.displayImages = function() {
|
||||
scope.showImageButton = false;
|
||||
iframe.contentWindow.postMessage({
|
||||
html: html,
|
||||
html: mail.html,
|
||||
removeImages: false
|
||||
}, '*');
|
||||
};
|
||||
}
|
||||
|
||||
$timeout(scaleToFit, 0);
|
||||
} else if (mail.body) {
|
||||
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() {
|
||||
var parentWidth = elm.parent().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 = 'scale(' + scale + ',' + scale + ')';
|
||||
}
|
||||
|
||||
elm.css({
|
||||
'-webkit-transform-origin': '0 0',
|
||||
'-moz-transform-origin': '0 0',
|
||||
'-ms-transform-origin': '0 0',
|
||||
'transform-origin': '0 0',
|
||||
'-webkit-transform': scale,
|
||||
'-moz-transform': scale,
|
||||
'-ms-transform': scale,
|
||||
'transform': scale
|
||||
});
|
||||
}
|
||||
|
@ -4,12 +4,9 @@ var ngModule = angular.module('woEmail');
|
||||
ngModule.service('account', Account);
|
||||
module.exports = Account;
|
||||
|
||||
var axe = require('axe-logger'),
|
||||
util = require('crypto-lib').util,
|
||||
PgpMailer = require('pgpmailer'),
|
||||
ImapClient = require('imap-client');
|
||||
var util = require('crypto-lib').util;
|
||||
|
||||
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._auth = auth;
|
||||
this._accountStore = accountStore;
|
||||
@ -17,7 +14,6 @@ function Account(appConfig, auth, accountStore, email, outbox, keychain, updateH
|
||||
this._outbox = outbox;
|
||||
this._keychain = keychain;
|
||||
this._updateHandler = updateHandler;
|
||||
this._pgpbuilder = pgpbuilder;
|
||||
this._dialog = dialog;
|
||||
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.
|
||||
*/
|
||||
Account.prototype.onConnect = function(callback) {
|
||||
var self = this;
|
||||
var config = self._appConfig.config;
|
||||
|
||||
callback = callback || self._dialog.error;
|
||||
|
||||
if (!self.isOnline() || !self._emailDao || !self._emailDao._account) {
|
||||
if (!this._emailDao || !this._emailDao._account) {
|
||||
// prevent connection infinite loop
|
||||
return;
|
||||
}
|
||||
|
||||
// init imap/smtp clients
|
||||
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);
|
||||
}
|
||||
this._emailDao.onConnect().then(callback).catch(callback);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -180,6 +124,10 @@ Account.prototype.logout = function() {
|
||||
var self = this;
|
||||
// clear app config store
|
||||
return self._auth.logout().then(function() {
|
||||
// clear the account DB, including keys and messages
|
||||
return self._accountStore.clear();
|
||||
|
||||
}).then(function() {
|
||||
// delete instance of imap-client and pgp-mailer
|
||||
return self._emailDao.onDisconnect();
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -4,6 +4,7 @@ angular.module('woEmail', ['woAppConfig', 'woUtil', 'woServices', 'woCrypto']);
|
||||
|
||||
require('./mailreader');
|
||||
require('./pgpbuilder');
|
||||
require('./mailbuild');
|
||||
require('./email');
|
||||
require('./outbox');
|
||||
require('./account');
|
||||
|
8
src/js/email/mailbuild.js
Normal file
8
src/js/email/mailbuild.js
Normal file
@ -0,0 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
var Mailbuild = require('mailbuild');
|
||||
|
||||
var ngModule = angular.module('woEmail');
|
||||
ngModule.factory('mailbuild', function() {
|
||||
return Mailbuild;
|
||||
});
|
@ -62,6 +62,12 @@ Outbox.prototype.put = function(mail) {
|
||||
var self = this,
|
||||
allReaders = mail.from.concat(mail.to.concat(mail.cc.concat(mail.bcc))); // all the users that should be able to read the mail
|
||||
|
||||
if (mail.to.concat(mail.cc.concat(mail.bcc)).length === 0) {
|
||||
return new Promise(function() {
|
||||
throw new Error('Message has no recipients!');
|
||||
});
|
||||
}
|
||||
|
||||
mail.publicKeysArmored = []; // gather the public keys
|
||||
mail.uid = mail.id = util.UUID(); // the mail needs a random id & uid for storage in the database
|
||||
|
||||
@ -118,8 +124,7 @@ Outbox.prototype.put = function(mail) {
|
||||
* @param {Function} callback(error, pendingMailsCount) Callback that informs you about the count of pending mails.
|
||||
*/
|
||||
Outbox.prototype._processOutbox = function(callback) {
|
||||
var self = this,
|
||||
unsentMails = 0;
|
||||
var self = this;
|
||||
|
||||
// also, if a _processOutbox call is still in progress, ignore it.
|
||||
if (self._outboxBusy) {
|
||||
@ -129,10 +134,9 @@ Outbox.prototype._processOutbox = function(callback) {
|
||||
self._outboxBusy = true;
|
||||
|
||||
// get pending mails from the outbox
|
||||
self._devicestorage.listItems(outboxDb, 0, null).then(function(pendingMails) {
|
||||
self._devicestorage.listItems(outboxDb).then(function(pendingMails) {
|
||||
// if we're not online, don't even bother sending mails.
|
||||
if (!self._emailDao._account.online || _.isEmpty(pendingMails)) {
|
||||
unsentMails = pendingMails.length;
|
||||
return;
|
||||
}
|
||||
|
||||
@ -148,7 +152,7 @@ Outbox.prototype._processOutbox = function(callback) {
|
||||
|
||||
}).then(function() {
|
||||
self._outboxBusy = false;
|
||||
callback(null, unsentMails);
|
||||
callback();
|
||||
|
||||
}).catch(function(err) {
|
||||
self._outboxBusy = false;
|
||||
|
91
src/js/offline-cache.js
Normal file
91
src/js/offline-cache.js
Normal 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();
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
@ -30,7 +30,7 @@ Admin.prototype.createUser = function(options) {
|
||||
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;
|
||||
}
|
||||
|
||||
throw new Error('Validation failed!');
|
||||
throw new Error('Validation failed! Reason: ' + err.message);
|
||||
});
|
||||
};
|
@ -94,6 +94,8 @@ Auth.prototype.getCredentials = function() {
|
||||
var credentials = {
|
||||
imap: {
|
||||
secure: self.imap.secure,
|
||||
requireTLS: self.imap.requireTLS,
|
||||
ignoreTLS: self.imap.ignoreTLS,
|
||||
port: self.imap.port,
|
||||
host: self.imap.host,
|
||||
ca: self.imap.ca,
|
||||
@ -105,6 +107,8 @@ Auth.prototype.getCredentials = function() {
|
||||
},
|
||||
smtp: {
|
||||
secure: self.smtp.secure,
|
||||
requireTLS: self.smtp.requireTLS,
|
||||
ignoreTLS: self.smtp.ignoreTLS,
|
||||
port: self.smtp.port,
|
||||
host: self.smtp.host,
|
||||
ca: self.smtp.ca,
|
||||
@ -240,16 +244,8 @@ Auth.prototype.useOAuth = function(hostname) {
|
||||
Auth.prototype.getOAuthToken = function() {
|
||||
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
|
||||
return self._oauth.getOAuthToken(self.emailAddress).then(onToken);
|
||||
}
|
||||
|
||||
function onToken(oauthToken) {
|
||||
// shortcut if the email address is already known
|
||||
@ -305,7 +301,7 @@ Auth.prototype._loadCredentials = function() {
|
||||
});
|
||||
|
||||
function loadFromDB(key) {
|
||||
return self._appConfigStore.listItems(key, 0, null).then(function(cachedItems) {
|
||||
return self._appConfigStore.listItems(key).then(function(cachedItems) {
|
||||
return cachedItems && cachedItems[0];
|
||||
});
|
||||
}
|
||||
@ -317,7 +313,7 @@ Auth.prototype._loadCredentials = function() {
|
||||
* @param {Function} callback The error handler
|
||||
* @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;
|
||||
|
||||
axe.debug('new ssl certificate received: ' + pemEncodedCert);
|
||||
@ -351,7 +347,7 @@ Auth.prototype.handleCertificateUpdate = function(component, onConnect, callback
|
||||
self[component].ca = pemEncodedCert;
|
||||
self.credentialsDirty = true;
|
||||
self.storeCredentials().then(function() {
|
||||
onConnect(callback);
|
||||
reconnectCallback(callback);
|
||||
}).catch(callback);
|
||||
}
|
||||
});
|
||||
|
@ -83,14 +83,13 @@ DeviceStorage.prototype.removeList = function(type) {
|
||||
|
||||
/**
|
||||
* List stored items of a given type
|
||||
* @param type [String] The type of item e.g. 'email'
|
||||
* @param offset [Number] The offset of items to fetch (0 is the last stored item)
|
||||
* @param num [Number] The number of items to fetch (null means fetch all)
|
||||
* @param {String/Array} query The type of item e.g. 'email'
|
||||
* @param {Boolean} exactMatchOnly Specifies if only exact matches are extracted from the DB as opposed to keys that start with the query
|
||||
* @return {Promise}
|
||||
*/
|
||||
DeviceStorage.prototype.listItems = function(type, offset, num) {
|
||||
// fetch all items of a certain type from the data-store
|
||||
return this._lawnchairDAO.list(type, offset, num);
|
||||
DeviceStorage.prototype.listItems = function(query, exactMatchOnly) {
|
||||
// fetch all items of a certain query from the data-store
|
||||
return this._lawnchairDAO.list(query, exactMatchOnly);
|
||||
};
|
||||
|
||||
/**
|
||||
|
19
src/js/service/hkp.js
Normal file
19
src/js/service/hkp.js
Normal 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);
|
||||
};
|
@ -9,8 +9,10 @@ require('./newsletter');
|
||||
require('./oauth');
|
||||
require('./privatekey');
|
||||
require('./publickey');
|
||||
require('./hkp');
|
||||
require('./admin');
|
||||
require('./lawnchair');
|
||||
require('./devicestorage');
|
||||
require('./auth');
|
||||
require('./keychain');
|
||||
require('./publickey-verifier');
|
@ -8,13 +8,33 @@ module.exports = Invitation;
|
||||
* The Invitation is a high level Data Access Object that access the invitation service REST endpoint.
|
||||
* @param {Object} restDao The REST Data Access Object abstraction
|
||||
*/
|
||||
function Invitation(invitationRestDao) {
|
||||
function Invitation(invitationRestDao, appConfig) {
|
||||
this._restDao = invitationRestDao;
|
||||
this._appConfig = appConfig;
|
||||
}
|
||||
|
||||
//
|
||||
// API
|
||||
//
|
||||
/**
|
||||
* Create the invitation mail object
|
||||
* @param {String} options.sender The sender's email address
|
||||
* @param {String} options.recipient The recipient's email address
|
||||
* @return {Object} The mail object
|
||||
*/
|
||||
Invitation.prototype.createMail = function(options) {
|
||||
var str = this._appConfig.string;
|
||||
|
||||
return {
|
||||
from: [{
|
||||
address: options.sender
|
||||
}],
|
||||
to: [{
|
||||
address: options.recipient
|
||||
}],
|
||||
cc: [],
|
||||
bcc: [],
|
||||
subject: str.invitationSubject,
|
||||
body: str.invitationMessage
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Notes an invite for the recipient by the sender in the invitation web service
|
||||
|
@ -4,12 +4,8 @@ var ngModule = angular.module('woServices');
|
||||
ngModule.service('keychain', Keychain);
|
||||
module.exports = Keychain;
|
||||
|
||||
var util = require('crypto-lib').util;
|
||||
|
||||
var DB_PUBLICKEY = 'publickey',
|
||||
DB_PRIVATEKEY = 'privatekey',
|
||||
DB_DEVICENAME = 'devicename',
|
||||
DB_DEVICE_SECRET = 'devicesecret';
|
||||
DB_PRIVATEKEY = 'privatekey';
|
||||
|
||||
/**
|
||||
* A high-level Data-Access Api for handling Keypair synchronization
|
||||
@ -57,40 +53,6 @@ Keychain.prototype.verifyPublicKey = function(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
|
||||
* @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
|
||||
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) {
|
||||
// the key is present on the server, all is well
|
||||
return localKey;
|
||||
}
|
||||
// the key has changed, update the key
|
||||
return updateKey(localKey);
|
||||
return updateKey(localKey, cloudKey);
|
||||
|
||||
}).catch(function(err) {
|
||||
if (err && err.code === 42) {
|
||||
@ -134,9 +96,7 @@ Keychain.prototype.refreshKeyForUserId = function(options) {
|
||||
});
|
||||
}
|
||||
|
||||
function updateKey(localKey) {
|
||||
// look for an updated key for the user id
|
||||
return self._publicKeyDao.getByUserId(userId).then(function(newKey) {
|
||||
function updateKey(localKey, newKey) {
|
||||
// the public key has changed, we need to ask for permission to update the key
|
||||
if (overridePermission) {
|
||||
// don't query the user, update the public key right away
|
||||
@ -144,14 +104,6 @@ Keychain.prototype.refreshKeyForUserId = function(options) {
|
||||
} else {
|
||||
return requestPermission(localKey, newKey);
|
||||
}
|
||||
|
||||
}).catch(function(err) {
|
||||
// offline?
|
||||
if (err && err.code === 42) {
|
||||
return localKey;
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
function requestPermission(localKey, newKey) {
|
||||
@ -195,15 +147,17 @@ Keychain.prototype.getReceiverPublicKey = function(userId) {
|
||||
var self = this;
|
||||
|
||||
// search local keyring for public key
|
||||
return self._lawnchairDAO.list(DB_PUBLICKEY, 0, null).then(function(allPubkeys) {
|
||||
return self._lawnchairDAO.list(DB_PUBLICKEY).then(function(allPubkeys) {
|
||||
var userIds;
|
||||
// query primary email address
|
||||
var pubkey = _.findWhere(allPubkeys, {
|
||||
userId: userId
|
||||
});
|
||||
// query mutliple userIds (for imported public keys)
|
||||
// query mutliple userIds
|
||||
if (!pubkey) {
|
||||
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
|
||||
});
|
||||
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
|
||||
//
|
||||
@ -639,12 +209,12 @@ Keychain.prototype.getUserKeyPair = function(userId) {
|
||||
var self = this;
|
||||
|
||||
// search for user's public key locally
|
||||
return self._lawnchairDAO.list(DB_PUBLICKEY, 0, null).then(function(allPubkeys) {
|
||||
return self._lawnchairDAO.list(DB_PUBLICKEY).then(function(allPubkeys) {
|
||||
var pubkey = _.findWhere(allPubkeys, {
|
||||
userId: userId
|
||||
});
|
||||
|
||||
if (pubkey && pubkey._id) {
|
||||
if (pubkey && pubkey._id && !pubkey.source) {
|
||||
// that user's public key is already in local storage...
|
||||
// sync keypair to the cloud
|
||||
return syncKeypair(pubkey._id);
|
||||
@ -653,13 +223,13 @@ Keychain.prototype.getUserKeyPair = function(userId) {
|
||||
// no public key by that user id in storage
|
||||
// find from cloud by email address
|
||||
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...
|
||||
// sync keypair to local storage
|
||||
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
|
||||
return self.lookupPublicKey(keypairId).then(function(pub) {
|
||||
savedPubkey = pub;
|
||||
|
||||
// persist private key in local storage
|
||||
return self.lookupPrivateKey(keypairId).then(function(priv) {
|
||||
return self.lookupPrivateKey(keypairId);
|
||||
|
||||
}).then(function(priv) {
|
||||
savedPrivkey = priv;
|
||||
});
|
||||
|
||||
}).then(function() {
|
||||
var keys = {};
|
||||
@ -699,7 +271,7 @@ Keychain.prototype.putUserKeyPair = function(keypair) {
|
||||
// validate input
|
||||
if (!keypair || !keypair.publicKey || !keypair.privateKey || !keypair.publicKey.userId || keypair.publicKey.userId !== keypair.privateKey.userId) {
|
||||
return new Promise(function() {
|
||||
throw new Error('Incorrect input!');
|
||||
throw new Error('Cannot put user key pair: Incorrect input!');
|
||||
});
|
||||
}
|
||||
|
||||
@ -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
|
||||
//
|
||||
@ -752,7 +342,7 @@ Keychain.prototype.lookupPublicKey = function(id) {
|
||||
*/
|
||||
Keychain.prototype.listLocalPublicKeys = function() {
|
||||
// search local keyring for public key
|
||||
return this._lawnchairDAO.list(DB_PUBLICKEY, 0, null);
|
||||
return this._lawnchairDAO.list(DB_PUBLICKEY);
|
||||
};
|
||||
|
||||
Keychain.prototype.removeLocalPublicKey = function(id) {
|
||||
|
@ -104,60 +104,49 @@ LawnchairDAO.prototype.read = function(key) {
|
||||
/**
|
||||
* List all the items of a certain type
|
||||
* @param type [String] The type of item e.g. 'email'
|
||||
* @param offset [Number] The offset of items to fetch (0 is the last stored item)
|
||||
* @param num [Number] The number of items to fetch (null means fetch all)
|
||||
* @return {Promise}
|
||||
*/
|
||||
LawnchairDAO.prototype.list = function(type, offset, num) {
|
||||
LawnchairDAO.prototype.list = function(query, exactMatchOnly) {
|
||||
var self = this;
|
||||
return new Promise(function(resolve) {
|
||||
var i, from, to,
|
||||
matchingKeys = [],
|
||||
intervalKeys = [],
|
||||
list = [];
|
||||
var matchingKeys = [];
|
||||
|
||||
// validate input
|
||||
if (!type || typeof offset === 'undefined' || typeof num === 'undefined') {
|
||||
if ((Array.isArray(query) && query.length === 0) || (!Array.isArray(query) && !query)) {
|
||||
throw new Error('Args not is not set!');
|
||||
}
|
||||
|
||||
// this method operates on arrays of keys, so normalize input 'key' -> ['key']
|
||||
if (!Array.isArray(query)) {
|
||||
query = [query];
|
||||
}
|
||||
|
||||
// get all keys
|
||||
self._db.keys(function(keys) {
|
||||
// check if key begins with type
|
||||
keys.forEach(function(key) {
|
||||
if (key.indexOf(type) === 0) {
|
||||
matchingKeys.push(key);
|
||||
// check if there are keys in the db that start with the respective query
|
||||
matchingKeys = keys.filter(function(key) {
|
||||
return query.filter(function(type) {
|
||||
if (exactMatchOnly) {
|
||||
return key === type;
|
||||
} else {
|
||||
return key.indexOf(type) === 0;
|
||||
}
|
||||
}).length > 0;
|
||||
});
|
||||
|
||||
// sort keys
|
||||
matchingKeys.sort();
|
||||
|
||||
// set window of items to fetch
|
||||
// if num is null, list all items
|
||||
from = (num) ? matchingKeys.length - offset - num : 0;
|
||||
to = matchingKeys.length - 1 - offset;
|
||||
// filter items within requested interval
|
||||
for (i = 0; i < matchingKeys.length; i++) {
|
||||
if (i >= from && i <= to) {
|
||||
intervalKeys.push(matchingKeys[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// return if there are no matching keys
|
||||
if (intervalKeys.length === 0) {
|
||||
resolve(list);
|
||||
if (matchingKeys.length === 0) {
|
||||
// no matching keys, resolve
|
||||
resolve([]);
|
||||
return;
|
||||
}
|
||||
|
||||
// fetch all items from data-store with matching key
|
||||
self._db.get(intervalKeys, function(intervalList) {
|
||||
intervalList.forEach(function(item) {
|
||||
list.push(item.object);
|
||||
// fetch all items from data-store with matching keys
|
||||
self._db.get(matchingKeys, function(intervalList) {
|
||||
var result = intervalList.map(function(item) {
|
||||
return item.object;
|
||||
});
|
||||
|
||||
// return only the interval between offset and num
|
||||
resolve(list);
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -4,96 +4,89 @@ var ngModule = angular.module('woServices');
|
||||
ngModule.service('privateKey', PrivateKey);
|
||||
module.exports = PrivateKey;
|
||||
|
||||
function PrivateKey(privateKeyRestDao) {
|
||||
this._restDao = privateKeyRestDao;
|
||||
var ImapClient = require('imap-client');
|
||||
var util = require('crypto-lib').util;
|
||||
|
||||
var IMAP_KEYS_FOLDER = 'openpgp_keys';
|
||||
var MIME_TYPE = 'application/x.encrypted-pgp-key';
|
||||
var MSG_PART_TYPE_ATTACHMENT = 'attachment';
|
||||
|
||||
function PrivateKey(auth, mailbuild, mailreader, appConfig, pgp, crypto, axe) {
|
||||
this._auth = auth;
|
||||
this._Mailbuild = mailbuild;
|
||||
this._mailreader = mailreader;
|
||||
this._appConfig = appConfig;
|
||||
this._pgp = pgp;
|
||||
this._crypto = crypto;
|
||||
this._axe = axe;
|
||||
}
|
||||
|
||||
//
|
||||
// Device registration functions
|
||||
//
|
||||
|
||||
/**
|
||||
* Request registration of a new device by fetching registration session key.
|
||||
* @param {String} options.userId The user's email address
|
||||
* @param {String} options.deviceName The device's memorable name
|
||||
* @return {Object} {encryptedRegSessionKey:[base64]}
|
||||
* Configure the local imap client used for key-sync with credentials from the auth module.
|
||||
*/
|
||||
PrivateKey.prototype.requestDeviceRegistration = function(options) {
|
||||
PrivateKey.prototype.init = function() {
|
||||
var self = this;
|
||||
return new Promise(function(resolve) {
|
||||
if (!options.userId || !options.deviceName) {
|
||||
throw new Error('Incomplete arguments!');
|
||||
}
|
||||
resolve();
|
||||
|
||||
}).then(function() {
|
||||
var uri = '/device/user/' + options.userId + '/devicename/' + options.deviceName;
|
||||
return self._restDao.post(undefined, uri);
|
||||
return self._auth.getCredentials().then(function(credentials) {
|
||||
// tls socket worker path for multithreaded tls in non-native tls environments
|
||||
credentials.imap.tlsWorkerPath = self._appConfig.config.workerPath + '/tcp-socket-tls-worker.min.js';
|
||||
self._imap = new ImapClient(credentials.imap);
|
||||
self._imap.onError = self._axe.error;
|
||||
// login to the imap server
|
||||
return self._imap.login();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Authenticate device registration by uploading the deviceSecret encrypted with the regSessionKeys.
|
||||
* @param {String} options.userId The user's email address
|
||||
* @param {String} options.deviceName The device's memorable name
|
||||
* @param {String} options.encryptedDeviceSecret The base64 encoded encrypted device secret
|
||||
* @param {String} options.iv The iv used for encryption
|
||||
* Cleanup by logging out of the imap client.
|
||||
*/
|
||||
PrivateKey.prototype.uploadDeviceSecret = function(options) {
|
||||
var self = this;
|
||||
PrivateKey.prototype.destroy = function() {
|
||||
this._imap.logout();
|
||||
// don't wait for logout to complete
|
||||
return new Promise(function(resolve) {
|
||||
if (!options.userId || !options.deviceName || !options.encryptedDeviceSecret || !options.iv) {
|
||||
throw new Error('Incomplete arguments!');
|
||||
}
|
||||
resolve();
|
||||
|
||||
}).then(function() {
|
||||
var uri = '/device/user/' + options.userId + '/devicename/' + options.deviceName;
|
||||
return self._restDao.put(options, uri);
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Private key functions
|
||||
//
|
||||
|
||||
/**
|
||||
* Request authSessionKeys required for upload the encrypted private PGP key.
|
||||
* @param {String} options.userId The user's email address
|
||||
* @return {Object} {sessionId, encryptedAuthSessionKey:[base64 encoded], encryptedChallenge:[base64 encoded]}
|
||||
*/
|
||||
PrivateKey.prototype.requestAuthSessionKey = function(options) {
|
||||
var self = this;
|
||||
return new Promise(function(resolve) {
|
||||
if (!options.userId) {
|
||||
throw new Error('Incomplete arguments!');
|
||||
}
|
||||
resolve();
|
||||
|
||||
}).then(function() {
|
||||
var uri = '/auth/user/' + options.userId;
|
||||
return self._restDao.post(undefined, uri);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Verifiy authentication by uploading the challenge and deviceSecret encrypted with the authSessionKeys as a response.
|
||||
* @param {String} options.userId The user's email address
|
||||
* @param {String} options.encryptedChallenge The server's base64 encoded challenge encrypted using the authSessionKey
|
||||
* @param {String} options.encryptedDeviceSecret The server's base64 encoded deviceSecret encrypted using the authSessionKey
|
||||
* @param {String} options.iv The iv used for encryption
|
||||
* Encrypt and upload the private PGP key to the server.
|
||||
* @param {String} code The randomly generated or self selected code used to derive the key for the encryption of the private PGP key
|
||||
*/
|
||||
PrivateKey.prototype.verifyAuthentication = function(options) {
|
||||
var self = this;
|
||||
return new Promise(function(resolve) {
|
||||
if (!options.userId || !options.sessionId || !options.encryptedChallenge || !options.encryptedDeviceSecret || !options.iv) {
|
||||
throw new Error('Incomplete arguments!');
|
||||
}
|
||||
resolve();
|
||||
PrivateKey.prototype.encrypt = function(code) {
|
||||
var self = this,
|
||||
config = self._appConfig.config,
|
||||
keySize = config.symKeySize,
|
||||
encryptionKey, salt, iv, privkeyId;
|
||||
|
||||
}).then(function() {
|
||||
var uri = '/auth/user/' + options.userId + '/session/' + options.sessionId;
|
||||
return self._restDao.put(options, uri);
|
||||
if (!code) {
|
||||
return new Promise(function() {
|
||||
throw new Error('Incomplete arguments!');
|
||||
});
|
||||
}
|
||||
|
||||
// generate random salt and iv
|
||||
salt = util.random(keySize);
|
||||
iv = util.random(config.symIvSize);
|
||||
|
||||
// derive key from the code using PBKDF2
|
||||
return self._crypto.deriveKey(code, salt, keySize).then(function(key) {
|
||||
encryptionKey = key;
|
||||
|
||||
// get private key from local storage
|
||||
return self._pgp.exportKeys();
|
||||
}).then(function(keypair) {
|
||||
privkeyId = keypair.keyId;
|
||||
|
||||
// encrypt the private key with the derived key
|
||||
return self._crypto.encrypt(keypair.privateKeyArmored, encryptionKey, iv);
|
||||
|
||||
}).then(function(ct) {
|
||||
return {
|
||||
_id: privkeyId,
|
||||
encryptedPrivateKey: ct,
|
||||
salt: salt,
|
||||
iv: iv
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
@ -102,104 +95,304 @@ PrivateKey.prototype.verifyAuthentication = function(options) {
|
||||
* @param {String} options._id The hex encoded capital 16 char key id
|
||||
* @param {String} options.userId The user's email address
|
||||
* @param {String} options.encryptedPrivateKey The base64 encoded encrypted private PGP key
|
||||
* @param {String} options.sessionId The session id
|
||||
*/
|
||||
PrivateKey.prototype.upload = function(options) {
|
||||
var self = this;
|
||||
var self = this,
|
||||
path;
|
||||
|
||||
return new Promise(function(resolve) {
|
||||
if (!options._id || !options.userId || !options.encryptedPrivateKey || !options.sessionId || !options.salt || !options.iv) {
|
||||
throw new Error('Incomplete arguments!');
|
||||
if (!options._id || !options.userId || !options.encryptedPrivateKey || !options.salt || !options.iv) {
|
||||
throw new Error('Incomplete arguments for key upload!');
|
||||
}
|
||||
resolve();
|
||||
|
||||
}).then(function() {
|
||||
var uri = '/privatekey/user/' + options.userId + '/session/' + options.sessionId;
|
||||
return self._restDao.post(options, uri);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Query if an encrypted private PGP key exists on the server without initializing the recovery procedure.
|
||||
* @param {String} options.userId The user's email address
|
||||
* @param {String} options.keyId The private PGP key id
|
||||
* @return {Boolean} whether the key was found on the server or not.
|
||||
*/
|
||||
PrivateKey.prototype.hasPrivateKey = function(options) {
|
||||
var self = this;
|
||||
return new Promise(function(resolve) {
|
||||
if (!options.userId || !options.keyId) {
|
||||
throw new Error('Incomplete arguments!');
|
||||
}
|
||||
resolve();
|
||||
// Some servers (Exchange, Cyrus) error when creating an existing IMAP mailbox instead of
|
||||
// responding with ALREADYEXISTS. Hence we search for the folder before uploading.
|
||||
|
||||
}).then(function() {
|
||||
return self._restDao.get({
|
||||
uri: '/privatekey/user/' + options.userId + '/key/' + options.keyId + '?ignoreRecovery=true',
|
||||
});
|
||||
self._axe.debug('Searching imap folder for key upload...');
|
||||
|
||||
}).then(function() {
|
||||
return true;
|
||||
return self._getFolder().then(function(fullPath) {
|
||||
path = fullPath;
|
||||
}).catch(function() {
|
||||
|
||||
// create imap folder
|
||||
self._axe.debug('Folder not found, creating imap folder.');
|
||||
return self._imap.createFolder({
|
||||
path: IMAP_KEYS_FOLDER
|
||||
}).then(function(fullPath) {
|
||||
path = fullPath;
|
||||
self._axe.debug('Successfully created imap folder ' + path);
|
||||
}).catch(function(err) {
|
||||
// 404: there is no encrypted private key on the server
|
||||
if (err.code && err.code !== 200) {
|
||||
return false;
|
||||
}
|
||||
|
||||
throw err;
|
||||
var prettyErr = new Error('Creating imap folder ' + IMAP_KEYS_FOLDER + ' failed: ' + err.message);
|
||||
self._axe.error(prettyErr);
|
||||
throw prettyErr;
|
||||
});
|
||||
});
|
||||
|
||||
}).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.
|
||||
* @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.
|
||||
* Check if matching private key is stored in IMAP.
|
||||
*/
|
||||
PrivateKey.prototype.requestDownload = function(options) {
|
||||
PrivateKey.prototype.isSynced = function() {
|
||||
var self = this;
|
||||
return new Promise(function(resolve) {
|
||||
if (!options.userId || !options.keyId) {
|
||||
throw new Error('Incomplete arguments!');
|
||||
}
|
||||
resolve();
|
||||
|
||||
}).then(function() {
|
||||
return self._restDao.get({
|
||||
uri: '/privatekey/user/' + options.userId + '/key/' + options.keyId
|
||||
return self._getFolder().then(function(path) {
|
||||
return self._fetchMessage({
|
||||
keyId: self._pgp.getKeyId(),
|
||||
path: path
|
||||
});
|
||||
|
||||
}).then(function() {
|
||||
return true;
|
||||
|
||||
}).catch(function(err) {
|
||||
// 404: there is no encrypted private key on the server
|
||||
if (err.code && err.code !== 200) {
|
||||
}).then(function(msg) {
|
||||
return !!msg;
|
||||
}).catch(function() {
|
||||
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.keyId The private key id
|
||||
* @param {String} options.recoveryToken The token proving the user own the email account
|
||||
* @return {Object} {_id:[hex encoded capital 16 char key id], encryptedPrivateKey:[base64 encoded], encryptedUserId: [base64 encoded]}
|
||||
*/
|
||||
PrivateKey.prototype.download = function(options) {
|
||||
var self = this;
|
||||
return new Promise(function(resolve) {
|
||||
if (!options.userId || !options.keyId || !options.recoveryToken) {
|
||||
throw new Error('Incomplete arguments!');
|
||||
var self = this,
|
||||
path, message;
|
||||
|
||||
return self._getFolder().then(function(fullPath) {
|
||||
path = fullPath;
|
||||
return self._fetchMessage({
|
||||
keyId: options.keyId,
|
||||
path: path
|
||||
}).then(function(msg) {
|
||||
if (!msg) {
|
||||
throw new Error('Private key not synced!');
|
||||
}
|
||||
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() {
|
||||
return self._restDao.get({
|
||||
uri: '/privatekey/user/' + options.userId + '/key/' + options.keyId + '/recovery/' + options.recoveryToken
|
||||
// parse the message
|
||||
return self._parse(message);
|
||||
|
||||
}).then(function(root) {
|
||||
var payloadBuf = filterBodyParts(root, MSG_PART_TYPE_ATTACHMENT)[0].content;
|
||||
var offset = 0;
|
||||
var SALT_LEN = 32;
|
||||
var IV_LEN = 12;
|
||||
|
||||
// check version
|
||||
var version = payloadBuf[offset];
|
||||
offset++;
|
||||
if (version !== 1) {
|
||||
throw new Error('Unsupported key sync protocol version!');
|
||||
}
|
||||
// salt
|
||||
var saltBuf = payloadBuf.subarray(offset, offset + SALT_LEN);
|
||||
offset += SALT_LEN;
|
||||
// iv
|
||||
var ivBuf = payloadBuf.subarray(offset, offset + IV_LEN);
|
||||
offset += IV_LEN;
|
||||
// encrypted private key
|
||||
var encryptedKeyBuf = payloadBuf.subarray(offset, payloadBuf.length);
|
||||
|
||||
return {
|
||||
_id: options.keyId,
|
||||
userId: options.userId,
|
||||
encryptedPrivateKey: util.str2Base64(util.uint8Arr2BinStr(encryptedKeyBuf)),
|
||||
salt: util.str2Base64(util.uint8Arr2BinStr(saltBuf)),
|
||||
iv: util.str2Base64(util.uint8Arr2BinStr(ivBuf))
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* This is called after the encrypted private key has successfully been downloaded and it's ready to be decrypted and stored in localstorage.
|
||||
* @param {String} options._id The private PGP key id
|
||||
* @param {String} options.userId The user's email address
|
||||
* @param {String} options.code The randomly generated or self selected code used to derive the key for the decryption of the private PGP key
|
||||
* @param {String} options.encryptedPrivateKey The encrypted private PGP key
|
||||
* @param {String} options.salt The salt required to derive the code derived key
|
||||
* @param {String} options.iv The iv used to encrypt the private PGP key
|
||||
*/
|
||||
PrivateKey.prototype.decrypt = function(options) {
|
||||
var self = this,
|
||||
config = self._appConfig.config,
|
||||
keySize = config.symKeySize;
|
||||
|
||||
if (!options._id || !options.userId || !options.code || !options.salt || !options.encryptedPrivateKey || !options.iv) {
|
||||
return new Promise(function() {
|
||||
throw new Error('Incomplete arguments!');
|
||||
});
|
||||
}
|
||||
|
||||
// derive key from the code and the salt using PBKDF2
|
||||
return self._crypto.deriveKey(options.code, options.salt, keySize).then(function(derivedKey) {
|
||||
// decrypt the private key with the derived key
|
||||
return self._crypto.decrypt(options.encryptedPrivateKey, derivedKey, options.iv).catch(function() {
|
||||
throw new Error('Invalid backup code!');
|
||||
});
|
||||
|
||||
}).then(function(privateKeyArmored) {
|
||||
// validate pgp key
|
||||
var keyParams;
|
||||
try {
|
||||
keyParams = self._pgp.getKeyParams(privateKeyArmored);
|
||||
} catch (e) {
|
||||
throw new Error('Error parsing private PGP key!');
|
||||
}
|
||||
|
||||
if (keyParams._id !== options._id || keyParams.userId !== options.userId) {
|
||||
throw new Error('Private key parameters don\'t match with public key\'s!');
|
||||
}
|
||||
|
||||
return {
|
||||
_id: options._id,
|
||||
userId: options.userId,
|
||||
encryptedKey: privateKeyArmored
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
PrivateKey.prototype._getFolder = function() {
|
||||
var self = this;
|
||||
|
||||
return self._imap.listWellKnownFolders().then(function(wellKnownFolders) {
|
||||
var paths = []; // gathers paths
|
||||
|
||||
// extract the paths from the folder arrays
|
||||
for (var folderType in wellKnownFolders) {
|
||||
if (wellKnownFolders.hasOwnProperty(folderType) && Array.isArray(wellKnownFolders[folderType])) {
|
||||
paths = paths.concat(_.pluck(wellKnownFolders[folderType], 'path'));
|
||||
}
|
||||
}
|
||||
|
||||
paths = paths.filter(function(path) {
|
||||
// find a folder that ends with IMAP_KEYS_FOLDER
|
||||
var lastIndex = path.lastIndexOf(IMAP_KEYS_FOLDER);
|
||||
return (lastIndex !== -1) && (lastIndex + IMAP_KEYS_FOLDER.length === path.length);
|
||||
});
|
||||
|
||||
if (paths.length > 1) {
|
||||
self._axe.warn('Multiple folders matching path ' + IMAP_KEYS_FOLDER + ' found, PGP key target folder unclear. Picking first one: ' + paths.join(', '));
|
||||
}
|
||||
|
||||
if (paths.length === 0) {
|
||||
throw new Error('Imap folder ' + IMAP_KEYS_FOLDER + ' does not exist for key sync!');
|
||||
}
|
||||
|
||||
return paths[0];
|
||||
});
|
||||
};
|
||||
|
||||
PrivateKey.prototype._fetchMessage = function(options) {
|
||||
var self = this;
|
||||
|
||||
if (!options.keyId) {
|
||||
return new Promise(function() {
|
||||
throw new Error('Incomplete arguments!');
|
||||
});
|
||||
}
|
||||
|
||||
// get the metadata for the message
|
||||
return self._imap.listMessages({
|
||||
path: options.path
|
||||
}).then(function(messages) {
|
||||
if (!messages.length) {
|
||||
// message has been deleted in the meantime
|
||||
return;
|
||||
}
|
||||
|
||||
// get matching private key if multiple keys uloaded
|
||||
return _.findWhere(messages, {
|
||||
subject: options.keyId
|
||||
});
|
||||
}).catch(function(e) {
|
||||
throw new Error('Failed to retrieve PGP key message from IMAP! Reason: ' + e.message);
|
||||
});
|
||||
};
|
||||
|
||||
PrivateKey.prototype._parse = function(options) {
|
||||
var self = this;
|
||||
return new Promise(function(resolve, reject) {
|
||||
self._mailreader.parse(options, function(err, root) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(root);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function that recursively traverses the body parts tree. Looks for bodyParts that match the provided type and aggregates them
|
||||
*
|
||||
* @param {Array} bodyParts The bodyParts array
|
||||
* @param {String} type The type to look up
|
||||
* @param {undefined} result Leave undefined, only used for recursion
|
||||
*/
|
||||
function filterBodyParts(bodyParts, type, result) {
|
||||
result = result || [];
|
||||
bodyParts.forEach(function(part) {
|
||||
if (part.type === type) {
|
||||
result.push(part);
|
||||
} else if (Array.isArray(part.content)) {
|
||||
filterBodyParts(part.content, type, result);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
247
src/js/service/publickey-verifier.js
Normal file
247
src/js/service/publickey-verifier.js
Normal 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;
|
||||
}
|
@ -5,21 +5,14 @@ var ngModule = angular.module('woServices');
|
||||
// rest dao for use in the public key service
|
||||
ngModule.factory('publicKeyRestDao', function(appConfig) {
|
||||
var dao = new RestDAO();
|
||||
dao.setBaseUri(appConfig.config.cloudUrl);
|
||||
return dao;
|
||||
});
|
||||
|
||||
// rest dao for use in the private key service
|
||||
ngModule.factory('privateKeyRestDao', function(appConfig) {
|
||||
var dao = new RestDAO();
|
||||
dao.setBaseUri(appConfig.config.privkeyServerUrl);
|
||||
dao.setBaseUri(appConfig.config.keyServerUrl);
|
||||
return dao;
|
||||
});
|
||||
|
||||
// rest dao for use in the invitation service
|
||||
ngModule.factory('invitationRestDao', function(appConfig) {
|
||||
var dao = new RestDAO();
|
||||
dao.setBaseUri(appConfig.config.cloudUrl);
|
||||
dao.setBaseUri(appConfig.config.keyServerUrl);
|
||||
return dao;
|
||||
});
|
||||
|
||||
@ -62,11 +55,12 @@ RestDAO.prototype.get = function(options) {
|
||||
/**
|
||||
* POST (create) request
|
||||
*/
|
||||
RestDAO.prototype.post = function(item, uri) {
|
||||
RestDAO.prototype.post = function(item, uri, type) {
|
||||
return this._processRequest({
|
||||
method: 'POST',
|
||||
payload: item,
|
||||
uri: uri
|
||||
uri: uri,
|
||||
type: type
|
||||
});
|
||||
};
|
||||
|
||||
@ -98,62 +92,65 @@ RestDAO.prototype.remove = function(uri) {
|
||||
RestDAO.prototype._processRequest = function(options) {
|
||||
var self = this;
|
||||
return new Promise(function(resolve, reject) {
|
||||
var xhr, format;
|
||||
var xhr, format, accept, payload;
|
||||
|
||||
if (typeof options.uri === 'undefined') {
|
||||
throw {
|
||||
code: 400,
|
||||
message: 'Bad Request! URI is a mandatory parameter.'
|
||||
};
|
||||
throw createError(400, 'Bad Request! URI is a mandatory parameter.');
|
||||
}
|
||||
|
||||
options.type = options.type || 'json';
|
||||
payload = options.payload;
|
||||
|
||||
if (options.type === 'json') {
|
||||
format = 'application/json';
|
||||
payload = payload ? JSON.stringify(payload) : undefined;
|
||||
} else if (options.type === 'xml') {
|
||||
format = 'application/xml';
|
||||
} else if (options.type === 'text') {
|
||||
format = 'text/plain';
|
||||
} else if (options.type === 'form') {
|
||||
format = 'application/x-www-form-urlencoded; charset=UTF-8';
|
||||
accept = 'text/html; charset=UTF-8';
|
||||
} else {
|
||||
throw {
|
||||
code: 400,
|
||||
message: 'Bad Request! Unhandled data type.'
|
||||
};
|
||||
throw createError(400, 'Bad Request! Unhandled data type.');
|
||||
}
|
||||
|
||||
xhr = new XMLHttpRequest();
|
||||
xhr.open(options.method, self._baseUri + options.uri);
|
||||
xhr.setRequestHeader('Accept', format);
|
||||
xhr.setRequestHeader('Accept', accept || format);
|
||||
xhr.setRequestHeader('Content-Type', format);
|
||||
|
||||
xhr.onload = function() {
|
||||
var res;
|
||||
|
||||
if (xhr.readyState === 4 && (xhr.status === 200 || xhr.status === 201 || xhr.status === 304)) {
|
||||
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 {
|
||||
res = xhr.responseText;
|
||||
}
|
||||
|
||||
if (xhr.readyState === 4 && (xhr.status === 200 || xhr.status === 201 || xhr.status === 304)) {
|
||||
resolve(res);
|
||||
return;
|
||||
}
|
||||
|
||||
reject({
|
||||
code: xhr.status,
|
||||
message: xhr.statusText
|
||||
});
|
||||
reject(createError(xhr.status, (res && res.error) || xhr.statusText));
|
||||
};
|
||||
|
||||
xhr.onerror = function() {
|
||||
reject({
|
||||
code: 42,
|
||||
message: 'Error calling ' + options.method + ' on ' + options.uri
|
||||
});
|
||||
reject(createError(42, 'Error calling ' + options.method + ' on ' + options.uri));
|
||||
};
|
||||
|
||||
xhr.send(options.payload ? JSON.stringify(options.payload) : undefined);
|
||||
xhr.send(payload);
|
||||
});
|
||||
};
|
||||
|
||||
function createError(code, message) {
|
||||
var error = new Error(message);
|
||||
error.code = code;
|
||||
return error;
|
||||
}
|
@ -53,6 +53,7 @@ ConnectionDoctor.prototype.configure = function(credentials) {
|
||||
port: this.credentials.imap.port,
|
||||
secure: this.credentials.imap.secure,
|
||||
ignoreTLS: this.credentials.imap.ignoreTLS,
|
||||
requireTLS: this.credentials.imap.requireTLS,
|
||||
ca: this.credentials.imap.ca,
|
||||
tlsWorkerPath: this._workerPath,
|
||||
auth: {
|
||||
@ -65,6 +66,7 @@ ConnectionDoctor.prototype.configure = function(credentials) {
|
||||
this._smtp = new SmtpClient(this.credentials.smtp.host, this.credentials.smtp.port, {
|
||||
useSecureTransport: this.credentials.smtp.secure,
|
||||
ignoreTLS: this.credentials.smtp.ignoreTLS,
|
||||
requireTLS: this.credentials.smtp.requireTLS,
|
||||
ca: this.credentials.smtp.ca,
|
||||
tlsWorkerPath: this._workerPath,
|
||||
auth: {
|
||||
@ -218,25 +220,21 @@ ConnectionDoctor.prototype._checkImap = function() {
|
||||
}
|
||||
};
|
||||
|
||||
self._imap.login(function() {
|
||||
self._imap.login().then(function() {
|
||||
loggedIn = true;
|
||||
|
||||
self._imap.listWellKnownFolders(function(error, wellKnownFolders) {
|
||||
if (error) {
|
||||
reject(createError(GENERIC_ERROR, str.connDocGenericError.replace('{0}', host).replace('{1}', error.message), error));
|
||||
return;
|
||||
}
|
||||
|
||||
return self._imap.listWellKnownFolders();
|
||||
}).then(function(wellKnownFolders) {
|
||||
if (wellKnownFolders.Inbox.length === 0) {
|
||||
// the client needs at least an inbox folder to work properly
|
||||
reject(createError(NO_INBOX, str.connDocNoInbox.replace('{0}', host)));
|
||||
return;
|
||||
}
|
||||
|
||||
self._imap.logout(function() {
|
||||
return self._imap.logout();
|
||||
}).then(function(){
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}).catch(function(error) {
|
||||
reject(createError(GENERIC_ERROR, str.connDocGenericError.replace('{0}', host).replace('{1}', error.message), error));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
@ -25,10 +25,10 @@ Download.prototype.createDownload = function(options) {
|
||||
supportsBlob = !!new Blob();
|
||||
} catch (e) {}
|
||||
|
||||
if (typeof a.download !== "undefined" && supportsBlob) {
|
||||
if (typeof a.download !== 'undefined' && supportsBlob) {
|
||||
// ff 30+, chrome 27+ (android: 37+)
|
||||
document.body.appendChild(a);
|
||||
a.style = "display: none";
|
||||
a.style.display = 'none';
|
||||
a.href = window.URL.createObjectURL(new Blob([content], {
|
||||
type: contentType
|
||||
}));
|
||||
@ -52,15 +52,15 @@ Download.prototype.createDownload = function(options) {
|
||||
var url = window.URL.createObjectURL(new Blob([content], {
|
||||
type: contentType
|
||||
}));
|
||||
var newTab = window.open(url, "_blank");
|
||||
var newTab = window.open(url, '_blank');
|
||||
if (!newTab) {
|
||||
window.location.href = url;
|
||||
}
|
||||
} else {
|
||||
// anything else, where anything at all is better than nothing
|
||||
if (typeof content !== "string" && content.buffer) {
|
||||
if (typeof content !== 'string' && content.buffer) {
|
||||
content = util.arrBuf2BinStr(content.buffer);
|
||||
}
|
||||
window.open('data:' + contentType + ';base64,' + btoa(content), "_blank");
|
||||
window.open('data:' + contentType + ';base64,' + btoa(content), '_blank');
|
||||
}
|
||||
};
|
@ -45,6 +45,27 @@ Dummy.prototype.listFolders = function() {
|
||||
name: 'Junk',
|
||||
count: 0,
|
||||
path: 'JUNK'
|
||||
}, {
|
||||
name: 'Foo',
|
||||
count: 0,
|
||||
path: 'FOO'
|
||||
}, {
|
||||
name: 'Snafu',
|
||||
count: 0,
|
||||
path: 'SNAFU'
|
||||
}, {
|
||||
name: 'Tralalalala',
|
||||
count: 0,
|
||||
path: 'TRALALALALA'
|
||||
}, {
|
||||
name: 'Another one',
|
||||
count: 0,
|
||||
path: 'ANOTHERONE'
|
||||
}, {
|
||||
name: 'Mucho Folder',
|
||||
count: 0,
|
||||
path: 'MUCHOFOLDER'
|
||||
|
||||
}];
|
||||
|
||||
return dummies;
|
||||
@ -101,9 +122,11 @@ Dummy.prototype.listMails = function() {
|
||||
'>> from 0.7.0.1\n' +
|
||||
'>>\n' +
|
||||
'>> God speed!'; // plaintext body
|
||||
//this.html = '<!DOCTYPE html><html><head></head><body><h1 style="border: 1px solid red; width: 500px;">Hello there' + Math.random() + '</h1></body></html>';
|
||||
//this.html = '<!DOCTYPE html><html><head></head><body><h1 style="border: 1px solid red; width: 500px; margin:0;">Hello there' + Math.random() + '</h1></body></html>';
|
||||
this.encrypted = true;
|
||||
this.decrypted = true;
|
||||
this.signed = true;
|
||||
this.signaturesValid = true;
|
||||
};
|
||||
|
||||
var dummies = [],
|
||||
|
@ -4,8 +4,9 @@ var ngModule = angular.module('woUtil');
|
||||
ngModule.service('notification', Notif);
|
||||
module.exports = Notif;
|
||||
|
||||
function Notif(appConfig) {
|
||||
function Notif(appConfig, axe) {
|
||||
this._appConfig = appConfig;
|
||||
this._axe = axe;
|
||||
|
||||
if (window.Notification) {
|
||||
this.hasPermission = Notification.permission === "granted";
|
||||
@ -39,10 +40,17 @@ Notif.prototype.create = function(options) {
|
||||
});
|
||||
}
|
||||
|
||||
var notification = new Notification(options.title, {
|
||||
var notification;
|
||||
try {
|
||||
notification = new Notification(options.title, {
|
||||
body: options.message,
|
||||
icon: self._appConfig.config.iconPath
|
||||
});
|
||||
} catch (err) {
|
||||
self._axe.error('Displaying notification failed: ' + err.message);
|
||||
return;
|
||||
}
|
||||
|
||||
notification.onclick = function() {
|
||||
window.focus();
|
||||
options.onClick();
|
||||
|
@ -10,7 +10,8 @@ var axe = require('axe-logger'),
|
||||
updateV2 = require('./update-v2'),
|
||||
updateV3 = require('./update-v3'),
|
||||
updateV4 = require('./update-v4'),
|
||||
updateV5 = require('./update-v5');
|
||||
updateV5 = require('./update-v5'),
|
||||
updateV6 = require('./update-v6');
|
||||
|
||||
/**
|
||||
* Handles database migration
|
||||
@ -18,7 +19,7 @@ var axe = require('axe-logger'),
|
||||
function UpdateHandler(appConfigStore, accountStore, auth, dialog) {
|
||||
this._appConfigStorage = appConfigStore;
|
||||
this._userStorage = accountStore;
|
||||
this._updateScripts = [updateV1, updateV2, updateV3, updateV4, updateV5];
|
||||
this._updateScripts = [updateV1, updateV2, updateV3, updateV4, updateV5, updateV6];
|
||||
this._auth = auth;
|
||||
this._dialog = dialog;
|
||||
}
|
||||
@ -32,7 +33,7 @@ UpdateHandler.prototype.update = function() {
|
||||
targetVersion = cfg.dbVersion,
|
||||
versionDbType = 'dbVersion';
|
||||
|
||||
return self._appConfigStorage.listItems(versionDbType, 0, null).then(function(items) {
|
||||
return self._appConfigStorage.listItems(versionDbType).then(function(items) {
|
||||
// parse the database version number
|
||||
if (items && items.length > 0) {
|
||||
currentVersion = parseInt(items[0], 10);
|
||||
|
@ -66,7 +66,7 @@ function update(options) {
|
||||
});
|
||||
|
||||
function loadFromDB(key) {
|
||||
return options.appConfigStorage.listItems(key, 0, null).then(function(cachedItems) {
|
||||
return options.appConfigStorage.listItems(key).then(function(cachedItems) {
|
||||
return cachedItems && cachedItems[0];
|
||||
});
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ var POST_UPDATE_DB_VERSION = 5;
|
||||
*/
|
||||
function update(options) {
|
||||
// remove the emails
|
||||
return options.userStorage.listItems(FOLDER_DB_TYPE, 0, null).then(function(stored) {
|
||||
return options.userStorage.listItems(FOLDER_DB_TYPE).then(function(stored) {
|
||||
var folders = stored[0] || [];
|
||||
[FOLDER_TYPE_INBOX, FOLDER_TYPE_SENT, FOLDER_TYPE_DRAFTS, FOLDER_TYPE_TRASH].forEach(function(mbxType) {
|
||||
var foldersForType = folders.filter(function(mbx) {
|
||||
|
18
src/js/util/update/update-v6.js
Normal file
18
src/js/util/update/update-v6.js
Normal 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;
|
10
src/lib/angular/angular-animate.js
vendored
10
src/lib/angular/angular-animate.js
vendored
@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @license AngularJS v1.3.7
|
||||
* @license AngularJS v1.3.15
|
||||
* (c) 2010-2014 Google, Inc. http://angularjs.org
|
||||
* License: MIT
|
||||
*/
|
||||
@ -839,7 +839,8 @@ angular.module('ngAnimate', ['ng'])
|
||||
* promise that was returned when the animation was started.
|
||||
*
|
||||
* ```js
|
||||
* var promise = $animate.addClass(element, 'super-long-animation').then(function() {
|
||||
* var promise = $animate.addClass(element, 'super-long-animation');
|
||||
* promise.then(function() {
|
||||
* //this will still be called even if cancelled
|
||||
* });
|
||||
*
|
||||
@ -1332,8 +1333,7 @@ angular.module('ngAnimate', ['ng'])
|
||||
} else if (lastAnimation.event == 'setClass') {
|
||||
animationsToCancel.push(lastAnimation);
|
||||
cleanup(element, className);
|
||||
}
|
||||
else if (runningAnimations[className]) {
|
||||
} else if (runningAnimations[className]) {
|
||||
var current = runningAnimations[className];
|
||||
if (current.event == animationEvent) {
|
||||
skipAnimation = true;
|
||||
@ -1874,7 +1874,7 @@ angular.module('ngAnimate', ['ng'])
|
||||
return;
|
||||
}
|
||||
|
||||
if (!staggerTime && styles) {
|
||||
if (!staggerTime && styles && Object.keys(styles).length > 0) {
|
||||
if (!timings.transitionDuration) {
|
||||
element.css('transition', timings.animationDuration + 's linear all');
|
||||
appliedStyles.push('transition');
|
||||
|
138
src/lib/angular/angular-mocks.js
vendored
138
src/lib/angular/angular-mocks.js
vendored
@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @license AngularJS v1.3.7
|
||||
* @license AngularJS v1.3.15
|
||||
* (c) 2010-2014 Google, Inc. http://angularjs.org
|
||||
* License: MIT
|
||||
*/
|
||||
@ -250,31 +250,31 @@ angular.mock.$ExceptionHandlerProvider = function() {
|
||||
*
|
||||
* @param {string} mode Mode of operation, defaults to `rethrow`.
|
||||
*
|
||||
* - `rethrow`: If any errors are passed to the handler in tests, it typically means that there
|
||||
* is a bug in the application or test, so this mock will make these tests fail.
|
||||
* - `log`: Sometimes it is desirable to test that an error is thrown, for this case the `log`
|
||||
* mode stores an array of errors in `$exceptionHandler.errors`, to allow later
|
||||
* assertion of them. See {@link ngMock.$log#assertEmpty assertEmpty()} and
|
||||
* {@link ngMock.$log#reset reset()}
|
||||
* - `rethrow`: If any errors are passed to the handler in tests, it typically means that there
|
||||
* is a bug in the application or test, so this mock will make these tests fail.
|
||||
* For any implementations that expect exceptions to be thrown, the `rethrow` mode
|
||||
* will also maintain a log of thrown errors.
|
||||
*/
|
||||
this.mode = function(mode) {
|
||||
switch (mode) {
|
||||
case 'rethrow':
|
||||
handler = function(e) {
|
||||
throw e;
|
||||
};
|
||||
break;
|
||||
case 'log':
|
||||
var errors = [];
|
||||
|
||||
switch (mode) {
|
||||
case 'log':
|
||||
case 'rethrow':
|
||||
var errors = [];
|
||||
handler = function(e) {
|
||||
if (arguments.length == 1) {
|
||||
errors.push(e);
|
||||
} else {
|
||||
errors.push([].slice.call(arguments, 0));
|
||||
}
|
||||
if (mode === "rethrow") {
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
handler.errors = errors;
|
||||
break;
|
||||
default:
|
||||
@ -1283,7 +1283,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
|
||||
* @param {string|RegExp|function(string)} url HTTP url or function that receives the url
|
||||
* and returns true if the url match the current definition.
|
||||
* @param {(Object|function(Object))=} headers HTTP headers.
|
||||
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
|
||||
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
|
||||
* request is handled. You can save this object for later use and invoke `respond` again in
|
||||
* order to change how a matched request is handled.
|
||||
*/
|
||||
@ -1297,7 +1297,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
|
||||
* @param {string|RegExp|function(string)} url HTTP url or function that receives the url
|
||||
* and returns true if the url match the current definition.
|
||||
* @param {(Object|function(Object))=} headers HTTP headers.
|
||||
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
|
||||
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
|
||||
* request is handled. You can save this object for later use and invoke `respond` again in
|
||||
* order to change how a matched request is handled.
|
||||
*/
|
||||
@ -1311,7 +1311,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
|
||||
* @param {string|RegExp|function(string)} url HTTP url or function that receives the url
|
||||
* and returns true if the url match the current definition.
|
||||
* @param {(Object|function(Object))=} headers HTTP headers.
|
||||
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
|
||||
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
|
||||
* request is handled. You can save this object for later use and invoke `respond` again in
|
||||
* order to change how a matched request is handled.
|
||||
*/
|
||||
@ -1327,7 +1327,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
|
||||
* @param {(string|RegExp|function(string))=} data HTTP request body or function that receives
|
||||
* data string and returns true if the data is as expected.
|
||||
* @param {(Object|function(Object))=} headers HTTP headers.
|
||||
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
|
||||
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
|
||||
* request is handled. You can save this object for later use and invoke `respond` again in
|
||||
* order to change how a matched request is handled.
|
||||
*/
|
||||
@ -1343,7 +1343,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
|
||||
* @param {(string|RegExp|function(string))=} data HTTP request body or function that receives
|
||||
* data string and returns true if the data is as expected.
|
||||
* @param {(Object|function(Object))=} headers HTTP headers.
|
||||
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
|
||||
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
|
||||
* request is handled. You can save this object for later use and invoke `respond` again in
|
||||
* order to change how a matched request is handled.
|
||||
*/
|
||||
@ -1356,7 +1356,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
|
||||
*
|
||||
* @param {string|RegExp|function(string)} url HTTP url or function that receives the url
|
||||
* and returns true if the url match the current definition.
|
||||
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
|
||||
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
|
||||
* request is handled. You can save this object for later use and invoke `respond` again in
|
||||
* order to change how a matched request is handled.
|
||||
*/
|
||||
@ -1377,7 +1377,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
|
||||
* is in JSON format.
|
||||
* @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
|
||||
* object and returns true if the headers match the current expectation.
|
||||
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
|
||||
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
|
||||
* request is handled. You can save this object for later use and invoke `respond` again in
|
||||
* order to change how a matched request is handled.
|
||||
*
|
||||
@ -1412,7 +1412,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
|
||||
* @param {string|RegExp|function(string)} url HTTP url or function that receives the url
|
||||
* and returns true if the url match the current definition.
|
||||
* @param {Object=} headers HTTP headers.
|
||||
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
|
||||
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
|
||||
* request is handled. You can save this object for later use and invoke `respond` again in
|
||||
* order to change how a matched request is handled. See #expect for more info.
|
||||
*/
|
||||
@ -1426,7 +1426,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
|
||||
* @param {string|RegExp|function(string)} url HTTP url or function that receives the url
|
||||
* and returns true if the url match the current definition.
|
||||
* @param {Object=} headers HTTP headers.
|
||||
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
|
||||
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
|
||||
* request is handled. You can save this object for later use and invoke `respond` again in
|
||||
* order to change how a matched request is handled.
|
||||
*/
|
||||
@ -1440,7 +1440,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
|
||||
* @param {string|RegExp|function(string)} url HTTP url or function that receives the url
|
||||
* and returns true if the url match the current definition.
|
||||
* @param {Object=} headers HTTP headers.
|
||||
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
|
||||
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
|
||||
* request is handled. You can save this object for later use and invoke `respond` again in
|
||||
* order to change how a matched request is handled.
|
||||
*/
|
||||
@ -1457,7 +1457,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
|
||||
* receives data string and returns true if the data is as expected, or Object if request body
|
||||
* is in JSON format.
|
||||
* @param {Object=} headers HTTP headers.
|
||||
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
|
||||
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
|
||||
* request is handled. You can save this object for later use and invoke `respond` again in
|
||||
* order to change how a matched request is handled.
|
||||
*/
|
||||
@ -1474,7 +1474,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
|
||||
* receives data string and returns true if the data is as expected, or Object if request body
|
||||
* is in JSON format.
|
||||
* @param {Object=} headers HTTP headers.
|
||||
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
|
||||
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
|
||||
* request is handled. You can save this object for later use and invoke `respond` again in
|
||||
* order to change how a matched request is handled.
|
||||
*/
|
||||
@ -1491,7 +1491,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
|
||||
* receives data string and returns true if the data is as expected, or Object if request body
|
||||
* is in JSON format.
|
||||
* @param {Object=} headers HTTP headers.
|
||||
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
|
||||
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
|
||||
* request is handled. You can save this object for later use and invoke `respond` again in
|
||||
* order to change how a matched request is handled.
|
||||
*/
|
||||
@ -1504,7 +1504,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
|
||||
*
|
||||
* @param {string|RegExp|function(string)} url HTTP url or function that receives the url
|
||||
* and returns true if the url match the current definition.
|
||||
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
|
||||
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
|
||||
* request is handled. You can save this object for later use and invoke `respond` again in
|
||||
* order to change how a matched request is handled.
|
||||
*/
|
||||
@ -1809,6 +1809,77 @@ angular.mock.$RootElementProvider = function() {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @ngdoc service
|
||||
* @name $controller
|
||||
* @description
|
||||
* A decorator for {@link ng.$controller} with additional `bindings` parameter, useful when testing
|
||||
* controllers of directives that use {@link $compile#-bindtocontroller- `bindToController`}.
|
||||
*
|
||||
*
|
||||
* ## Example
|
||||
*
|
||||
* ```js
|
||||
*
|
||||
* // Directive definition ...
|
||||
*
|
||||
* myMod.directive('myDirective', {
|
||||
* controller: 'MyDirectiveController',
|
||||
* bindToController: {
|
||||
* name: '@'
|
||||
* }
|
||||
* });
|
||||
*
|
||||
*
|
||||
* // Controller definition ...
|
||||
*
|
||||
* myMod.controller('MyDirectiveController', ['log', function($log) {
|
||||
* $log.info(this.name);
|
||||
* })];
|
||||
*
|
||||
*
|
||||
* // In a test ...
|
||||
*
|
||||
* describe('myDirectiveController', function() {
|
||||
* it('should write the bound name to the log', inject(function($controller, $log) {
|
||||
* var ctrl = $controller('MyDirective', { /* no locals */ }, { name: 'Clark Kent' });
|
||||
* expect(ctrl.name).toEqual('Clark Kent');
|
||||
* expect($log.info.logs).toEqual(['Clark Kent']);
|
||||
* });
|
||||
* });
|
||||
*
|
||||
* ```
|
||||
*
|
||||
* @param {Function|string} constructor If called with a function then it's considered to be the
|
||||
* controller constructor function. Otherwise it's considered to be a string which is used
|
||||
* to retrieve the controller constructor using the following steps:
|
||||
*
|
||||
* * check if a controller with given name is registered via `$controllerProvider`
|
||||
* * check if evaluating the string on the current scope returns a constructor
|
||||
* * if $controllerProvider#allowGlobals, check `window[constructor]` on the global
|
||||
* `window` object (not recommended)
|
||||
*
|
||||
* The string can use the `controller as property` syntax, where the controller instance is published
|
||||
* as the specified property on the `scope`; the `scope` must be injected into `locals` param for this
|
||||
* to work correctly.
|
||||
*
|
||||
* @param {Object} locals Injection locals for Controller.
|
||||
* @param {Object=} bindings Properties to add to the controller before invoking the constructor. This is used
|
||||
* to simulate the `bindToController` feature and simplify certain kinds of tests.
|
||||
* @return {Object} Instance of given controller.
|
||||
*/
|
||||
angular.mock.$ControllerDecorator = ['$delegate', function($delegate) {
|
||||
return function(expression, locals, later, ident) {
|
||||
if (later && typeof later === 'object') {
|
||||
var create = $delegate(expression, locals, true, ident);
|
||||
angular.extend(create.instance, later);
|
||||
return create();
|
||||
}
|
||||
return $delegate(expression, locals, later, ident);
|
||||
};
|
||||
}];
|
||||
|
||||
|
||||
/**
|
||||
* @ngdoc module
|
||||
* @name ngMock
|
||||
@ -1837,6 +1908,7 @@ angular.module('ngMock', ['ng']).provider({
|
||||
$provide.decorator('$$rAF', angular.mock.$RAFDecorator);
|
||||
$provide.decorator('$$asyncCallback', angular.mock.$AsyncCallbackDecorator);
|
||||
$provide.decorator('$rootScope', angular.mock.$RootScopeDecorator);
|
||||
$provide.decorator('$controller', angular.mock.$ControllerDecorator);
|
||||
}]);
|
||||
|
||||
/**
|
||||
@ -2134,18 +2206,32 @@ angular.mock.$RootScopeDecorator = ['$delegate', function($delegate) {
|
||||
if (window.jasmine || window.mocha) {
|
||||
|
||||
var currentSpec = null,
|
||||
annotatedFunctions = [],
|
||||
isSpecRunning = function() {
|
||||
return !!currentSpec;
|
||||
};
|
||||
|
||||
angular.mock.$$annotate = angular.injector.$$annotate;
|
||||
angular.injector.$$annotate = function(fn) {
|
||||
if (typeof fn === 'function' && !fn.$inject) {
|
||||
annotatedFunctions.push(fn);
|
||||
}
|
||||
return angular.mock.$$annotate.apply(this, arguments);
|
||||
};
|
||||
|
||||
|
||||
(window.beforeEach || window.setup)(function() {
|
||||
annotatedFunctions = [];
|
||||
currentSpec = this;
|
||||
});
|
||||
|
||||
(window.afterEach || window.teardown)(function() {
|
||||
var injector = currentSpec.$injector;
|
||||
|
||||
annotatedFunctions.forEach(function(fn) {
|
||||
delete fn.$inject;
|
||||
});
|
||||
|
||||
angular.forEach(currentSpec.$modules, function(module) {
|
||||
if (module && module.$$hashKey) {
|
||||
module.$$hashKey = undefined;
|
||||
|
16
src/lib/angular/angular-route.js
vendored
16
src/lib/angular/angular-route.js
vendored
@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @license AngularJS v1.3.7
|
||||
* @license AngularJS v1.3.15
|
||||
* (c) 2010-2014 Google, Inc. http://angularjs.org
|
||||
* License: MIT
|
||||
*/
|
||||
@ -482,21 +482,15 @@ function $RouteProvider() {
|
||||
* definitions will be interpolated into the location's path, while
|
||||
* remaining properties will be treated as query params.
|
||||
*
|
||||
* @param {Object} newParams mapping of URL parameter names to values
|
||||
* @param {!Object<string, string>} newParams mapping of URL parameter names to values
|
||||
*/
|
||||
updateParams: function(newParams) {
|
||||
if (this.current && this.current.$$route) {
|
||||
var searchParams = {}, self=this;
|
||||
|
||||
angular.forEach(Object.keys(newParams), function(key) {
|
||||
if (!self.current.pathParams[key]) searchParams[key] = newParams[key];
|
||||
});
|
||||
|
||||
newParams = angular.extend({}, this.current.params, newParams);
|
||||
$location.path(interpolate(this.current.$$route.originalPath, newParams));
|
||||
$location.search(angular.extend({}, $location.search(), searchParams));
|
||||
}
|
||||
else {
|
||||
// interpolate modifies newParams, only query params are left
|
||||
$location.search(newParams);
|
||||
} else {
|
||||
throw $routeMinErr('norout', 'Tried updating route when with no current route');
|
||||
}
|
||||
}
|
||||
|
4014
src/lib/angular/angular.js
vendored
4014
src/lib/angular/angular.js
vendored
File diff suppressed because it is too large
Load Diff
2
src/lib/forge/forge.min.js
vendored
2
src/lib/forge/forge.min.js
vendored
File diff suppressed because one or more lines are too long
8
src/lib/openpgp/openpgp.min.js
vendored
8
src/lib/openpgp/openpgp.min.js
vendored
File diff suppressed because one or more lines are too long
1
src/lib/openpgp/openpgp.worker.min.js
vendored
1
src/lib/openpgp/openpgp.worker.min.js
vendored
@ -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]);
|
6
src/lib/underscore/underscore-min.js
vendored
6
src/lib/underscore/underscore-min.js
vendored
File diff suppressed because one or more lines are too long
@ -12,7 +12,6 @@
|
||||
"unlimitedStorage",
|
||||
"notifications",
|
||||
"https://keys-test.whiteout.io/",
|
||||
"https://keychain-test.whiteout.io/",
|
||||
"https://settings.whiteout.io/",
|
||||
"https://admin-node.whiteout.io/",
|
||||
"https://www.googleapis.com/",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"packageId": "io.whiteout.WhiteoutMail",
|
||||
"versionCode": 12,
|
||||
"versionCode": 28,
|
||||
"CFBundleVersion": "1",
|
||||
|
||||
"ios": {
|
||||
|
@ -1,10 +1,11 @@
|
||||
{
|
||||
"name": "Whiteout Mail",
|
||||
"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",
|
||||
"icons": {
|
||||
"128": "/img/icon-128.png"
|
||||
"128": "/img/icon-128-chrome.png",
|
||||
"196": "/img/icon-196-universal.png"
|
||||
},
|
||||
"developer": {
|
||||
"name": "Whiteout Networks GmbH",
|
||||
|
@ -22,7 +22,7 @@ $color-red-light: #ff878d;
|
||||
$color-grey: #666;
|
||||
$color-grey-input: #949494;
|
||||
$color-grey-dark: #333;
|
||||
$color-grey-medium: #999;
|
||||
$color-grey-medium: #888;
|
||||
$color-grey-light: #ccc;
|
||||
$color-grey-lighter: #ddd;
|
||||
$color-grey-lighterer: #f4f4f4;
|
||||
|
@ -4,7 +4,12 @@
|
||||
left: -9999px;
|
||||
display: block;
|
||||
z-index: 9000;
|
||||
max-height: 400px;
|
||||
max-width: 300px;
|
||||
min-width: 150px;
|
||||
overflow-y: auto;
|
||||
// allow scrolling on iOS
|
||||
-webkit-overflow-scrolling: touch;
|
||||
text-align: left;
|
||||
font-size: $font-size-base;
|
||||
background: $color-bg;
|
||||
@ -39,6 +44,9 @@
|
||||
padding: 0.5em 15px 0.5em 15px;
|
||||
color: $color-main;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
& > svg {
|
||||
display: inline-block;
|
||||
|
@ -10,10 +10,6 @@
|
||||
margin-bottom: 10px;
|
||||
color: $color-error;
|
||||
}
|
||||
&__password-strong-message {
|
||||
margin-bottom: 10px;
|
||||
color: green;
|
||||
}
|
||||
|
||||
&__row {
|
||||
margin-bottom: 10px;
|
||||
@ -105,6 +101,7 @@
|
||||
font-size: $font-size-base;
|
||||
padding: 0.5em 0.7em;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
// ios
|
||||
border-radius: 0;
|
||||
-webkit-appearance: none;
|
||||
@ -123,6 +120,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.textarea {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
border: 1px solid $color-border-light;
|
||||
resize: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
// Attention: Webkit support only!
|
||||
.input-select {
|
||||
position: relative;
|
||||
@ -234,6 +239,7 @@
|
||||
line-height: 1em;
|
||||
border: 1px solid $color-text-light;
|
||||
text-align: center;
|
||||
background-color: $color-bg;
|
||||
svg {
|
||||
display: inline-block;
|
||||
fill: $color-main;
|
||||
|
@ -27,4 +27,5 @@
|
||||
.typo-code {
|
||||
font-family: monospace;
|
||||
font-weight: bold;
|
||||
user-select: text;
|
||||
}
|
@ -8,6 +8,8 @@
|
||||
z-index: 1000;
|
||||
background: $color-grey-dark-alpha;
|
||||
overflow: auto;
|
||||
// allow scrolling on iOS
|
||||
-webkit-overflow-scrolling: touch;
|
||||
text-align: center;
|
||||
|
||||
@include respond-to(md) {
|
||||
@ -21,17 +23,16 @@
|
||||
padding: 15px;
|
||||
background: $color-bg;
|
||||
color: $color-text;
|
||||
backface-visibility: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: left;
|
||||
margin: 0 auto;
|
||||
will-change: transform;
|
||||
|
||||
@include respond-to(md) {
|
||||
width: 90%;
|
||||
max-width: 762px;
|
||||
min-height: 0;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -32,6 +32,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
.toolbar__label {
|
||||
@include respond-to(xs-only) {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__main {
|
||||
flex-grow: 1;
|
||||
margin: 0 auto 20px;
|
||||
|
@ -332,7 +332,7 @@
|
||||
|
||||
display: table-row;
|
||||
background: $color-bg-dark;
|
||||
color: $color-grey;
|
||||
color: $color-text;
|
||||
cursor: pointer;
|
||||
|
||||
// Flags
|
||||
|
@ -23,17 +23,53 @@
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
&__working {
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
padding: 0 $padding-horizontal;
|
||||
& > div {
|
||||
@include scut-vcenter-tt;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: $font-size-bigger;
|
||||
|
||||
strong {
|
||||
color: $color-text-light;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
&__content {
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
// allow scrolling on iOS
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
// Header components
|
||||
|
||||
&__header {
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 1em;
|
||||
padding: $padding-vertical $padding-horizontal 0;
|
||||
|
||||
& > .attachments {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.signature-status {
|
||||
& > svg {
|
||||
vertical-align: middle;
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
margin-bottom: .2em;
|
||||
fill: $color-main;
|
||||
}
|
||||
}
|
||||
.signature-status--invalid {
|
||||
& > svg {
|
||||
fill: $color-error-area;
|
||||
}
|
||||
}
|
||||
}
|
||||
// only visible in stripped version of read view
|
||||
.mail-addresses__stripped {
|
||||
@ -41,15 +77,29 @@
|
||||
}
|
||||
&__controls {
|
||||
display: none;
|
||||
float: right;
|
||||
margin-left: 1em;
|
||||
.btn-icon-light {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: $scrollbar-width; // don't cover scrollbar
|
||||
padding: $padding-vertical $padding-horizontal;
|
||||
background-color: $color-white;
|
||||
z-index: 999; // places the buttons on top of the content
|
||||
.btn-icon-light + .btn-icon-light {
|
||||
margin-left: 1.4em;
|
||||
}
|
||||
@include respond-to(md) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
&__controls__dummy {
|
||||
display: none;
|
||||
float: right;
|
||||
// the size of the real controls
|
||||
width: 242px;
|
||||
height: 39px;
|
||||
@include respond-to(md) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
&__subject {
|
||||
font-weight: normal;
|
||||
color: $color-text;
|
||||
@ -102,49 +152,18 @@
|
||||
|
||||
// Content components
|
||||
|
||||
&__signature-status {
|
||||
flex-shrink: 0;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5em;
|
||||
text-align: center;
|
||||
color: $color-error;
|
||||
padding: 0 $padding-horizontal;
|
||||
}
|
||||
&__display-images {
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 0.5em;
|
||||
text-align: center;
|
||||
padding: 0 $padding-horizontal;
|
||||
}
|
||||
&__working {
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
padding: 0 $padding-horizontal;
|
||||
& > div {
|
||||
@include scut-vcenter-tt;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: $font-size-bigger;
|
||||
|
||||
strong {
|
||||
color: $color-text-light;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
&__body {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
// allow scrolling on iOS
|
||||
overflow: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding: 0 $padding-horizontal $padding-vertical;
|
||||
overflow: hidden; // necessary due to iframe scaling via transitions
|
||||
|
||||
iframe {
|
||||
flex-grow: 1;
|
||||
border: none;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@ -172,6 +191,9 @@
|
||||
.mail-addresses__stripped {
|
||||
display: inline;
|
||||
}
|
||||
.read__sender-address {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -21,6 +21,33 @@
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
}
|
||||
&__invite {
|
||||
position: relative;
|
||||
margin-top: 1.3em;
|
||||
border: 1px solid $color-red-light;
|
||||
|
||||
p {
|
||||
color: $color-red-light;
|
||||
margin: 0.7em 1em;
|
||||
|
||||
svg {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
fill: $color-red-light;
|
||||
|
||||
// for better valignment
|
||||
position: relative;
|
||||
top: 0.15em;
|
||||
margin-right: 0.3em;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
}
|
||||
}
|
||||
&__subject {
|
||||
position: relative;
|
||||
margin-top: 1.3em;
|
||||
|
@ -5,24 +5,11 @@
|
||||
// Mixins
|
||||
|
||||
@import "mixins/responsive";
|
||||
@import "mixins/scrollbar";
|
||||
|
||||
@include scrollbar();
|
||||
|
||||
html {
|
||||
// use overflow auto and not scroll otherwise IE shows scrollbars always
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.scale-body {
|
||||
// necessary to compute overflowing content width in JS
|
||||
float: left;
|
||||
}
|
||||
|
||||
.view-read-body {
|
||||
font-family: $font-family-base;
|
||||
font-size: $font-size-base;
|
||||
|
18
src/sass/styleguide.scss
Executable file
18
src/sass/styleguide.scss
Executable 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";
|
20
src/sass/styleguide/_icon-list.scss
Normal file
20
src/sass/styleguide/_icon-list.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
82
src/sass/styleguide/_layout.scss
Normal file
82
src/sass/styleguide/_layout.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
33
src/sass/styleguide/_sections.scss
Normal file
33
src/sass/styleguide/_sections.scss
Normal 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%;
|
||||
}
|
||||
}
|
||||
}
|
42
src/sass/styleguide/_typo.scss
Normal file
42
src/sass/styleguide/_typo.scss
Normal 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;
|
||||
}
|
11
src/styleguide/helpers/strip-file-extension.js
Normal file
11
src/styleguide/helpers/strip-file-extension.js
Normal 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
66
src/styleguide/index.hbs
Normal 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"><use></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}}
|
47
src/styleguide/layouts/default.hbs
Normal file
47
src/styleguide/layouts/default.hbs
Normal 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>
|
||||
© {{formatDate currentDate "%Y"}} Whiteout Networks GmbH
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
21
src/styleguide/sections/attachments.hbs
Normal file
21
src/styleguide/sections/attachments.hbs
Normal 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>
|
16
src/styleguide/sections/buttons/btn.hbs
Normal file
16
src/styleguide/sections/buttons/btn.hbs
Normal 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>
|
9
src/styleguide/sections/buttons/btn_big.hbs
Normal file
9
src/styleguide/sections/buttons/btn_big.hbs
Normal 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>
|
8
src/styleguide/sections/buttons/btn_icon.hbs
Normal file
8
src/styleguide/sections/buttons/btn_icon.hbs
Normal 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
Loading…
Reference in New Issue
Block a user