From c36cd069e003ef91f0b107f5625e5d3a8565a7e0 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Thu, 2 Oct 2014 22:05:44 +0200 Subject: [PATCH 01/27] WIP: begin browserify build --- .gitignore | 3 - .jshintrc | 16 +- Gruntfile.js | 140 +- package.json | 22 +- src/index.html | 4 +- src/js/app-config.js | 416 ++- src/js/app-controller.js | 437 ++- src/js/app.js | 234 +- src/js/bo/auth.js | 756 ++-- src/js/bo/outbox.js | 389 ++- src/js/controller/about.js | 48 +- src/js/controller/account.js | 108 +- src/js/controller/add-account.js | 210 +- src/js/controller/contacts.js | 254 +- src/js/controller/dialog.js | 22 +- src/js/controller/login-existing.js | 128 +- src/js/controller/login-initial.js | 226 +- src/js/controller/login-new-device.js | 239 +- .../controller/login-privatekey-download.js | 252 +- src/js/controller/login-set-credentials.js | 214 +- src/js/controller/login.js | 160 +- src/js/controller/mail-list.js | 1088 +++--- src/js/controller/navigation.js | 386 +-- src/js/controller/popover.js | 80 +- src/js/controller/privatekey-upload.js | 323 +- src/js/controller/read-sandbox.js | 399 ++- src/js/controller/read.js | 507 ++- src/js/controller/set-passphrase.js | 258 +- src/js/controller/write.js | 938 +++-- src/js/crypto/crypto.js | 205 +- src/js/crypto/pbkdf2-worker.js | 51 +- src/js/crypto/pbkdf2.js | 35 +- src/js/crypto/pgp.js | 866 +++-- src/js/dao/admin-dao.js | 104 +- src/js/dao/devicestorage-dao.js | 170 +- src/js/dao/email-dao.js | 3068 ++++++++--------- src/js/dao/invitation-dao.js | 102 +- src/js/dao/keychain-dao.js | 1760 +++++----- src/js/dao/lawnchair-dao.js | 388 +-- src/js/dao/privatekey-dao.js | 350 +- src/js/dao/publickey-dao.js | 174 +- src/js/dao/rest-dao.js | 206 +- src/js/util/backbutton-handler.js | 104 +- src/js/util/connection-doctor.js | 482 ++- src/js/util/download.js | 104 +- src/js/util/error.js | 54 +- src/js/util/notification.js | 98 +- src/js/util/oauth.js | 170 +- src/js/util/update/update-handler.js | 230 +- src/js/util/update/update-v1.js | 48 +- src/js/util/update/update-v2.js | 46 +- src/js/util/update/update-v3.js | 46 +- src/js/util/update/update-v4.js | 118 +- src/js/util/update/update-v5.js | 88 +- src/lib/uuid/uuid.js | 245 -- src/package.json | 10 - src/require-config.js | 68 - 57 files changed, 8559 insertions(+), 9088 deletions(-) delete mode 100644 src/lib/uuid/uuid.js delete mode 100644 src/package.json delete mode 100644 src/require-config.js diff --git a/.gitignore b/.gitignore index d9c457d..97d8d25 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,4 @@ src/css/ dist/ release/ test/integration/src/ -src/lib/*.js -src/js/crypto/aes-gcm.js -src/js/crypto/util.js .elasticbeanstalk/ diff --git a/.jshintrc b/.jshintrc index c6b8a83..79d663e 100644 --- a/.jshintrc +++ b/.jshintrc @@ -23,15 +23,7 @@ "importScripts", "process", "Event", - "QUnit", - "test", - "asyncTest", - "ok", - "equal", - "deepEqual", - "start", "chrome", - "requirejs", "define", "self", "describe", @@ -42,7 +34,13 @@ "before", "beforeEach", "after", - "afterEach" + "afterEach", + "FastClick", + "angular", + "forge", + "Lawnchair", + "_", + "openpgp" ], "globals": { diff --git a/Gruntfile.js b/Gruntfile.js index fa57114..dacb48e 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -6,6 +6,7 @@ module.exports = function(grunt) { // Project configuration. grunt.initConfig({ + connect: { dev: { options: { @@ -47,8 +48,9 @@ module.exports = function(grunt) { }, clean: { - dist: ['dist', 'src/lib/*.js', 'test/lib', 'test/integration/src'] + dist: ['dist', 'test/lib', 'test/integration/src'] }, + sass: { dist: { files: { @@ -57,6 +59,7 @@ module.exports = function(grunt) { } } }, + autoprefixer: { options: { browsers: ['last 2 versions'] @@ -68,9 +71,10 @@ module.exports = function(grunt) { } } }, + csso: { options: { - banner: '/*! Copyright © 2013, Whiteout Networks GmbH. All rights reserved.*/\n' + banner: '/*! Copyright © <%= grunt.template.today("yyyy") %>, Whiteout Networks GmbH.*/\n' }, dist: { files: { @@ -79,6 +83,7 @@ module.exports = function(grunt) { } } }, + watch: { css: { files: ['src/sass/**/*.scss'], @@ -86,7 +91,7 @@ module.exports = function(grunt) { }, js: { files: ['src/js/**/*.js'], - tasks: ['copy:js', 'copy:integration', 'manifest'] + tasks: ['dist-js', 'copy:integration', 'manifest'] }, lib: { files: ['src/lib/**/*.js'], @@ -97,36 +102,52 @@ module.exports = function(grunt) { tasks: ['copy:app', 'copy:ca', 'copy:tpl', 'copy:img', 'copy:font', 'manifest-dev', 'manifest'] } }, - copy: { - npm: { - expand: true, - flatten: true, - cwd: 'node_modules/', - src: [ - 'requirejs/require.js', - 'imap-client/src/*.js', - 'imap-client/node_modules/browserbox/src/*.js', - 'imap-client/node_modules/browserbox/node_modules/wo-imap-handler/src/*.js', - 'imap-client/node_modules/browserbox/node_modules/mimefuncs/src/*.js', - 'imap-client/node_modules/browserbox/node_modules/tcp-socket/src/*.js', - 'imap-client/node_modules/browserbox/node_modules/wo-utf7/src/*.js', - 'mailreader/src/*.js', - 'mailreader/node_modules/mimeparser/src/*.js', - 'mailreader/node_modules/mimeparser/node_modules/wo-addressparser/src/*.js', - 'pgpbuilder/src/*.js', - 'pgpbuilder/node_modules/mailbuild/src/*.js', - 'pgpbuilder/node_modules/mailbuild/node_modules/mimetypes/src/*.js', - 'pgpbuilder/node_modules/mailbuild/node_modules/punycode/punycode.min.js', - 'pgpmailer/src/*.js', - 'pgpmailer/node_modules/wo-smtpclient/src/*.js', - 'pgpmailer/node_modules/wo-smtpclient/node_modules/wo-stringencoding/dist/stringencoding.js', - 'axe-logger/axe.js', - 'dompurify/purify.js', - 'jquery/dist/jquery.min.js', - 'ng-infinite-scroll/build/ng-infinite-scroll.min.js' - ], - dest: 'src/lib/' + + browserify: { + all: { + files: { + 'dist/js/app.min.js': ['src/js/app.js'] + }, + options: { + external: ['node-forge', 'net', 'tls'] // common.js apis not required at build time + } }, + /* TODO: + tls-worker: {}, + mailreader-worker: {}, + pbkdf2-worker: {}, + unitTest: {}, + unitTest: {}, + integrationTest: {} + */ + }, + + uglify: { + all: { + files: { + 'dist/js/app.min.js': [ + 'src/lib/underscore/underscore-min.js', + 'node_modules/jquery/dist/jquery.min.js', + 'src/lib/angular/angular.min.js', + 'src/lib/angular/angular-route.min.js', + 'src/lib/angular/angular-animate.min.js', + 'src/lib/ngtagsinput/ng-tags-input.min.js', + 'src/lib/fastclick/fastclick.js', + 'node_modules/ng-infinite-scroll/build/ng-infinite-scroll.min.js', + 'src/lib/lawnchair/lawnchair-git.js', + 'src/lib/lawnchair/lawnchair-adapter-webkit-sqlite-git.js', + 'src/lib/lawnchair/lawnchair-adapter-indexed-db-git.js', + 'node_modules/dompurify/purify.js', + 'dist/js/app.min.js' + ] + } + }, + options: { + banner: '/*! Copyright © <%= grunt.template.today("yyyy") %>, Whiteout Networks GmbH.*/\n' + } + }, + + copy: { npmDev: { expand: true, flatten: true, @@ -134,24 +155,6 @@ module.exports = function(grunt) { src: ['requirejs/require.js', 'mocha/mocha.css', 'mocha/mocha.js', 'chai/chai.js', 'sinon/pkg/sinon.js', 'angularjs/src/ngMock/angular-mocks.js', 'browsercrow/src/*.js', 'browsersmtp/src/*.js'], dest: 'test/lib/' }, - cryptoLib: { - expand: true, - cwd: 'node_modules/crypto-lib/src/', - src: ['*.js'], - dest: 'src/js/crypto/' - }, - lib: { - expand: true, - cwd: 'src/lib/', - src: ['**'], - dest: 'dist/lib/' - }, - js: { - expand: true, - cwd: 'src/js/', - src: ['**'], - dest: 'dist/js/' - }, font: { expand: true, cwd: 'src/font/', @@ -170,12 +173,6 @@ module.exports = function(grunt) { src: ['*'], dest: 'dist/tpl/' }, - ca: { - expand: true, - cwd: 'src/ca/', - src: ['*'], - dest: 'dist/ca/' - }, app: { expand: true, cwd: 'src/', @@ -200,16 +197,6 @@ module.exports = function(grunt) { cwd: 'dist/', src: ['**/*'], dest: 'release/' - }, - nodeWebkit: { - options: { - mode: 'zip', - archive: 'release/whiteout-mail_' + zipName + '.nw' - }, - expand: true, - cwd: 'dist/', - src: ['**/*'], - dest: '/' } }, @@ -226,22 +213,13 @@ module.exports = function(grunt) { src: ['**/*.*'], dest: 'dist/appcache.manifest' } - }, + } - nodewebkit: { - options: { - version: '0.9.2', // node-webkit version - build_dir: './release/node-webkit/', // Where the build version of my node-webkit app is saved - mac: true, // We want to build it for mac - win: false, // We want to build it for win - linux32: false, // We don't need linux32 - linux64: false, // We don't need linux64 - }, - src: ['./dist/**/*'] // Your node-webkit app - }, }); // Load the plugin(s) + grunt.loadNpmTasks('grunt-browserify'); + grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.loadNpmTasks('grunt-contrib-connect'); grunt.loadNpmTasks('grunt-contrib-jshint'); grunt.loadNpmTasks('grunt-mocha'); @@ -252,14 +230,14 @@ module.exports = function(grunt) { grunt.loadNpmTasks('grunt-contrib-watch'); grunt.loadNpmTasks('grunt-contrib-copy'); grunt.loadNpmTasks('grunt-contrib-compress'); - grunt.loadNpmTasks('grunt-node-webkit-builder'); grunt.loadNpmTasks('grunt-manifest'); // Build tasks - grunt.registerTask('dist-npm', ['copy:npm', 'copy:npmDev', 'copy:cryptoLib']); + grunt.registerTask('dist-npm', ['copy:npmDev']); grunt.registerTask('dist-css', ['sass', 'autoprefixer', 'csso']); + grunt.registerTask('dist-js', ['browserify', 'uglify']); grunt.registerTask('dist-copy', ['copy']); - grunt.registerTask('dist', ['clean', 'dist-npm', 'dist-css', 'dist-copy', 'manifest']); + grunt.registerTask('dist', ['clean', 'dist-npm', 'dist-css', 'dist-js', 'dist-copy', 'manifest']); // Test/Dev tasks grunt.registerTask('dev', ['connect:dev']); diff --git a/package.json b/package.json index 1ded9b2..a6e3162 100644 --- a/package.json +++ b/package.json @@ -29,20 +29,21 @@ "dependencies": { "axe-logger": "~0.0.2", "compression": "^1.0.11", + "config": "^1.0.2", "crypto-lib": "~0.2.1", "dompurify": "~0.4.2", "express": "^4.8.3", "imap-client": "~0.4.3", "jquery": "~2.1.1", "mailreader": "~0.3.5", + "morgan": "^1.2.3", "ng-infinite-scroll": "~1.1.2", + "npmlog": "^0.1.1", "pgpbuilder": "~0.4.0", "pgpmailer": "~0.4.0", - "requirejs": "~2.1.14", - "config": "^1.0.2", - "morgan": "^1.2.3", - "npmlog": "^0.1.1", - "socket.io": "^1.0.6" + "socket.io": "^1.0.6", + "tcp-socket": "^0.3.9", + "wo-smtpclient": "^0.3.8" }, "devDependencies": { "angularjs": "https://github.com/whiteout-io/angular.js/tarball/npm-version", @@ -50,19 +51,20 @@ "browsersmtp": "https://github.com/whiteout-io/browsersmtp/tarball/master", "chai": "~1.7.2", "grunt": "~0.4.1", - "grunt-contrib-clean": "~0.5.0", - "grunt-contrib-copy": "~0.4.1", - "grunt-manifest": "^0.4.0", "grunt-autoprefixer": "~0.7.2", + "grunt-browserify": "^3.0.1", + "grunt-contrib-clean": "~0.5.0", "grunt-contrib-compress": "~0.5.2", "grunt-contrib-connect": "~0.5.0", + "grunt-contrib-copy": "~0.4.1", "grunt-contrib-jshint": "~0.6.4", "grunt-contrib-sass": "~0.7.3", + "grunt-contrib-uglify": "^0.6.0", "grunt-contrib-watch": "~0.5.3", "grunt-csso": "~0.6.1", + "grunt-manifest": "^0.4.0", "grunt-mocha": "~0.4.1", - "grunt-node-webkit-builder": "~0.1.17", "mocha": "~1.13.0", "sinon": "~1.7.3" } -} +} \ No newline at end of file diff --git a/src/index.html b/src/index.html index d0a577f..c1fad5b 100644 --- a/src/index.html +++ b/src/index.html @@ -26,9 +26,7 @@ - - - + diff --git a/src/js/app-config.js b/src/js/app-config.js index b520f42..fe3e99e 100644 --- a/src/js/app-config.js +++ b/src/js/app-config.js @@ -1,223 +1,217 @@ -define(function(require) { - 'use strict'; +'use strict'; - var _ = require('underscore'), - app = {}, - appVersion, cloudUrl, keychainUrl, clientId; +var appVersion, cloudUrl, keychainUrl, clientId; - // parse manifest to get configurations for current runtime - try { - var manifest = chrome.runtime.getManifest(); - // get key server base url - cloudUrl = _.find(manifest.permissions, function(permission) { - return typeof permission === 'string' && permission.indexOf('https://keys') === 0; - }); - // remove last '/' from url due to required syntax in manifest - cloudUrl = cloudUrl.substring(0, cloudUrl.length - 1); - // get keychain server base url - keychainUrl = _.find(manifest.permissions, function(permission) { - return typeof permission === 'string' && permission.indexOf('https://keychain') === 0; - }); - // remove last '/' from url due to required syntax in manifest - keychainUrl = keychainUrl.substring(0, keychainUrl.length - 1); - // get client ID for OAuth requests - clientId = manifest.oauth2.client_id; - // get the app version - appVersion = manifest.version; - } catch (e) {} +// parse manifest to get configurations for current runtime +try { + var manifest = chrome.runtime.getManifest(); + // get key server base url + cloudUrl = _.find(manifest.permissions, function(permission) { + return typeof permission === 'string' && permission.indexOf('https://keys') === 0; + }); + // remove last '/' from url due to required syntax in manifest + cloudUrl = cloudUrl.substring(0, cloudUrl.length - 1); + // get keychain server base url + keychainUrl = _.find(manifest.permissions, function(permission) { + return typeof permission === 'string' && permission.indexOf('https://keychain') === 0; + }); + // remove last '/' from url due to required syntax in manifest + keychainUrl = keychainUrl.substring(0, keychainUrl.length - 1); + // get client ID for OAuth requests + clientId = manifest.oauth2.client_id; + // get the app version + appVersion = manifest.version; +} catch (e) {} - /** - * Global app configurations - */ - app.config = { - cloudUrl: cloudUrl || 'https://keys.whiteout.io', - privkeyServerUrl: keychainUrl || 'https://keychain.whiteout.io', - adminUrl: 'https://admin-node.whiteout.io', - wmailDomain: 'wmail.io', - serverPrivateKeyId: 'EE342F0DDBB0F3BE', - symKeySize: 256, - symIvSize: 96, - asymKeySize: 2048, - workerPath: 'js', - reconnectInterval: 10000, - wmail: { - imap: { - host: 'imap.wmail.io', - port: 993, - secure: true, - ca: '-----BEGIN CERTIFICATE-----\r\nMIIGiTCCBXGgAwIBAgIDAn03MA0GCSqGSIb3DQEBBQUAMIGMMQswCQYDVQQGEwJJ\r\nTDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0\r\nYWwgQ2VydGlmaWNhdGUgU2lnbmluZzE4MDYGA1UEAxMvU3RhcnRDb20gQ2xhc3Mg\r\nMiBQcmltYXJ5IEludGVybWVkaWF0ZSBTZXJ2ZXIgQ0EwHhcNMTQwOTIxMTQ0ODQ0\r\nWhcNMTYwOTIxMTE0MzIxWjCBmTEZMBcGA1UEDRMQQ1NIeEdsdlg4Z3lRQ2c3TzEL\r\nMAkGA1UEBhMCREUxDzANBgNVBAgTBkJheWVybjEPMA0GA1UEBxMGTXVuaWNoMRUw\r\nEwYDVQQKEwxUYW5rcmVkIEhhc2UxEzARBgNVBAMUCioud21haWwuaW8xITAfBgkq\r\nhkiG9w0BCQEWEndlYm1hc3RlckB3bWFpbC5pbzCCASIwDQYJKoZIhvcNAQEBBQAD\r\nggEPADCCAQoCggEBAMkbMzFfZLYqfWG8yw2HvuNiFs5ajBFMrjG3fobePE674mPd\r\niFtXqbl3ydQ+umTtbJ6bztgxB3KgrL3lhp6IkD4VxB8YQJoYGhU6YH7FhP4QMm8l\r\ncnFLUZXEbcpcCg1tjL6+vvoTMUWEbV/zNtF/oiJ4AIOKwf0zUMZkTu1FCNrOrvpj\r\n6SAkOdBGzLTOAP5vxP43PfpZPZ4dLL2Be7ENYKXqPs0jSlUWpdT9l4AZG8rHKa2d\r\nccWvRoAsCfvpzGDoMYEx9+a1F1XjNZfzo7yJQcHdaE3Mj1eNqbjZe0+Vmact1S/3\r\nsubdcDVkFSfUMsAB5rH0D5mEhy7sMpt1WWCvOF8CAwEAAaOCAuMwggLfMAkGA1Ud\r\nEwQCMAAwCwYDVR0PBAQDAgOoMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcD\r\nATAdBgNVHQ4EFgQUv+9x4PISm5Roq4pdNoaFJeIZFfUwHwYDVR0jBBgwFoAUEdsj\r\nRf1UzGpxb4SKA9e+9wEvJoYwHwYDVR0RBBgwFoIKKi53bWFpbC5pb4IId21haWwu\r\naW8wggFWBgNVHSAEggFNMIIBSTAIBgZngQwBAgIwggE7BgsrBgEEAYG1NwECAzCC\r\nASowLgYIKwYBBQUHAgEWImh0dHA6Ly93d3cuc3RhcnRzc2wuY29tL3BvbGljeS5w\r\nZGYwgfcGCCsGAQUFBwICMIHqMCcWIFN0YXJ0Q29tIENlcnRpZmljYXRpb24gQXV0\r\naG9yaXR5MAMCAQEagb5UaGlzIGNlcnRpZmljYXRlIHdhcyBpc3N1ZWQgYWNjb3Jk\r\naW5nIHRvIHRoZSBDbGFzcyAyIFZhbGlkYXRpb24gcmVxdWlyZW1lbnRzIG9mIHRo\r\nZSBTdGFydENvbSBDQSBwb2xpY3ksIHJlbGlhbmNlIG9ubHkgZm9yIHRoZSBpbnRl\r\nbmRlZCBwdXJwb3NlIGluIGNvbXBsaWFuY2Ugb2YgdGhlIHJlbHlpbmcgcGFydHkg\r\nb2JsaWdhdGlvbnMuMDUGA1UdHwQuMCwwKqAooCaGJGh0dHA6Ly9jcmwuc3RhcnRz\r\nc2wuY29tL2NydDItY3JsLmNybDCBjgYIKwYBBQUHAQEEgYEwfzA5BggrBgEFBQcw\r\nAYYtaHR0cDovL29jc3Auc3RhcnRzc2wuY29tL3N1Yi9jbGFzczIvc2VydmVyL2Nh\r\nMEIGCCsGAQUFBzAChjZodHRwOi8vYWlhLnN0YXJ0c3NsLmNvbS9jZXJ0cy9zdWIu\r\nY2xhc3MyLnNlcnZlci5jYS5jcnQwIwYDVR0SBBwwGoYYaHR0cDovL3d3dy5zdGFy\r\ndHNzbC5jb20vMA0GCSqGSIb3DQEBBQUAA4IBAQDEDbCBvdfs/lEsEJk0xGB6OghO\r\ngUAHplmWJEXoHb0h7p2QAIcs/QKorMDi35bu/J2vbVRIP7i5wBTZLIHBaf03mqAR\r\nZxYFy/ymyROVmmKl1x1/ry1aumomfU86UN8hCkvJc+40V2KC1lCZcjqPWufECjpo\r\n37QXFA+te/rVyVgvoMhIq+zOBZEK0/2mYGCwlpEQo3HElCQIwV9upChOHnQ2jZyF\r\nCmoUyv0JNr8dkh6H7+KTV6FzWaC+b6Liier9bpfEq/zDAp41GR+L/pdbaliSDtRg\r\nfPhAyCZqwsXid6HgIyTBqxUpGDGRiuygqghmqlFfppZuAqz02wrNceDHH7Up\r\n-----END CERTIFICATE-----\r\n', - pinned: true, - ignoreTLS: false +/** + * Global app configurations + */ +exports.config = { + cloudUrl: cloudUrl || 'https://keys.whiteout.io', + privkeyServerUrl: keychainUrl || 'https://keychain.whiteout.io', + adminUrl: 'https://admin-node.whiteout.io', + wmailDomain: 'wmail.io', + serverPrivateKeyId: 'EE342F0DDBB0F3BE', + symKeySize: 256, + symIvSize: 96, + asymKeySize: 2048, + workerPath: 'js', + reconnectInterval: 10000, + wmail: { + imap: { + host: 'imap.wmail.io', + port: 993, + secure: true, + ca: '-----BEGIN CERTIFICATE-----\r\nMIIGiTCCBXGgAwIBAgIDAn03MA0GCSqGSIb3DQEBBQUAMIGMMQswCQYDVQQGEwJJ\r\nTDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0\r\nYWwgQ2VydGlmaWNhdGUgU2lnbmluZzE4MDYGA1UEAxMvU3RhcnRDb20gQ2xhc3Mg\r\nMiBQcmltYXJ5IEludGVybWVkaWF0ZSBTZXJ2ZXIgQ0EwHhcNMTQwOTIxMTQ0ODQ0\r\nWhcNMTYwOTIxMTE0MzIxWjCBmTEZMBcGA1UEDRMQQ1NIeEdsdlg4Z3lRQ2c3TzEL\r\nMAkGA1UEBhMCREUxDzANBgNVBAgTBkJheWVybjEPMA0GA1UEBxMGTXVuaWNoMRUw\r\nEwYDVQQKEwxUYW5rcmVkIEhhc2UxEzARBgNVBAMUCioud21haWwuaW8xITAfBgkq\r\nhkiG9w0BCQEWEndlYm1hc3RlckB3bWFpbC5pbzCCASIwDQYJKoZIhvcNAQEBBQAD\r\nggEPADCCAQoCggEBAMkbMzFfZLYqfWG8yw2HvuNiFs5ajBFMrjG3fobePE674mPd\r\niFtXqbl3ydQ+umTtbJ6bztgxB3KgrL3lhp6IkD4VxB8YQJoYGhU6YH7FhP4QMm8l\r\ncnFLUZXEbcpcCg1tjL6+vvoTMUWEbV/zNtF/oiJ4AIOKwf0zUMZkTu1FCNrOrvpj\r\n6SAkOdBGzLTOAP5vxP43PfpZPZ4dLL2Be7ENYKXqPs0jSlUWpdT9l4AZG8rHKa2d\r\nccWvRoAsCfvpzGDoMYEx9+a1F1XjNZfzo7yJQcHdaE3Mj1eNqbjZe0+Vmact1S/3\r\nsubdcDVkFSfUMsAB5rH0D5mEhy7sMpt1WWCvOF8CAwEAAaOCAuMwggLfMAkGA1Ud\r\nEwQCMAAwCwYDVR0PBAQDAgOoMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcD\r\nATAdBgNVHQ4EFgQUv+9x4PISm5Roq4pdNoaFJeIZFfUwHwYDVR0jBBgwFoAUEdsj\r\nRf1UzGpxb4SKA9e+9wEvJoYwHwYDVR0RBBgwFoIKKi53bWFpbC5pb4IId21haWwu\r\naW8wggFWBgNVHSAEggFNMIIBSTAIBgZngQwBAgIwggE7BgsrBgEEAYG1NwECAzCC\r\nASowLgYIKwYBBQUHAgEWImh0dHA6Ly93d3cuc3RhcnRzc2wuY29tL3BvbGljeS5w\r\nZGYwgfcGCCsGAQUFBwICMIHqMCcWIFN0YXJ0Q29tIENlcnRpZmljYXRpb24gQXV0\r\naG9yaXR5MAMCAQEagb5UaGlzIGNlcnRpZmljYXRlIHdhcyBpc3N1ZWQgYWNjb3Jk\r\naW5nIHRvIHRoZSBDbGFzcyAyIFZhbGlkYXRpb24gcmVxdWlyZW1lbnRzIG9mIHRo\r\nZSBTdGFydENvbSBDQSBwb2xpY3ksIHJlbGlhbmNlIG9ubHkgZm9yIHRoZSBpbnRl\r\nbmRlZCBwdXJwb3NlIGluIGNvbXBsaWFuY2Ugb2YgdGhlIHJlbHlpbmcgcGFydHkg\r\nb2JsaWdhdGlvbnMuMDUGA1UdHwQuMCwwKqAooCaGJGh0dHA6Ly9jcmwuc3RhcnRz\r\nc2wuY29tL2NydDItY3JsLmNybDCBjgYIKwYBBQUHAQEEgYEwfzA5BggrBgEFBQcw\r\nAYYtaHR0cDovL29jc3Auc3RhcnRzc2wuY29tL3N1Yi9jbGFzczIvc2VydmVyL2Nh\r\nMEIGCCsGAQUFBzAChjZodHRwOi8vYWlhLnN0YXJ0c3NsLmNvbS9jZXJ0cy9zdWIu\r\nY2xhc3MyLnNlcnZlci5jYS5jcnQwIwYDVR0SBBwwGoYYaHR0cDovL3d3dy5zdGFy\r\ndHNzbC5jb20vMA0GCSqGSIb3DQEBBQUAA4IBAQDEDbCBvdfs/lEsEJk0xGB6OghO\r\ngUAHplmWJEXoHb0h7p2QAIcs/QKorMDi35bu/J2vbVRIP7i5wBTZLIHBaf03mqAR\r\nZxYFy/ymyROVmmKl1x1/ry1aumomfU86UN8hCkvJc+40V2KC1lCZcjqPWufECjpo\r\n37QXFA+te/rVyVgvoMhIq+zOBZEK0/2mYGCwlpEQo3HElCQIwV9upChOHnQ2jZyF\r\nCmoUyv0JNr8dkh6H7+KTV6FzWaC+b6Liier9bpfEq/zDAp41GR+L/pdbaliSDtRg\r\nfPhAyCZqwsXid6HgIyTBqxUpGDGRiuygqghmqlFfppZuAqz02wrNceDHH7Up\r\n-----END CERTIFICATE-----\r\n', + pinned: true, + ignoreTLS: false - }, - smtp: { - host: 'smtp.wmail.io', - port: 465, - secure: true, - ca: '-----BEGIN CERTIFICATE-----\r\nMIIGiTCCBXGgAwIBAgIDAn03MA0GCSqGSIb3DQEBBQUAMIGMMQswCQYDVQQGEwJJ\r\nTDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0\r\nYWwgQ2VydGlmaWNhdGUgU2lnbmluZzE4MDYGA1UEAxMvU3RhcnRDb20gQ2xhc3Mg\r\nMiBQcmltYXJ5IEludGVybWVkaWF0ZSBTZXJ2ZXIgQ0EwHhcNMTQwOTIxMTQ0ODQ0\r\nWhcNMTYwOTIxMTE0MzIxWjCBmTEZMBcGA1UEDRMQQ1NIeEdsdlg4Z3lRQ2c3TzEL\r\nMAkGA1UEBhMCREUxDzANBgNVBAgTBkJheWVybjEPMA0GA1UEBxMGTXVuaWNoMRUw\r\nEwYDVQQKEwxUYW5rcmVkIEhhc2UxEzARBgNVBAMUCioud21haWwuaW8xITAfBgkq\r\nhkiG9w0BCQEWEndlYm1hc3RlckB3bWFpbC5pbzCCASIwDQYJKoZIhvcNAQEBBQAD\r\nggEPADCCAQoCggEBAMkbMzFfZLYqfWG8yw2HvuNiFs5ajBFMrjG3fobePE674mPd\r\niFtXqbl3ydQ+umTtbJ6bztgxB3KgrL3lhp6IkD4VxB8YQJoYGhU6YH7FhP4QMm8l\r\ncnFLUZXEbcpcCg1tjL6+vvoTMUWEbV/zNtF/oiJ4AIOKwf0zUMZkTu1FCNrOrvpj\r\n6SAkOdBGzLTOAP5vxP43PfpZPZ4dLL2Be7ENYKXqPs0jSlUWpdT9l4AZG8rHKa2d\r\nccWvRoAsCfvpzGDoMYEx9+a1F1XjNZfzo7yJQcHdaE3Mj1eNqbjZe0+Vmact1S/3\r\nsubdcDVkFSfUMsAB5rH0D5mEhy7sMpt1WWCvOF8CAwEAAaOCAuMwggLfMAkGA1Ud\r\nEwQCMAAwCwYDVR0PBAQDAgOoMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcD\r\nATAdBgNVHQ4EFgQUv+9x4PISm5Roq4pdNoaFJeIZFfUwHwYDVR0jBBgwFoAUEdsj\r\nRf1UzGpxb4SKA9e+9wEvJoYwHwYDVR0RBBgwFoIKKi53bWFpbC5pb4IId21haWwu\r\naW8wggFWBgNVHSAEggFNMIIBSTAIBgZngQwBAgIwggE7BgsrBgEEAYG1NwECAzCC\r\nASowLgYIKwYBBQUHAgEWImh0dHA6Ly93d3cuc3RhcnRzc2wuY29tL3BvbGljeS5w\r\nZGYwgfcGCCsGAQUFBwICMIHqMCcWIFN0YXJ0Q29tIENlcnRpZmljYXRpb24gQXV0\r\naG9yaXR5MAMCAQEagb5UaGlzIGNlcnRpZmljYXRlIHdhcyBpc3N1ZWQgYWNjb3Jk\r\naW5nIHRvIHRoZSBDbGFzcyAyIFZhbGlkYXRpb24gcmVxdWlyZW1lbnRzIG9mIHRo\r\nZSBTdGFydENvbSBDQSBwb2xpY3ksIHJlbGlhbmNlIG9ubHkgZm9yIHRoZSBpbnRl\r\nbmRlZCBwdXJwb3NlIGluIGNvbXBsaWFuY2Ugb2YgdGhlIHJlbHlpbmcgcGFydHkg\r\nb2JsaWdhdGlvbnMuMDUGA1UdHwQuMCwwKqAooCaGJGh0dHA6Ly9jcmwuc3RhcnRz\r\nc2wuY29tL2NydDItY3JsLmNybDCBjgYIKwYBBQUHAQEEgYEwfzA5BggrBgEFBQcw\r\nAYYtaHR0cDovL29jc3Auc3RhcnRzc2wuY29tL3N1Yi9jbGFzczIvc2VydmVyL2Nh\r\nMEIGCCsGAQUFBzAChjZodHRwOi8vYWlhLnN0YXJ0c3NsLmNvbS9jZXJ0cy9zdWIu\r\nY2xhc3MyLnNlcnZlci5jYS5jcnQwIwYDVR0SBBwwGoYYaHR0cDovL3d3dy5zdGFy\r\ndHNzbC5jb20vMA0GCSqGSIb3DQEBBQUAA4IBAQDEDbCBvdfs/lEsEJk0xGB6OghO\r\ngUAHplmWJEXoHb0h7p2QAIcs/QKorMDi35bu/J2vbVRIP7i5wBTZLIHBaf03mqAR\r\nZxYFy/ymyROVmmKl1x1/ry1aumomfU86UN8hCkvJc+40V2KC1lCZcjqPWufECjpo\r\n37QXFA+te/rVyVgvoMhIq+zOBZEK0/2mYGCwlpEQo3HElCQIwV9upChOHnQ2jZyF\r\nCmoUyv0JNr8dkh6H7+KTV6FzWaC+b6Liier9bpfEq/zDAp41GR+L/pdbaliSDtRg\r\nfPhAyCZqwsXid6HgIyTBqxUpGDGRiuygqghmqlFfppZuAqz02wrNceDHH7Up\r\n-----END CERTIFICATE-----\r\n', - pinned: true, - ignoreTLS: false - } }, - gmail: { - clientId: clientId || '440907777130.apps.googleusercontent.com', - imap: { - host: 'imap.gmail.com', - port: 993, - secure: true, - ca: '-----BEGIN CERTIFICATE-----\r\nMIIEBDCCAuygAwIBAgIDAjppMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT\r\nMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i\r\nYWwgQ0EwHhcNMTMwNDA1MTUxNTU1WhcNMTUwNDA0MTUxNTU1WjBJMQswCQYDVQQG\r\nEwJVUzETMBEGA1UEChMKR29vZ2xlIEluYzElMCMGA1UEAxMcR29vZ2xlIEludGVy\r\nbmV0IEF1dGhvcml0eSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\r\nAJwqBHdc2FCROgajguDYUEi8iT/xGXAaiEZ+4I/F8YnOIe5a/mENtzJEiaB0C1NP\r\nVaTOgmKV7utZX8bhBYASxF6UP7xbSDj0U/ck5vuR6RXEz/RTDfRK/J9U3n2+oGtv\r\nh8DQUB8oMANA2ghzUWx//zo8pzcGjr1LEQTrfSTe5vn8MXH7lNVg8y5Kr0LSy+rE\r\nahqyzFPdFUuLH8gZYR/Nnag+YyuENWllhMgZxUYi+FOVvuOAShDGKuy6lyARxzmZ\r\nEASg8GF6lSWMTlJ14rbtCMoU/M4iarNOz0YDl5cDfsCx3nuvRTPPuj5xt970JSXC\r\nDTWJnZ37DhF5iR43xa+OcmkCAwEAAaOB+zCB+DAfBgNVHSMEGDAWgBTAephojYn7\r\nqwVkDBF9qn1luMrMTjAdBgNVHQ4EFgQUSt0GFhu89mi1dvWBtrtiGrpagS8wEgYD\r\nVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAQYwOgYDVR0fBDMwMTAvoC2g\r\nK4YpaHR0cDovL2NybC5nZW90cnVzdC5jb20vY3Jscy9ndGdsb2JhbC5jcmwwPQYI\r\nKwYBBQUHAQEEMTAvMC0GCCsGAQUFBzABhiFodHRwOi8vZ3RnbG9iYWwtb2NzcC5n\r\nZW90cnVzdC5jb20wFwYDVR0gBBAwDjAMBgorBgEEAdZ5AgUBMA0GCSqGSIb3DQEB\r\nBQUAA4IBAQA21waAESetKhSbOHezI6B1WLuxfoNCunLaHtiONgaX4PCVOzf9G0JY\r\n/iLIa704XtE7JW4S615ndkZAkNoUyHgN7ZVm2o6Gb4ChulYylYbc3GrKBIxbf/a/\r\nzG+FA1jDaFETzf3I93k9mTXwVqO94FntT0QJo544evZG0R0SnU++0ED8Vf4GXjza\r\nHFa9llF7b1cq26KqltyMdMKVvvBulRP/F/A8rLIQjcxz++iPAsbw+zOzlTvjwsto\r\nWHPbqCRiOwY1nQ2pM714A5AuTHhdUDqB1O6gyHA43LL5Z/qHQF1hwFGPa4NrzQU6\r\nyuGnBXj8ytqU0CwIPX4WecigUCAkVDNx\r\n-----END CERTIFICATE-----', - pinned: true, - ignoreTLS: false - }, - smtp: { - host: 'smtp.gmail.com', - port: 465, - secure: true, - ca: '-----BEGIN CERTIFICATE-----\r\nMIIEBDCCAuygAwIBAgIDAjppMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT\r\nMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i\r\nYWwgQ0EwHhcNMTMwNDA1MTUxNTU1WhcNMTUwNDA0MTUxNTU1WjBJMQswCQYDVQQG\r\nEwJVUzETMBEGA1UEChMKR29vZ2xlIEluYzElMCMGA1UEAxMcR29vZ2xlIEludGVy\r\nbmV0IEF1dGhvcml0eSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\r\nAJwqBHdc2FCROgajguDYUEi8iT/xGXAaiEZ+4I/F8YnOIe5a/mENtzJEiaB0C1NP\r\nVaTOgmKV7utZX8bhBYASxF6UP7xbSDj0U/ck5vuR6RXEz/RTDfRK/J9U3n2+oGtv\r\nh8DQUB8oMANA2ghzUWx//zo8pzcGjr1LEQTrfSTe5vn8MXH7lNVg8y5Kr0LSy+rE\r\nahqyzFPdFUuLH8gZYR/Nnag+YyuENWllhMgZxUYi+FOVvuOAShDGKuy6lyARxzmZ\r\nEASg8GF6lSWMTlJ14rbtCMoU/M4iarNOz0YDl5cDfsCx3nuvRTPPuj5xt970JSXC\r\nDTWJnZ37DhF5iR43xa+OcmkCAwEAAaOB+zCB+DAfBgNVHSMEGDAWgBTAephojYn7\r\nqwVkDBF9qn1luMrMTjAdBgNVHQ4EFgQUSt0GFhu89mi1dvWBtrtiGrpagS8wEgYD\r\nVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAQYwOgYDVR0fBDMwMTAvoC2g\r\nK4YpaHR0cDovL2NybC5nZW90cnVzdC5jb20vY3Jscy9ndGdsb2JhbC5jcmwwPQYI\r\nKwYBBQUHAQEEMTAvMC0GCCsGAQUFBzABhiFodHRwOi8vZ3RnbG9iYWwtb2NzcC5n\r\nZW90cnVzdC5jb20wFwYDVR0gBBAwDjAMBgorBgEEAdZ5AgUBMA0GCSqGSIb3DQEB\r\nBQUAA4IBAQA21waAESetKhSbOHezI6B1WLuxfoNCunLaHtiONgaX4PCVOzf9G0JY\r\n/iLIa704XtE7JW4S615ndkZAkNoUyHgN7ZVm2o6Gb4ChulYylYbc3GrKBIxbf/a/\r\nzG+FA1jDaFETzf3I93k9mTXwVqO94FntT0QJo544evZG0R0SnU++0ED8Vf4GXjza\r\nHFa9llF7b1cq26KqltyMdMKVvvBulRP/F/A8rLIQjcxz++iPAsbw+zOzlTvjwsto\r\nWHPbqCRiOwY1nQ2pM714A5AuTHhdUDqB1O6gyHA43LL5Z/qHQF1hwFGPa4NrzQU6\r\nyuGnBXj8ytqU0CwIPX4WecigUCAkVDNx\r\n-----END CERTIFICATE-----', - pinned: true, - ignoreTLS: false - }, - ignoreUploadOnSent: true + smtp: { + host: 'smtp.wmail.io', + port: 465, + secure: true, + ca: '-----BEGIN CERTIFICATE-----\r\nMIIGiTCCBXGgAwIBAgIDAn03MA0GCSqGSIb3DQEBBQUAMIGMMQswCQYDVQQGEwJJ\r\nTDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0\r\nYWwgQ2VydGlmaWNhdGUgU2lnbmluZzE4MDYGA1UEAxMvU3RhcnRDb20gQ2xhc3Mg\r\nMiBQcmltYXJ5IEludGVybWVkaWF0ZSBTZXJ2ZXIgQ0EwHhcNMTQwOTIxMTQ0ODQ0\r\nWhcNMTYwOTIxMTE0MzIxWjCBmTEZMBcGA1UEDRMQQ1NIeEdsdlg4Z3lRQ2c3TzEL\r\nMAkGA1UEBhMCREUxDzANBgNVBAgTBkJheWVybjEPMA0GA1UEBxMGTXVuaWNoMRUw\r\nEwYDVQQKEwxUYW5rcmVkIEhhc2UxEzARBgNVBAMUCioud21haWwuaW8xITAfBgkq\r\nhkiG9w0BCQEWEndlYm1hc3RlckB3bWFpbC5pbzCCASIwDQYJKoZIhvcNAQEBBQAD\r\nggEPADCCAQoCggEBAMkbMzFfZLYqfWG8yw2HvuNiFs5ajBFMrjG3fobePE674mPd\r\niFtXqbl3ydQ+umTtbJ6bztgxB3KgrL3lhp6IkD4VxB8YQJoYGhU6YH7FhP4QMm8l\r\ncnFLUZXEbcpcCg1tjL6+vvoTMUWEbV/zNtF/oiJ4AIOKwf0zUMZkTu1FCNrOrvpj\r\n6SAkOdBGzLTOAP5vxP43PfpZPZ4dLL2Be7ENYKXqPs0jSlUWpdT9l4AZG8rHKa2d\r\nccWvRoAsCfvpzGDoMYEx9+a1F1XjNZfzo7yJQcHdaE3Mj1eNqbjZe0+Vmact1S/3\r\nsubdcDVkFSfUMsAB5rH0D5mEhy7sMpt1WWCvOF8CAwEAAaOCAuMwggLfMAkGA1Ud\r\nEwQCMAAwCwYDVR0PBAQDAgOoMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcD\r\nATAdBgNVHQ4EFgQUv+9x4PISm5Roq4pdNoaFJeIZFfUwHwYDVR0jBBgwFoAUEdsj\r\nRf1UzGpxb4SKA9e+9wEvJoYwHwYDVR0RBBgwFoIKKi53bWFpbC5pb4IId21haWwu\r\naW8wggFWBgNVHSAEggFNMIIBSTAIBgZngQwBAgIwggE7BgsrBgEEAYG1NwECAzCC\r\nASowLgYIKwYBBQUHAgEWImh0dHA6Ly93d3cuc3RhcnRzc2wuY29tL3BvbGljeS5w\r\nZGYwgfcGCCsGAQUFBwICMIHqMCcWIFN0YXJ0Q29tIENlcnRpZmljYXRpb24gQXV0\r\naG9yaXR5MAMCAQEagb5UaGlzIGNlcnRpZmljYXRlIHdhcyBpc3N1ZWQgYWNjb3Jk\r\naW5nIHRvIHRoZSBDbGFzcyAyIFZhbGlkYXRpb24gcmVxdWlyZW1lbnRzIG9mIHRo\r\nZSBTdGFydENvbSBDQSBwb2xpY3ksIHJlbGlhbmNlIG9ubHkgZm9yIHRoZSBpbnRl\r\nbmRlZCBwdXJwb3NlIGluIGNvbXBsaWFuY2Ugb2YgdGhlIHJlbHlpbmcgcGFydHkg\r\nb2JsaWdhdGlvbnMuMDUGA1UdHwQuMCwwKqAooCaGJGh0dHA6Ly9jcmwuc3RhcnRz\r\nc2wuY29tL2NydDItY3JsLmNybDCBjgYIKwYBBQUHAQEEgYEwfzA5BggrBgEFBQcw\r\nAYYtaHR0cDovL29jc3Auc3RhcnRzc2wuY29tL3N1Yi9jbGFzczIvc2VydmVyL2Nh\r\nMEIGCCsGAQUFBzAChjZodHRwOi8vYWlhLnN0YXJ0c3NsLmNvbS9jZXJ0cy9zdWIu\r\nY2xhc3MyLnNlcnZlci5jYS5jcnQwIwYDVR0SBBwwGoYYaHR0cDovL3d3dy5zdGFy\r\ndHNzbC5jb20vMA0GCSqGSIb3DQEBBQUAA4IBAQDEDbCBvdfs/lEsEJk0xGB6OghO\r\ngUAHplmWJEXoHb0h7p2QAIcs/QKorMDi35bu/J2vbVRIP7i5wBTZLIHBaf03mqAR\r\nZxYFy/ymyROVmmKl1x1/ry1aumomfU86UN8hCkvJc+40V2KC1lCZcjqPWufECjpo\r\n37QXFA+te/rVyVgvoMhIq+zOBZEK0/2mYGCwlpEQo3HElCQIwV9upChOHnQ2jZyF\r\nCmoUyv0JNr8dkh6H7+KTV6FzWaC+b6Liier9bpfEq/zDAp41GR+L/pdbaliSDtRg\r\nfPhAyCZqwsXid6HgIyTBqxUpGDGRiuygqghmqlFfppZuAqz02wrNceDHH7Up\r\n-----END CERTIFICATE-----\r\n', + pinned: true, + ignoreTLS: false + } + }, + gmail: { + clientId: clientId || '440907777130.apps.googleusercontent.com', + imap: { + host: 'imap.gmail.com', + port: 993, + secure: true, + ca: '-----BEGIN CERTIFICATE-----\r\nMIIEBDCCAuygAwIBAgIDAjppMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT\r\nMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i\r\nYWwgQ0EwHhcNMTMwNDA1MTUxNTU1WhcNMTUwNDA0MTUxNTU1WjBJMQswCQYDVQQG\r\nEwJVUzETMBEGA1UEChMKR29vZ2xlIEluYzElMCMGA1UEAxMcR29vZ2xlIEludGVy\r\nbmV0IEF1dGhvcml0eSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\r\nAJwqBHdc2FCROgajguDYUEi8iT/xGXAaiEZ+4I/F8YnOIe5a/mENtzJEiaB0C1NP\r\nVaTOgmKV7utZX8bhBYASxF6UP7xbSDj0U/ck5vuR6RXEz/RTDfRK/J9U3n2+oGtv\r\nh8DQUB8oMANA2ghzUWx//zo8pzcGjr1LEQTrfSTe5vn8MXH7lNVg8y5Kr0LSy+rE\r\nahqyzFPdFUuLH8gZYR/Nnag+YyuENWllhMgZxUYi+FOVvuOAShDGKuy6lyARxzmZ\r\nEASg8GF6lSWMTlJ14rbtCMoU/M4iarNOz0YDl5cDfsCx3nuvRTPPuj5xt970JSXC\r\nDTWJnZ37DhF5iR43xa+OcmkCAwEAAaOB+zCB+DAfBgNVHSMEGDAWgBTAephojYn7\r\nqwVkDBF9qn1luMrMTjAdBgNVHQ4EFgQUSt0GFhu89mi1dvWBtrtiGrpagS8wEgYD\r\nVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAQYwOgYDVR0fBDMwMTAvoC2g\r\nK4YpaHR0cDovL2NybC5nZW90cnVzdC5jb20vY3Jscy9ndGdsb2JhbC5jcmwwPQYI\r\nKwYBBQUHAQEEMTAvMC0GCCsGAQUFBzABhiFodHRwOi8vZ3RnbG9iYWwtb2NzcC5n\r\nZW90cnVzdC5jb20wFwYDVR0gBBAwDjAMBgorBgEEAdZ5AgUBMA0GCSqGSIb3DQEB\r\nBQUAA4IBAQA21waAESetKhSbOHezI6B1WLuxfoNCunLaHtiONgaX4PCVOzf9G0JY\r\n/iLIa704XtE7JW4S615ndkZAkNoUyHgN7ZVm2o6Gb4ChulYylYbc3GrKBIxbf/a/\r\nzG+FA1jDaFETzf3I93k9mTXwVqO94FntT0QJo544evZG0R0SnU++0ED8Vf4GXjza\r\nHFa9llF7b1cq26KqltyMdMKVvvBulRP/F/A8rLIQjcxz++iPAsbw+zOzlTvjwsto\r\nWHPbqCRiOwY1nQ2pM714A5AuTHhdUDqB1O6gyHA43LL5Z/qHQF1hwFGPa4NrzQU6\r\nyuGnBXj8ytqU0CwIPX4WecigUCAkVDNx\r\n-----END CERTIFICATE-----', + pinned: true, + ignoreTLS: false }, - yahoo: { - imap: { - host: 'imap.mail.yahoo.com', - port: 993, - secure: true, - ca: '-----BEGIN CERTIFICATE-----\r\nMIIFUjCCBDqgAwIBAgIQdThnkckQvgouzHOsQA7ctTANBgkqhkiG9w0BAQUFADCB\r\ntTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL\r\nExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTswOQYDVQQLEzJUZXJtcyBvZiB1c2Ug\r\nYXQgaHR0cHM6Ly93d3cudmVyaXNpZ24uY29tL3JwYSAoYykxMDEvMC0GA1UEAxMm\r\nVmVyaVNpZ24gQ2xhc3MgMyBTZWN1cmUgU2VydmVyIENBIC0gRzMwHhcNMTQwNDIy\r\nMDAwMDAwWhcNMTUwNDIzMjM1OTU5WjCBjDELMAkGA1UEBhMCVVMxEzARBgNVBAgT\r\nCkNhbGlmb3JuaWExEjAQBgNVBAcUCVN1bm55dmFsZTETMBEGA1UEChQKWWFob28g\r\nSW5jLjEfMB0GA1UECxQWSW5mb3JtYXRpb24gVGVjaG5vbG9neTEeMBwGA1UEAxQV\r\nKi5pbWFwLm1haWwueWFob28uY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\r\nCgKCAQEAw36HN2sgtMNQ0TZlqGgfqInK/UT6y3ZgqDdmFRU9D5D5675hfxcwJoS+\r\nb0hIn/UixbZDpSLhNkjAkOTAbFEFWar7628D2dU5WtCUlFMiwg2TA0Un8B9EbUi5\r\nwDrqzXDyABVnBVR5I2eKwr5cuB9ldjxAabcCyqQhVKdH0+IskRpUrvxAb84uQtJg\r\nJyNieOZAWdxg9fkubk1YKw/MgJHnaY8P4lUlYY8fIY39d6gW6My8oT0IersrH1X1\r\n/oCmqUIGM1PawXBvvpPKYdI4fCH75/UaEQ41BFSUn1NsinFYZUPlVcBCOvLFEOQi\r\nuU+4Tjybq3x7NNhd3uBxfm4jo4h5zQIDAQABo4IBgzCCAX8wNQYDVR0RBC4wLIIV\r\nKi5pbWFwLm1haWwueWFob28uY29tghNpbWFwLm1haWwueWFob28uY29tMAkGA1Ud\r\nEwQCMAAwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEF\r\nBQcDAjBlBgNVHSAEXjBcMFoGCmCGSAGG+EUBBzYwTDAjBggrBgEFBQcCARYXaHR0\r\ncHM6Ly9kLnN5bWNiLmNvbS9jcHMwJQYIKwYBBQUHAgIwGRoXaHR0cHM6Ly9kLnN5\r\nbWNiLmNvbS9ycGEwHwYDVR0jBBgwFoAUDURcFlNEwYJ+HSCrJfQBY9i+eaUwKwYD\r\nVR0fBCQwIjAgoB6gHIYaaHR0cDovL3NkLnN5bWNiLmNvbS9zZC5jcmwwVwYIKwYB\r\nBQUHAQEESzBJMB8GCCsGAQUFBzABhhNodHRwOi8vc2Quc3ltY2QuY29tMCYGCCsG\r\nAQUFBzAChhpodHRwOi8vc2Quc3ltY2IuY29tL3NkLmNydDANBgkqhkiG9w0BAQUF\r\nAAOCAQEAVxsglXJTtBTCoTwOd6j0iJQ+P9cxFVqHcmbshEfEQBlPwr4Sp9tLJ4kj\r\nfVi0XorWU6e6e57dtYtxpcPz+6WNSNKT0B0IBOTUTIBwSLJMHxEZI6gSS/fo1agt\r\n81B06rB8Rhn4yHwyDO/9uRvXbNYiEgpa5e6gIpXY6h6p1HscQMcuROaUA9ETvGd8\r\nDKG4XSZE7QAF9iB9WSLa/IQUD4sGMDaMp2q4XkoWZTnyL1bEDKwUvw9Z17PxVmrF\r\n8c7S5HTNU+1kyZw2LJRu3SgtsYXSWA88WFiKUPuqU+EBXmbrwLAwLAJ6mVc2bGFC\r\ng5fLGbtTscaARBlb1u3Iee2Fd419jg==\r\n-----END CERTIFICATE-----', - pinned: true, - ignoreTLS: false - }, - smtp: { - host: 'smtp.mail.yahoo.com', - port: 465, - secure: true, - ca: '-----BEGIN CERTIFICATE-----\r\nMIIHITCCBgmgAwIBAgIQBlVSvxlwsqw8Kc8eVV5EKTANBgkqhkiG9w0BAQUFADBm\r\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\r\nd3cuZGlnaWNlcnQuY29tMSUwIwYDVQQDExxEaWdpQ2VydCBIaWdoIEFzc3VyYW5j\r\nZSBDQS0zMB4XDTE0MDYwNTAwMDAwMFoXDTE1MDYyMzEyMDAwMFowYjELMAkGA1UE\r\nBhMCVVMxCzAJBgNVBAgTAkNBMRIwEAYDVQQHEwlTdW5ueXZhbGUxFDASBgNVBAoM\r\nC1lhaG9vISBJbmMuMRwwGgYDVQQDExNzbXRwLm1haWwueWFob28uY29tMIIBIjAN\r\nBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA339OSxTIuW6NVqKwYHFBbg4/tmEx\r\nMYpzohtObPaWgvLTsYdce+WlQFBU5rxaCMUtVsVAGJWs5uQRDVHXRLPnOWe+foqZ\r\n5ItgjYIQc1XeMv6BZEeaF3Bum5ehUyLB3y48cjbWxma1QlkZ8XAK0f9AS7ySWAen\r\no5OXJlMFAqXGtKMAhY46dzSY0wjgdrvgiomFRy0iQKV1DxGsXoXSMEszlUTQoNQ3\r\nWTsiA3O//cdWE06wWeA3/90cb7QkU3KflSoyfi878BQGpPR1L+rLNsqnc8QuucbD\r\nz4Q++rxGqgg4QYrOtmZfAn96TXhPWCwKld6FN+f3uV5ITOBFx44M5v1ILQIDAQAB\r\no4IDzTCCA8kwHwYDVR0jBBgwFoAUUOpzidsp+xCPnuUBINTeeZlIg/cwHQYDVR0O\r\nBBYEFEhOpSFQjO/QrSNfVwyInZUhyzhsMIICJAYDVR0RBIICGzCCAheCE3NtdHAu\r\nbWFpbC55YWhvby5jb22CFnNtdHAubWFpbC55YWhvby5jb20uYXKCFnNtdHAubWFp\r\nbC55YWhvby5jb20uYXWCFnNtdHAubWFpbC55YWhvby5jb20uYnKCFnNtdHAubWFp\r\nbC55YWhvby5jb20uY26CFnNtdHAubWFpbC55YWhvby5jb20uaGuCFnNtdHAubWFp\r\nbC55YWhvby5jb20ubXmCFnNtdHAubWFpbC55YWhvby5jb20ucGiCFnNtdHAubWFp\r\nbC55YWhvby5jb20uc2eCFnNtdHAubWFpbC55YWhvby5jb20udHeCFnNtdHAubWFp\r\nbC55YWhvby5jb20udm6CFXNtdHAubWFpbC55YWhvby5jby5pZIIVc210cC5tYWls\r\nLnlhaG9vLmNvLmlughVzbXRwLm1haWwueWFob28uY28ua3KCFXNtdHAubWFpbC55\r\nYWhvby5jby50aIIVc210cC5tYWlsLnlhaG9vLmNvLnVrghJzbXRwLm1haWwueWFo\r\nb28uY2GCEnNtdHAubWFpbC55YWhvby5jboISc210cC5tYWlsLnlhaG9vLmRlghJz\r\nbXRwLm1haWwueWFob28uZXOCEnNtdHAubWFpbC55YWhvby5mcoISc210cC5tYWls\r\nLnlhaG9vLml0gg9zbXRwLnk3bWFpbC5jb22CFHNtdHAuY29ycmVvLnlhaG9vLmVz\r\nMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIw\r\nYQYDVR0fBFowWDAqoCigJoYkaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL2NhMy1n\r\nMjguY3JsMCqgKKAmhiRodHRwOi8vY3JsNC5kaWdpY2VydC5jb20vY2EzLWcyOC5j\r\ncmwwQgYDVR0gBDswOTA3BglghkgBhv1sAQEwKjAoBggrBgEFBQcCARYcaHR0cHM6\r\nLy93d3cuZGlnaWNlcnQuY29tL0NQUzB7BggrBgEFBQcBAQRvMG0wJAYIKwYBBQUH\r\nMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBFBggrBgEFBQcwAoY5aHR0cDov\r\nL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0SGlnaEFzc3VyYW5jZUNBLTMu\r\nY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQEFBQADggEBABshjUND4hADHGfe\r\ncV9XGXcr9h3u7mT5kgdcgGFhcrFAlwkzt0NUCbuN0X8DWHE72Tpb3zRE25CmUUJe\r\nktBbq9PQb4b5/Wt1htAEw0qUs3BsUbqejK9OHJ/11Jn2ek4+SLJuYlijzc7KM3F/\r\nyz7ZTZtKR0PglkXfqbpvWYGabYpfL2FRLoJ7alTLsMJcFxbSLYcAIMxufj7RyTBJ\r\nbKgRJl4wmP4+Zc2Q1p59mENY0u5HqVAAOmWc0jNb0/31+tRr5f6EgXxK++7TQOpF\r\n0TOaFsXlzRlpKfmIbzVr2nfwghV5/bRZj96TK3g1OoOz4C8ksK4INHnUdTAqZ18M\r\nvHpnJw4=\r\n-----END CERTIFICATE-----', - pinned: true, - ignoreTLS: false - } + smtp: { + host: 'smtp.gmail.com', + port: 465, + secure: true, + ca: '-----BEGIN CERTIFICATE-----\r\nMIIEBDCCAuygAwIBAgIDAjppMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT\r\nMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i\r\nYWwgQ0EwHhcNMTMwNDA1MTUxNTU1WhcNMTUwNDA0MTUxNTU1WjBJMQswCQYDVQQG\r\nEwJVUzETMBEGA1UEChMKR29vZ2xlIEluYzElMCMGA1UEAxMcR29vZ2xlIEludGVy\r\nbmV0IEF1dGhvcml0eSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\r\nAJwqBHdc2FCROgajguDYUEi8iT/xGXAaiEZ+4I/F8YnOIe5a/mENtzJEiaB0C1NP\r\nVaTOgmKV7utZX8bhBYASxF6UP7xbSDj0U/ck5vuR6RXEz/RTDfRK/J9U3n2+oGtv\r\nh8DQUB8oMANA2ghzUWx//zo8pzcGjr1LEQTrfSTe5vn8MXH7lNVg8y5Kr0LSy+rE\r\nahqyzFPdFUuLH8gZYR/Nnag+YyuENWllhMgZxUYi+FOVvuOAShDGKuy6lyARxzmZ\r\nEASg8GF6lSWMTlJ14rbtCMoU/M4iarNOz0YDl5cDfsCx3nuvRTPPuj5xt970JSXC\r\nDTWJnZ37DhF5iR43xa+OcmkCAwEAAaOB+zCB+DAfBgNVHSMEGDAWgBTAephojYn7\r\nqwVkDBF9qn1luMrMTjAdBgNVHQ4EFgQUSt0GFhu89mi1dvWBtrtiGrpagS8wEgYD\r\nVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAQYwOgYDVR0fBDMwMTAvoC2g\r\nK4YpaHR0cDovL2NybC5nZW90cnVzdC5jb20vY3Jscy9ndGdsb2JhbC5jcmwwPQYI\r\nKwYBBQUHAQEEMTAvMC0GCCsGAQUFBzABhiFodHRwOi8vZ3RnbG9iYWwtb2NzcC5n\r\nZW90cnVzdC5jb20wFwYDVR0gBBAwDjAMBgorBgEEAdZ5AgUBMA0GCSqGSIb3DQEB\r\nBQUAA4IBAQA21waAESetKhSbOHezI6B1WLuxfoNCunLaHtiONgaX4PCVOzf9G0JY\r\n/iLIa704XtE7JW4S615ndkZAkNoUyHgN7ZVm2o6Gb4ChulYylYbc3GrKBIxbf/a/\r\nzG+FA1jDaFETzf3I93k9mTXwVqO94FntT0QJo544evZG0R0SnU++0ED8Vf4GXjza\r\nHFa9llF7b1cq26KqltyMdMKVvvBulRP/F/A8rLIQjcxz++iPAsbw+zOzlTvjwsto\r\nWHPbqCRiOwY1nQ2pM714A5AuTHhdUDqB1O6gyHA43LL5Z/qHQF1hwFGPa4NrzQU6\r\nyuGnBXj8ytqU0CwIPX4WecigUCAkVDNx\r\n-----END CERTIFICATE-----', + pinned: true, + ignoreTLS: false }, - tonline: { - imap: { - host: 'secureimap.t-online.de', - port: 993, - secure: true, - ca: '-----BEGIN CERTIFICATE-----\r\nMIIGmzCCBYOgAwIBAgIIIbZ3foy9DqgwDQYJKoZIhvcNAQEFBQAwcTELMAkGA1UE\r\nBhMCREUxHDAaBgNVBAoTE0RldXRzY2hlIFRlbGVrb20gQUcxHzAdBgNVBAsTFlQt\r\nVGVsZVNlYyBUcnVzdCBDZW50ZXIxIzAhBgNVBAMTGkRldXRzY2hlIFRlbGVrb20g\r\nUm9vdCBDQSAyMB4XDTEzMDMwMTEzNTgyOVoXDTE5MDcwOTIzNTkwMFowgckxCzAJ\r\nBgNVBAYTAkRFMSUwIwYDVQQKExxULVN5c3RlbXMgSW50ZXJuYXRpb25hbCBHbWJI\r\nMR8wHQYDVQQLExZULVN5c3RlbXMgVHJ1c3QgQ2VudGVyMQwwCgYDVQQIEwNOUlcx\r\nDjAMBgNVBBETBTU3MjUwMRAwDgYDVQQHEwdOZXRwaGVuMSAwHgYDVQQJExdVbnRl\r\ncmUgSW5kdXN0cmllc3RyLiAyMDEgMB4GA1UEAxMXVGVsZVNlYyBTZXJ2ZXJQYXNz\r\nIERFLTEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCwg+9QiuYAxX9/\r\n4F9XRZrS1o0q+aa9L/5/K3vc+RqPpliiZ24vTkJc1JDpXrbWXS25uT3yzHukQrhI\r\nq0AbcRqNEAeFi5EhUiEM/vtb4BYGHdqfXQ3etgYYcCtJ43NAHaSgsyQ9kyGV2lmM\r\nwkeAX3qZ2CGE9/cR6w+bOogHArBdk2JaHG09myNZDytr6oUbWLjLd/qhC9YzyZSX\r\nbZgE/kh5L5Y6P9paw2pDdn7+Ni4pXzlmoj1k43uiz+h2ibe3DO9dKMZAaEKeyG1O\r\ng0f0r53M8O+8Bm2sXtWelrAgrfFlISgWzO1hkNs12rWpr4c5Ygde/behx9OQmPwp\r\nmS+e3WvTAgMBAAGjggLcMIIC2DAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFGJP\r\nE842Z4TNGfygTxmL7xVUATIcMB8GA1UdIwQYMBaAFDHDeRu69VPXF+CJei0XbAqz\r\nK50zMBIGA1UdEwEB/wQIMAYBAf8CAQAwWQYDVR0gBFIwUDBEBgkrBgEEAb1HDQIw\r\nNzA1BggrBgEFBQcCARYpaHR0cDovL3d3dy50ZWxlc2VjLmRlL3NlcnZlcnBhc3Mv\r\nY3BzLmh0bWwwCAYGZ4EMAQICMIHvBgNVHR8EgecwgeQwOqA4oDaGNGh0dHA6Ly9j\r\ncmwuc2VydmVycGFzcy50ZWxlc2VjLmRlL3JsL0RUX1JPT1RfQ0FfMi5jcmwwgaWg\r\ngaKggZ+GgZxsZGFwOi8vbGRhcC5zZXJ2ZXJwYXNzLnRlbGVzZWMuZGUvQ049RGV1\r\ndHNjaGUlMjBUZWxla29tJTIwUm9vdCUyMENBJTIwMixPVT1ULVRlbGVTZWMlMjBU\r\ncnVzdCUyMENlbnRlcixPPURldXRzY2hlJTIwVGVsZWtvbSUyMEFHLEM9REU/QXV0\r\naG9yaXR5UmV2b2NhdGlvbkxpc3QwggEjBggrBgEFBQcBAQSCARUwggERMCoGCCsG\r\nAQUFBzABhh5odHRwOi8vb2NzcDAyLnRlbGVzZWMuZGUvb2NzcHIwQQYIKwYBBQUH\r\nMAKGNWh0dHA6Ly9jcmwuc2VydmVycGFzcy50ZWxlc2VjLmRlL2NydC9EVF9ST09U\r\nX0NBXzIuY2VyMIGfBggrBgEFBQcwAoaBkmxkYXA6Ly9sZGFwLnNlcnZlcnBhc3Mu\r\ndGVsZXNlYy5kZS9DTj1EZXV0c2NoZSUyMFRlbGVrb20lMjBSb290JTIwQ0ElMjAy\r\nLE9VPVQtVGVsZVNlYyUyMFRydXN0JTIwQ2VudGVyLE89RGV1dHNjaGUlMjBUZWxl\r\na29tJTIwQUcsQz1ERT9jQUNlcnRpZmljYXRlMA0GCSqGSIb3DQEBBQUAA4IBAQBO\r\nE04qoEkEc9ad+WwSurVYfcDdjGvpqrtbI89woXDsWLQTMhA7D7jVuls90SJns0vc\r\nK9qoYkEGt0/ZlawLe2lyNWtueHfUf+dgleUunwHYLxuj3jQ2ERzQLVLrswjecRpX\r\nvGAGej89WpGQ9PMq27WGNC5WCmzVC9rk5naFgacsbwKwyjU0LoBArtAQnAAlpHDw\r\nPenv1Pe7MhUkCK0LqdTvkI/AHFzPYg/l5E3j8lQQ8hiKx8U6wf9xVKECLA2RlRqY\r\nUX2rpjQNxnvEq/mEQv3x3mLOEFJ3TAKI+soDgOOi0OG8+ywhm6S+7Z9lTlJ+BcD6\r\noy1MNKd4CQbltHLMTFUH\r\n-----END CERTIFICATE-----', - pinned: true, - ignoreTLS: false - }, - smtp: { - host: 'securesmtp.t-online.de', - port: 465, - secure: true, - ca: '-----BEGIN CERTIFICATE-----\r\nMIIGmzCCBYOgAwIBAgIIIbZ3foy9DqgwDQYJKoZIhvcNAQEFBQAwcTELMAkGA1UE\r\nBhMCREUxHDAaBgNVBAoTE0RldXRzY2hlIFRlbGVrb20gQUcxHzAdBgNVBAsTFlQt\r\nVGVsZVNlYyBUcnVzdCBDZW50ZXIxIzAhBgNVBAMTGkRldXRzY2hlIFRlbGVrb20g\r\nUm9vdCBDQSAyMB4XDTEzMDMwMTEzNTgyOVoXDTE5MDcwOTIzNTkwMFowgckxCzAJ\r\nBgNVBAYTAkRFMSUwIwYDVQQKExxULVN5c3RlbXMgSW50ZXJuYXRpb25hbCBHbWJI\r\nMR8wHQYDVQQLExZULVN5c3RlbXMgVHJ1c3QgQ2VudGVyMQwwCgYDVQQIEwNOUlcx\r\nDjAMBgNVBBETBTU3MjUwMRAwDgYDVQQHEwdOZXRwaGVuMSAwHgYDVQQJExdVbnRl\r\ncmUgSW5kdXN0cmllc3RyLiAyMDEgMB4GA1UEAxMXVGVsZVNlYyBTZXJ2ZXJQYXNz\r\nIERFLTEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCwg+9QiuYAxX9/\r\n4F9XRZrS1o0q+aa9L/5/K3vc+RqPpliiZ24vTkJc1JDpXrbWXS25uT3yzHukQrhI\r\nq0AbcRqNEAeFi5EhUiEM/vtb4BYGHdqfXQ3etgYYcCtJ43NAHaSgsyQ9kyGV2lmM\r\nwkeAX3qZ2CGE9/cR6w+bOogHArBdk2JaHG09myNZDytr6oUbWLjLd/qhC9YzyZSX\r\nbZgE/kh5L5Y6P9paw2pDdn7+Ni4pXzlmoj1k43uiz+h2ibe3DO9dKMZAaEKeyG1O\r\ng0f0r53M8O+8Bm2sXtWelrAgrfFlISgWzO1hkNs12rWpr4c5Ygde/behx9OQmPwp\r\nmS+e3WvTAgMBAAGjggLcMIIC2DAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFGJP\r\nE842Z4TNGfygTxmL7xVUATIcMB8GA1UdIwQYMBaAFDHDeRu69VPXF+CJei0XbAqz\r\nK50zMBIGA1UdEwEB/wQIMAYBAf8CAQAwWQYDVR0gBFIwUDBEBgkrBgEEAb1HDQIw\r\nNzA1BggrBgEFBQcCARYpaHR0cDovL3d3dy50ZWxlc2VjLmRlL3NlcnZlcnBhc3Mv\r\nY3BzLmh0bWwwCAYGZ4EMAQICMIHvBgNVHR8EgecwgeQwOqA4oDaGNGh0dHA6Ly9j\r\ncmwuc2VydmVycGFzcy50ZWxlc2VjLmRlL3JsL0RUX1JPT1RfQ0FfMi5jcmwwgaWg\r\ngaKggZ+GgZxsZGFwOi8vbGRhcC5zZXJ2ZXJwYXNzLnRlbGVzZWMuZGUvQ049RGV1\r\ndHNjaGUlMjBUZWxla29tJTIwUm9vdCUyMENBJTIwMixPVT1ULVRlbGVTZWMlMjBU\r\ncnVzdCUyMENlbnRlcixPPURldXRzY2hlJTIwVGVsZWtvbSUyMEFHLEM9REU/QXV0\r\naG9yaXR5UmV2b2NhdGlvbkxpc3QwggEjBggrBgEFBQcBAQSCARUwggERMCoGCCsG\r\nAQUFBzABhh5odHRwOi8vb2NzcDAyLnRlbGVzZWMuZGUvb2NzcHIwQQYIKwYBBQUH\r\nMAKGNWh0dHA6Ly9jcmwuc2VydmVycGFzcy50ZWxlc2VjLmRlL2NydC9EVF9ST09U\r\nX0NBXzIuY2VyMIGfBggrBgEFBQcwAoaBkmxkYXA6Ly9sZGFwLnNlcnZlcnBhc3Mu\r\ndGVsZXNlYy5kZS9DTj1EZXV0c2NoZSUyMFRlbGVrb20lMjBSb290JTIwQ0ElMjAy\r\nLE9VPVQtVGVsZVNlYyUyMFRydXN0JTIwQ2VudGVyLE89RGV1dHNjaGUlMjBUZWxl\r\na29tJTIwQUcsQz1ERT9jQUNlcnRpZmljYXRlMA0GCSqGSIb3DQEBBQUAA4IBAQBO\r\nE04qoEkEc9ad+WwSurVYfcDdjGvpqrtbI89woXDsWLQTMhA7D7jVuls90SJns0vc\r\nK9qoYkEGt0/ZlawLe2lyNWtueHfUf+dgleUunwHYLxuj3jQ2ERzQLVLrswjecRpX\r\nvGAGej89WpGQ9PMq27WGNC5WCmzVC9rk5naFgacsbwKwyjU0LoBArtAQnAAlpHDw\r\nPenv1Pe7MhUkCK0LqdTvkI/AHFzPYg/l5E3j8lQQ8hiKx8U6wf9xVKECLA2RlRqY\r\nUX2rpjQNxnvEq/mEQv3x3mLOEFJ3TAKI+soDgOOi0OG8+ywhm6S+7Z9lTlJ+BcD6\r\noy1MNKd4CQbltHLMTFUH\r\n-----END CERTIFICATE-----', - pinned: true, - ignoreTLS: false - } + ignoreUploadOnSent: true + }, + yahoo: { + imap: { + host: 'imap.mail.yahoo.com', + port: 993, + secure: true, + ca: '-----BEGIN CERTIFICATE-----\r\nMIIFUjCCBDqgAwIBAgIQdThnkckQvgouzHOsQA7ctTANBgkqhkiG9w0BAQUFADCB\r\ntTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL\r\nExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTswOQYDVQQLEzJUZXJtcyBvZiB1c2Ug\r\nYXQgaHR0cHM6Ly93d3cudmVyaXNpZ24uY29tL3JwYSAoYykxMDEvMC0GA1UEAxMm\r\nVmVyaVNpZ24gQ2xhc3MgMyBTZWN1cmUgU2VydmVyIENBIC0gRzMwHhcNMTQwNDIy\r\nMDAwMDAwWhcNMTUwNDIzMjM1OTU5WjCBjDELMAkGA1UEBhMCVVMxEzARBgNVBAgT\r\nCkNhbGlmb3JuaWExEjAQBgNVBAcUCVN1bm55dmFsZTETMBEGA1UEChQKWWFob28g\r\nSW5jLjEfMB0GA1UECxQWSW5mb3JtYXRpb24gVGVjaG5vbG9neTEeMBwGA1UEAxQV\r\nKi5pbWFwLm1haWwueWFob28uY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\r\nCgKCAQEAw36HN2sgtMNQ0TZlqGgfqInK/UT6y3ZgqDdmFRU9D5D5675hfxcwJoS+\r\nb0hIn/UixbZDpSLhNkjAkOTAbFEFWar7628D2dU5WtCUlFMiwg2TA0Un8B9EbUi5\r\nwDrqzXDyABVnBVR5I2eKwr5cuB9ldjxAabcCyqQhVKdH0+IskRpUrvxAb84uQtJg\r\nJyNieOZAWdxg9fkubk1YKw/MgJHnaY8P4lUlYY8fIY39d6gW6My8oT0IersrH1X1\r\n/oCmqUIGM1PawXBvvpPKYdI4fCH75/UaEQ41BFSUn1NsinFYZUPlVcBCOvLFEOQi\r\nuU+4Tjybq3x7NNhd3uBxfm4jo4h5zQIDAQABo4IBgzCCAX8wNQYDVR0RBC4wLIIV\r\nKi5pbWFwLm1haWwueWFob28uY29tghNpbWFwLm1haWwueWFob28uY29tMAkGA1Ud\r\nEwQCMAAwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEF\r\nBQcDAjBlBgNVHSAEXjBcMFoGCmCGSAGG+EUBBzYwTDAjBggrBgEFBQcCARYXaHR0\r\ncHM6Ly9kLnN5bWNiLmNvbS9jcHMwJQYIKwYBBQUHAgIwGRoXaHR0cHM6Ly9kLnN5\r\nbWNiLmNvbS9ycGEwHwYDVR0jBBgwFoAUDURcFlNEwYJ+HSCrJfQBY9i+eaUwKwYD\r\nVR0fBCQwIjAgoB6gHIYaaHR0cDovL3NkLnN5bWNiLmNvbS9zZC5jcmwwVwYIKwYB\r\nBQUHAQEESzBJMB8GCCsGAQUFBzABhhNodHRwOi8vc2Quc3ltY2QuY29tMCYGCCsG\r\nAQUFBzAChhpodHRwOi8vc2Quc3ltY2IuY29tL3NkLmNydDANBgkqhkiG9w0BAQUF\r\nAAOCAQEAVxsglXJTtBTCoTwOd6j0iJQ+P9cxFVqHcmbshEfEQBlPwr4Sp9tLJ4kj\r\nfVi0XorWU6e6e57dtYtxpcPz+6WNSNKT0B0IBOTUTIBwSLJMHxEZI6gSS/fo1agt\r\n81B06rB8Rhn4yHwyDO/9uRvXbNYiEgpa5e6gIpXY6h6p1HscQMcuROaUA9ETvGd8\r\nDKG4XSZE7QAF9iB9WSLa/IQUD4sGMDaMp2q4XkoWZTnyL1bEDKwUvw9Z17PxVmrF\r\n8c7S5HTNU+1kyZw2LJRu3SgtsYXSWA88WFiKUPuqU+EBXmbrwLAwLAJ6mVc2bGFC\r\ng5fLGbtTscaARBlb1u3Iee2Fd419jg==\r\n-----END CERTIFICATE-----', + pinned: true, + ignoreTLS: false }, - outlook: { - imap: { - host: 'imap-mail.outlook.com', - port: 993, - secure: true, - ca: '-----BEGIN CERTIFICATE-----\r\nMIIFQjCCBCqgAwIBAgISESHl0vjrML7zKmGlv42YL75vMA0GCSqGSIb3DQEBBQUA\r\nMF0xCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMTMwMQYD\r\nVQQDEypHbG9iYWxTaWduIE9yZ2FuaXphdGlvbiBWYWxpZGF0aW9uIENBIC0gRzIw\r\nHhcNMTMwNDI0MjAzNTA5WhcNMTYwNDI0MjAzNTA5WjBsMQswCQYDVQQGEwJVUzET\r\nMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMV\r\nTWljcm9zb2Z0IENvcnBvcmF0aW9uMRYwFAYDVQQDDA0qLmhvdG1haWwuY29tMIIB\r\nIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAumSiBWrzHZf6WFP5a/j4+K7D\r\n1izLoYKj5Omll0pdxKvKcBRDf+iaIkCbSOPNpx2uWGZdwNwkabYCQavaBf2ebwmS\r\nS8i1CJpHflO+k0qYd5WUi7sSsZ3+6RaCMdLoDIPGyYMQuy7TFtVO7LSt5+qscyyi\r\nET8c3lE2aj/XW13UZvRrV65ZJvMjUtwaDnIcAxGeasYoebLsKdqHQ2uTr4PmNwCc\r\nviGVFSOzkGAoC0PfyqKB2xUWy3Kc5zRI2xvUW8Jb2b/9Ze3g55pIUzKsjpglkQTm\r\nedVPSYYPGNz6Kl/ZshBXdBAk398q1JkSmUaTMa2hJgBbcC+73ax40AJDGJlz+QID\r\nAQABo4IB6zCCAecwDgYDVR0PAQH/BAQDAgWgMEkGA1UdIARCMEAwPgYGZ4EMAQIC\r\nMDQwMgYIKwYBBQUHAgEWJmh0dHBzOi8vd3d3Lmdsb2JhbHNpZ24uY29tL3JlcG9z\r\naXRvcnkvMEAGA1UdEQQ5MDeCDSouaG90bWFpbC5jb22CCioubGl2ZS5jb22CDSou\r\nb3V0bG9vay5jb22CC2hvdG1haWwuY29tMAkGA1UdEwQCMAAwHQYDVR0lBBYwFAYI\r\nKwYBBQUHAwEGCCsGAQUFBwMCMEUGA1UdHwQ+MDwwOqA4oDaGNGh0dHA6Ly9jcmwu\r\nZ2xvYmFsc2lnbi5jb20vZ3MvZ3Nvcmdhbml6YXRpb252YWxnMi5jcmwwgZYGCCsG\r\nAQUFBwEBBIGJMIGGMEcGCCsGAQUFBzAChjtodHRwOi8vc2VjdXJlLmdsb2JhbHNp\r\nZ24uY29tL2NhY2VydC9nc29yZ2FuaXphdGlvbnZhbGcyLmNydDA7BggrBgEFBQcw\r\nAYYvaHR0cDovL29jc3AyLmdsb2JhbHNpZ24uY29tL2dzb3JnYW5pemF0aW9udmFs\r\nZzIwHQYDVR0OBBYEFHbgHqTLsXDt7uMRyE62rnDEfLn9MB8GA1UdIwQYMBaAFF1G\r\nso3ES3Qcu+31c7Y6tziPdZ5+MA0GCSqGSIb3DQEBBQUAA4IBAQByy1+3N6ZRVooI\r\nxqw8Ng+UFz0g7UHkbPEnvTu1uxJ2AojFuP/P1PAk+/6uMRvpPlWg/5uqmOIWxKxJ\r\nLo6xSbkDf4LN+KYwes3XSuPyziZ4QbPnehHhZ0377iiA8fpRJADg9NWKCRHh5aAd\r\ne9QvJUW/GgYkBN+F4yYc2jIjR3Rehv4JYOKS3iXO9OoHsDS2CcCFaS2imgQVfYLg\r\nslBwT/A08PCOhW5huiluSmih7x5Qf7sFDv8jineu6ehKzi8pKnOq4k8G4QiWn38Y\r\nCeiBkkwFOwj7T3M/ITiiSS9DHDGeokj16eBi83Zx3YYiJ9YZvnQ+4GvqJ5eJJ6pR\r\nKKvemr+m\r\n-----END CERTIFICATE-----', - pinned: true, - ignoreTLS: false - }, - smtp: { - host: 'smtp-mail.outlook.com', - port: 587, - secure: false, - ca: '-----BEGIN CERTIFICATE-----\r\nMIIFQjCCBCqgAwIBAgISESHl0vjrML7zKmGlv42YL75vMA0GCSqGSIb3DQEBBQUA\r\nMF0xCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMTMwMQYD\r\nVQQDEypHbG9iYWxTaWduIE9yZ2FuaXphdGlvbiBWYWxpZGF0aW9uIENBIC0gRzIw\r\nHhcNMTMwNDI0MjAzNTA5WhcNMTYwNDI0MjAzNTA5WjBsMQswCQYDVQQGEwJVUzET\r\nMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMV\r\nTWljcm9zb2Z0IENvcnBvcmF0aW9uMRYwFAYDVQQDDA0qLmhvdG1haWwuY29tMIIB\r\nIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAumSiBWrzHZf6WFP5a/j4+K7D\r\n1izLoYKj5Omll0pdxKvKcBRDf+iaIkCbSOPNpx2uWGZdwNwkabYCQavaBf2ebwmS\r\nS8i1CJpHflO+k0qYd5WUi7sSsZ3+6RaCMdLoDIPGyYMQuy7TFtVO7LSt5+qscyyi\r\nET8c3lE2aj/XW13UZvRrV65ZJvMjUtwaDnIcAxGeasYoebLsKdqHQ2uTr4PmNwCc\r\nviGVFSOzkGAoC0PfyqKB2xUWy3Kc5zRI2xvUW8Jb2b/9Ze3g55pIUzKsjpglkQTm\r\nedVPSYYPGNz6Kl/ZshBXdBAk398q1JkSmUaTMa2hJgBbcC+73ax40AJDGJlz+QID\r\nAQABo4IB6zCCAecwDgYDVR0PAQH/BAQDAgWgMEkGA1UdIARCMEAwPgYGZ4EMAQIC\r\nMDQwMgYIKwYBBQUHAgEWJmh0dHBzOi8vd3d3Lmdsb2JhbHNpZ24uY29tL3JlcG9z\r\naXRvcnkvMEAGA1UdEQQ5MDeCDSouaG90bWFpbC5jb22CCioubGl2ZS5jb22CDSou\r\nb3V0bG9vay5jb22CC2hvdG1haWwuY29tMAkGA1UdEwQCMAAwHQYDVR0lBBYwFAYI\r\nKwYBBQUHAwEGCCsGAQUFBwMCMEUGA1UdHwQ+MDwwOqA4oDaGNGh0dHA6Ly9jcmwu\r\nZ2xvYmFsc2lnbi5jb20vZ3MvZ3Nvcmdhbml6YXRpb252YWxnMi5jcmwwgZYGCCsG\r\nAQUFBwEBBIGJMIGGMEcGCCsGAQUFBzAChjtodHRwOi8vc2VjdXJlLmdsb2JhbHNp\r\nZ24uY29tL2NhY2VydC9nc29yZ2FuaXphdGlvbnZhbGcyLmNydDA7BggrBgEFBQcw\r\nAYYvaHR0cDovL29jc3AyLmdsb2JhbHNpZ24uY29tL2dzb3JnYW5pemF0aW9udmFs\r\nZzIwHQYDVR0OBBYEFHbgHqTLsXDt7uMRyE62rnDEfLn9MB8GA1UdIwQYMBaAFF1G\r\nso3ES3Qcu+31c7Y6tziPdZ5+MA0GCSqGSIb3DQEBBQUAA4IBAQByy1+3N6ZRVooI\r\nxqw8Ng+UFz0g7UHkbPEnvTu1uxJ2AojFuP/P1PAk+/6uMRvpPlWg/5uqmOIWxKxJ\r\nLo6xSbkDf4LN+KYwes3XSuPyziZ4QbPnehHhZ0377iiA8fpRJADg9NWKCRHh5aAd\r\ne9QvJUW/GgYkBN+F4yYc2jIjR3Rehv4JYOKS3iXO9OoHsDS2CcCFaS2imgQVfYLg\r\nslBwT/A08PCOhW5huiluSmih7x5Qf7sFDv8jineu6ehKzi8pKnOq4k8G4QiWn38Y\r\nCeiBkkwFOwj7T3M/ITiiSS9DHDGeokj16eBi83Zx3YYiJ9YZvnQ+4GvqJ5eJJ6pR\r\nKKvemr+m\r\n-----END CERTIFICATE-----', - pinned: true, - ignoreTLS: false - } + smtp: { + host: 'smtp.mail.yahoo.com', + port: 465, + secure: true, + ca: '-----BEGIN CERTIFICATE-----\r\nMIIHITCCBgmgAwIBAgIQBlVSvxlwsqw8Kc8eVV5EKTANBgkqhkiG9w0BAQUFADBm\r\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\r\nd3cuZGlnaWNlcnQuY29tMSUwIwYDVQQDExxEaWdpQ2VydCBIaWdoIEFzc3VyYW5j\r\nZSBDQS0zMB4XDTE0MDYwNTAwMDAwMFoXDTE1MDYyMzEyMDAwMFowYjELMAkGA1UE\r\nBhMCVVMxCzAJBgNVBAgTAkNBMRIwEAYDVQQHEwlTdW5ueXZhbGUxFDASBgNVBAoM\r\nC1lhaG9vISBJbmMuMRwwGgYDVQQDExNzbXRwLm1haWwueWFob28uY29tMIIBIjAN\r\nBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA339OSxTIuW6NVqKwYHFBbg4/tmEx\r\nMYpzohtObPaWgvLTsYdce+WlQFBU5rxaCMUtVsVAGJWs5uQRDVHXRLPnOWe+foqZ\r\n5ItgjYIQc1XeMv6BZEeaF3Bum5ehUyLB3y48cjbWxma1QlkZ8XAK0f9AS7ySWAen\r\no5OXJlMFAqXGtKMAhY46dzSY0wjgdrvgiomFRy0iQKV1DxGsXoXSMEszlUTQoNQ3\r\nWTsiA3O//cdWE06wWeA3/90cb7QkU3KflSoyfi878BQGpPR1L+rLNsqnc8QuucbD\r\nz4Q++rxGqgg4QYrOtmZfAn96TXhPWCwKld6FN+f3uV5ITOBFx44M5v1ILQIDAQAB\r\no4IDzTCCA8kwHwYDVR0jBBgwFoAUUOpzidsp+xCPnuUBINTeeZlIg/cwHQYDVR0O\r\nBBYEFEhOpSFQjO/QrSNfVwyInZUhyzhsMIICJAYDVR0RBIICGzCCAheCE3NtdHAu\r\nbWFpbC55YWhvby5jb22CFnNtdHAubWFpbC55YWhvby5jb20uYXKCFnNtdHAubWFp\r\nbC55YWhvby5jb20uYXWCFnNtdHAubWFpbC55YWhvby5jb20uYnKCFnNtdHAubWFp\r\nbC55YWhvby5jb20uY26CFnNtdHAubWFpbC55YWhvby5jb20uaGuCFnNtdHAubWFp\r\nbC55YWhvby5jb20ubXmCFnNtdHAubWFpbC55YWhvby5jb20ucGiCFnNtdHAubWFp\r\nbC55YWhvby5jb20uc2eCFnNtdHAubWFpbC55YWhvby5jb20udHeCFnNtdHAubWFp\r\nbC55YWhvby5jb20udm6CFXNtdHAubWFpbC55YWhvby5jby5pZIIVc210cC5tYWls\r\nLnlhaG9vLmNvLmlughVzbXRwLm1haWwueWFob28uY28ua3KCFXNtdHAubWFpbC55\r\nYWhvby5jby50aIIVc210cC5tYWlsLnlhaG9vLmNvLnVrghJzbXRwLm1haWwueWFo\r\nb28uY2GCEnNtdHAubWFpbC55YWhvby5jboISc210cC5tYWlsLnlhaG9vLmRlghJz\r\nbXRwLm1haWwueWFob28uZXOCEnNtdHAubWFpbC55YWhvby5mcoISc210cC5tYWls\r\nLnlhaG9vLml0gg9zbXRwLnk3bWFpbC5jb22CFHNtdHAuY29ycmVvLnlhaG9vLmVz\r\nMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIw\r\nYQYDVR0fBFowWDAqoCigJoYkaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL2NhMy1n\r\nMjguY3JsMCqgKKAmhiRodHRwOi8vY3JsNC5kaWdpY2VydC5jb20vY2EzLWcyOC5j\r\ncmwwQgYDVR0gBDswOTA3BglghkgBhv1sAQEwKjAoBggrBgEFBQcCARYcaHR0cHM6\r\nLy93d3cuZGlnaWNlcnQuY29tL0NQUzB7BggrBgEFBQcBAQRvMG0wJAYIKwYBBQUH\r\nMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBFBggrBgEFBQcwAoY5aHR0cDov\r\nL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0SGlnaEFzc3VyYW5jZUNBLTMu\r\nY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQEFBQADggEBABshjUND4hADHGfe\r\ncV9XGXcr9h3u7mT5kgdcgGFhcrFAlwkzt0NUCbuN0X8DWHE72Tpb3zRE25CmUUJe\r\nktBbq9PQb4b5/Wt1htAEw0qUs3BsUbqejK9OHJ/11Jn2ek4+SLJuYlijzc7KM3F/\r\nyz7ZTZtKR0PglkXfqbpvWYGabYpfL2FRLoJ7alTLsMJcFxbSLYcAIMxufj7RyTBJ\r\nbKgRJl4wmP4+Zc2Q1p59mENY0u5HqVAAOmWc0jNb0/31+tRr5f6EgXxK++7TQOpF\r\n0TOaFsXlzRlpKfmIbzVr2nfwghV5/bRZj96TK3g1OoOz4C8ksK4INHnUdTAqZ18M\r\nvHpnJw4=\r\n-----END CERTIFICATE-----', + pinned: true, + ignoreTLS: false + } + }, + tonline: { + imap: { + host: 'secureimap.t-online.de', + port: 993, + secure: true, + ca: '-----BEGIN CERTIFICATE-----\r\nMIIGmzCCBYOgAwIBAgIIIbZ3foy9DqgwDQYJKoZIhvcNAQEFBQAwcTELMAkGA1UE\r\nBhMCREUxHDAaBgNVBAoTE0RldXRzY2hlIFRlbGVrb20gQUcxHzAdBgNVBAsTFlQt\r\nVGVsZVNlYyBUcnVzdCBDZW50ZXIxIzAhBgNVBAMTGkRldXRzY2hlIFRlbGVrb20g\r\nUm9vdCBDQSAyMB4XDTEzMDMwMTEzNTgyOVoXDTE5MDcwOTIzNTkwMFowgckxCzAJ\r\nBgNVBAYTAkRFMSUwIwYDVQQKExxULVN5c3RlbXMgSW50ZXJuYXRpb25hbCBHbWJI\r\nMR8wHQYDVQQLExZULVN5c3RlbXMgVHJ1c3QgQ2VudGVyMQwwCgYDVQQIEwNOUlcx\r\nDjAMBgNVBBETBTU3MjUwMRAwDgYDVQQHEwdOZXRwaGVuMSAwHgYDVQQJExdVbnRl\r\ncmUgSW5kdXN0cmllc3RyLiAyMDEgMB4GA1UEAxMXVGVsZVNlYyBTZXJ2ZXJQYXNz\r\nIERFLTEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCwg+9QiuYAxX9/\r\n4F9XRZrS1o0q+aa9L/5/K3vc+RqPpliiZ24vTkJc1JDpXrbWXS25uT3yzHukQrhI\r\nq0AbcRqNEAeFi5EhUiEM/vtb4BYGHdqfXQ3etgYYcCtJ43NAHaSgsyQ9kyGV2lmM\r\nwkeAX3qZ2CGE9/cR6w+bOogHArBdk2JaHG09myNZDytr6oUbWLjLd/qhC9YzyZSX\r\nbZgE/kh5L5Y6P9paw2pDdn7+Ni4pXzlmoj1k43uiz+h2ibe3DO9dKMZAaEKeyG1O\r\ng0f0r53M8O+8Bm2sXtWelrAgrfFlISgWzO1hkNs12rWpr4c5Ygde/behx9OQmPwp\r\nmS+e3WvTAgMBAAGjggLcMIIC2DAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFGJP\r\nE842Z4TNGfygTxmL7xVUATIcMB8GA1UdIwQYMBaAFDHDeRu69VPXF+CJei0XbAqz\r\nK50zMBIGA1UdEwEB/wQIMAYBAf8CAQAwWQYDVR0gBFIwUDBEBgkrBgEEAb1HDQIw\r\nNzA1BggrBgEFBQcCARYpaHR0cDovL3d3dy50ZWxlc2VjLmRlL3NlcnZlcnBhc3Mv\r\nY3BzLmh0bWwwCAYGZ4EMAQICMIHvBgNVHR8EgecwgeQwOqA4oDaGNGh0dHA6Ly9j\r\ncmwuc2VydmVycGFzcy50ZWxlc2VjLmRlL3JsL0RUX1JPT1RfQ0FfMi5jcmwwgaWg\r\ngaKggZ+GgZxsZGFwOi8vbGRhcC5zZXJ2ZXJwYXNzLnRlbGVzZWMuZGUvQ049RGV1\r\ndHNjaGUlMjBUZWxla29tJTIwUm9vdCUyMENBJTIwMixPVT1ULVRlbGVTZWMlMjBU\r\ncnVzdCUyMENlbnRlcixPPURldXRzY2hlJTIwVGVsZWtvbSUyMEFHLEM9REU/QXV0\r\naG9yaXR5UmV2b2NhdGlvbkxpc3QwggEjBggrBgEFBQcBAQSCARUwggERMCoGCCsG\r\nAQUFBzABhh5odHRwOi8vb2NzcDAyLnRlbGVzZWMuZGUvb2NzcHIwQQYIKwYBBQUH\r\nMAKGNWh0dHA6Ly9jcmwuc2VydmVycGFzcy50ZWxlc2VjLmRlL2NydC9EVF9ST09U\r\nX0NBXzIuY2VyMIGfBggrBgEFBQcwAoaBkmxkYXA6Ly9sZGFwLnNlcnZlcnBhc3Mu\r\ndGVsZXNlYy5kZS9DTj1EZXV0c2NoZSUyMFRlbGVrb20lMjBSb290JTIwQ0ElMjAy\r\nLE9VPVQtVGVsZVNlYyUyMFRydXN0JTIwQ2VudGVyLE89RGV1dHNjaGUlMjBUZWxl\r\na29tJTIwQUcsQz1ERT9jQUNlcnRpZmljYXRlMA0GCSqGSIb3DQEBBQUAA4IBAQBO\r\nE04qoEkEc9ad+WwSurVYfcDdjGvpqrtbI89woXDsWLQTMhA7D7jVuls90SJns0vc\r\nK9qoYkEGt0/ZlawLe2lyNWtueHfUf+dgleUunwHYLxuj3jQ2ERzQLVLrswjecRpX\r\nvGAGej89WpGQ9PMq27WGNC5WCmzVC9rk5naFgacsbwKwyjU0LoBArtAQnAAlpHDw\r\nPenv1Pe7MhUkCK0LqdTvkI/AHFzPYg/l5E3j8lQQ8hiKx8U6wf9xVKECLA2RlRqY\r\nUX2rpjQNxnvEq/mEQv3x3mLOEFJ3TAKI+soDgOOi0OG8+ywhm6S+7Z9lTlJ+BcD6\r\noy1MNKd4CQbltHLMTFUH\r\n-----END CERTIFICATE-----', + pinned: true, + ignoreTLS: false }, - gmx: { - imap: { - host: 'imap.gmx.net', - port: 993, - secure: true, - ca: '-----BEGIN CERTIFICATE-----\r\nMIIHUDCCBjigAwIBAgIIM52e2Oc5J8kwDQYJKoZIhvcNAQEFBQAwgckxCzAJBgNV\r\nBAYTAkRFMSUwIwYDVQQKExxULVN5c3RlbXMgSW50ZXJuYXRpb25hbCBHbWJIMR8w\r\nHQYDVQQLExZULVN5c3RlbXMgVHJ1c3QgQ2VudGVyMQwwCgYDVQQIEwNOUlcxDjAM\r\nBgNVBBETBTU3MjUwMRAwDgYDVQQHEwdOZXRwaGVuMSAwHgYDVQQJExdVbnRlcmUg\r\nSW5kdXN0cmllc3RyLiAyMDEgMB4GA1UEAxMXVGVsZVNlYyBTZXJ2ZXJQYXNzIERF\r\nLTEwHhcNMTMxMTEyMTAxNzMxWhcNMTYxMTE3MjM1OTU5WjCBnTELMAkGA1UEBhMC\r\nREUxHjAcBgNVBAoMFTEmMSBNYWlsICYgTWVkaWEgR21iSDEdMBsGA1UECBMUUmhp\r\nbmVsYW5kLVBhbGF0aW5hdGUxEjAQBgNVBAcTCU1vbnRhYmF1cjEkMCIGCSqGSIb3\r\nDQEJARYVc2VydmVyLWNlcnRzQDF1bmQxLmRlMRUwEwYDVQQDEwxpbWFwLmdteC5u\r\nZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDHP3QN1ztX5m8LaC9m\r\nt2nrcLBEfxb61FQCieXW4OV/D5HEy7vgQIAeS+pg2/9ClawprQFbLUa46owUcJuq\r\no2SaefsYqjRhIP/tPeyGRJQmfnyCRRoHIYno57/bz2qmHjzvkqEUMvNGVVej7BJv\r\nGukNKPXYicodkRodF3EicTPnGitGeXOsAxyPuCPIsBPAMuuuBzKenS7nj+yseSGe\r\nkM7/kjtNgbjDyBXi2BvoUS/U+Gm9p1lq0ymT9fhGj/k9/Qz312GHYxfZB1bhL0pk\r\nadF25P8fTIy5tKMWqJfgCF+eaOwgkYecxeCcrt2E3D8ThLvmkBRrXsTT51ZzD7sp\r\n7hPpAgMBAAGjggNkMIIDYDAfBgNVHSMEGDAWgBRiTxPONmeEzRn8oE8Zi+8VVAEy\r\nHDAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMB\r\nMB0GA1UdDgQWBBRfb2gYyD6V+oBN2Ii5WQj0AddemjBZBgNVHSAEUjBQMEQGCSsG\r\nAQQBvUcNAjA3MDUGCCsGAQUFBwIBFilodHRwOi8vd3d3LnRlbGVzZWMuZGUvc2Vy\r\ndmVycGFzcy9jcHMuaHRtbDAIBgZngQwBAgIwggEhBgNVHR8EggEYMIIBFDBFoEOg\r\nQYY/aHR0cDovL2NybC5zZXJ2ZXJwYXNzLnRlbGVzZWMuZGUvcmwvVGVsZVNlY19T\r\nZXJ2ZXJQYXNzX0RFLTEuY3JsMIHKoIHHoIHEhoHBbGRhcDovL2xkYXAuc2VydmVy\r\ncGFzcy50ZWxlc2VjLmRlL2NuPVRlbGVTZWMlMjBTZXJ2ZXJQYXNzJTIwREUtMSxv\r\ndT1ULVN5c3RlbXMlMjBUcnVzdCUyMENlbnRlcixvPVQtU3lzdGVtcyUyMEludGVy\r\nbmF0aW9uYWwlMjBHbWJILGM9ZGU/Y2VydGlmaWNhdGVSZXZvY2F0aW9ubGlzdD9i\r\nYXNlP2NlcnRpZmljYXRlUmV2b2NhdGlvbmxpc3Q9KjCCATkGCCsGAQUFBwEBBIIB\r\nKzCCAScwMwYIKwYBBQUHMAGGJ2h0dHA6Ly9vY3NwLnNlcnZlcnBhc3MudGVsZXNl\r\nYy5kZS9vY3NwcjBMBggrBgEFBQcwAoZAaHR0cDovL2NybC5zZXJ2ZXJwYXNzLnRl\r\nbGVzZWMuZGUvY3J0L1RlbGVTZWNfU2VydmVyUGFzc19ERS0xLmNlcjCBoQYIKwYB\r\nBQUHMAKGgZRsZGFwOi8vbGRhcC5zZXJ2ZXJwYXNzLnRlbGVzZWMuZGUvY249VGVs\r\nZVNlYyUyMFNlcnZlclBhc3MlMjBERS0xLG91PVQtU3lzdGVtcyUyMFRydXN0JTIw\r\nQ2VudGVyLG89VC1TeXN0ZW1zJTIwSW50ZXJuYXRpb25hbCUyMEdtYkgsYz1kZT9j\r\nQUNlcnRpZmljYXRlMAwGA1UdEwEB/wQCMAAwJAYDVR0RBB0wG4IMaW1hcC5nbXgu\r\nbmV0ggtpbWFwLmdteC5kZTANBgkqhkiG9w0BAQUFAAOCAQEADl442s2ouynDNEzl\r\nBPzhCxjp47TmKzgzrCGh1AH3+UOkw54mYU65UaY9cJ51jgOu+pjNB4KxVfcqyG5O\r\nJcowS09ZtZ+khb1OkzXAAbEqNj0oyH/1K550d5Ir5VbXF0ZdVKVTzEFRyjvUt5NJ\r\n/b6Q2bbY/sPm8QiK9SPaYkO5/3J1KB0u7PkqqfOhZ9UJE6mIN66T2bpFmAwle3zL\r\n6+L/VwYSgUNs/w2l6xWlYP3pFFaT12TO43Q057I3vP7yCt3QE/VLDpdQOQ3H0oy2\r\nJQGPJLFby9MPYNkmEmwjh4e95TAY4ZkZMe08ix0J1Smy8DwjTz6SXqYC+EC/PQlh\r\nE0UHxA==\r\n-----END CERTIFICATE-----', - pinned: true, - ignoreTLS: false - }, - smtp: { - host: 'mail.gmx.net', - port: 587, - secure: false, - ca: '-----BEGIN CERTIFICATE-----\r\nMIIHbDCCBlSgAwIBAgIJAJ74Ek9UaA0TMA0GCSqGSIb3DQEBBQUAMIHJMQswCQYD\r\nVQQGEwJERTElMCMGA1UEChMcVC1TeXN0ZW1zIEludGVybmF0aW9uYWwgR21iSDEf\r\nMB0GA1UECxMWVC1TeXN0ZW1zIFRydXN0IENlbnRlcjEMMAoGA1UECBMDTlJXMQ4w\r\nDAYDVQQREwU1NzI1MDEQMA4GA1UEBxMHTmV0cGhlbjEgMB4GA1UECRMXVW50ZXJl\r\nIEluZHVzdHJpZXN0ci4gMjAxIDAeBgNVBAMTF1RlbGVTZWMgU2VydmVyUGFzcyBE\r\nRS0xMB4XDTEzMTExMjEwMTY0N1oXDTE2MTExNzIzNTk1OVowgZ0xCzAJBgNVBAYT\r\nAkRFMR4wHAYDVQQKDBUxJjEgTWFpbCAmIE1lZGlhIEdtYkgxHTAbBgNVBAgTFFJo\r\naW5lbGFuZC1QYWxhdGluYXRlMRIwEAYDVQQHEwlNb250YWJhdXIxJDAiBgkqhkiG\r\n9w0BCQEWFXNlcnZlci1jZXJ0c0AxdW5kMS5kZTEVMBMGA1UEAxMMbWFpbC5nbXgu\r\nbmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsEUYppbJXUpQELIL\r\nETAFUwreDfSmbyi9gJPUqquogROHEoRV+bBFZ2YCiHgLU8AggcLmM74HAQMFt8F5\r\neS2OyuEar3E3tNW5XFiI1QblTtY3B7o1coKyq/i+tV3b1jGq4+1aJeeD3UpcraBm\r\nk2XuQgQ6WAeY+XCYyI/OzZ56ZjQ17LHMMI0ayu7SSV+VBpG9loC9E5A82iVtZsqo\r\nSaUZpZf/xICpUgNJ+RlalSsoE6FdCywE/gEEmkpAUh+Mv0WvHITk6aqtMFWex8tE\r\n8cwJGQbmJGt9x8BExbhMY6uQ+HsnsDCPCMVh5JGKx7/e+qyxtHYGChOeI16m2+MT\r\nz47jSQIDAQABo4IDfzCCA3swHwYDVR0jBBgwFoAUYk8TzjZnhM0Z/KBPGYvvFVQB\r\nMhwwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcD\r\nATAdBgNVHQ4EFgQUH8mLfxsd5FEh9M+3t3rP6qLiFTEwWQYDVR0gBFIwUDBEBgkr\r\nBgEEAb1HDQIwNzA1BggrBgEFBQcCARYpaHR0cDovL3d3dy50ZWxlc2VjLmRlL3Nl\r\ncnZlcnBhc3MvY3BzLmh0bWwwCAYGZ4EMAQICMIIBIQYDVR0fBIIBGDCCARQwRaBD\r\noEGGP2h0dHA6Ly9jcmwuc2VydmVycGFzcy50ZWxlc2VjLmRlL3JsL1RlbGVTZWNf\r\nU2VydmVyUGFzc19ERS0xLmNybDCByqCBx6CBxIaBwWxkYXA6Ly9sZGFwLnNlcnZl\r\ncnBhc3MudGVsZXNlYy5kZS9jbj1UZWxlU2VjJTIwU2VydmVyUGFzcyUyMERFLTEs\r\nb3U9VC1TeXN0ZW1zJTIwVHJ1c3QlMjBDZW50ZXIsbz1ULVN5c3RlbXMlMjBJbnRl\r\ncm5hdGlvbmFsJTIwR21iSCxjPWRlP2NlcnRpZmljYXRlUmV2b2NhdGlvbmxpc3Q/\r\nYmFzZT9jZXJ0aWZpY2F0ZVJldm9jYXRpb25saXN0PSowggE5BggrBgEFBQcBAQSC\r\nASswggEnMDMGCCsGAQUFBzABhidodHRwOi8vb2NzcC5zZXJ2ZXJwYXNzLnRlbGVz\r\nZWMuZGUvb2NzcHIwTAYIKwYBBQUHMAKGQGh0dHA6Ly9jcmwuc2VydmVycGFzcy50\r\nZWxlc2VjLmRlL2NydC9UZWxlU2VjX1NlcnZlclBhc3NfREUtMS5jZXIwgaEGCCsG\r\nAQUFBzAChoGUbGRhcDovL2xkYXAuc2VydmVycGFzcy50ZWxlc2VjLmRlL2NuPVRl\r\nbGVTZWMlMjBTZXJ2ZXJQYXNzJTIwREUtMSxvdT1ULVN5c3RlbXMlMjBUcnVzdCUy\r\nMENlbnRlcixvPVQtU3lzdGVtcyUyMEludGVybmF0aW9uYWwlMjBHbWJILGM9ZGU/\r\nY0FDZXJ0aWZpY2F0ZTAMBgNVHRMBAf8EAjAAMD8GA1UdEQQ4MDaCDG1haWwuZ214\r\nLm5ldIILbWFpbC5nbXguZGWCDHNtdHAuZ214Lm5ldIILc210cC5nbXguZGUwDQYJ\r\nKoZIhvcNAQEFBQADggEBAJ5SKsBXoOTO0ztsFh8RVd/iu6sijEMpKVF+/cs74TZN\r\nK1QWIz8Ay1f3SUPWXTvkmvayUXWGtxZfYoRIrx3feWBdUozJxe0wz1O3tDQOQgRX\r\nnkcwk7nibTUmW2rk5AIwV0jzOHtIbAGjSdc6my0543e7dVkhcuoCOC+g2NmD+pHF\r\ni1KpmqTLecDlFvprIZUJ23AT1uTnuXYg/tFHeDn3ga00Gce82xZQJelRirJs01SR\r\nDGEBaPXBjJVJNoAP/qEy3jQNRWo5TbSgwn3DTR81FJvltmf0c+zlt4fmcpjpr5ni\r\nFvK7L0rZQL5MOGHPpgRIukEBZCowYr3OYpZYBEaB94I=\r\n-----END CERTIFICATE-----', - pinned: true, - ignoreTLS: false - } + smtp: { + host: 'securesmtp.t-online.de', + port: 465, + secure: true, + ca: '-----BEGIN CERTIFICATE-----\r\nMIIGmzCCBYOgAwIBAgIIIbZ3foy9DqgwDQYJKoZIhvcNAQEFBQAwcTELMAkGA1UE\r\nBhMCREUxHDAaBgNVBAoTE0RldXRzY2hlIFRlbGVrb20gQUcxHzAdBgNVBAsTFlQt\r\nVGVsZVNlYyBUcnVzdCBDZW50ZXIxIzAhBgNVBAMTGkRldXRzY2hlIFRlbGVrb20g\r\nUm9vdCBDQSAyMB4XDTEzMDMwMTEzNTgyOVoXDTE5MDcwOTIzNTkwMFowgckxCzAJ\r\nBgNVBAYTAkRFMSUwIwYDVQQKExxULVN5c3RlbXMgSW50ZXJuYXRpb25hbCBHbWJI\r\nMR8wHQYDVQQLExZULVN5c3RlbXMgVHJ1c3QgQ2VudGVyMQwwCgYDVQQIEwNOUlcx\r\nDjAMBgNVBBETBTU3MjUwMRAwDgYDVQQHEwdOZXRwaGVuMSAwHgYDVQQJExdVbnRl\r\ncmUgSW5kdXN0cmllc3RyLiAyMDEgMB4GA1UEAxMXVGVsZVNlYyBTZXJ2ZXJQYXNz\r\nIERFLTEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCwg+9QiuYAxX9/\r\n4F9XRZrS1o0q+aa9L/5/K3vc+RqPpliiZ24vTkJc1JDpXrbWXS25uT3yzHukQrhI\r\nq0AbcRqNEAeFi5EhUiEM/vtb4BYGHdqfXQ3etgYYcCtJ43NAHaSgsyQ9kyGV2lmM\r\nwkeAX3qZ2CGE9/cR6w+bOogHArBdk2JaHG09myNZDytr6oUbWLjLd/qhC9YzyZSX\r\nbZgE/kh5L5Y6P9paw2pDdn7+Ni4pXzlmoj1k43uiz+h2ibe3DO9dKMZAaEKeyG1O\r\ng0f0r53M8O+8Bm2sXtWelrAgrfFlISgWzO1hkNs12rWpr4c5Ygde/behx9OQmPwp\r\nmS+e3WvTAgMBAAGjggLcMIIC2DAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFGJP\r\nE842Z4TNGfygTxmL7xVUATIcMB8GA1UdIwQYMBaAFDHDeRu69VPXF+CJei0XbAqz\r\nK50zMBIGA1UdEwEB/wQIMAYBAf8CAQAwWQYDVR0gBFIwUDBEBgkrBgEEAb1HDQIw\r\nNzA1BggrBgEFBQcCARYpaHR0cDovL3d3dy50ZWxlc2VjLmRlL3NlcnZlcnBhc3Mv\r\nY3BzLmh0bWwwCAYGZ4EMAQICMIHvBgNVHR8EgecwgeQwOqA4oDaGNGh0dHA6Ly9j\r\ncmwuc2VydmVycGFzcy50ZWxlc2VjLmRlL3JsL0RUX1JPT1RfQ0FfMi5jcmwwgaWg\r\ngaKggZ+GgZxsZGFwOi8vbGRhcC5zZXJ2ZXJwYXNzLnRlbGVzZWMuZGUvQ049RGV1\r\ndHNjaGUlMjBUZWxla29tJTIwUm9vdCUyMENBJTIwMixPVT1ULVRlbGVTZWMlMjBU\r\ncnVzdCUyMENlbnRlcixPPURldXRzY2hlJTIwVGVsZWtvbSUyMEFHLEM9REU/QXV0\r\naG9yaXR5UmV2b2NhdGlvbkxpc3QwggEjBggrBgEFBQcBAQSCARUwggERMCoGCCsG\r\nAQUFBzABhh5odHRwOi8vb2NzcDAyLnRlbGVzZWMuZGUvb2NzcHIwQQYIKwYBBQUH\r\nMAKGNWh0dHA6Ly9jcmwuc2VydmVycGFzcy50ZWxlc2VjLmRlL2NydC9EVF9ST09U\r\nX0NBXzIuY2VyMIGfBggrBgEFBQcwAoaBkmxkYXA6Ly9sZGFwLnNlcnZlcnBhc3Mu\r\ndGVsZXNlYy5kZS9DTj1EZXV0c2NoZSUyMFRlbGVrb20lMjBSb290JTIwQ0ElMjAy\r\nLE9VPVQtVGVsZVNlYyUyMFRydXN0JTIwQ2VudGVyLE89RGV1dHNjaGUlMjBUZWxl\r\na29tJTIwQUcsQz1ERT9jQUNlcnRpZmljYXRlMA0GCSqGSIb3DQEBBQUAA4IBAQBO\r\nE04qoEkEc9ad+WwSurVYfcDdjGvpqrtbI89woXDsWLQTMhA7D7jVuls90SJns0vc\r\nK9qoYkEGt0/ZlawLe2lyNWtueHfUf+dgleUunwHYLxuj3jQ2ERzQLVLrswjecRpX\r\nvGAGej89WpGQ9PMq27WGNC5WCmzVC9rk5naFgacsbwKwyjU0LoBArtAQnAAlpHDw\r\nPenv1Pe7MhUkCK0LqdTvkI/AHFzPYg/l5E3j8lQQ8hiKx8U6wf9xVKECLA2RlRqY\r\nUX2rpjQNxnvEq/mEQv3x3mLOEFJ3TAKI+soDgOOi0OG8+ywhm6S+7Z9lTlJ+BcD6\r\noy1MNKd4CQbltHLMTFUH\r\n-----END CERTIFICATE-----', + pinned: true, + ignoreTLS: false + } + }, + outlook: { + imap: { + host: 'imap-mail.outlook.com', + port: 993, + secure: true, + ca: '-----BEGIN CERTIFICATE-----\r\nMIIFQjCCBCqgAwIBAgISESHl0vjrML7zKmGlv42YL75vMA0GCSqGSIb3DQEBBQUA\r\nMF0xCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMTMwMQYD\r\nVQQDEypHbG9iYWxTaWduIE9yZ2FuaXphdGlvbiBWYWxpZGF0aW9uIENBIC0gRzIw\r\nHhcNMTMwNDI0MjAzNTA5WhcNMTYwNDI0MjAzNTA5WjBsMQswCQYDVQQGEwJVUzET\r\nMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMV\r\nTWljcm9zb2Z0IENvcnBvcmF0aW9uMRYwFAYDVQQDDA0qLmhvdG1haWwuY29tMIIB\r\nIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAumSiBWrzHZf6WFP5a/j4+K7D\r\n1izLoYKj5Omll0pdxKvKcBRDf+iaIkCbSOPNpx2uWGZdwNwkabYCQavaBf2ebwmS\r\nS8i1CJpHflO+k0qYd5WUi7sSsZ3+6RaCMdLoDIPGyYMQuy7TFtVO7LSt5+qscyyi\r\nET8c3lE2aj/XW13UZvRrV65ZJvMjUtwaDnIcAxGeasYoebLsKdqHQ2uTr4PmNwCc\r\nviGVFSOzkGAoC0PfyqKB2xUWy3Kc5zRI2xvUW8Jb2b/9Ze3g55pIUzKsjpglkQTm\r\nedVPSYYPGNz6Kl/ZshBXdBAk398q1JkSmUaTMa2hJgBbcC+73ax40AJDGJlz+QID\r\nAQABo4IB6zCCAecwDgYDVR0PAQH/BAQDAgWgMEkGA1UdIARCMEAwPgYGZ4EMAQIC\r\nMDQwMgYIKwYBBQUHAgEWJmh0dHBzOi8vd3d3Lmdsb2JhbHNpZ24uY29tL3JlcG9z\r\naXRvcnkvMEAGA1UdEQQ5MDeCDSouaG90bWFpbC5jb22CCioubGl2ZS5jb22CDSou\r\nb3V0bG9vay5jb22CC2hvdG1haWwuY29tMAkGA1UdEwQCMAAwHQYDVR0lBBYwFAYI\r\nKwYBBQUHAwEGCCsGAQUFBwMCMEUGA1UdHwQ+MDwwOqA4oDaGNGh0dHA6Ly9jcmwu\r\nZ2xvYmFsc2lnbi5jb20vZ3MvZ3Nvcmdhbml6YXRpb252YWxnMi5jcmwwgZYGCCsG\r\nAQUFBwEBBIGJMIGGMEcGCCsGAQUFBzAChjtodHRwOi8vc2VjdXJlLmdsb2JhbHNp\r\nZ24uY29tL2NhY2VydC9nc29yZ2FuaXphdGlvbnZhbGcyLmNydDA7BggrBgEFBQcw\r\nAYYvaHR0cDovL29jc3AyLmdsb2JhbHNpZ24uY29tL2dzb3JnYW5pemF0aW9udmFs\r\nZzIwHQYDVR0OBBYEFHbgHqTLsXDt7uMRyE62rnDEfLn9MB8GA1UdIwQYMBaAFF1G\r\nso3ES3Qcu+31c7Y6tziPdZ5+MA0GCSqGSIb3DQEBBQUAA4IBAQByy1+3N6ZRVooI\r\nxqw8Ng+UFz0g7UHkbPEnvTu1uxJ2AojFuP/P1PAk+/6uMRvpPlWg/5uqmOIWxKxJ\r\nLo6xSbkDf4LN+KYwes3XSuPyziZ4QbPnehHhZ0377iiA8fpRJADg9NWKCRHh5aAd\r\ne9QvJUW/GgYkBN+F4yYc2jIjR3Rehv4JYOKS3iXO9OoHsDS2CcCFaS2imgQVfYLg\r\nslBwT/A08PCOhW5huiluSmih7x5Qf7sFDv8jineu6ehKzi8pKnOq4k8G4QiWn38Y\r\nCeiBkkwFOwj7T3M/ITiiSS9DHDGeokj16eBi83Zx3YYiJ9YZvnQ+4GvqJ5eJJ6pR\r\nKKvemr+m\r\n-----END CERTIFICATE-----', + pinned: true, + ignoreTLS: false }, - webde: { - imap: { - host: 'imap.web.de', - port: 993, - secure: true, - ca: '-----BEGIN CERTIFICATE-----\r\nMIIHUjCCBjqgAwIBAgIIHvqUOScyxYUwDQYJKoZIhvcNAQEFBQAwgckxCzAJBgNV\r\nBAYTAkRFMSUwIwYDVQQKExxULVN5c3RlbXMgSW50ZXJuYXRpb25hbCBHbWJIMR8w\r\nHQYDVQQLExZULVN5c3RlbXMgVHJ1c3QgQ2VudGVyMQwwCgYDVQQIEwNOUlcxDjAM\r\nBgNVBBETBTU3MjUwMRAwDgYDVQQHEwdOZXRwaGVuMSAwHgYDVQQJExdVbnRlcmUg\r\nSW5kdXN0cmllc3RyLiAyMDEgMB4GA1UEAxMXVGVsZVNlYyBTZXJ2ZXJQYXNzIERF\r\nLTEwHhcNMTMwODIxMDgzMTE2WhcNMTYwODI2MjM1OTU5WjCBrTELMAkGA1UEBhMC\r\nREUxHjAcBgNVBAoMFTEmMSBNYWlsICYgTWVkaWEgR21iSDEPMA0GA1UECxMGV0VC\r\nLkRFMR0wGwYDVQQIExRSaGluZWxhbmQtUGFsYXRpbmF0ZTESMBAGA1UEBxMJTW9u\r\ndGFiYXVyMSQwIgYJKoZIhvcNAQkBFhVzZXJ2ZXItY2VydHNAMXVuZDEuZGUxFDAS\r\nBgNVBAMTC2ltYXAud2ViLmRlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC\r\nAQEA5dvpcnYczFs9GANTYN5N3mP6ATFVm7P4nYhRIQMj/YssSjmbrOPIEbe12JNC\r\nRO9CI7Z4Dnk42BM5TiH+QKGpcfnitofOv4gKPjwCMcPDHfY152/+YDaiyU/md2Hg\r\n/WrZ/50KwC8Sw1tZkYDXWB0zeJkfPdS2r4ATNrpMR0fYcG08+elz5T2SWNg+c1xL\r\nKFdCh33wZLkijOfW0HA164QjXaLPBjxa+GyZFY19ywOQ85KdFVVLmGUrLz8n4ZLc\r\n7K6KUuzYttnUuVxctFauQ4DRHE/CfUvvNHHgn5d+A2XS7jNUgmUb0gjzy3OtlTP3\r\ngM4ostVufmnlS7qqFDR7A3v4lQIDAQABo4IDVjCCA1IwHwYDVR0jBBgwFoAUYk8T\r\nzjZnhM0Z/KBPGYvvFVQBMhwwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsG\r\nAQUFBwMCBggrBgEFBQcDATAdBgNVHQ4EFgQUDH66P/Y4NlKGphPniIzmutEI9ocw\r\nWQYDVR0gBFIwUDBEBgkrBgEEAb1HDQIwNzA1BggrBgEFBQcCARYpaHR0cDovL3d3\r\ndy50ZWxlc2VjLmRlL3NlcnZlcnBhc3MvY3BzLmh0bWwwCAYGZ4EMAQICMIIBIQYD\r\nVR0fBIIBGDCCARQwRaBDoEGGP2h0dHA6Ly9jcmwuc2VydmVycGFzcy50ZWxlc2Vj\r\nLmRlL3JsL1RlbGVTZWNfU2VydmVyUGFzc19ERS0xLmNybDCByqCBx6CBxIaBwWxk\r\nYXA6Ly9sZGFwLnNlcnZlcnBhc3MudGVsZXNlYy5kZS9jbj1UZWxlU2VjJTIwU2Vy\r\ndmVyUGFzcyUyMERFLTEsb3U9VC1TeXN0ZW1zJTIwVHJ1c3QlMjBDZW50ZXIsbz1U\r\nLVN5c3RlbXMlMjBJbnRlcm5hdGlvbmFsJTIwR21iSCxjPWRlP2NlcnRpZmljYXRl\r\nUmV2b2NhdGlvbmxpc3Q/YmFzZT9jZXJ0aWZpY2F0ZVJldm9jYXRpb25saXN0PSow\r\nggE5BggrBgEFBQcBAQSCASswggEnMDMGCCsGAQUFBzABhidodHRwOi8vb2NzcC5z\r\nZXJ2ZXJwYXNzLnRlbGVzZWMuZGUvb2NzcHIwTAYIKwYBBQUHMAKGQGh0dHA6Ly9j\r\ncmwuc2VydmVycGFzcy50ZWxlc2VjLmRlL2NydC9UZWxlU2VjX1NlcnZlclBhc3Nf\r\nREUtMS5jZXIwgaEGCCsGAQUFBzAChoGUbGRhcDovL2xkYXAuc2VydmVycGFzcy50\r\nZWxlc2VjLmRlL2NuPVRlbGVTZWMlMjBTZXJ2ZXJQYXNzJTIwREUtMSxvdT1ULVN5\r\nc3RlbXMlMjBUcnVzdCUyMENlbnRlcixvPVQtU3lzdGVtcyUyMEludGVybmF0aW9u\r\nYWwlMjBHbWJILGM9ZGU/Y0FDZXJ0aWZpY2F0ZTAMBgNVHRMBAf8EAjAAMBYGA1Ud\r\nEQQPMA2CC2ltYXAud2ViLmRlMA0GCSqGSIb3DQEBBQUAA4IBAQAp7MQjjOWQ0M7N\r\n21GrPDfWSMR3eJnuMs37I9G2t9i99w7xKtmoVBPyYMORL1zRHn/DEguo4j5ua7CH\r\nrKLh9Sd1wDKqVWGFxvmP4f/mvEx5YVI68mg+M2VQf/h58IXTTZRbUcsv3HVeruI6\r\npgQNUsEQqRVJmrgT/iPd98RhhzBqef6Wfrt3Ns6N835egphxUVcVj/v/PqBCZKQb\r\nNl5QsoaHDqh3XC+Og2awOGvWHUzxUKEqkP5nsMs2YjfOtcQRtxdislQAfRktuUBA\r\nWMZAlvoVF/CIyFvcUH11KNur7HfK5PptvtdvLQaF2c+lN+LGP6D+nbTLqAo1v09o\r\nnZpHbQOt\r\n-----END CERTIFICATE-----', - pinned: true, - ignoreTLS: false - }, - smtp: { - host: 'smtp.web.de', - port: 587, - secure: false, - ca: '-----BEGIN CERTIFICATE-----\r\nMIIHUzCCBjugAwIBAgIJAKaEwtMgQ2s5MA0GCSqGSIb3DQEBBQUAMIHJMQswCQYD\r\nVQQGEwJERTElMCMGA1UEChMcVC1TeXN0ZW1zIEludGVybmF0aW9uYWwgR21iSDEf\r\nMB0GA1UECxMWVC1TeXN0ZW1zIFRydXN0IENlbnRlcjEMMAoGA1UECBMDTlJXMQ4w\r\nDAYDVQQREwU1NzI1MDEQMA4GA1UEBxMHTmV0cGhlbjEgMB4GA1UECRMXVW50ZXJl\r\nIEluZHVzdHJpZXN0ci4gMjAxIDAeBgNVBAMTF1RlbGVTZWMgU2VydmVyUGFzcyBE\r\nRS0xMB4XDTEzMDgyMTA4MzU1MVoXDTE2MDgyNjIzNTk1OVowga0xCzAJBgNVBAYT\r\nAkRFMR4wHAYDVQQKDBUxJjEgTWFpbCAmIE1lZGlhIEdtYkgxDzANBgNVBAsTBldF\r\nQi5ERTEdMBsGA1UECBMUUmhpbmVsYW5kLVBhbGF0aW5hdGUxEjAQBgNVBAcTCU1v\r\nbnRhYmF1cjEkMCIGCSqGSIb3DQEJARYVc2VydmVyLWNlcnRzQDF1bmQxLmRlMRQw\r\nEgYDVQQDEwtzbXRwLndlYi5kZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC\r\nggEBAL1WfyFqmifaEr6h9yntJKs33fnEVu0pQqS5RV9rnQU0E/cJYJ9RJOoTjhpE\r\nkT1LW8DyTM5vUzpgtFHQM0BOO6jOeZXUYNUaopK3yO4l/cpiitigtk9ZnWfKzbU3\r\n9hlhf+YwUUvoqjUA8I6fSu+VNPNlOBvamo18oHljXAPltL9oiwAxvTLNktBMy4T9\r\njxU1DeGoPbJKGwV7zIBQ2qUHuLkMvy5/H39t79Tih+zfzgfY/xIUfdmPNY9dK8ZY\r\nAtyF/RiUS199pd9dV4Vwh6JLvtNlWchBnKVhragLCGbkjnvUXmS1BPrclLR23s1v\r\nMehgOD2WhXl8PoqrOPb6y+lgMlECAwEAAaOCA1YwggNSMB8GA1UdIwQYMBaAFGJP\r\nE842Z4TNGfygTxmL7xVUATIcMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggr\r\nBgEFBQcDAgYIKwYBBQUHAwEwHQYDVR0OBBYEFAUuppozNCv2UzMV5p6CTpkEhsRS\r\nMFkGA1UdIARSMFAwRAYJKwYBBAG9Rw0CMDcwNQYIKwYBBQUHAgEWKWh0dHA6Ly93\r\nd3cudGVsZXNlYy5kZS9zZXJ2ZXJwYXNzL2Nwcy5odG1sMAgGBmeBDAECAjCCASEG\r\nA1UdHwSCARgwggEUMEWgQ6BBhj9odHRwOi8vY3JsLnNlcnZlcnBhc3MudGVsZXNl\r\nYy5kZS9ybC9UZWxlU2VjX1NlcnZlclBhc3NfREUtMS5jcmwwgcqggceggcSGgcFs\r\nZGFwOi8vbGRhcC5zZXJ2ZXJwYXNzLnRlbGVzZWMuZGUvY249VGVsZVNlYyUyMFNl\r\ncnZlclBhc3MlMjBERS0xLG91PVQtU3lzdGVtcyUyMFRydXN0JTIwQ2VudGVyLG89\r\nVC1TeXN0ZW1zJTIwSW50ZXJuYXRpb25hbCUyMEdtYkgsYz1kZT9jZXJ0aWZpY2F0\r\nZVJldm9jYXRpb25saXN0P2Jhc2U/Y2VydGlmaWNhdGVSZXZvY2F0aW9ubGlzdD0q\r\nMIIBOQYIKwYBBQUHAQEEggErMIIBJzAzBggrBgEFBQcwAYYnaHR0cDovL29jc3Au\r\nc2VydmVycGFzcy50ZWxlc2VjLmRlL29jc3ByMEwGCCsGAQUFBzAChkBodHRwOi8v\r\nY3JsLnNlcnZlcnBhc3MudGVsZXNlYy5kZS9jcnQvVGVsZVNlY19TZXJ2ZXJQYXNz\r\nX0RFLTEuY2VyMIGhBggrBgEFBQcwAoaBlGxkYXA6Ly9sZGFwLnNlcnZlcnBhc3Mu\r\ndGVsZXNlYy5kZS9jbj1UZWxlU2VjJTIwU2VydmVyUGFzcyUyMERFLTEsb3U9VC1T\r\neXN0ZW1zJTIwVHJ1c3QlMjBDZW50ZXIsbz1ULVN5c3RlbXMlMjBJbnRlcm5hdGlv\r\nbmFsJTIwR21iSCxjPWRlP2NBQ2VydGlmaWNhdGUwDAYDVR0TAQH/BAIwADAWBgNV\r\nHREEDzANggtzbXRwLndlYi5kZTANBgkqhkiG9w0BAQUFAAOCAQEAGFtJxVxWrURy\r\nFfR4UfmW+N1cZZx9sfC5jolV8LGje87DgbWdqu5TRL9FoQ1pwOTbM9mc3yWSoIbU\r\nx6E+rziJK+SFGMIy+Lt13P9M9Oc8JzVHoVEAgqlEeO6OYxrGE6SaSjZODJxPaEtU\r\nzEAKf0HVJQoGaU6fD+/8l3yhksAlsF/L85nP+KcZtoancOkJWE0GQMZp7pdLU0Ou\r\nFUQoAcqMIyHQfDqJ5iwvx/9C7jmy3Nvw9tXdPrdn2O7ywrnFeJuT2xiorZzg6ezn\r\nUlc/sMvd/LPX0f60pSQr9tkZgU4f8Jvx9EvPUCFTRXlXqkBIhgJhgCadZC+wKB1P\r\neQMS8rksXw==\r\n-----END CERTIFICATE-----', - pinned: true, - ignoreTLS: false - } + smtp: { + host: 'smtp-mail.outlook.com', + port: 587, + secure: false, + ca: '-----BEGIN CERTIFICATE-----\r\nMIIFQjCCBCqgAwIBAgISESHl0vjrML7zKmGlv42YL75vMA0GCSqGSIb3DQEBBQUA\r\nMF0xCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMTMwMQYD\r\nVQQDEypHbG9iYWxTaWduIE9yZ2FuaXphdGlvbiBWYWxpZGF0aW9uIENBIC0gRzIw\r\nHhcNMTMwNDI0MjAzNTA5WhcNMTYwNDI0MjAzNTA5WjBsMQswCQYDVQQGEwJVUzET\r\nMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMV\r\nTWljcm9zb2Z0IENvcnBvcmF0aW9uMRYwFAYDVQQDDA0qLmhvdG1haWwuY29tMIIB\r\nIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAumSiBWrzHZf6WFP5a/j4+K7D\r\n1izLoYKj5Omll0pdxKvKcBRDf+iaIkCbSOPNpx2uWGZdwNwkabYCQavaBf2ebwmS\r\nS8i1CJpHflO+k0qYd5WUi7sSsZ3+6RaCMdLoDIPGyYMQuy7TFtVO7LSt5+qscyyi\r\nET8c3lE2aj/XW13UZvRrV65ZJvMjUtwaDnIcAxGeasYoebLsKdqHQ2uTr4PmNwCc\r\nviGVFSOzkGAoC0PfyqKB2xUWy3Kc5zRI2xvUW8Jb2b/9Ze3g55pIUzKsjpglkQTm\r\nedVPSYYPGNz6Kl/ZshBXdBAk398q1JkSmUaTMa2hJgBbcC+73ax40AJDGJlz+QID\r\nAQABo4IB6zCCAecwDgYDVR0PAQH/BAQDAgWgMEkGA1UdIARCMEAwPgYGZ4EMAQIC\r\nMDQwMgYIKwYBBQUHAgEWJmh0dHBzOi8vd3d3Lmdsb2JhbHNpZ24uY29tL3JlcG9z\r\naXRvcnkvMEAGA1UdEQQ5MDeCDSouaG90bWFpbC5jb22CCioubGl2ZS5jb22CDSou\r\nb3V0bG9vay5jb22CC2hvdG1haWwuY29tMAkGA1UdEwQCMAAwHQYDVR0lBBYwFAYI\r\nKwYBBQUHAwEGCCsGAQUFBwMCMEUGA1UdHwQ+MDwwOqA4oDaGNGh0dHA6Ly9jcmwu\r\nZ2xvYmFsc2lnbi5jb20vZ3MvZ3Nvcmdhbml6YXRpb252YWxnMi5jcmwwgZYGCCsG\r\nAQUFBwEBBIGJMIGGMEcGCCsGAQUFBzAChjtodHRwOi8vc2VjdXJlLmdsb2JhbHNp\r\nZ24uY29tL2NhY2VydC9nc29yZ2FuaXphdGlvbnZhbGcyLmNydDA7BggrBgEFBQcw\r\nAYYvaHR0cDovL29jc3AyLmdsb2JhbHNpZ24uY29tL2dzb3JnYW5pemF0aW9udmFs\r\nZzIwHQYDVR0OBBYEFHbgHqTLsXDt7uMRyE62rnDEfLn9MB8GA1UdIwQYMBaAFF1G\r\nso3ES3Qcu+31c7Y6tziPdZ5+MA0GCSqGSIb3DQEBBQUAA4IBAQByy1+3N6ZRVooI\r\nxqw8Ng+UFz0g7UHkbPEnvTu1uxJ2AojFuP/P1PAk+/6uMRvpPlWg/5uqmOIWxKxJ\r\nLo6xSbkDf4LN+KYwes3XSuPyziZ4QbPnehHhZ0377iiA8fpRJADg9NWKCRHh5aAd\r\ne9QvJUW/GgYkBN+F4yYc2jIjR3Rehv4JYOKS3iXO9OoHsDS2CcCFaS2imgQVfYLg\r\nslBwT/A08PCOhW5huiluSmih7x5Qf7sFDv8jineu6ehKzi8pKnOq4k8G4QiWn38Y\r\nCeiBkkwFOwj7T3M/ITiiSS9DHDGeokj16eBi83Zx3YYiJ9YZvnQ+4GvqJ5eJJ6pR\r\nKKvemr+m\r\n-----END CERTIFICATE-----', + pinned: true, + ignoreTLS: false + } + }, + gmx: { + imap: { + host: 'imap.gmx.net', + port: 993, + secure: true, + ca: '-----BEGIN CERTIFICATE-----\r\nMIIHUDCCBjigAwIBAgIIM52e2Oc5J8kwDQYJKoZIhvcNAQEFBQAwgckxCzAJBgNV\r\nBAYTAkRFMSUwIwYDVQQKExxULVN5c3RlbXMgSW50ZXJuYXRpb25hbCBHbWJIMR8w\r\nHQYDVQQLExZULVN5c3RlbXMgVHJ1c3QgQ2VudGVyMQwwCgYDVQQIEwNOUlcxDjAM\r\nBgNVBBETBTU3MjUwMRAwDgYDVQQHEwdOZXRwaGVuMSAwHgYDVQQJExdVbnRlcmUg\r\nSW5kdXN0cmllc3RyLiAyMDEgMB4GA1UEAxMXVGVsZVNlYyBTZXJ2ZXJQYXNzIERF\r\nLTEwHhcNMTMxMTEyMTAxNzMxWhcNMTYxMTE3MjM1OTU5WjCBnTELMAkGA1UEBhMC\r\nREUxHjAcBgNVBAoMFTEmMSBNYWlsICYgTWVkaWEgR21iSDEdMBsGA1UECBMUUmhp\r\nbmVsYW5kLVBhbGF0aW5hdGUxEjAQBgNVBAcTCU1vbnRhYmF1cjEkMCIGCSqGSIb3\r\nDQEJARYVc2VydmVyLWNlcnRzQDF1bmQxLmRlMRUwEwYDVQQDEwxpbWFwLmdteC5u\r\nZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDHP3QN1ztX5m8LaC9m\r\nt2nrcLBEfxb61FQCieXW4OV/D5HEy7vgQIAeS+pg2/9ClawprQFbLUa46owUcJuq\r\no2SaefsYqjRhIP/tPeyGRJQmfnyCRRoHIYno57/bz2qmHjzvkqEUMvNGVVej7BJv\r\nGukNKPXYicodkRodF3EicTPnGitGeXOsAxyPuCPIsBPAMuuuBzKenS7nj+yseSGe\r\nkM7/kjtNgbjDyBXi2BvoUS/U+Gm9p1lq0ymT9fhGj/k9/Qz312GHYxfZB1bhL0pk\r\nadF25P8fTIy5tKMWqJfgCF+eaOwgkYecxeCcrt2E3D8ThLvmkBRrXsTT51ZzD7sp\r\n7hPpAgMBAAGjggNkMIIDYDAfBgNVHSMEGDAWgBRiTxPONmeEzRn8oE8Zi+8VVAEy\r\nHDAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMB\r\nMB0GA1UdDgQWBBRfb2gYyD6V+oBN2Ii5WQj0AddemjBZBgNVHSAEUjBQMEQGCSsG\r\nAQQBvUcNAjA3MDUGCCsGAQUFBwIBFilodHRwOi8vd3d3LnRlbGVzZWMuZGUvc2Vy\r\ndmVycGFzcy9jcHMuaHRtbDAIBgZngQwBAgIwggEhBgNVHR8EggEYMIIBFDBFoEOg\r\nQYY/aHR0cDovL2NybC5zZXJ2ZXJwYXNzLnRlbGVzZWMuZGUvcmwvVGVsZVNlY19T\r\nZXJ2ZXJQYXNzX0RFLTEuY3JsMIHKoIHHoIHEhoHBbGRhcDovL2xkYXAuc2VydmVy\r\ncGFzcy50ZWxlc2VjLmRlL2NuPVRlbGVTZWMlMjBTZXJ2ZXJQYXNzJTIwREUtMSxv\r\ndT1ULVN5c3RlbXMlMjBUcnVzdCUyMENlbnRlcixvPVQtU3lzdGVtcyUyMEludGVy\r\nbmF0aW9uYWwlMjBHbWJILGM9ZGU/Y2VydGlmaWNhdGVSZXZvY2F0aW9ubGlzdD9i\r\nYXNlP2NlcnRpZmljYXRlUmV2b2NhdGlvbmxpc3Q9KjCCATkGCCsGAQUFBwEBBIIB\r\nKzCCAScwMwYIKwYBBQUHMAGGJ2h0dHA6Ly9vY3NwLnNlcnZlcnBhc3MudGVsZXNl\r\nYy5kZS9vY3NwcjBMBggrBgEFBQcwAoZAaHR0cDovL2NybC5zZXJ2ZXJwYXNzLnRl\r\nbGVzZWMuZGUvY3J0L1RlbGVTZWNfU2VydmVyUGFzc19ERS0xLmNlcjCBoQYIKwYB\r\nBQUHMAKGgZRsZGFwOi8vbGRhcC5zZXJ2ZXJwYXNzLnRlbGVzZWMuZGUvY249VGVs\r\nZVNlYyUyMFNlcnZlclBhc3MlMjBERS0xLG91PVQtU3lzdGVtcyUyMFRydXN0JTIw\r\nQ2VudGVyLG89VC1TeXN0ZW1zJTIwSW50ZXJuYXRpb25hbCUyMEdtYkgsYz1kZT9j\r\nQUNlcnRpZmljYXRlMAwGA1UdEwEB/wQCMAAwJAYDVR0RBB0wG4IMaW1hcC5nbXgu\r\nbmV0ggtpbWFwLmdteC5kZTANBgkqhkiG9w0BAQUFAAOCAQEADl442s2ouynDNEzl\r\nBPzhCxjp47TmKzgzrCGh1AH3+UOkw54mYU65UaY9cJ51jgOu+pjNB4KxVfcqyG5O\r\nJcowS09ZtZ+khb1OkzXAAbEqNj0oyH/1K550d5Ir5VbXF0ZdVKVTzEFRyjvUt5NJ\r\n/b6Q2bbY/sPm8QiK9SPaYkO5/3J1KB0u7PkqqfOhZ9UJE6mIN66T2bpFmAwle3zL\r\n6+L/VwYSgUNs/w2l6xWlYP3pFFaT12TO43Q057I3vP7yCt3QE/VLDpdQOQ3H0oy2\r\nJQGPJLFby9MPYNkmEmwjh4e95TAY4ZkZMe08ix0J1Smy8DwjTz6SXqYC+EC/PQlh\r\nE0UHxA==\r\n-----END CERTIFICATE-----', + pinned: true, + ignoreTLS: false }, - checkOutboxInterval: 5000, - iconPath: '/img/icon-128-chrome.png', - verificationUrl: '/verify/', - verificationUuidLength: 36, - dbVersion: 5, - appVersion: appVersion, - outboxMailboxPath: 'OUTBOX', - outboxMailboxName: 'Outbox', - outboxMailboxType: 'Outbox', - connDocTimeout: 5000, - imapUpdateBatchSize: 25 - }; + smtp: { + host: 'mail.gmx.net', + port: 587, + secure: false, + ca: '-----BEGIN CERTIFICATE-----\r\nMIIHbDCCBlSgAwIBAgIJAJ74Ek9UaA0TMA0GCSqGSIb3DQEBBQUAMIHJMQswCQYD\r\nVQQGEwJERTElMCMGA1UEChMcVC1TeXN0ZW1zIEludGVybmF0aW9uYWwgR21iSDEf\r\nMB0GA1UECxMWVC1TeXN0ZW1zIFRydXN0IENlbnRlcjEMMAoGA1UECBMDTlJXMQ4w\r\nDAYDVQQREwU1NzI1MDEQMA4GA1UEBxMHTmV0cGhlbjEgMB4GA1UECRMXVW50ZXJl\r\nIEluZHVzdHJpZXN0ci4gMjAxIDAeBgNVBAMTF1RlbGVTZWMgU2VydmVyUGFzcyBE\r\nRS0xMB4XDTEzMTExMjEwMTY0N1oXDTE2MTExNzIzNTk1OVowgZ0xCzAJBgNVBAYT\r\nAkRFMR4wHAYDVQQKDBUxJjEgTWFpbCAmIE1lZGlhIEdtYkgxHTAbBgNVBAgTFFJo\r\naW5lbGFuZC1QYWxhdGluYXRlMRIwEAYDVQQHEwlNb250YWJhdXIxJDAiBgkqhkiG\r\n9w0BCQEWFXNlcnZlci1jZXJ0c0AxdW5kMS5kZTEVMBMGA1UEAxMMbWFpbC5nbXgu\r\nbmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsEUYppbJXUpQELIL\r\nETAFUwreDfSmbyi9gJPUqquogROHEoRV+bBFZ2YCiHgLU8AggcLmM74HAQMFt8F5\r\neS2OyuEar3E3tNW5XFiI1QblTtY3B7o1coKyq/i+tV3b1jGq4+1aJeeD3UpcraBm\r\nk2XuQgQ6WAeY+XCYyI/OzZ56ZjQ17LHMMI0ayu7SSV+VBpG9loC9E5A82iVtZsqo\r\nSaUZpZf/xICpUgNJ+RlalSsoE6FdCywE/gEEmkpAUh+Mv0WvHITk6aqtMFWex8tE\r\n8cwJGQbmJGt9x8BExbhMY6uQ+HsnsDCPCMVh5JGKx7/e+qyxtHYGChOeI16m2+MT\r\nz47jSQIDAQABo4IDfzCCA3swHwYDVR0jBBgwFoAUYk8TzjZnhM0Z/KBPGYvvFVQB\r\nMhwwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcD\r\nATAdBgNVHQ4EFgQUH8mLfxsd5FEh9M+3t3rP6qLiFTEwWQYDVR0gBFIwUDBEBgkr\r\nBgEEAb1HDQIwNzA1BggrBgEFBQcCARYpaHR0cDovL3d3dy50ZWxlc2VjLmRlL3Nl\r\ncnZlcnBhc3MvY3BzLmh0bWwwCAYGZ4EMAQICMIIBIQYDVR0fBIIBGDCCARQwRaBD\r\noEGGP2h0dHA6Ly9jcmwuc2VydmVycGFzcy50ZWxlc2VjLmRlL3JsL1RlbGVTZWNf\r\nU2VydmVyUGFzc19ERS0xLmNybDCByqCBx6CBxIaBwWxkYXA6Ly9sZGFwLnNlcnZl\r\ncnBhc3MudGVsZXNlYy5kZS9jbj1UZWxlU2VjJTIwU2VydmVyUGFzcyUyMERFLTEs\r\nb3U9VC1TeXN0ZW1zJTIwVHJ1c3QlMjBDZW50ZXIsbz1ULVN5c3RlbXMlMjBJbnRl\r\ncm5hdGlvbmFsJTIwR21iSCxjPWRlP2NlcnRpZmljYXRlUmV2b2NhdGlvbmxpc3Q/\r\nYmFzZT9jZXJ0aWZpY2F0ZVJldm9jYXRpb25saXN0PSowggE5BggrBgEFBQcBAQSC\r\nASswggEnMDMGCCsGAQUFBzABhidodHRwOi8vb2NzcC5zZXJ2ZXJwYXNzLnRlbGVz\r\nZWMuZGUvb2NzcHIwTAYIKwYBBQUHMAKGQGh0dHA6Ly9jcmwuc2VydmVycGFzcy50\r\nZWxlc2VjLmRlL2NydC9UZWxlU2VjX1NlcnZlclBhc3NfREUtMS5jZXIwgaEGCCsG\r\nAQUFBzAChoGUbGRhcDovL2xkYXAuc2VydmVycGFzcy50ZWxlc2VjLmRlL2NuPVRl\r\nbGVTZWMlMjBTZXJ2ZXJQYXNzJTIwREUtMSxvdT1ULVN5c3RlbXMlMjBUcnVzdCUy\r\nMENlbnRlcixvPVQtU3lzdGVtcyUyMEludGVybmF0aW9uYWwlMjBHbWJILGM9ZGU/\r\nY0FDZXJ0aWZpY2F0ZTAMBgNVHRMBAf8EAjAAMD8GA1UdEQQ4MDaCDG1haWwuZ214\r\nLm5ldIILbWFpbC5nbXguZGWCDHNtdHAuZ214Lm5ldIILc210cC5nbXguZGUwDQYJ\r\nKoZIhvcNAQEFBQADggEBAJ5SKsBXoOTO0ztsFh8RVd/iu6sijEMpKVF+/cs74TZN\r\nK1QWIz8Ay1f3SUPWXTvkmvayUXWGtxZfYoRIrx3feWBdUozJxe0wz1O3tDQOQgRX\r\nnkcwk7nibTUmW2rk5AIwV0jzOHtIbAGjSdc6my0543e7dVkhcuoCOC+g2NmD+pHF\r\ni1KpmqTLecDlFvprIZUJ23AT1uTnuXYg/tFHeDn3ga00Gce82xZQJelRirJs01SR\r\nDGEBaPXBjJVJNoAP/qEy3jQNRWo5TbSgwn3DTR81FJvltmf0c+zlt4fmcpjpr5ni\r\nFvK7L0rZQL5MOGHPpgRIukEBZCowYr3OYpZYBEaB94I=\r\n-----END CERTIFICATE-----', + pinned: true, + ignoreTLS: false + } + }, + webde: { + imap: { + host: 'imap.web.de', + port: 993, + secure: true, + ca: '-----BEGIN CERTIFICATE-----\r\nMIIHUjCCBjqgAwIBAgIIHvqUOScyxYUwDQYJKoZIhvcNAQEFBQAwgckxCzAJBgNV\r\nBAYTAkRFMSUwIwYDVQQKExxULVN5c3RlbXMgSW50ZXJuYXRpb25hbCBHbWJIMR8w\r\nHQYDVQQLExZULVN5c3RlbXMgVHJ1c3QgQ2VudGVyMQwwCgYDVQQIEwNOUlcxDjAM\r\nBgNVBBETBTU3MjUwMRAwDgYDVQQHEwdOZXRwaGVuMSAwHgYDVQQJExdVbnRlcmUg\r\nSW5kdXN0cmllc3RyLiAyMDEgMB4GA1UEAxMXVGVsZVNlYyBTZXJ2ZXJQYXNzIERF\r\nLTEwHhcNMTMwODIxMDgzMTE2WhcNMTYwODI2MjM1OTU5WjCBrTELMAkGA1UEBhMC\r\nREUxHjAcBgNVBAoMFTEmMSBNYWlsICYgTWVkaWEgR21iSDEPMA0GA1UECxMGV0VC\r\nLkRFMR0wGwYDVQQIExRSaGluZWxhbmQtUGFsYXRpbmF0ZTESMBAGA1UEBxMJTW9u\r\ndGFiYXVyMSQwIgYJKoZIhvcNAQkBFhVzZXJ2ZXItY2VydHNAMXVuZDEuZGUxFDAS\r\nBgNVBAMTC2ltYXAud2ViLmRlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC\r\nAQEA5dvpcnYczFs9GANTYN5N3mP6ATFVm7P4nYhRIQMj/YssSjmbrOPIEbe12JNC\r\nRO9CI7Z4Dnk42BM5TiH+QKGpcfnitofOv4gKPjwCMcPDHfY152/+YDaiyU/md2Hg\r\n/WrZ/50KwC8Sw1tZkYDXWB0zeJkfPdS2r4ATNrpMR0fYcG08+elz5T2SWNg+c1xL\r\nKFdCh33wZLkijOfW0HA164QjXaLPBjxa+GyZFY19ywOQ85KdFVVLmGUrLz8n4ZLc\r\n7K6KUuzYttnUuVxctFauQ4DRHE/CfUvvNHHgn5d+A2XS7jNUgmUb0gjzy3OtlTP3\r\ngM4ostVufmnlS7qqFDR7A3v4lQIDAQABo4IDVjCCA1IwHwYDVR0jBBgwFoAUYk8T\r\nzjZnhM0Z/KBPGYvvFVQBMhwwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsG\r\nAQUFBwMCBggrBgEFBQcDATAdBgNVHQ4EFgQUDH66P/Y4NlKGphPniIzmutEI9ocw\r\nWQYDVR0gBFIwUDBEBgkrBgEEAb1HDQIwNzA1BggrBgEFBQcCARYpaHR0cDovL3d3\r\ndy50ZWxlc2VjLmRlL3NlcnZlcnBhc3MvY3BzLmh0bWwwCAYGZ4EMAQICMIIBIQYD\r\nVR0fBIIBGDCCARQwRaBDoEGGP2h0dHA6Ly9jcmwuc2VydmVycGFzcy50ZWxlc2Vj\r\nLmRlL3JsL1RlbGVTZWNfU2VydmVyUGFzc19ERS0xLmNybDCByqCBx6CBxIaBwWxk\r\nYXA6Ly9sZGFwLnNlcnZlcnBhc3MudGVsZXNlYy5kZS9jbj1UZWxlU2VjJTIwU2Vy\r\ndmVyUGFzcyUyMERFLTEsb3U9VC1TeXN0ZW1zJTIwVHJ1c3QlMjBDZW50ZXIsbz1U\r\nLVN5c3RlbXMlMjBJbnRlcm5hdGlvbmFsJTIwR21iSCxjPWRlP2NlcnRpZmljYXRl\r\nUmV2b2NhdGlvbmxpc3Q/YmFzZT9jZXJ0aWZpY2F0ZVJldm9jYXRpb25saXN0PSow\r\nggE5BggrBgEFBQcBAQSCASswggEnMDMGCCsGAQUFBzABhidodHRwOi8vb2NzcC5z\r\nZXJ2ZXJwYXNzLnRlbGVzZWMuZGUvb2NzcHIwTAYIKwYBBQUHMAKGQGh0dHA6Ly9j\r\ncmwuc2VydmVycGFzcy50ZWxlc2VjLmRlL2NydC9UZWxlU2VjX1NlcnZlclBhc3Nf\r\nREUtMS5jZXIwgaEGCCsGAQUFBzAChoGUbGRhcDovL2xkYXAuc2VydmVycGFzcy50\r\nZWxlc2VjLmRlL2NuPVRlbGVTZWMlMjBTZXJ2ZXJQYXNzJTIwREUtMSxvdT1ULVN5\r\nc3RlbXMlMjBUcnVzdCUyMENlbnRlcixvPVQtU3lzdGVtcyUyMEludGVybmF0aW9u\r\nYWwlMjBHbWJILGM9ZGU/Y0FDZXJ0aWZpY2F0ZTAMBgNVHRMBAf8EAjAAMBYGA1Ud\r\nEQQPMA2CC2ltYXAud2ViLmRlMA0GCSqGSIb3DQEBBQUAA4IBAQAp7MQjjOWQ0M7N\r\n21GrPDfWSMR3eJnuMs37I9G2t9i99w7xKtmoVBPyYMORL1zRHn/DEguo4j5ua7CH\r\nrKLh9Sd1wDKqVWGFxvmP4f/mvEx5YVI68mg+M2VQf/h58IXTTZRbUcsv3HVeruI6\r\npgQNUsEQqRVJmrgT/iPd98RhhzBqef6Wfrt3Ns6N835egphxUVcVj/v/PqBCZKQb\r\nNl5QsoaHDqh3XC+Og2awOGvWHUzxUKEqkP5nsMs2YjfOtcQRtxdislQAfRktuUBA\r\nWMZAlvoVF/CIyFvcUH11KNur7HfK5PptvtdvLQaF2c+lN+LGP6D+nbTLqAo1v09o\r\nnZpHbQOt\r\n-----END CERTIFICATE-----', + pinned: true, + ignoreTLS: false + }, + smtp: { + host: 'smtp.web.de', + port: 587, + secure: false, + ca: '-----BEGIN CERTIFICATE-----\r\nMIIHUzCCBjugAwIBAgIJAKaEwtMgQ2s5MA0GCSqGSIb3DQEBBQUAMIHJMQswCQYD\r\nVQQGEwJERTElMCMGA1UEChMcVC1TeXN0ZW1zIEludGVybmF0aW9uYWwgR21iSDEf\r\nMB0GA1UECxMWVC1TeXN0ZW1zIFRydXN0IENlbnRlcjEMMAoGA1UECBMDTlJXMQ4w\r\nDAYDVQQREwU1NzI1MDEQMA4GA1UEBxMHTmV0cGhlbjEgMB4GA1UECRMXVW50ZXJl\r\nIEluZHVzdHJpZXN0ci4gMjAxIDAeBgNVBAMTF1RlbGVTZWMgU2VydmVyUGFzcyBE\r\nRS0xMB4XDTEzMDgyMTA4MzU1MVoXDTE2MDgyNjIzNTk1OVowga0xCzAJBgNVBAYT\r\nAkRFMR4wHAYDVQQKDBUxJjEgTWFpbCAmIE1lZGlhIEdtYkgxDzANBgNVBAsTBldF\r\nQi5ERTEdMBsGA1UECBMUUmhpbmVsYW5kLVBhbGF0aW5hdGUxEjAQBgNVBAcTCU1v\r\nbnRhYmF1cjEkMCIGCSqGSIb3DQEJARYVc2VydmVyLWNlcnRzQDF1bmQxLmRlMRQw\r\nEgYDVQQDEwtzbXRwLndlYi5kZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC\r\nggEBAL1WfyFqmifaEr6h9yntJKs33fnEVu0pQqS5RV9rnQU0E/cJYJ9RJOoTjhpE\r\nkT1LW8DyTM5vUzpgtFHQM0BOO6jOeZXUYNUaopK3yO4l/cpiitigtk9ZnWfKzbU3\r\n9hlhf+YwUUvoqjUA8I6fSu+VNPNlOBvamo18oHljXAPltL9oiwAxvTLNktBMy4T9\r\njxU1DeGoPbJKGwV7zIBQ2qUHuLkMvy5/H39t79Tih+zfzgfY/xIUfdmPNY9dK8ZY\r\nAtyF/RiUS199pd9dV4Vwh6JLvtNlWchBnKVhragLCGbkjnvUXmS1BPrclLR23s1v\r\nMehgOD2WhXl8PoqrOPb6y+lgMlECAwEAAaOCA1YwggNSMB8GA1UdIwQYMBaAFGJP\r\nE842Z4TNGfygTxmL7xVUATIcMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggr\r\nBgEFBQcDAgYIKwYBBQUHAwEwHQYDVR0OBBYEFAUuppozNCv2UzMV5p6CTpkEhsRS\r\nMFkGA1UdIARSMFAwRAYJKwYBBAG9Rw0CMDcwNQYIKwYBBQUHAgEWKWh0dHA6Ly93\r\nd3cudGVsZXNlYy5kZS9zZXJ2ZXJwYXNzL2Nwcy5odG1sMAgGBmeBDAECAjCCASEG\r\nA1UdHwSCARgwggEUMEWgQ6BBhj9odHRwOi8vY3JsLnNlcnZlcnBhc3MudGVsZXNl\r\nYy5kZS9ybC9UZWxlU2VjX1NlcnZlclBhc3NfREUtMS5jcmwwgcqggceggcSGgcFs\r\nZGFwOi8vbGRhcC5zZXJ2ZXJwYXNzLnRlbGVzZWMuZGUvY249VGVsZVNlYyUyMFNl\r\ncnZlclBhc3MlMjBERS0xLG91PVQtU3lzdGVtcyUyMFRydXN0JTIwQ2VudGVyLG89\r\nVC1TeXN0ZW1zJTIwSW50ZXJuYXRpb25hbCUyMEdtYkgsYz1kZT9jZXJ0aWZpY2F0\r\nZVJldm9jYXRpb25saXN0P2Jhc2U/Y2VydGlmaWNhdGVSZXZvY2F0aW9ubGlzdD0q\r\nMIIBOQYIKwYBBQUHAQEEggErMIIBJzAzBggrBgEFBQcwAYYnaHR0cDovL29jc3Au\r\nc2VydmVycGFzcy50ZWxlc2VjLmRlL29jc3ByMEwGCCsGAQUFBzAChkBodHRwOi8v\r\nY3JsLnNlcnZlcnBhc3MudGVsZXNlYy5kZS9jcnQvVGVsZVNlY19TZXJ2ZXJQYXNz\r\nX0RFLTEuY2VyMIGhBggrBgEFBQcwAoaBlGxkYXA6Ly9sZGFwLnNlcnZlcnBhc3Mu\r\ndGVsZXNlYy5kZS9jbj1UZWxlU2VjJTIwU2VydmVyUGFzcyUyMERFLTEsb3U9VC1T\r\neXN0ZW1zJTIwVHJ1c3QlMjBDZW50ZXIsbz1ULVN5c3RlbXMlMjBJbnRlcm5hdGlv\r\nbmFsJTIwR21iSCxjPWRlP2NBQ2VydGlmaWNhdGUwDAYDVR0TAQH/BAIwADAWBgNV\r\nHREEDzANggtzbXRwLndlYi5kZTANBgkqhkiG9w0BAQUFAAOCAQEAGFtJxVxWrURy\r\nFfR4UfmW+N1cZZx9sfC5jolV8LGje87DgbWdqu5TRL9FoQ1pwOTbM9mc3yWSoIbU\r\nx6E+rziJK+SFGMIy+Lt13P9M9Oc8JzVHoVEAgqlEeO6OYxrGE6SaSjZODJxPaEtU\r\nzEAKf0HVJQoGaU6fD+/8l3yhksAlsF/L85nP+KcZtoancOkJWE0GQMZp7pdLU0Ou\r\nFUQoAcqMIyHQfDqJ5iwvx/9C7jmy3Nvw9tXdPrdn2O7ywrnFeJuT2xiorZzg6ezn\r\nUlc/sMvd/LPX0f60pSQr9tkZgU4f8Jvx9EvPUCFTRXlXqkBIhgJhgCadZC+wKB1P\r\neQMS8rksXw==\r\n-----END CERTIFICATE-----', + pinned: true, + ignoreTLS: false + } + }, + checkOutboxInterval: 5000, + iconPath: '/img/icon-128-chrome.png', + verificationUrl: '/verify/', + verificationUuidLength: 36, + dbVersion: 5, + appVersion: appVersion, + outboxMailboxPath: 'OUTBOX', + outboxMailboxName: 'Outbox', + outboxMailboxType: 'Outbox', + connDocTimeout: 5000, + imapUpdateBatchSize: 25 +}; - /** - * Strings are maintained here - */ - app.string = { - fallbackSubject: '(no subject)', - invitationSubject: 'Invitation to a private conversation', - invitationMessage: 'Hi,\n\nI use Whiteout Mail to send and receive encrypted email. I would like to exchange encrypted messages with you as well.\n\nPlease install the Whiteout Mail application. This application makes it easy to read and write messages securely with PGP encryption applied.\n\nGo to the Whiteout Networks homepage to learn more and to download the application: https://whiteout.io\n\n', - signature: '\n\n\n--\nSent from Whiteout Mail - https://whiteout.io\n\nMy PGP key: ', - webSite: 'http://whiteout.io', - verificationSubject: '[whiteout] New public key uploaded', - sendBtnClear: 'Send', - sendBtnSecure: 'Send securely', - updatePublicKeyTitle: 'Public Key Updated', - updatePublicKeyMsgNewKey: '{0} updated his key and may not be able to read encrypted messages sent with his old key. Update the key?', - updatePublicKeyMsgRemovedKey: '{0} revoked his key and may no longer be able to read encrypted messages. Remove the key?', - updatePublicKeyPosBtn: 'Yes', - updatePublicKeyNegBtn: 'No', - outdatedCertificateTitle: 'Warning', - outdatedCertificateMessage: 'The SSL certificate for the mail server {0} changed, the connection was refused.', - updateCertificateTitle: 'Warning', - updateCertificateMessage: 'The SSL certificate for the mail server {0} changed. Do you want to proceed?', - updateCertificatePosBtn: 'Yes', - updateCertificateNegBtn: 'No', - 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\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.', - connDocHostUnreachable: 'We could not establish a connection to {0}. Please check the server settings!', - connDocHostTimeout: 'We could not establish a connection to {0} within {1} ms. Please check the server settings and encryption mode!', - connDocAuthRejected: 'Your credentials for {0} were rejected. Please check your username and password!', - 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}' - }; - - return app; -}); \ No newline at end of file +/** + * Strings are maintained here + */ +exports.string = { + fallbackSubject: '(no subject)', + invitationSubject: 'Invitation to a private conversation', + invitationMessage: 'Hi,\n\nI use Whiteout Mail to send and receive encrypted email. I would like to exchange encrypted messages with you as well.\n\nPlease install the Whiteout Mail application. This application makes it easy to read and write messages securely with PGP encryption applied.\n\nGo to the Whiteout Networks homepage to learn more and to download the application: https://whiteout.io\n\n', + signature: '\n\n\n--\nSent from Whiteout Mail - https://whiteout.io\n\nMy PGP key: ', + webSite: 'http://whiteout.io', + verificationSubject: '[whiteout] New public key uploaded', + sendBtnClear: 'Send', + sendBtnSecure: 'Send securely', + updatePublicKeyTitle: 'Public Key Updated', + updatePublicKeyMsgNewKey: '{0} updated his key and may not be able to read encrypted messages sent with his old key. Update the key?', + updatePublicKeyMsgRemovedKey: '{0} revoked his key and may no longer be able to read encrypted messages. Remove the key?', + updatePublicKeyPosBtn: 'Yes', + updatePublicKeyNegBtn: 'No', + outdatedCertificateTitle: 'Warning', + outdatedCertificateMessage: 'The SSL certificate for the mail server {0} changed, the connection was refused.', + updateCertificateTitle: 'Warning', + updateCertificateMessage: 'The SSL certificate for the mail server {0} changed. Do you want to proceed?', + updateCertificatePosBtn: 'Yes', + updateCertificateNegBtn: 'No', + 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\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.', + connDocHostUnreachable: 'We could not establish a connection to {0}. Please check the server settings!', + connDocHostTimeout: 'We could not establish a connection to {0} within {1} ms. Please check the server settings and encryption mode!', + connDocAuthRejected: 'Your credentials for {0} were rejected. Please check your username and password!', + 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}' +}; \ No newline at end of file diff --git a/src/js/app-controller.js b/src/js/app-controller.js index ba358e0..82e0408 100644 --- a/src/js/app-controller.js +++ b/src/js/app-controller.js @@ -1,271 +1,268 @@ /** * The main application controller */ -define(function(require) { - 'use strict'; - var axe = require('axe'), - Auth = require('js/bo/auth'), - PGP = require('js/crypto/pgp'), - PgpMailer = require('pgpmailer'), - OAuth = require('js/util/oauth'), - PgpBuilder = require('pgpbuilder'), - OutboxBO = require('js/bo/outbox'), - mailreader = require('mailreader'), - ImapClient = require('imap-client'), - Crypto = require('js/crypto/crypto'), - RestDAO = require('js/dao/rest-dao'), - appConfig = require('js/app-config'), - EmailDAO = require('js/dao/email-dao'), - AdminDao = require('js/dao/admin-dao'), - KeychainDAO = require('js/dao/keychain-dao'), - PublicKeyDAO = require('js/dao/publickey-dao'), - LawnchairDAO = require('js/dao/lawnchair-dao'), - PrivateKeyDAO = require('js/dao/privatekey-dao'), - InvitationDAO = require('js/dao/invitation-dao'), - DeviceStorageDAO = require('js/dao/devicestorage-dao'), - ConnectionDoctor = require('js/util/connection-doctor'), - UpdateHandler = require('js/util/update/update-handler'), - config = appConfig.config, - str = appConfig.string; +'use strict'; - var self = {}; +var axe = require('axe-logger'), + Auth = require('./bo/auth'), + PGP = require('./crypto/pgp'), + PgpMailer = require('pgpmailer'), + OAuth = require('./util/oauth'), + PgpBuilder = require('pgpbuilder'), + OutboxBO = require('./bo/outbox'), + mailreader = require('mailreader'), + ImapClient = require('imap-client'), + Crypto = require('./crypto/crypto'), + RestDAO = require('./dao/rest-dao'), + appConfig = require('./app-config'), + EmailDAO = require('./dao/email-dao'), + AdminDao = require('./dao/admin-dao'), + KeychainDAO = require('./dao/keychain-dao'), + PublicKeyDAO = require('./dao/publickey-dao'), + LawnchairDAO = require('./dao/lawnchair-dao'), + PrivateKeyDAO = require('./dao/privatekey-dao'), + InvitationDAO = require('./dao/invitation-dao'), + DeviceStorageDAO = require('./dao/devicestorage-dao'), + ConnectionDoctor = require('./util/connection-doctor'), + UpdateHandler = require('./util/update/update-handler'), + config = appConfig.config, + str = appConfig.string; - /** - * Start the application. - */ - self.start = function(options, callback) { - if (self.started) { - return callback(); - } +var self = {}; - self.started = true; - self.onError = options.onError; +/** + * Start the application. + */ +self.start = function(options, callback) { + if (self.started) { + return callback(); + } - // are we running in a cordova app or in a browser environment? - if (window.cordova) { - // wait for 'deviceready' event to make sure plugins are loaded - axe.debug('Assuming Cordova environment...'); - document.addEventListener("deviceready", onDeviceReady, false); - } else { - // No need to wait on events... just start the app - axe.debug('Assuming Browser environment...'); - onDeviceReady(); - } + self.started = true; + self.onError = options.onError; - function onDeviceReady() { - axe.debug('Starting app.'); + // are we running in a cordova app or in a browser environment? + if (window.cordova) { + // wait for 'deviceready' event to make sure plugins are loaded + axe.debug('Assuming Cordova environment...'); + document.addEventListener("deviceready", onDeviceReady, false); + } else { + // No need to wait on events... just start the app + axe.debug('Assuming Browser environment...'); + onDeviceReady(); + } - self.buildModules(); + function onDeviceReady() { + axe.debug('Starting app.'); - // Handle offline and online gracefully - window.addEventListener('online', self.onConnect.bind(self, self.onError)); - window.addEventListener('offline', self.onDisconnect.bind(self)); + self.buildModules(); - self._appConfigStore.init('app-config', callback); - } + // Handle offline and online gracefully + window.addEventListener('online', self.onConnect.bind(self, self.onError)); + window.addEventListener('offline', self.onDisconnect.bind(self)); + + self._appConfigStore.init('app-config', callback); + } +}; + +/** + * Initialize the dependency tree. + */ +self.buildModules = function() { + var lawnchairDao, restDao, pubkeyDao, privkeyDao, crypto, emailDao, keychain, pgp, userStorage, pgpbuilder, oauth, appConfigStore, auth; + + // start the mailreader's worker thread + mailreader.startWorker(config.workerPath + '/../lib/mailreader-parser-worker.js'); + + // init objects and inject dependencies + restDao = new RestDAO(); + lawnchairDao = new LawnchairDAO(); + pubkeyDao = new PublicKeyDAO(restDao); + privkeyDao = new PrivateKeyDAO(new RestDAO(config.privkeyServerUrl)); + oauth = new OAuth(new RestDAO('https://www.googleapis.com')); + + crypto = new Crypto(); + self._pgp = pgp = new PGP(); + self._keychain = keychain = new KeychainDAO(lawnchairDao, pubkeyDao, privkeyDao, crypto, pgp); + keychain.requestPermissionForKeyUpdate = function(params, callback) { + var message = params.newKey ? str.updatePublicKeyMsgNewKey : str.updatePublicKeyMsgRemovedKey; + message = message.replace('{0}', params.userId); + + self.onError({ + title: str.updatePublicKeyTitle, + message: message, + positiveBtnStr: str.updatePublicKeyPosBtn, + negativeBtnStr: str.updatePublicKeyNegBtn, + showNegativeBtn: true, + callback: callback + }); }; - /** - * Initialize the dependency tree. - */ - self.buildModules = function() { - var lawnchairDao, restDao, pubkeyDao, privkeyDao, crypto, emailDao, keychain, pgp, userStorage, pgpbuilder, oauth, appConfigStore, auth; + self._appConfigStore = appConfigStore = new DeviceStorageDAO(new LawnchairDAO()); + self._auth = auth = new Auth(appConfigStore, oauth, pgp); + self._userStorage = userStorage = new DeviceStorageDAO(lawnchairDao); + self._invitationDao = new InvitationDAO(restDao); + self._pgpbuilder = pgpbuilder = new PgpBuilder(); + self._emailDao = emailDao = new EmailDAO(keychain, pgp, userStorage, pgpbuilder, mailreader); + self._outboxBo = new OutboxBO(emailDao, keychain, userStorage); + self._updateHandler = new UpdateHandler(appConfigStore, userStorage, auth); + self._adminDao = new AdminDao(new RestDAO(config.adminUrl)); + self._doctor = new ConnectionDoctor(); - // start the mailreader's worker thread - mailreader.startWorker(config.workerPath + '/../lib/mailreader-parser-worker.js'); + emailDao.onError = self.onError; +}; - // init objects and inject dependencies - restDao = new RestDAO(); - lawnchairDao = new LawnchairDAO(); - pubkeyDao = new PublicKeyDAO(restDao); - privkeyDao = new PrivateKeyDAO(new RestDAO(config.privkeyServerUrl)); - oauth = new OAuth(new RestDAO('https://www.googleapis.com')); +/** + * Calls runtime hooks to check if an app update is available. + */ +self.checkForUpdate = function() { + self._updateHandler.checkForUpdate(self.onError); +}; - crypto = new Crypto(); - self._pgp = pgp = new PGP(); - self._keychain = keychain = new KeychainDAO(lawnchairDao, pubkeyDao, privkeyDao, crypto, pgp); - keychain.requestPermissionForKeyUpdate = function(params, callback) { - var message = params.newKey ? str.updatePublicKeyMsgNewKey : str.updatePublicKeyMsgRemovedKey; - message = message.replace('{0}', params.userId); +/** + * Instanciate the mail email data access object and its dependencies. Login to imap on init. + */ +self.init = function(options, callback) { + // init user's local database + self._userStorage.init(options.emailAddress, function(err) { + if (err) { + callback(err); + return; + } - self.onError({ - title: str.updatePublicKeyTitle, - message: message, - positiveBtnStr: str.updatePublicKeyPosBtn, - negativeBtnStr: str.updatePublicKeyNegBtn, - showNegativeBtn: true, - callback: callback + // Migrate the databases if necessary + self._updateHandler.update(onUpdate); + }); + + function onUpdate(err) { + if (err) { + callback({ + errMsg: 'Update failed, please reinstall the app.', + err: err }); + return; + } + + // account information for the email dao + var account = { + realname: options.realname, + emailAddress: options.emailAddress, + asymKeySize: config.asymKeySize }; - self._appConfigStore = appConfigStore = new DeviceStorageDAO(new LawnchairDAO()); - self._auth = auth = new Auth(appConfigStore, oauth, pgp); - self._userStorage = userStorage = new DeviceStorageDAO(lawnchairDao); - self._invitationDao = new InvitationDAO(restDao); - self._pgpbuilder = pgpbuilder = new PgpBuilder(); - self._emailDao = emailDao = new EmailDAO(keychain, pgp, userStorage, pgpbuilder, mailreader); - self._outboxBo = new OutboxBO(emailDao, keychain, userStorage); - self._updateHandler = new UpdateHandler(appConfigStore, userStorage, auth); - self._adminDao = new AdminDao(new RestDAO(config.adminUrl)); - self._doctor = new ConnectionDoctor(); - - emailDao.onError = self.onError; - }; - - /** - * Calls runtime hooks to check if an app update is available. - */ - self.checkForUpdate = function() { - self._updateHandler.checkForUpdate(self.onError); - }; - - /** - * Instanciate the mail email data access object and its dependencies. Login to imap on init. - */ - self.init = function(options, callback) { - // init user's local database - self._userStorage.init(options.emailAddress, function(err) { + // init email dao + self._emailDao.init({ + account: account + }, function(err, keypair) { if (err) { callback(err); return; } - // Migrate the databases if necessary - self._updateHandler.update(onUpdate); + callback(null, keypair); }); + } +}; - function onUpdate(err) { - if (err) { - callback({ - errMsg: 'Update failed, please reinstall the app.', - err: err - }); - return; - } +/** + * Check if the user agent is online. + */ +self.isOnline = function() { + return navigator.onLine; +}; - // account information for the email dao - var account = { - realname: options.realname, - emailAddress: options.emailAddress, - asymKeySize: config.asymKeySize - }; +/** + * Event handler that is called when the user agent goes offline. + */ +self.onDisconnect = function() { + self._emailDao.onDisconnect(); +}; - // init email dao - self._emailDao.init({ - account: account - }, function(err, keypair) { - if (err) { - callback(err); - return; - } - - callback(null, keypair); - }); +/** + * Log the current user out by clear the app config store and deleting instances of imap-client and pgp-mailer. + */ +self.logout = function() { + // clear app config store + self._auth.logout(function(err) { + if (err) { + self.onError(err); + return; } - }; - /** - * Check if the user agent is online. - */ - self.isOnline = function() { - return navigator.onLine; - }; - - /** - * Event handler that is called when the user agent goes offline. - */ - self.onDisconnect = function() { - self._emailDao.onDisconnect(); - }; - - /** - * Log the current user out by clear the app config store and deleting instances of imap-client and pgp-mailer. - */ - self.logout = function() { - var self = this; - - // clear app config store - self._auth.logout(function(err) { + // delete instance of imap-client and pgp-mailer + self._emailDao.onDisconnect(function(err) { if (err) { self.onError(err); return; } - // delete instance of imap-client and pgp-mailer - self._emailDao.onDisconnect(function(err) { - if (err) { - self.onError(err); - return; - } - - // navigate to login - window.location.href = '/'; - }); + // navigate to login + window.location.href = '/'; }); - }; + }); +}; - /** - * 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. - */ - self.onConnect = function(callback) { - if (!self.isOnline() || !self._emailDao || !self._emailDao._account) { - // prevent connection infinite loop - callback(); +/** + * 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. + */ +self.onConnect = function(callback) { + if (!self.isOnline() || !self._emailDao || !self._emailDao._account) { + // prevent connection infinite loop + callback(); + return; + } + + self._auth.getCredentials(function(err, credentials) { + if (err) { + callback(err); return; } - self._auth.getCredentials(function(err, credentials) { - if (err) { - callback(err); - return; - } + initClients(credentials); + }); - initClients(credentials); - }); + function initClients(credentials) { + // add the maximum update batch size for imap folders to the imap configuration + credentials.imap.maxUpdateSize = config.imapUpdateBatchSize; - function initClients(credentials) { - // add the maximum update batch size for imap folders to the imap configuration - credentials.imap.maxUpdateSize = config.imapUpdateBatchSize; + var pgpMailer = new PgpMailer(credentials.smtp, self._pgpbuilder); + var imapClient = new ImapClient(credentials.imap); + imapClient.onError = onConnectionError; + pgpMailer.onError = onConnectionError; - 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, self.onError); + pgpMailer.onCert = self._auth.handleCertificateUpdate.bind(self._auth, 'smtp', self.onConnect, self.onError); - // certificate update handling - imapClient.onCert = self._auth.handleCertificateUpdate.bind(self._auth, 'imap', self.onConnect, self.onError); - pgpMailer.onCert = self._auth.handleCertificateUpdate.bind(self._auth, 'smtp', self.onConnect, self.onError); + // after-setup configuration depending on the provider: + // gmail does not require you to upload to the sent items folder + // after successful sending, whereas most other providers do + self._emailDao.ignoreUploadOnSent = !!(config[self._auth.provider] && config[self._auth.provider].ignoreUploadOnSent); - // after-setup configuration depending on the provider: - // gmail does not require you to upload to the sent items folder - // after successful sending, whereas most other providers do - self._emailDao.ignoreUploadOnSent = !!(config[self._auth.provider] && config[self._auth.provider].ignoreUploadOnSent); + // connect to clients + self._emailDao.onConnect({ + imapClient: imapClient, + pgpMailer: pgpMailer + }, callback); + } - // connect to clients - self._emailDao.onConnect({ - imapClient: imapClient, - pgpMailer: pgpMailer - }, callback); - } + function onConnectionError(error) { + axe.debug('Connection error. Attempting reconnect in ' + config.reconnectInterval + ' ms. Error: ' + (error.errMsg || error.message) + (error.stack ? ('\n' + error.stack) : '')); - 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; + } - 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); + } +}; - axe.debug('Reconnect attempt complete.'); - }); - }, config.reconnectInterval); - } - }; - - return self; -}); \ No newline at end of file +exports = self; \ No newline at end of file diff --git a/src/js/app.js b/src/js/app.js index cfb3fd9..9b13e26 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -14,148 +14,104 @@ if (typeof window.applicationCache !== 'undefined') { }; } -// hey Angular, we're bootstrapping manually! -window.name = 'NG_DEFER_BOOTSTRAP!'; +var DialogCtrl = require('./controller/dialog'), + PopoverCtrl = require('./controller/popover'), + AddAccountCtrl = require('./controller/add-account'), + AccountCtrl = require('./controller/account'), + SetPassphraseCtrl = require('./controller/set-passphrase'), + PrivateKeyUploadCtrl = require('./controller/privatekey-upload'), + ContactsCtrl = require('./controller/contacts'), + AboutCtrl = require('./controller/about'), + LoginCtrl = require('./controller/login'), + LoginInitialCtrl = require('./controller/login-initial'), + LoginNewDeviceCtrl = require('./controller/login-new-device'), + LoginExistingCtrl = require('./controller/login-existing'), + LoginPrivateKeyDownloadCtrl = require('./controller/login-privatekey-download'), + LoginSetCredentialsCtrl = require('./controller/login-set-credentials'), + MailListCtrl = require('./controller/mail-list'), + ReadCtrl = require('./controller/read'), + WriteCtrl = require('./controller/write'), + NavigationCtrl = require('./controller/navigation'), + errorUtil = require('./util/error'), + backButtonUtil = require('./util/backbutton-handler'); -requirejs([ - 'angular', - 'js/controller/dialog', - 'js/controller/popover', - 'js/controller/add-account', - 'js/controller/account', - 'js/controller/set-passphrase', - 'js/controller/privatekey-upload', - 'js/controller/contacts', - 'js/controller/about', - 'js/controller/login', - 'js/controller/login-initial', - 'js/controller/login-new-device', - 'js/controller/login-existing', - 'js/controller/login-privatekey-download', - 'js/controller/login-set-credentials', - 'js/controller/mail-list', - 'js/controller/read', - 'js/controller/write', - 'js/controller/navigation', - 'js/crypto/util', - 'js/util/error', - 'js/util/backbutton-handler', - 'fastclick', - 'angularRoute', - 'angularAnimate', - 'ngInfiniteScroll', +// init main angular module including dependencies +var app = angular.module('mail', [ + 'ngRoute', + 'ngAnimate', + 'navigation', + 'mail-list', + 'write', + 'read', + 'contacts', + 'login-new-device', + 'privatekey-upload', + 'popover', + 'infinite-scroll', 'ngTagsInput' -], function( - angular, - DialogCtrl, - PopoverCtrl, - AddAccountCtrl, - AccountCtrl, - SetPassphraseCtrl, - PrivateKeyUploadCtrl, - ContactsCtrl, - AboutCtrl, - LoginCtrl, - LoginInitialCtrl, - LoginNewDeviceCtrl, - LoginExistingCtrl, - LoginPrivateKeyDownloadCtrl, - LoginSetCredentialsCtrl, - MailListCtrl, - ReadCtrl, - WriteCtrl, - NavigationCtrl, - util, - errorUtil, - backButtonUtil, - FastClick -) { - // reset window.name - window.name = util.UUID(); +]); - // init main angular module including dependencies - var app = angular.module('mail', [ - 'ngRoute', - 'ngAnimate', - 'navigation', - 'mail-list', - 'write', - 'read', - 'contacts', - 'login-new-device', - 'privatekey-upload', - 'popover', - 'infinite-scroll', - 'ngTagsInput' - ]); - - // set router paths - app.config(function($routeProvider) { - $routeProvider.when('/add-account', { - templateUrl: 'tpl/add-account.html', - controller: AddAccountCtrl - }); - $routeProvider.when('/login', { - templateUrl: 'tpl/login.html', - controller: LoginCtrl - }); - $routeProvider.when('/login-set-credentials', { - templateUrl: 'tpl/login-set-credentials.html', - controller: LoginSetCredentialsCtrl - }); - $routeProvider.when('/login-existing', { - templateUrl: 'tpl/login-existing.html', - controller: LoginExistingCtrl - }); - $routeProvider.when('/login-initial', { - templateUrl: 'tpl/login-initial.html', - controller: LoginInitialCtrl - }); - $routeProvider.when('/login-new-device', { - templateUrl: 'tpl/login-new-device.html', - controller: LoginNewDeviceCtrl - }); - $routeProvider.when('/login-privatekey-download', { - templateUrl: 'tpl/login-privatekey-download.html', - controller: LoginPrivateKeyDownloadCtrl - }); - $routeProvider.when('/desktop', { - templateUrl: 'tpl/desktop.html', - controller: NavigationCtrl - }); - $routeProvider.otherwise({ - redirectTo: '/login' - }); +// set router paths +app.config(function($routeProvider) { + $routeProvider.when('/add-account', { + templateUrl: 'tpl/add-account.html', + controller: AddAccountCtrl }); - - app.run(function($rootScope) { - // global state... inherited to all child scopes - $rootScope.state = {}; - - // attach global error handler - errorUtil.attachHandler($rootScope); - - // attach the back button handler to the root scope - backButtonUtil.attachHandler($rootScope); - - // attach fastclick - FastClick.attach(document.body); + $routeProvider.when('/login', { + templateUrl: 'tpl/login.html', + controller: LoginCtrl }); - - // inject controllers from ng-included view templates - app.controller('ReadCtrl', ReadCtrl); - app.controller('WriteCtrl', WriteCtrl); - app.controller('MailListCtrl', MailListCtrl); - app.controller('AccountCtrl', AccountCtrl); - app.controller('SetPassphraseCtrl', SetPassphraseCtrl); - app.controller('PrivateKeyUploadCtrl', PrivateKeyUploadCtrl); - app.controller('ContactsCtrl', ContactsCtrl); - app.controller('AboutCtrl', AboutCtrl); - app.controller('DialogCtrl', DialogCtrl); - app.controller('PopoverCtrl', PopoverCtrl); - - // manually bootstrap angular due to require.js - angular.element().ready(function() { - angular.bootstrap(document, ['mail']); + $routeProvider.when('/login-set-credentials', { + templateUrl: 'tpl/login-set-credentials.html', + controller: LoginSetCredentialsCtrl }); -}); \ No newline at end of file + $routeProvider.when('/login-existing', { + templateUrl: 'tpl/login-existing.html', + controller: LoginExistingCtrl + }); + $routeProvider.when('/login-initial', { + templateUrl: 'tpl/login-initial.html', + controller: LoginInitialCtrl + }); + $routeProvider.when('/login-new-device', { + templateUrl: 'tpl/login-new-device.html', + controller: LoginNewDeviceCtrl + }); + $routeProvider.when('/login-privatekey-download', { + templateUrl: 'tpl/login-privatekey-download.html', + controller: LoginPrivateKeyDownloadCtrl + }); + $routeProvider.when('/desktop', { + templateUrl: 'tpl/desktop.html', + controller: NavigationCtrl + }); + $routeProvider.otherwise({ + redirectTo: '/login' + }); +}); + +app.run(function($rootScope) { + // global state... inherited to all child scopes + $rootScope.state = {}; + + // attach global error handler + errorUtil.attachHandler($rootScope); + + // attach the back button handler to the root scope + backButtonUtil.attachHandler($rootScope); + + // attach fastclick + FastClick.attach(document.body); +}); + +// inject controllers from ng-included view templates +app.controller('ReadCtrl', ReadCtrl); +app.controller('WriteCtrl', WriteCtrl); +app.controller('MailListCtrl', MailListCtrl); +app.controller('AccountCtrl', AccountCtrl); +app.controller('SetPassphraseCtrl', SetPassphraseCtrl); +app.controller('PrivateKeyUploadCtrl', PrivateKeyUploadCtrl); +app.controller('ContactsCtrl', ContactsCtrl); +app.controller('AboutCtrl', AboutCtrl); +app.controller('DialogCtrl', DialogCtrl); +app.controller('PopoverCtrl', PopoverCtrl); \ No newline at end of file diff --git a/src/js/bo/auth.js b/src/js/bo/auth.js index be7d46d..e7780d1 100644 --- a/src/js/bo/auth.js +++ b/src/js/bo/auth.js @@ -1,355 +1,210 @@ -define(function(require) { - 'use strict'; +'use strict'; - var axe = require('axe'), - str = require('js/app-config').string; +var axe = require('axe-logger'), + str = require('../app-config').string; - var EMAIL_ADDR_DB_KEY = 'emailaddress'; - var USERNAME_DB_KEY = 'username'; - var REALNAME_DB_KEY = 'realname'; - var PASSWD_DB_KEY = 'password'; - var PROVIDER_DB_KEY = 'provider'; - var IMAP_DB_KEY = 'imap'; - var SMTP_DB_KEY = 'smtp'; +var EMAIL_ADDR_DB_KEY = 'emailaddress'; +var USERNAME_DB_KEY = 'username'; +var REALNAME_DB_KEY = 'realname'; +var PASSWD_DB_KEY = 'password'; +var PROVIDER_DB_KEY = 'provider'; +var IMAP_DB_KEY = 'imap'; +var SMTP_DB_KEY = 'smtp'; - /** - * The Auth BO handles the rough edges and gaps between user/password authentication - * and OAuth via Chrome Identity API. - * Typical usage: - * var auth = new Auth(...); - * auth.setCredentials(...); // during the account setup - * auth.getEmailAddress(...); // called from the login controller to determine if there is already a user present on the device - * auth.getCredentials(...); // called to gather all the information to connect to IMAP/SMTP, e.g. pinned intermediate certificates, - * username, password / oauth token, IMAP/SMTP server host names, ... - */ - var Auth = function(appConfigStore, oauth, pgp) { - this._appConfigStore = appConfigStore; - this._oauth = oauth; - this._pgp = pgp; - }; +/** + * The Auth BO handles the rough edges and gaps between user/password authentication + * and OAuth via Chrome Identity API. + * Typical usage: + * var auth = new Auth(...); + * auth.setCredentials(...); // during the account setup + * auth.getEmailAddress(...); // called from the login controller to determine if there is already a user present on the device + * auth.getCredentials(...); // called to gather all the information to connect to IMAP/SMTP, e.g. pinned intermediate certificates, + * username, password / oauth token, IMAP/SMTP server host names, ... + */ +var Auth = function(appConfigStore, oauth, pgp) { + this._appConfigStore = appConfigStore; + this._oauth = oauth; + this._pgp = pgp; +}; - /** - * Retrieves credentials and IMAP/SMTP settings: - * 1) Fetches the credentials from disk, then... - * 2 a) ... in an oauth setting, retrieves a fresh oauth token from the Chrome Identity API. - * 2 b) ... in a user/passwd setting, does not need to do additional work. - * 3) Loads the intermediate certs from the configuration. - * - * @param {Function} callback(err, credentials) - */ - Auth.prototype.getCredentials = function(callback) { - var self = this; - - if (!self.provider || !self.emailAddress) { - // we're not yet initialized, so let's load our stuff from disk - self._loadCredentials(function(err) { - if (err) { - return callback(err); - } - - chooseLogin(); - }); - return; - } - - chooseLogin(); - - function chooseLogin() { - if (self.provider === 'gmail' && !self.password) { - // oauth login for gmail - self.getOAuthToken(function(err) { - if (err) { - return callback(err); - } - - done(); - }); - return; - } - - if (self.passwordNeedsDecryption) { - // decrypt password - self._pgp.decrypt(self.password, undefined, function(err, cleartext) { - if (err) { - return callback(err); - } - - self.passwordNeedsDecryption = false; - self.password = cleartext; - - done(); - }); - return; - } - - done(); - } - - function done() { - var credentials = { - imap: { - secure: self.imap.secure, - port: self.imap.port, - host: self.imap.host, - ca: self.imap.ca, - pinned: self.imap.pinned, - auth: { - user: self.username, - xoauth2: self.oauthToken, // password or oauthToken is undefined - pass: self.password - } - }, - smtp: { - secure: self.smtp.secure, - port: self.smtp.port, - host: self.smtp.host, - ca: self.smtp.ca, - pinned: self.smtp.pinned, - auth: { - user: self.username, - xoauth2: self.oauthToken, - pass: self.password // password or oauthToken is undefined - } - } - }; - - callback(null, credentials); - } - }; - - /** - * Set the credentials - * - * @param {String} options.provider The service provider, e.g. 'gmail', 'yahoo', 'tonline'. Matches the entry in the app-config. - * @param {String} options.emailAddress The email address - * @param {String} options.username The user name - * @param {String} options.realname The user's real name - * @param {String} options.password The password, only in user/passwd setting - * @param {String} options.smtp The smtp settings (host, port, secure) - * @param {String} options.imap The imap settings (host, port, secure) - */ - Auth.prototype.setCredentials = function(options) { - this.credentialsDirty = true; - this.provider = options.provider; - this.emailAddress = options.emailAddress; - this.username = options.username; - this.realname = options.realname ? options.realname : ''; - this.password = options.password; - this.smtp = options.smtp; // host, port, secure, ca, pinned - this.imap = options.imap; // host, port, secure, ca, pinned - }; - - Auth.prototype.storeCredentials = function(callback) { - var self = this; - - if (!self.credentialsDirty) { - return callback(); - } - - // persist the provider - self._appConfigStore.storeList([self.smtp], SMTP_DB_KEY, function(err) { - if (err) { - return callback(err); - } - - self._appConfigStore.storeList([self.imap], IMAP_DB_KEY, function(err) { - if (err) { - return callback(err); - } - - self._appConfigStore.storeList([self.provider], PROVIDER_DB_KEY, function(err) { - if (err) { - return callback(err); - } - - self._appConfigStore.storeList([self.emailAddress], EMAIL_ADDR_DB_KEY, function(err) { - if (err) { - return callback(err); - } - - self._appConfigStore.storeList([self.username], USERNAME_DB_KEY, function(err) { - if (err) { - return callback(err); - } - - self._appConfigStore.storeList([self.realname], REALNAME_DB_KEY, function(err) { - if (err) { - return callback(err); - } - - if (!self.password) { - self.credentialsDirty = false; - return callback(); - } - - if (self.passwordNeedsDecryption) { - // password is not decrypted yet, so no need to re-encrypt it before storing... - self._appConfigStore.storeList([self.password], PASSWD_DB_KEY, function(err) { - if (err) { - return callback(err); - } - - self.credentialsDirty = false; - callback(); - }); - return; - } - - self._pgp.encrypt(self.password, undefined, function(err, ciphertext) { - if (err) { - return callback(err); - } - - self._appConfigStore.storeList([ciphertext], PASSWD_DB_KEY, function(err) { - if (err) { - return callback(err); - } - - self.credentialsDirty = false; - callback(); - }); - }); - }); - }); - }); - }); - }); - }); - }; - - /** - * Returns the email address. Loads it from disk, if necessary - */ - Auth.prototype.getEmailAddress = function(callback) { - var self = this; - - if (self.emailAddress) { - return callback(null, { - emailAddress: self.emailAddress, - realname: self.realname - }); - } +/** + * Retrieves credentials and IMAP/SMTP settings: + * 1) Fetches the credentials from disk, then... + * 2 a) ... in an oauth setting, retrieves a fresh oauth token from the Chrome Identity API. + * 2 b) ... in a user/passwd setting, does not need to do additional work. + * 3) Loads the intermediate certs from the configuration. + * + * @param {Function} callback(err, credentials) + */ +Auth.prototype.getCredentials = function(callback) { + var self = this; + if (!self.provider || !self.emailAddress) { + // we're not yet initialized, so let's load our stuff from disk self._loadCredentials(function(err) { if (err) { return callback(err); } - callback(null, { - emailAddress: self.emailAddress, - realname: self.realname - }); + chooseLogin(); }); - }; + return; + } - /** - * READ FIRST b/c usage of the oauth api is weird. - * the chrome identity api will let you query an oauth token for an email account without knowing - * the corresponding email address. also, android has multiple accounts whereas desktop chrome only - * has one user logged in. - * 1) try to read the email address from the configuration (see above) - * 2) fetch the oauth token. if we already HAVE an email address at this point, we can spare - * popping up the account picker on android! if not, the account picker will pop up. this - * is android only, since the desktop chrome will query the user that is logged into chrome - * 3) fetch the email address for the oauth token from the chrome identity api - */ - Auth.prototype.getOAuthToken = function(callback) { - var self = this; + chooseLogin(); - if (self.oauthToken) { - // removed cached token and get a new one - self._oauth.refreshToken({ - emailAddress: self.emailAddress, - oldToken: self.oauthToken - }, onToken); - } else { - // get a fresh oauth token - self._oauth.getOAuthToken(self.emailAddress, onToken); - } - - function onToken(err, oauthToken) { - if (err) { - return callback(err); - } - - // shortcut if the email address is already known - if (self.emailAddress) { - self.oauthToken = oauthToken; - return callback(); - } - - // query the email address - self._oauth.queryEmailAddress(oauthToken, function(err, emailAddress) { + function chooseLogin() { + if (self.provider === 'gmail' && !self.password) { + // oauth login for gmail + self.getOAuthToken(function(err) { if (err) { return callback(err); } - self.oauthToken = oauthToken; - self.emailAddress = emailAddress; - callback(); + done(); }); - } - }; - - /** - * Loads email address, password, provider, ... from disk and sets them on `this` - */ - Auth.prototype._loadCredentials = function(callback) { - var self = this; - - if (self.initialized) { - callback(); + return; } - loadFromDB(SMTP_DB_KEY, function(err, smtp) { - if (err) { - return callback(err); - } - - - loadFromDB(IMAP_DB_KEY, function(err, imap) { + if (self.passwordNeedsDecryption) { + // decrypt password + self._pgp.decrypt(self.password, undefined, function(err, cleartext) { if (err) { return callback(err); } + self.passwordNeedsDecryption = false; + self.password = cleartext; - loadFromDB(USERNAME_DB_KEY, function(err, username) { + done(); + }); + return; + } + + done(); + } + + function done() { + var credentials = { + imap: { + secure: self.imap.secure, + port: self.imap.port, + host: self.imap.host, + ca: self.imap.ca, + pinned: self.imap.pinned, + auth: { + user: self.username, + xoauth2: self.oauthToken, // password or oauthToken is undefined + pass: self.password + } + }, + smtp: { + secure: self.smtp.secure, + port: self.smtp.port, + host: self.smtp.host, + ca: self.smtp.ca, + pinned: self.smtp.pinned, + auth: { + user: self.username, + xoauth2: self.oauthToken, + pass: self.password // password or oauthToken is undefined + } + } + }; + + callback(null, credentials); + } +}; + +/** + * Set the credentials + * + * @param {String} options.provider The service provider, e.g. 'gmail', 'yahoo', 'tonline'. Matches the entry in the app-config. + * @param {String} options.emailAddress The email address + * @param {String} options.username The user name + * @param {String} options.realname The user's real name + * @param {String} options.password The password, only in user/passwd setting + * @param {String} options.smtp The smtp settings (host, port, secure) + * @param {String} options.imap The imap settings (host, port, secure) + */ +Auth.prototype.setCredentials = function(options) { + this.credentialsDirty = true; + this.provider = options.provider; + this.emailAddress = options.emailAddress; + this.username = options.username; + this.realname = options.realname ? options.realname : ''; + this.password = options.password; + this.smtp = options.smtp; // host, port, secure, ca, pinned + this.imap = options.imap; // host, port, secure, ca, pinned +}; + +Auth.prototype.storeCredentials = function(callback) { + var self = this; + + if (!self.credentialsDirty) { + return callback(); + } + + // persist the provider + self._appConfigStore.storeList([self.smtp], SMTP_DB_KEY, function(err) { + if (err) { + return callback(err); + } + + self._appConfigStore.storeList([self.imap], IMAP_DB_KEY, function(err) { + if (err) { + return callback(err); + } + + self._appConfigStore.storeList([self.provider], PROVIDER_DB_KEY, function(err) { + if (err) { + return callback(err); + } + + self._appConfigStore.storeList([self.emailAddress], EMAIL_ADDR_DB_KEY, function(err) { if (err) { return callback(err); } - - loadFromDB(REALNAME_DB_KEY, function(err, realname) { + self._appConfigStore.storeList([self.username], USERNAME_DB_KEY, function(err) { if (err) { return callback(err); } - - loadFromDB(EMAIL_ADDR_DB_KEY, function(err, emailAddress) { + self._appConfigStore.storeList([self.realname], REALNAME_DB_KEY, function(err) { if (err) { return callback(err); } - loadFromDB(PASSWD_DB_KEY, function(err, password) { - if (err) { - return callback(err); - } + if (!self.password) { + self.credentialsDirty = false; + return callback(); + } - loadFromDB(PROVIDER_DB_KEY, function(err, provider) { + if (self.passwordNeedsDecryption) { + // password is not decrypted yet, so no need to re-encrypt it before storing... + self._appConfigStore.storeList([self.password], PASSWD_DB_KEY, function(err) { if (err) { return callback(err); } - self.emailAddress = emailAddress; - self.password = password; - self.passwordNeedsDecryption = !!password; - self.provider = provider; - self.username = username; - self.realname = realname; - self.smtp = smtp; - self.imap = imap; - self.initialized = true; + self.credentialsDirty = false; + callback(); + }); + return; + } + self._pgp.encrypt(self.password, undefined, function(err, ciphertext) { + if (err) { + return callback(err); + } + + self._appConfigStore.storeList([ciphertext], PASSWD_DB_KEY, function(err) { + if (err) { + return callback(err); + } + + self.credentialsDirty = false; callback(); }); }); @@ -358,96 +213,239 @@ define(function(require) { }); }); }); + }); +}; - function loadFromDB(key, callback) { - self._appConfigStore.listItems(key, 0, null, function(err, cachedItems) { - callback(err, (!err && cachedItems && cachedItems[0])); - }); - } - }; +/** + * Returns the email address. Loads it from disk, if necessary + */ +Auth.prototype.getEmailAddress = function(callback) { + var self = this; - /** - * Handles certificate updates and errors by notifying the user. - * @param {String} component Either imap or smtp - * @param {Function} callback The error handler - * @param {[type]} pemEncodedCert The PEM encoded SSL certificate - */ - Auth.prototype.handleCertificateUpdate = function(component, onConnect, callback, pemEncodedCert) { - var self = this; + if (self.emailAddress) { + return callback(null, { + emailAddress: self.emailAddress, + realname: self.realname + }); + } - axe.debug('new ssl certificate received: ' + pemEncodedCert); - - if (!self[component].ca) { - // no previous ssl cert, trust on first use - self[component].ca = pemEncodedCert; - self.credentialsDirty = true; - self.storeCredentials(callback); - return; + self._loadCredentials(function(err) { + if (err) { + return callback(err); } - if (self[component].ca === pemEncodedCert) { - // ignore multiple successive tls handshakes, e.g. for gmail - return; + callback(null, { + emailAddress: self.emailAddress, + realname: self.realname + }); + }); +}; + +/** + * READ FIRST b/c usage of the oauth api is weird. + * the chrome identity api will let you query an oauth token for an email account without knowing + * the corresponding email address. also, android has multiple accounts whereas desktop chrome only + * has one user logged in. + * 1) try to read the email address from the configuration (see above) + * 2) fetch the oauth token. if we already HAVE an email address at this point, we can spare + * popping up the account picker on android! if not, the account picker will pop up. this + * is android only, since the desktop chrome will query the user that is logged into chrome + * 3) fetch the email address for the oauth token from the chrome identity api + */ +Auth.prototype.getOAuthToken = function(callback) { + var self = this; + + if (self.oauthToken) { + // removed cached token and get a new one + self._oauth.refreshToken({ + emailAddress: self.emailAddress, + oldToken: self.oauthToken + }, onToken); + } else { + // get a fresh oauth token + self._oauth.getOAuthToken(self.emailAddress, onToken); + } + + function onToken(err, oauthToken) { + if (err) { + return callback(err); } - if (self[component].ca && self[component].pinned) { - // do not update the pinned certificates! - callback({ - title: str.outdatedCertificateTitle, - message: str.outdatedCertificateMessage.replace('{0}', self[component].host), - faqLink: str.certificateFaqLink, - }); - return; + // shortcut if the email address is already known + if (self.emailAddress) { + self.oauthToken = oauthToken; + return callback(); } - // previous ssl cert known, does not match: query user and certificate - callback({ - title: str.updateCertificateTitle, - message: str.updateCertificateMessage.replace('{0}', self[component].host), - positiveBtnStr: str.updateCertificatePosBtn, - negativeBtnStr: str.updateCertificateNegBtn, - showNegativeBtn: true, - faqLink: str.certificateFaqLink, - callback: function(granted) { - if (!granted) { - return; + // query the email address + self._oauth.queryEmailAddress(oauthToken, function(err, emailAddress) { + if (err) { + return callback(err); + } + + self.oauthToken = oauthToken; + self.emailAddress = emailAddress; + callback(); + }); + } +}; + +/** + * Loads email address, password, provider, ... from disk and sets them on `this` + */ +Auth.prototype._loadCredentials = function(callback) { + var self = this; + + if (self.initialized) { + callback(); + } + + loadFromDB(SMTP_DB_KEY, function(err, smtp) { + if (err) { + return callback(err); + } + + + loadFromDB(IMAP_DB_KEY, function(err, imap) { + if (err) { + return callback(err); + } + + + loadFromDB(USERNAME_DB_KEY, function(err, username) { + if (err) { + return callback(err); } - self[component].ca = pemEncodedCert; - self.storeCredentials(function(err) { + + loadFromDB(REALNAME_DB_KEY, function(err, realname) { if (err) { - callback(err); - return; + return callback(err); } - onConnect(callback); + + loadFromDB(EMAIL_ADDR_DB_KEY, function(err, emailAddress) { + if (err) { + return callback(err); + } + + loadFromDB(PASSWD_DB_KEY, function(err, password) { + if (err) { + return callback(err); + } + + loadFromDB(PROVIDER_DB_KEY, function(err, provider) { + if (err) { + return callback(err); + } + + self.emailAddress = emailAddress; + self.password = password; + self.passwordNeedsDecryption = !!password; + self.provider = provider; + self.username = username; + self.realname = realname; + self.smtp = smtp; + self.imap = imap; + self.initialized = true; + + callback(); + }); + }); + }); }); - } + }); }); - }; + }); - /** - * Logout of the app by clearing the app config store and in memory credentials - */ - Auth.prototype.logout = function(callback) { - var self = this; + function loadFromDB(key, callback) { + self._appConfigStore.listItems(key, 0, null, function(err, cachedItems) { + callback(err, (!err && cachedItems && cachedItems[0])); + }); + } +}; - // clear app config db - self._appConfigStore.clear(function(err) { - if (err) { - callback(err); +/** + * Handles certificate updates and errors by notifying the user. + * @param {String} component Either imap or smtp + * @param {Function} callback The error handler + * @param {[type]} pemEncodedCert The PEM encoded SSL certificate + */ +Auth.prototype.handleCertificateUpdate = function(component, onConnect, callback, pemEncodedCert) { + var self = this; + + axe.debug('new ssl certificate received: ' + pemEncodedCert); + + if (!self[component].ca) { + // no previous ssl cert, trust on first use + self[component].ca = pemEncodedCert; + self.credentialsDirty = true; + self.storeCredentials(callback); + return; + } + + if (self[component].ca === pemEncodedCert) { + // ignore multiple successive tls handshakes, e.g. for gmail + return; + } + + if (self[component].ca && self[component].pinned) { + // do not update the pinned certificates! + callback({ + title: str.outdatedCertificateTitle, + message: str.outdatedCertificateMessage.replace('{0}', self[component].host), + faqLink: str.certificateFaqLink, + }); + return; + } + + // previous ssl cert known, does not match: query user and certificate + callback({ + title: str.updateCertificateTitle, + message: str.updateCertificateMessage.replace('{0}', self[component].host), + positiveBtnStr: str.updateCertificatePosBtn, + negativeBtnStr: str.updateCertificateNegBtn, + showNegativeBtn: true, + faqLink: str.certificateFaqLink, + callback: function(granted) { + if (!granted) { return; } - // clear in memory cache - self.setCredentials({}); - self.initialized = undefined; - self.credentialsDirty = undefined; - self.passwordNeedsDecryption = undefined; + self[component].ca = pemEncodedCert; + self.storeCredentials(function(err) { + if (err) { + callback(err); + return; + } - callback(); - }); - }; + onConnect(callback); + }); + } + }); +}; - return Auth; -}); \ No newline at end of file +/** + * Logout of the app by clearing the app config store and in memory credentials + */ +Auth.prototype.logout = function(callback) { + var self = this; + + // clear app config db + self._appConfigStore.clear(function(err) { + if (err) { + callback(err); + return; + } + + // clear in memory cache + self.setCredentials({}); + self.initialized = undefined; + self.credentialsDirty = undefined; + self.passwordNeedsDecryption = undefined; + + callback(); + }); +}; + +exports = Auth; \ No newline at end of file diff --git a/src/js/bo/outbox.js b/src/js/bo/outbox.js index 8a09cfb..d67a5e5 100644 --- a/src/js/bo/outbox.js +++ b/src/js/bo/outbox.js @@ -1,232 +1,229 @@ -define(function(require) { - 'use strict'; +'use strict'; - var _ = require('underscore'), - util = require('js/crypto/util'), - config = require('js/app-config').config, - outboxDb = 'email_OUTBOX'; +var util = require('crypto-lib').util, + config = require('../app-config').config, + outboxDb = 'email_OUTBOX'; + +/** + * High level business object that orchestrates the local outbox. + * The local outbox takes care of the emails before they are being sent. + * It also checks periodically if there are any mails in the local device storage to be sent. + */ +var OutboxBO = function(emailDao, keychain, devicestorage) { + /** @private */ + this._emailDao = emailDao; + + /** @private */ + this._keychain = keychain; + + /** @private */ + this._devicestorage = devicestorage; /** - * High level business object that orchestrates the local outbox. - * The local outbox takes care of the emails before they are being sent. - * It also checks periodically if there are any mails in the local device storage to be sent. - */ - var OutboxBO = function(emailDao, keychain, devicestorage) { - /** @private */ - this._emailDao = emailDao; + * Semaphore-esque flag to avoid 'concurrent' calls to _processOutbox when the timeout fires, but a call is still in process. + * @private */ + this._outboxBusy = false; +}; - /** @private */ - this._keychain = keychain; +/** + * This function activates the periodic checking of the local device storage for pending mails. + * @param {Function} callback(error, pendingMailsCount) Callback that informs you about the count of pending mails. + */ +OutboxBO.prototype.startChecking = function(callback) { + // remember global callback + this._onUpdate = callback; + // start periodic checking of outbox + this._intervalId = setInterval(this._processOutbox.bind(this, this._onUpdate), config.checkOutboxInterval); +}; - /** @private */ - this._devicestorage = devicestorage; +/** + * Outbox stops the periodic checking of the local device storage for pending mails. + */ +OutboxBO.prototype.stopChecking = function() { + if (!this._intervalId) { + return; + } - /** - * Semaphore-esque flag to avoid 'concurrent' calls to _processOutbox when the timeout fires, but a call is still in process. - * @private */ - this._outboxBusy = false; - }; + clearInterval(this._intervalId); + delete this._intervalId; +}; - /** - * This function activates the periodic checking of the local device storage for pending mails. - * @param {Function} callback(error, pendingMailsCount) Callback that informs you about the count of pending mails. - */ - OutboxBO.prototype.startChecking = function(callback) { - // remember global callback - this._onUpdate = callback; - // start periodic checking of outbox - this._intervalId = setInterval(this._processOutbox.bind(this, this._onUpdate), config.checkOutboxInterval); - }; +/** + * Put a email dto in the outbox for sending when ready + * @param {Object} mail The Email DTO + * @param {Function} callback Invoked when the object was encrypted and persisted to disk + */ +OutboxBO.prototype.put = function(mail, callback) { + 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 - /** - * Outbox stops the periodic checking of the local device storage for pending mails. - */ - OutboxBO.prototype.stopChecking = function() { - if (!this._intervalId) { - return; - } + mail.publicKeysArmored = []; // gather the public keys + mail.uid = mail.id = util.UUID(); // the mail needs a random id & uid for storage in the database - clearInterval(this._intervalId); - delete this._intervalId; - }; + // do not encrypt mails with a bcc recipient, due to a possible privacy leak + if (mail.bcc.length > 0) { + storeAndForward(mail); + return; + } - /** - * Put a email dto in the outbox for sending when ready - * @param {Object} mail The Email DTO - * @param {Function} callback Invoked when the object was encrypted and persisted to disk - */ - OutboxBO.prototype.put = function(mail, callback) { - 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 + checkRecipients(allReaders); - mail.publicKeysArmored = []; // gather the public keys - mail.uid = mail.id = util.UUID(); // the mail needs a random id & uid for storage in the database + // check if there are unregistered recipients + function checkRecipients(recipients) { + var after = _.after(recipients.length, function() { + checkEncrypt(); + }); - // do not encrypt mails with a bcc recipient, due to a possible privacy leak - if (mail.bcc.length > 0) { + // find out if there are unregistered users + recipients.forEach(function(recipient) { + self._keychain.getReceiverPublicKey(recipient.address, function(err, key) { + if (err) { + callback(err); + return; + } + + // if a public key is available, add the recipient's key to the armored public keys, + // otherwise remember the recipient as unregistered for later sending + if (key) { + mail.publicKeysArmored.push(key.publicKey); + } + + after(); + }); + }); + } + + function checkEncrypt() { + // only encrypt if all recipients have public keys + if (mail.publicKeysArmored.length < allReaders.length) { storeAndForward(mail); return; } - checkRecipients(allReaders); - - // check if there are unregistered recipients - function checkRecipients(recipients) { - var after = _.after(recipients.length, function() { - checkEncrypt(); - }); - - // find out if there are unregistered users - recipients.forEach(function(recipient) { - self._keychain.getReceiverPublicKey(recipient.address, function(err, key) { - if (err) { - callback(err); - return; - } - - // if a public key is available, add the recipient's key to the armored public keys, - // otherwise remember the recipient as unregistered for later sending - if (key) { - mail.publicKeysArmored.push(key.publicKey); - } - - after(); - }); - }); - } - - function checkEncrypt() { - // only encrypt if all recipients have public keys - if (mail.publicKeysArmored.length < allReaders.length) { - storeAndForward(mail); + // encrypts the body and attachments and persists the mail object + self._emailDao.encrypt({ + mail: mail, + publicKeysArmored: mail.publicKeysArmored + }, function(err) { + if (err) { + callback(err); return; } - // encrypts the body and attachments and persists the mail object - self._emailDao.encrypt({ - mail: mail, - publicKeysArmored: mail.publicKeysArmored - }, function(err) { - if (err) { - callback(err); - return; - } + storeAndForward(mail); + }); + } - storeAndForward(mail); - }); - } + function storeAndForward(mail) { + // store in outbox + self._devicestorage.storeList([mail], outboxDb, function(err) { + if (err) { + callback(err); + return; + } - function storeAndForward(mail) { - // store in outbox - self._devicestorage.storeList([mail], outboxDb, function(err) { - if (err) { - callback(err); - return; - } + callback(); + // don't wait for next round + self._processOutbox(self._onUpdate); + }); + } +}; - callback(); - // don't wait for next round - self._processOutbox(self._onUpdate); - }); - } - }; +/** + * Checks the local device storage for pending mails. + * @param {Function} callback(error, pendingMailsCount) Callback that informs you about the count of pending mails. + */ +OutboxBO.prototype._processOutbox = function(callback) { + var self = this, + unsentMails = 0; - /** - * Checks the local device storage for pending mails. - * @param {Function} callback(error, pendingMailsCount) Callback that informs you about the count of pending mails. - */ - OutboxBO.prototype._processOutbox = function(callback) { - var self = this, - unsentMails = 0; + // also, if a _processOutbox call is still in progress, ignore it. + if (self._outboxBusy) { + return; + } - // also, if a _processOutbox call is still in progress, ignore it. - if (self._outboxBusy) { + self._outboxBusy = true; + + // get pending mails from the outbox + self._devicestorage.listItems(outboxDb, 0, null, function(err, pendingMails) { + // error, we're done here + if (err) { + self._outboxBusy = false; + callback(err); return; } - self._outboxBusy = true; + // if we're not online, don't even bother sending mails. + if (!self._emailDao._account.online || _.isEmpty(pendingMails)) { + self._outboxBusy = false; + callback(null, pendingMails.length); + return; + } - // get pending mails from the outbox - self._devicestorage.listItems(outboxDb, 0, null, function(err, pendingMails) { - // error, we're done here + // we're done after all the mails have been handled + // update the outbox count... + var after = _.after(pendingMails.length, function() { + self._outboxBusy = false; + callback(null, unsentMails); + }); + + // send pending mails if possible + pendingMails.forEach(function(mail) { + send(mail, after); + }); + }); + + // send the message + function send(mail, done) { + + // check is email is to be sent encrypted or as plaintex + if (mail.encrypted === true) { + // email was already encrypted before persisting in outbox, tell pgpmailer to send encrypted and not encrypt again + self._emailDao.sendEncrypted({ + email: mail + }, onSend); + } else { + // send email as plaintext + self._emailDao.sendPlaintext({ + email: mail + }, onSend); + } + + function onSend(err) { + if (err) { + self._outboxBusy = false; + if (err.code === 42) { + // offline try again later + done(); + } else { + self._outboxBusy = false; + callback(err); + } + return; + } + + // remove the pending mail from the storage + removeFromStorage(mail, done); + + // fire sent notification + if (typeof self.onSent === 'function') { + self.onSent(mail); + } + } + } + + // removes the mail object from disk after successfully sending it + function removeFromStorage(mail, done) { + self._devicestorage.removeList(outboxDb + '_' + mail.uid, function(err) { if (err) { self._outboxBusy = false; callback(err); return; } - // if we're not online, don't even bother sending mails. - if (!self._emailDao._account.online || _.isEmpty(pendingMails)) { - self._outboxBusy = false; - callback(null, pendingMails.length); - return; - } - - // we're done after all the mails have been handled - // update the outbox count... - var after = _.after(pendingMails.length, function() { - self._outboxBusy = false; - callback(null, unsentMails); - }); - - // send pending mails if possible - pendingMails.forEach(function(mail) { - send(mail, after); - }); + done(); }); + } +}; - // send the message - function send(mail, done) { - - // check is email is to be sent encrypted or as plaintex - if (mail.encrypted === true) { - // email was already encrypted before persisting in outbox, tell pgpmailer to send encrypted and not encrypt again - self._emailDao.sendEncrypted({ - email: mail - }, onSend); - } else { - // send email as plaintext - self._emailDao.sendPlaintext({ - email: mail - }, onSend); - } - - function onSend(err) { - if (err) { - self._outboxBusy = false; - if (err.code === 42) { - // offline try again later - done(); - } else { - self._outboxBusy = false; - callback(err); - } - return; - } - - // remove the pending mail from the storage - removeFromStorage(mail, done); - - // fire sent notification - if (typeof self.onSent === 'function') { - self.onSent(mail); - } - } - } - - // removes the mail object from disk after successfully sending it - function removeFromStorage(mail, done) { - self._devicestorage.removeList(outboxDb + '_' + mail.uid, function(err) { - if (err) { - self._outboxBusy = false; - callback(err); - return; - } - - done(); - }); - } - }; - - return OutboxBO; -}); \ No newline at end of file +exports = OutboxBO; \ No newline at end of file diff --git a/src/js/controller/about.js b/src/js/controller/about.js index 7e952a0..c88f965 100644 --- a/src/js/controller/about.js +++ b/src/js/controller/about.js @@ -1,31 +1,29 @@ -define(function(require) { - 'use strict'; +'use strict'; - var cfg = require('js/app-config').config; +var cfg = require('../app-config').config; - // - // Controller - // +// +// Controller +// - var AboutCtrl = function($scope) { +var AboutCtrl = function($scope) { - $scope.state.about = { - toggle: function(to) { - $scope.state.lightbox = (to) ? 'about' : undefined; - } - }; - - // - // scope variables - // - - $scope.version = cfg.appVersion; - $scope.date = new Date(); - - // - // scope functions - // + $scope.state.about = { + toggle: function(to) { + $scope.state.lightbox = (to) ? 'about' : undefined; + } }; - return AboutCtrl; -}); \ No newline at end of file + // + // scope variables + // + + $scope.version = cfg.appVersion; + $scope.date = new Date(); + + // + // scope functions + // +}; + +exports = AboutCtrl; \ No newline at end of file diff --git a/src/js/controller/account.js b/src/js/controller/account.js index 7c409dd..1ca2437 100644 --- a/src/js/controller/account.js +++ b/src/js/controller/account.js @@ -1,61 +1,59 @@ -define(function(require) { - 'use strict'; +'use strict'; - var appController = require('js/app-controller'), - dl = require('js/util/download'), - config = require('js/app-config').config, - pgp, keychain, userId; +var appController = require('../app-controller'), + dl = require('../util/download'), + config = require('../app-config').config, + pgp, keychain, userId; - // - // Controller - // +// +// Controller +// - var AccountCtrl = function($scope) { - userId = appController._emailDao._account.emailAddress; - keychain = appController._keychain; - pgp = appController._pgp; +var AccountCtrl = function($scope) { + userId = appController._emailDao._account.emailAddress; + keychain = appController._keychain; + pgp = appController._pgp; - $scope.state.account = { - toggle: function(to) { - $scope.state.lightbox = (to) ? 'account' : undefined; - } - }; - - // - // scope variables - // - - var keyParams = pgp.getKeyParams(); - - $scope.eMail = userId; - $scope.keyId = keyParams._id.slice(8); - 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 = config.cloudUrl + '/' + userId; - - // - // scope functions - // - - $scope.exportKeyFile = function() { - keychain.getUserKeyPair(userId, function(err, keys) { - if (err) { - $scope.onError(err); - return; - } - - var keyId = keys.publicKey._id; - var file = 'whiteout_mail_' + userId + '_' + keyId.substring(8, keyId.length); - - dl.createDownload({ - content: keys.publicKey.publicKey + '\r\n' + keys.privateKey.encryptedKey, - filename: file + '.asc', - contentType: 'text/plain' - }); - }); - }; + $scope.state.account = { + toggle: function(to) { + $scope.state.lightbox = (to) ? 'account' : undefined; + } }; - return AccountCtrl; -}); \ No newline at end of file + // + // scope variables + // + + var keyParams = pgp.getKeyParams(); + + $scope.eMail = userId; + $scope.keyId = keyParams._id.slice(8); + 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 = config.cloudUrl + '/' + userId; + + // + // scope functions + // + + $scope.exportKeyFile = function() { + keychain.getUserKeyPair(userId, function(err, keys) { + if (err) { + $scope.onError(err); + return; + } + + var keyId = keys.publicKey._id; + var file = 'whiteout_mail_' + userId + '_' + keyId.substring(8, keyId.length); + + dl.createDownload({ + content: keys.publicKey.publicKey + '\r\n' + keys.privateKey.encryptedKey, + filename: file + '.asc', + contentType: 'text/plain' + }); + }); + }; +}; + +exports = AccountCtrl; \ No newline at end of file diff --git a/src/js/controller/add-account.js b/src/js/controller/add-account.js index 35c3288..622c923 100644 --- a/src/js/controller/add-account.js +++ b/src/js/controller/add-account.js @@ -1,120 +1,118 @@ -define(function(require) { - 'use strict'; +'use strict'; - var appCtrl = require('js/app-controller'), - cfg = require('js/app-config').config; +var appCtrl = require('../app-controller'), + cfg = require('../app-config').config; - var AddAccountCtrl = function($scope, $location, $routeParams) { - if (!appCtrl._auth && !$routeParams.dev) { - $location.path('/'); // init app +var AddAccountCtrl = function($scope, $location, $routeParams) { + if (!appCtrl._auth && !$routeParams.dev) { + $location.path('/'); // init app + return; + } + + $scope.step = 1; + + $scope.goTo = function(step) { + $scope.step = step; + }; + + $scope.createWhiteoutAccount = function() { + if ($scope.form.$invalid) { return; } - $scope.step = 1; + $scope.busy = true; + $scope.errMsg = undefined; // reset error msg + $scope.emailAddress = $scope.user + '@' + cfg.wmailDomain; - $scope.goTo = function(step) { - $scope.step = step; - }; + // call REST api + appCtrl._adminDao.createUser({ + emailAddress: $scope.emailAddress, + password: $scope.pass, + phone: $scope.phone.replace(/\s+/g, ''), // remove spaces from the phone number + betaCode: $scope.betaCode.toUpperCase() + }, function(err) { + $scope.busy = false; - $scope.createWhiteoutAccount = function() { - if ($scope.form.$invalid) { - return; - } - - $scope.busy = true; - $scope.errMsg = undefined; // reset error msg - $scope.emailAddress = $scope.user + '@' + cfg.wmailDomain; - - // call REST api - appCtrl._adminDao.createUser({ - emailAddress: $scope.emailAddress, - password: $scope.pass, - phone: $scope.phone.replace(/\s+/g, ''), // remove spaces from the phone number - betaCode: $scope.betaCode.toUpperCase() - }, function(err) { - $scope.busy = false; - - if (err) { - $scope.errMsg = err.errMsg || err.message; - $scope.$apply(); - return; - } - - $scope.goTo(3); + if (err) { + $scope.errMsg = err.errMsg || err.message; $scope.$apply(); - }); - }; - - $scope.validateUser = function() { - if ($scope.formValidate.$invalid) { return; } - $scope.busyValidate = true; - $scope.errMsgValidate = undefined; // reset error msg - - // verify user to REST api - appCtrl._adminDao.validateUser({ - emailAddress: $scope.emailAddress, - token: $scope.token.toUpperCase() - }, function(err) { - if (err) { - $scope.busyValidate = false; - $scope.errMsgValidate = err.errMsg || err.message; - $scope.$apply(); - return; - } - - // proceed to login - $scope.login(); - }); - }; - - $scope.login = function() { - // store credentials in memory - appCtrl._auth.setCredentials({ - provider: 'wmail', - emailAddress: $scope.emailAddress, - username: $scope.emailAddress, - realname: $scope.realname, - password: $scope.pass, - imap: cfg.wmail.imap, - smtp: cfg.wmail.smtp - }); - - // proceed to login and keygen - $location.path('/login'); + $scope.goTo(3); $scope.$apply(); - }; - - $scope.connectToGoogle = function() { - // test for oauth support - if (appCtrl._auth._oauth.isSupported()) { - // fetches the email address from the chrome identity api - appCtrl._auth.getOAuthToken(function(err) { - if (err) { - return $scope.onError(err); - } - $location.path('/login-set-credentials').search({ - provider: 'gmail' - }); - $scope.$apply(); - }); - return; - } - - // use normal user/password login - $location.path('/login-set-credentials').search({ - provider: 'gmail' - }); - }; - - $scope.connectTo = function(provider) { - $location.path('/login-set-credentials').search({ - provider: provider - }); - }; + }); }; - return AddAccountCtrl; -}); \ No newline at end of file + $scope.validateUser = function() { + if ($scope.formValidate.$invalid) { + return; + } + + $scope.busyValidate = true; + $scope.errMsgValidate = undefined; // reset error msg + + // verify user to REST api + appCtrl._adminDao.validateUser({ + emailAddress: $scope.emailAddress, + token: $scope.token.toUpperCase() + }, function(err) { + if (err) { + $scope.busyValidate = false; + $scope.errMsgValidate = err.errMsg || err.message; + $scope.$apply(); + return; + } + + // proceed to login + $scope.login(); + }); + }; + + $scope.login = function() { + // store credentials in memory + appCtrl._auth.setCredentials({ + provider: 'wmail', + emailAddress: $scope.emailAddress, + username: $scope.emailAddress, + realname: $scope.realname, + password: $scope.pass, + imap: cfg.wmail.imap, + smtp: cfg.wmail.smtp + }); + + // proceed to login and keygen + $location.path('/login'); + $scope.$apply(); + }; + + $scope.connectToGoogle = function() { + // test for oauth support + if (appCtrl._auth._oauth.isSupported()) { + // fetches the email address from the chrome identity api + appCtrl._auth.getOAuthToken(function(err) { + if (err) { + return $scope.onError(err); + } + $location.path('/login-set-credentials').search({ + provider: 'gmail' + }); + $scope.$apply(); + }); + return; + } + + // use normal user/password login + $location.path('/login-set-credentials').search({ + provider: 'gmail' + }); + }; + + $scope.connectTo = function(provider) { + $location.path('/login-set-credentials').search({ + provider: provider + }); + }; +}; + +exports = AddAccountCtrl; \ No newline at end of file diff --git a/src/js/controller/contacts.js b/src/js/controller/contacts.js index 10cd441..cdf11ea 100644 --- a/src/js/controller/contacts.js +++ b/src/js/controller/contacts.js @@ -1,143 +1,139 @@ -define(function(require) { - 'use strict'; +'use strict'; - var angular = require('angular'), - _ = require('underscore'), - appController = require('js/app-controller'), - keychain, pgp; +var appController = require('../app-controller'), + keychain, pgp; - // - // Controller - // +// +// Controller +// - var ContactsCtrl = function($scope) { - keychain = appController._keychain, - pgp = appController._pgp; +var ContactsCtrl = function($scope) { + keychain = appController._keychain, + pgp = appController._pgp; - $scope.state.contacts = { - toggle: function(to) { - $scope.state.lightbox = (to) ? 'contacts' : undefined; + $scope.state.contacts = { + toggle: function(to) { + $scope.state.lightbox = (to) ? 'contacts' : undefined; - $scope.listKeys(); - } - }; - - // set default value so that the popover height is correct on init - $scope.fingerprint = 'XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX'; - - // - // scope functions - // - - $scope.listKeys = function() { - keychain.listLocalPublicKeys(function(err, keys) { - if (err) { - $scope.onError(err); - return; - } - - keys.forEach(addParams); - - $scope.keys = keys; - $scope.$apply(); - - function addParams(key) { - var params = pgp.getKeyParams(key.publicKey); - _.extend(key, params); - } - }); - }; - - $scope.getFingerprint = function(key) { - var fpr = key.fingerprint; - var formatted = fpr.slice(0, 4) + ' ' + fpr.slice(4, 8) + ' ' + fpr.slice(8, 12) + ' ' + fpr.slice(12, 16) + ' ' + fpr.slice(16, 20) + ' ... ' + fpr.slice(20, 24) + ' ' + fpr.slice(24, 28) + ' ' + fpr.slice(28, 32) + ' ' + fpr.slice(32, 36) + ' ' + fpr.slice(36); - - $scope.fingerprint = formatted; - }; - - $scope.importKey = function(publicKeyArmored) { - var keyParams, pubkey; - - // verifiy public key string - if (publicKeyArmored.indexOf('-----BEGIN PGP PUBLIC KEY BLOCK-----') < 0) { - $scope.onError({ - showBugReporter: false, - message: 'Invalid public key!' - }); - return; - } - - try { - keyParams = pgp.getKeyParams(publicKeyArmored); - } catch (e) { - $scope.onError(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 - }; - - keychain.saveLocalPublicKey(pubkey, function(err) { - if (err) { - $scope.onError(err); - return; - } - - // update displayed keys - $scope.listKeys(); - }); - }; - - $scope.removeKey = function(key) { - keychain.removeLocalPublicKey(key._id, function(err) { - if (err) { - $scope.onError(err); - return; - } - - // update displayed keys - $scope.listKeys(); - }); - }; + $scope.listKeys(); + } }; + // set default value so that the popover height is correct on init + $scope.fingerprint = 'XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX'; + // - // Directives + // scope functions // - var ngModule = angular.module('contacts', []); - - ngModule.directive('keyfileInput', function() { - return function(scope, elm) { - elm.on('change', function(e) { - for (var i = 0; i < e.target.files.length; i++) { - importKey(e.target.files.item(i)); - } - }); - - function importKey(file) { - var reader = new FileReader(); - reader.onload = function(e) { - scope.importKey(e.target.result); - }; - reader.readAsText(file); + $scope.listKeys = function() { + keychain.listLocalPublicKeys(function(err, keys) { + if (err) { + $scope.onError(err); + return; } - }; - }); - ngModule.directive('keyfileBtn', function() { - return function(scope, elm) { - elm.on('click touchstart', function(e) { - e.preventDefault(); - document.querySelector('#keyfile-input').click(); + keys.forEach(addParams); + + $scope.keys = keys; + $scope.$apply(); + + function addParams(key) { + var params = pgp.getKeyParams(key.publicKey); + _.extend(key, params); + } + }); + }; + + $scope.getFingerprint = function(key) { + var fpr = key.fingerprint; + var formatted = fpr.slice(0, 4) + ' ' + fpr.slice(4, 8) + ' ' + fpr.slice(8, 12) + ' ' + fpr.slice(12, 16) + ' ' + fpr.slice(16, 20) + ' ... ' + fpr.slice(20, 24) + ' ' + fpr.slice(24, 28) + ' ' + fpr.slice(28, 32) + ' ' + fpr.slice(32, 36) + ' ' + fpr.slice(36); + + $scope.fingerprint = formatted; + }; + + $scope.importKey = function(publicKeyArmored) { + var keyParams, pubkey; + + // verifiy public key string + if (publicKeyArmored.indexOf('-----BEGIN PGP PUBLIC KEY BLOCK-----') < 0) { + $scope.onError({ + showBugReporter: false, + message: 'Invalid public key!' }); - }; - }); + return; + } - return ContactsCtrl; -}); \ No newline at end of file + try { + keyParams = pgp.getKeyParams(publicKeyArmored); + } catch (e) { + $scope.onError(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 + }; + + keychain.saveLocalPublicKey(pubkey, function(err) { + if (err) { + $scope.onError(err); + return; + } + + // update displayed keys + $scope.listKeys(); + }); + }; + + $scope.removeKey = function(key) { + keychain.removeLocalPublicKey(key._id, function(err) { + if (err) { + $scope.onError(err); + return; + } + + // update displayed keys + $scope.listKeys(); + }); + }; +}; + +// +// Directives +// + +var ngModule = angular.module('contacts', []); + +ngModule.directive('keyfileInput', function() { + return function(scope, elm) { + elm.on('change', function(e) { + for (var i = 0; i < e.target.files.length; i++) { + importKey(e.target.files.item(i)); + } + }); + + function importKey(file) { + var reader = new FileReader(); + reader.onload = function(e) { + scope.importKey(e.target.result); + }; + reader.readAsText(file); + } + }; +}); + +ngModule.directive('keyfileBtn', function() { + return function(scope, elm) { + elm.on('click touchstart', function(e) { + e.preventDefault(); + document.querySelector('#keyfile-input').click(); + }); + }; +}); + +exports = ContactsCtrl; \ No newline at end of file diff --git a/src/js/controller/dialog.js b/src/js/controller/dialog.js index 45108c1..912f7b9 100644 --- a/src/js/controller/dialog.js +++ b/src/js/controller/dialog.js @@ -1,16 +1,14 @@ -define(function() { - 'use strict'; +'use strict'; - var DialogCtrl = function($scope) { - $scope.confirm = function(ok) { - $scope.state.dialog.open = false; +var DialogCtrl = function($scope) { + $scope.confirm = function(ok) { + $scope.state.dialog.open = false; - if ($scope.state.dialog.callback) { - $scope.state.dialog.callback(ok); - } - $scope.state.dialog.callback = undefined; - }; + if ($scope.state.dialog.callback) { + $scope.state.dialog.callback(ok); + } + $scope.state.dialog.callback = undefined; }; +}; - return DialogCtrl; -}); \ No newline at end of file +exports = DialogCtrl; \ No newline at end of file diff --git a/src/js/controller/login-existing.js b/src/js/controller/login-existing.js index 35aa6bd..d520471 100644 --- a/src/js/controller/login-existing.js +++ b/src/js/controller/login-existing.js @@ -1,73 +1,71 @@ -define(function(require) { - 'use strict'; +'use strict'; - var appController = require('js/app-controller'); +var appController = require('../app-controller'); - var LoginExistingCtrl = function($scope, $location, $routeParams) { - if (!appController._emailDao && !$routeParams.dev) { - $location.path('/'); // init app +var LoginExistingCtrl = function($scope, $location, $routeParams) { + if (!appController._emailDao && !$routeParams.dev) { + $location.path('/'); // init app + return; + } + + var emailDao = appController._emailDao; + + $scope.buttonEnabled = true; + $scope.incorrect = false; + + $scope.change = function() { + $scope.incorrect = false; + }; + + $scope.confirmPassphrase = function() { + if (!$scope.passphrase) { return; } - var emailDao = appController._emailDao; - - $scope.buttonEnabled = true; + // disable button once loggin has started + $scope.buttonEnabled = false; $scope.incorrect = false; - - $scope.change = function() { - $scope.incorrect = false; - }; - - $scope.confirmPassphrase = function() { - if (!$scope.passphrase) { - return; - } - - // disable button once loggin has started - $scope.buttonEnabled = false; - $scope.incorrect = false; - unlockCrypto(); - }; - - function unlockCrypto() { - var userId = emailDao._account.emailAddress; - emailDao._keychain.getUserKeyPair(userId, function(err, keypair) { - if (err) { - handleError(err); - return; - } - - emailDao.unlock({ - keypair: keypair, - passphrase: $scope.passphrase - }, onUnlock); - }); - } - - function onUnlock(err) { - if (err) { - $scope.incorrect = true; - $scope.buttonEnabled = true; - $scope.$apply(); - return; - } - - appController._auth.storeCredentials(function(err) { - if (err) { - return $scope.onError(err); - } - - $location.path('/desktop'); - $scope.$apply(); - }); - } - - function handleError(err) { - $scope.incorrect = true; - $scope.buttonEnabled = true; - $scope.onError(err); - } + unlockCrypto(); }; - return LoginExistingCtrl; -}); \ No newline at end of file + function unlockCrypto() { + var userId = emailDao._account.emailAddress; + emailDao._keychain.getUserKeyPair(userId, function(err, keypair) { + if (err) { + handleError(err); + return; + } + + emailDao.unlock({ + keypair: keypair, + passphrase: $scope.passphrase + }, onUnlock); + }); + } + + function onUnlock(err) { + if (err) { + $scope.incorrect = true; + $scope.buttonEnabled = true; + $scope.$apply(); + return; + } + + appController._auth.storeCredentials(function(err) { + if (err) { + return $scope.onError(err); + } + + $location.path('/desktop'); + $scope.$apply(); + }); + } + + function handleError(err) { + $scope.incorrect = true; + $scope.buttonEnabled = true; + $scope.onError(err); + } +}; + +exports = LoginExistingCtrl; \ No newline at end of file diff --git a/src/js/controller/login-initial.js b/src/js/controller/login-initial.js index 8c04c7d..7d00616 100644 --- a/src/js/controller/login-initial.js +++ b/src/js/controller/login-initial.js @@ -1,121 +1,119 @@ -define(function(require) { - 'use strict'; +'use strict'; - var appController = require('js/app-controller'); +var appController = require('../app-controller'); - var LoginInitialCtrl = function($scope, $location, $routeParams) { - if (!appController._emailDao && !$routeParams.dev) { - $location.path('/'); // init app +var LoginInitialCtrl = function($scope, $location, $routeParams) { + if (!appController._emailDao && !$routeParams.dev) { + $location.path('/'); // init app + return; + } + + var emailDao = appController._emailDao, + states, termsMsg = 'You must accept the Terms of Service to continue.'; + + states = { + IDLE: 1, + PROCESSING: 2, + DONE: 3 + }; + $scope.state.ui = states.IDLE; // initial state + + // + // scope functions + // + + /** + * Continue to key import screen + */ + $scope.importKey = function() { + if (!$scope.state.agree) { + $scope.onError({ + message: termsMsg + }); return; } - var emailDao = appController._emailDao, - states, termsMsg = 'You must accept the Terms of Service to continue.'; - - states = { - IDLE: 1, - PROCESSING: 2, - DONE: 3 - }; - $scope.state.ui = states.IDLE; // initial state - - // - // scope functions - // - - /** - * Continue to key import screen - */ - $scope.importKey = function() { - if (!$scope.state.agree) { - $scope.onError({ - message: termsMsg - }); - return; - } - - // sing up to newsletter - $scope.signUpToNewsletter(); - // go to key import - $location.path('/login-new-device'); - }; - - /** - * Continue to keygen - */ - $scope.generateKey = function() { - if (!$scope.state.agree) { - $scope.onError({ - message: termsMsg - }); - return; - } - - // sing up to newsletter - $scope.signUpToNewsletter(); - // go to set keygen screen - $scope.setState(states.PROCESSING); - - setTimeout(function() { - emailDao.unlock({ - passphrase: undefined // generate key without passphrase - }, function(err) { - if (err) { - $scope.setState(states.IDLE); - $scope.onError(err); - return; - } - - appController._auth.storeCredentials(function(err) { - if (err) { - return $scope.onError(err); - } - - $location.path('/desktop'); - $scope.$apply(); - }); - }); - }, 500); - }; - - /** - * [signUpToNewsletter description] - * @param {Function} callback (optional) - */ - $scope.signUpToNewsletter = function(callback) { - if (!$scope.state.newsletter) { - return; - } - - var address = emailDao._account.emailAddress; - var uri = 'https://whiteout.us8.list-manage.com/subscribe/post?u=52ea5a9e1be9e1d194f184158&id=6538e8f09f'; - - var formData = new FormData(); - formData.append('EMAIL', address); - formData.append('b_52ea5a9e1be9e1d194f184158_6538e8f09f', ''); - - var xhr = new XMLHttpRequest(); - xhr.open('post', uri, true); - - xhr.onload = function() { - if (callback) { - callback(null, xhr); - } - }; - - xhr.onerror = function(err) { - if (callback) { - callback(err); - } - }; - - xhr.send(formData); - }; - - $scope.setState = function(state) { - $scope.state.ui = state; - }; + // sing up to newsletter + $scope.signUpToNewsletter(); + // go to key import + $location.path('/login-new-device'); }; - return LoginInitialCtrl; -}); \ No newline at end of file + /** + * Continue to keygen + */ + $scope.generateKey = function() { + if (!$scope.state.agree) { + $scope.onError({ + message: termsMsg + }); + return; + } + + // sing up to newsletter + $scope.signUpToNewsletter(); + // go to set keygen screen + $scope.setState(states.PROCESSING); + + setTimeout(function() { + emailDao.unlock({ + passphrase: undefined // generate key without passphrase + }, function(err) { + if (err) { + $scope.setState(states.IDLE); + $scope.onError(err); + return; + } + + appController._auth.storeCredentials(function(err) { + if (err) { + return $scope.onError(err); + } + + $location.path('/desktop'); + $scope.$apply(); + }); + }); + }, 500); + }; + + /** + * [signUpToNewsletter description] + * @param {Function} callback (optional) + */ + $scope.signUpToNewsletter = function(callback) { + if (!$scope.state.newsletter) { + return; + } + + var address = emailDao._account.emailAddress; + var uri = 'https://whiteout.us8.list-manage.com/subscribe/post?u=52ea5a9e1be9e1d194f184158&id=6538e8f09f'; + + var formData = new FormData(); + formData.append('EMAIL', address); + formData.append('b_52ea5a9e1be9e1d194f184158_6538e8f09f', ''); + + var xhr = new XMLHttpRequest(); + xhr.open('post', uri, true); + + xhr.onload = function() { + if (callback) { + callback(null, xhr); + } + }; + + xhr.onerror = function(err) { + if (callback) { + callback(err); + } + }; + + xhr.send(formData); + }; + + $scope.setState = function(state) { + $scope.state.ui = state; + }; +}; + +exports = LoginInitialCtrl; \ No newline at end of file diff --git a/src/js/controller/login-new-device.js b/src/js/controller/login-new-device.js index b39091a..e85c5e5 100644 --- a/src/js/controller/login-new-device.js +++ b/src/js/controller/login-new-device.js @@ -1,141 +1,138 @@ -define(function(require) { - 'use strict'; +'use strict'; - var angular = require('angular'), - appController = require('js/app-controller'); +var appController = require('../app-controller'); - var LoginExistingCtrl = function($scope, $location, $routeParams) { - if (!appController._emailDao && !$routeParams.dev) { - $location.path('/'); // init app - return; - } +var LoginExistingCtrl = function($scope, $location, $routeParams) { + if (!appController._emailDao && !$routeParams.dev) { + $location.path('/'); // init app + return; + } - var emailDao = appController._emailDao, - pgp = appController._pgp; + var emailDao = appController._emailDao, + pgp = appController._pgp; + $scope.incorrect = false; + + $scope.confirmPassphrase = function() { $scope.incorrect = false; + unlockCrypto(); + }; - $scope.confirmPassphrase = function() { - $scope.incorrect = false; - unlockCrypto(); - }; - - function unlockCrypto() { - var userId = emailDao._account.emailAddress; - // check if user already has a public key on the key server - emailDao._keychain.getUserKeyPair(userId, function(err, keypair) { - if (err) { - $scope.onError(err); - return; - } - - keypair = keypair || {}; - - // 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) { - try { - $scope.key.publicKeyArmored = pgp.extractPublicKey($scope.key.privateKeyArmored); - } catch (e) { - $scope.onError(new Error('Error parsing public key from private key!')); - return; - } - } - - // parse keypair params - var privKeyParams, pubKeyParams; - try { - privKeyParams = pgp.getKeyParams($scope.key.privateKeyArmored); - pubKeyParams = pgp.getKeyParams($scope.key.publicKeyArmored); - } catch (e) { - $scope.onError(new Error('Error reading key params!')); - return; - } - - // set parsed private key - keypair.privateKey = { - _id: privKeyParams._id, - userId: userId, - userIds: privKeyParams.userIds, - encryptedKey: $scope.key.privateKeyArmored - }; - - if (!keypair.publicKey) { - // there is no public key on the key server yet... use parsed - keypair.publicKey = { - _id: pubKeyParams._id, - userId: userId, - userIds: pubKeyParams.userIds, - publicKey: $scope.key.publicKeyArmored - }; - } - - // import and validate keypair - emailDao.unlock({ - keypair: keypair, - passphrase: $scope.passphrase - }, function(err) { - if (err) { - $scope.incorrect = true; - $scope.onError(err); - return; - } - - emailDao._keychain.putUserKeyPair(keypair, onUnlock); - }); - }); - } - - function onUnlock(err) { + function unlockCrypto() { + var userId = emailDao._account.emailAddress; + // check if user already has a public key on the key server + emailDao._keychain.getUserKeyPair(userId, function(err, keypair) { if (err) { $scope.onError(err); return; } - appController._auth.storeCredentials(function(err) { - if (err) { - return $scope.onError(err); + keypair = keypair || {}; + + // 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) { + try { + $scope.key.publicKeyArmored = pgp.extractPublicKey($scope.key.privateKeyArmored); + } catch (e) { + $scope.onError(new Error('Error parsing public key from private key!')); + return; } + } - $location.path('/desktop'); - $scope.$apply(); - }); - } - }; + // parse keypair params + var privKeyParams, pubKeyParams; + try { + privKeyParams = pgp.getKeyParams($scope.key.privateKeyArmored); + pubKeyParams = pgp.getKeyParams($scope.key.publicKeyArmored); + } catch (e) { + $scope.onError(new Error('Error reading key params!')); + return; + } - var ngModule = angular.module('login-new-device', []); - ngModule.directive('fileReader', function() { - return function(scope, elm) { - elm.bind('change', function(e) { - var files = e.target.files, - reader = new FileReader(); + // set parsed private key + keypair.privateKey = { + _id: privKeyParams._id, + userId: userId, + userIds: privKeyParams.userIds, + encryptedKey: $scope.key.privateKeyArmored + }; - if (files.length === 0) { + if (!keypair.publicKey) { + // there is no public key on the key server yet... use parsed + keypair.publicKey = { + _id: pubKeyParams._id, + userId: userId, + userIds: pubKeyParams.userIds, + publicKey: $scope.key.publicKeyArmored + }; + } + + // import and validate keypair + emailDao.unlock({ + keypair: keypair, + passphrase: $scope.passphrase + }, function(err) { + if (err) { + $scope.incorrect = true; + $scope.onError(err); return; } - reader.onload = function(e) { - var rawKeys = e.target.result, - index = rawKeys.indexOf('-----BEGIN PGP PRIVATE KEY BLOCK-----'), - keyParts; - - if (index === -1) { - scope.onError(new Error('Error parsing private PGP key block!')); - return; - } - - keyParts = { - publicKeyArmored: rawKeys.substring(0, index).trim(), - privateKeyArmored: rawKeys.substring(index, rawKeys.length).trim() - }; - - scope.$apply(function() { - scope.key = keyParts; - }); - }; - reader.readAsText(files[0]); + emailDao._keychain.putUserKeyPair(keypair, onUnlock); }); - }; - }); + }); + } - return LoginExistingCtrl; -}); \ No newline at end of file + function onUnlock(err) { + if (err) { + $scope.onError(err); + return; + } + + appController._auth.storeCredentials(function(err) { + if (err) { + return $scope.onError(err); + } + + $location.path('/desktop'); + $scope.$apply(); + }); + } +}; + +var ngModule = angular.module('login-new-device', []); +ngModule.directive('fileReader', function() { + return function(scope, elm) { + elm.bind('change', function(e) { + var files = e.target.files, + reader = new FileReader(); + + if (files.length === 0) { + return; + } + + reader.onload = function(e) { + var rawKeys = e.target.result, + index = rawKeys.indexOf('-----BEGIN PGP PRIVATE KEY BLOCK-----'), + keyParts; + + if (index === -1) { + scope.onError(new Error('Error parsing private PGP key block!')); + return; + } + + keyParts = { + publicKeyArmored: rawKeys.substring(0, index).trim(), + privateKeyArmored: rawKeys.substring(index, rawKeys.length).trim() + }; + + scope.$apply(function() { + scope.key = keyParts; + }); + }; + reader.readAsText(files[0]); + }); + }; +}); + +exports = LoginExistingCtrl; \ No newline at end of file diff --git a/src/js/controller/login-privatekey-download.js b/src/js/controller/login-privatekey-download.js index 48860ff..e120d7f 100644 --- a/src/js/controller/login-privatekey-download.js +++ b/src/js/controller/login-privatekey-download.js @@ -1,134 +1,132 @@ -define(function(require) { - 'use strict'; +'use strict'; - var appController = require('js/app-controller'); +var appController = require('../app-controller'); - var LoginPrivateKeyDownloadCtrl = function($scope, $location, $routeParams) { - if (!appController._emailDao && !$routeParams.dev) { - $location.path('/'); // init app +var LoginPrivateKeyDownloadCtrl = function($scope, $location, $routeParams) { + if (!appController._emailDao && !$routeParams.dev) { + $location.path('/'); // init app + return; + } + + var keychain = appController._keychain, + emailDao = appController._emailDao, + userId = emailDao._account.emailAddress; + + $scope.step = 1; + + $scope.handlePaste = function(event) { + var evt = event; + if (evt.originalEvent) { + evt = evt.originalEvent; + } + + var value = evt.clipboardData.getData('text/plain'); + if (!value) { return; } - var keychain = appController._keychain, - emailDao = appController._emailDao, - userId = emailDao._account.emailAddress; - - $scope.step = 1; - - $scope.handlePaste = function(event) { - var evt = event; - if (evt.originalEvent) { - evt = evt.originalEvent; - } - - var value = evt.clipboardData.getData('text/plain'); - if (!value) { - return; - } - - value = value.replace(/-/g, ''); - $scope.code0 = value.slice(0, 4); - $scope.code1 = value.slice(4, 8); - $scope.code2 = value.slice(8, 12); - $scope.code3 = value.slice(12, 16); - $scope.code4 = value.slice(16, 20); - $scope.code5 = value.slice(20, 24); - }; - - $scope.verifyRecoveryToken = function(callback) { - if (!$scope.recoveryToken) { - $scope.onError(new Error('Please set the recovery token!')); - return; - } - - keychain.getUserKeyPair(userId, function(err, keypair) { - if (err) { - $scope.onError(err); - return; - } - - // remember for storage later - $scope.cachedKeypair = keypair; - - keychain.downloadPrivateKey({ - userId: userId, - keyId: keypair.publicKey._id, - recoveryToken: $scope.recoveryToken.toUpperCase() - }, function(err, encryptedPrivateKey) { - if (err) { - $scope.onError(err); - return; - } - - $scope.encryptedPrivateKey = encryptedPrivateKey; - callback(); - }); - }); - }; - - $scope.decryptAndStorePrivateKeyLocally = function() { - var inputCode = '' + $scope.code0 + $scope.code1 + $scope.code2 + $scope.code3 + $scope.code4 + $scope.code5; - - if (!inputCode) { - $scope.onError(new Error('Please enter the keychain code!')); - return; - } - - var options = $scope.encryptedPrivateKey; - options.code = inputCode.toUpperCase(); - - keychain.decryptAndStorePrivateKeyLocally(options, function(err, privateKey) { - if (err) { - $scope.onError(err); - return; - } - - // add private key to cached keypair object - $scope.cachedKeypair.privateKey = privateKey; - - // try empty passphrase - emailDao.unlock({ - keypair: $scope.cachedKeypair, - passphrase: undefined - }, function(err) { - if (err) { - // go to passphrase login screen - $scope.goTo('/login-existing'); - return; - } - - // passphrase is corrent ... go to main app - appController._auth.storeCredentials(function(err) { - if (err) { - return $scope.onError(err); - } - - $scope.goTo('/desktop'); - }); - }); - }); - }; - - $scope.goForward = function() { - if ($scope.step === 1) { - $scope.verifyRecoveryToken(function() { - $scope.step++; - $scope.$apply(); - }); - return; - } - - if ($scope.step === 2) { - $scope.decryptAndStorePrivateKeyLocally(); - return; - } - }; - - $scope.goTo = function(location) { - $location.path(location); - $scope.$apply(); - }; + value = value.replace(/-/g, ''); + $scope.code0 = value.slice(0, 4); + $scope.code1 = value.slice(4, 8); + $scope.code2 = value.slice(8, 12); + $scope.code3 = value.slice(12, 16); + $scope.code4 = value.slice(16, 20); + $scope.code5 = value.slice(20, 24); }; - return LoginPrivateKeyDownloadCtrl; -}); \ No newline at end of file + $scope.verifyRecoveryToken = function(callback) { + if (!$scope.recoveryToken) { + $scope.onError(new Error('Please set the recovery token!')); + return; + } + + keychain.getUserKeyPair(userId, function(err, keypair) { + if (err) { + $scope.onError(err); + return; + } + + // remember for storage later + $scope.cachedKeypair = keypair; + + keychain.downloadPrivateKey({ + userId: userId, + keyId: keypair.publicKey._id, + recoveryToken: $scope.recoveryToken.toUpperCase() + }, function(err, encryptedPrivateKey) { + if (err) { + $scope.onError(err); + return; + } + + $scope.encryptedPrivateKey = encryptedPrivateKey; + callback(); + }); + }); + }; + + $scope.decryptAndStorePrivateKeyLocally = function() { + var inputCode = '' + $scope.code0 + $scope.code1 + $scope.code2 + $scope.code3 + $scope.code4 + $scope.code5; + + if (!inputCode) { + $scope.onError(new Error('Please enter the keychain code!')); + return; + } + + var options = $scope.encryptedPrivateKey; + options.code = inputCode.toUpperCase(); + + keychain.decryptAndStorePrivateKeyLocally(options, function(err, privateKey) { + if (err) { + $scope.onError(err); + return; + } + + // add private key to cached keypair object + $scope.cachedKeypair.privateKey = privateKey; + + // try empty passphrase + emailDao.unlock({ + keypair: $scope.cachedKeypair, + passphrase: undefined + }, function(err) { + if (err) { + // go to passphrase login screen + $scope.goTo('/login-existing'); + return; + } + + // passphrase is corrent ... go to main app + appController._auth.storeCredentials(function(err) { + if (err) { + return $scope.onError(err); + } + + $scope.goTo('/desktop'); + }); + }); + }); + }; + + $scope.goForward = function() { + if ($scope.step === 1) { + $scope.verifyRecoveryToken(function() { + $scope.step++; + $scope.$apply(); + }); + return; + } + + if ($scope.step === 2) { + $scope.decryptAndStorePrivateKeyLocally(); + return; + } + }; + + $scope.goTo = function(location) { + $location.path(location); + $scope.$apply(); + }; +}; + +exports = LoginPrivateKeyDownloadCtrl; \ No newline at end of file diff --git a/src/js/controller/login-set-credentials.js b/src/js/controller/login-set-credentials.js index 5b34df0..3e40baf 100644 --- a/src/js/controller/login-set-credentials.js +++ b/src/js/controller/login-set-credentials.js @@ -1,120 +1,118 @@ -define(function(require) { - 'use strict'; +'use strict'; - var ENCRYPTION_METHOD_NONE = 0; - var ENCRYPTION_METHOD_STARTTLS = 1; - var ENCRYPTION_METHOD_TLS = 2; +var ENCRYPTION_METHOD_NONE = 0; +var ENCRYPTION_METHOD_STARTTLS = 1; +var ENCRYPTION_METHOD_TLS = 2; - var appCtrl = require('js/app-controller'), - config = require('js/app-config').config; +var appCtrl = require('../app-controller'), + config = require('../app-config').config; - var SetCredentialsCtrl = function($scope, $location, $routeParams) { - if (!appCtrl._auth && !$routeParams.dev) { - $location.path('/'); // init app - return; +var SetCredentialsCtrl = function($scope, $location, $routeParams) { + if (!appCtrl._auth && !$routeParams.dev) { + $location.path('/'); // init app + return; + } + + var auth = appCtrl._auth; + var doctor = appCtrl._doctor; + + // + // Presets and Settings + // + + var provider = $location.search().provider; + $scope.hasProviderPreset = !!config[provider]; + $scope.useOAuth = !!auth.oauthToken; + $scope.showDetails = (provider === 'custom'); + + if ($scope.useOAuth) { + $scope.emailAddress = auth.emailAddress; + } + + if ($scope.hasProviderPreset) { + // use non-editable presets + + // SMTP config + $scope.smtpHost = config[provider].smtp.host; + $scope.smtpPort = config[provider].smtp.port; + $scope.smtpCert = config[provider].smtp.ca; + $scope.smtpPinned = config[provider].smtp.pinned; + + // transport encryption method + if (config[provider].smtp.secure && !config[provider].smtp.ignoreTLS) { + $scope.smtpEncryption = ENCRYPTION_METHOD_TLS; + } else if (!config[provider].smtp.secure && !config[provider].smtp.ignoreTLS) { + $scope.smtpEncryption = ENCRYPTION_METHOD_STARTTLS; + } else { + $scope.smtpEncryption = ENCRYPTION_METHOD_NONE; } - var auth = appCtrl._auth; - var doctor = appCtrl._doctor; + // IMAP config + $scope.imapHost = config[provider].imap.host; + $scope.imapPort = config[provider].imap.port; + $scope.imapCert = config[provider].imap.ca; + $scope.imapPinned = config[provider].imap.pinned; - // - // Presets and Settings - // - - var provider = $location.search().provider; - $scope.hasProviderPreset = !!config[provider]; - $scope.useOAuth = !!auth.oauthToken; - $scope.showDetails = (provider === 'custom'); - - if ($scope.useOAuth) { - $scope.emailAddress = auth.emailAddress; + // transport encryption method + if (config[provider].imap.secure && !config[provider].imap.ignoreTLS) { + $scope.imapEncryption = ENCRYPTION_METHOD_TLS; + } else if (!config[provider].imap.secure && !config[provider].imap.ignoreTLS) { + $scope.imapEncryption = ENCRYPTION_METHOD_STARTTLS; + } else { + $scope.imapEncryption = ENCRYPTION_METHOD_NONE; } + } - if ($scope.hasProviderPreset) { - // use non-editable presets - - // SMTP config - $scope.smtpHost = config[provider].smtp.host; - $scope.smtpPort = config[provider].smtp.port; - $scope.smtpCert = config[provider].smtp.ca; - $scope.smtpPinned = config[provider].smtp.pinned; + $scope.test = function() { + // parse the dropdown lists - var imapEncryption = parseInt($scope.imapEncryption, 10); - var smtpEncryption = parseInt($scope.smtpEncryption, 10); - - // build credentials object - var credentials = { - provider: provider, - emailAddress: $scope.emailAddress, - username: $scope.username || $scope.emailAddress, - realname: $scope.realname, - password: $scope.password, - xoauth2: auth.oauthToken, - imap: { - host: $scope.imapHost.toLowerCase(), - port: $scope.imapPort, - secure: imapEncryption === ENCRYPTION_METHOD_TLS, - ignoreTLS: imapEncryption === ENCRYPTION_METHOD_NONE, - ca: $scope.imapCert, - pinned: !!$scope.imapPinned - }, - smtp: { - host: $scope.smtpHost.toLowerCase(), - port: $scope.smtpPort, - secure: smtpEncryption === ENCRYPTION_METHOD_TLS, - ignoreTLS: smtpEncryption === ENCRYPTION_METHOD_NONE, - ca: $scope.smtpCert, - pinned: !!$scope.smtpPinned - } - }; - - // use the credentials in the connection doctor - doctor.configure(credentials); - - // run connection doctor test suite - $scope.busy = true; - doctor.check(function(err) { - if (err) { - // display the error in the settings UI - $scope.connectionError = err; - } else { - // persists the credentials and forwards to /login - auth.setCredentials(credentials); - $location.path('/login'); - } - - $scope.busy = false; - $scope.$apply(); - }); }; - }; - return SetCredentialsCtrl; -}); \ No newline at end of file + // use the credentials in the connection doctor + doctor.configure(credentials); + + // run connection doctor test suite + $scope.busy = true; + doctor.check(function(err) { + if (err) { + // display the error in the settings UI + $scope.connectionError = err; + } else { + // persists the credentials and forwards to /login + auth.setCredentials(credentials); + $location.path('/login'); + } + + $scope.busy = false; + $scope.$apply(); + }); + }; +}; + +exports = SetCredentialsCtrl; \ No newline at end of file diff --git a/src/js/controller/login.js b/src/js/controller/login.js index bb6bf62..26a5ee0 100644 --- a/src/js/controller/login.js +++ b/src/js/controller/login.js @@ -1,103 +1,101 @@ -define(function(require) { - 'use strict'; +'use strict'; - var appController = require('js/app-controller'); +var appController = require('../app-controller'); - var LoginCtrl = function($scope, $location) { +var LoginCtrl = function($scope, $location) { - // start main application controller - appController.start({ - onError: $scope.onError - }, function(err) { + // start main application controller + appController.start({ + onError: $scope.onError + }, function(err) { + if (err) { + $scope.onError(err); + return; + } + + // check for app update + appController.checkForUpdate(); + + initializeUser(); + }); + + function initializeUser() { + // get OAuth token from chrome + appController._auth.getEmailAddress(function(err, info) { if (err) { $scope.onError(err); return; } - // check for app update - appController.checkForUpdate(); + // check if account needs to be selected + if (!info.emailAddress) { + goTo('/add-account'); + return; + } - initializeUser(); - }); - - function initializeUser() { - // get OAuth token from chrome - appController._auth.getEmailAddress(function(err, info) { + // initiate controller by creating email dao + appController.init({ + emailAddress: info.emailAddress, + realname: info.realname + }, function(err, availableKeys) { if (err) { $scope.onError(err); return; } - // check if account needs to be selected - if (!info.emailAddress) { - goTo('/add-account'); + redirect(availableKeys); + }); + }); + } + + function redirect(availableKeys) { + // redirect if needed + if (typeof availableKeys === 'undefined') { + // no public key available, start onboarding process + goTo('/login-initial'); + + } else if (availableKeys && !availableKeys.privateKey) { + // check if private key is synced + appController._keychain.requestPrivateKeyDownload({ + userId: availableKeys.publicKey.userId, + keyId: availableKeys.publicKey._id, + }, function(err, privateKeySynced) { + if (err) { + $scope.onError(err); return; } - // initiate controller by creating email dao - appController.init({ - emailAddress: info.emailAddress, - realname: info.realname - }, function(err, availableKeys) { - if (err) { - $scope.onError(err); - return; - } + if (privateKeySynced) { + // private key is synced, proceed to download + goTo('/login-privatekey-download'); + return; + } - redirect(availableKeys); - }); + // no private key, import key file + goTo('/login-new-device'); + }); + + } else { + // public and private key available, try empty passphrase + appController._emailDao.unlock({ + keypair: availableKeys, + passphrase: undefined + }, function(err) { + if (err) { + goTo('/login-existing'); + return; + } + + goTo('/desktop'); }); } + } - function redirect(availableKeys) { - // redirect if needed - if (typeof availableKeys === 'undefined') { - // no public key available, start onboarding process - goTo('/login-initial'); + function goTo(location) { + $scope.$apply(function() { + $location.path(location); + }); + } +}; - } else if (availableKeys && !availableKeys.privateKey) { - // check if private key is synced - appController._keychain.requestPrivateKeyDownload({ - userId: availableKeys.publicKey.userId, - keyId: availableKeys.publicKey._id, - }, function(err, privateKeySynced) { - if (err) { - $scope.onError(err); - return; - } - - if (privateKeySynced) { - // private key is synced, proceed to download - goTo('/login-privatekey-download'); - return; - } - - // no private key, import key file - goTo('/login-new-device'); - }); - - } else { - // public and private key available, try empty passphrase - appController._emailDao.unlock({ - keypair: availableKeys, - passphrase: undefined - }, function(err) { - if (err) { - goTo('/login-existing'); - return; - } - - goTo('/desktop'); - }); - } - } - - function goTo(location) { - $scope.$apply(function() { - $location.path(location); - }); - } - }; - - return LoginCtrl; -}); \ No newline at end of file +exports = LoginCtrl; \ No newline at end of file diff --git a/src/js/controller/mail-list.js b/src/js/controller/mail-list.js index ed81585..332a4ae 100644 --- a/src/js/controller/mail-list.js +++ b/src/js/controller/mail-list.js @@ -1,600 +1,596 @@ -define(function(require) { - 'use strict'; +'use strict'; - var angular = require('angular'), - _ = require('underscore'), - appController = require('js/app-controller'), - notification = require('js/util/notification'), - emailDao, outboxBo, keychainDao, searchTimeout, firstSelect; +var appController = require('../app-controller'), + notification = require('../util/notification'), + emailDao, outboxBo, keychainDao, searchTimeout, firstSelect; - var INIT_DISPLAY_LEN = 20, - SCROLL_DISPLAY_LEN = 10, - FOLDER_TYPE_INBOX = 'Inbox'; +var INIT_DISPLAY_LEN = 20, + SCROLL_DISPLAY_LEN = 10, + FOLDER_TYPE_INBOX = 'Inbox'; - var MailListCtrl = function($scope, $routeParams) { - // - // Init - // +var MailListCtrl = function($scope, $routeParams) { + // + // Init + // - emailDao = appController._emailDao; - outboxBo = appController._outboxBo; - keychainDao = appController._keychain; + emailDao = appController._emailDao; + outboxBo = appController._outboxBo; + keychainDao = appController._keychain; - /** - * Gathers unread notifications to be cancelled later - */ - $scope.pendingNotifications = []; + /** + * Gathers unread notifications to be cancelled later + */ + $scope.pendingNotifications = []; - // - // scope functions - // + // + // scope functions + // - $scope.getBody = function(email) { - emailDao.getBody({ - folder: currentFolder(), - message: email - }, function(err) { - if (err && err.code !== 42) { - $scope.onError(err); - return; - } - - // display fetched body - $scope.$digest(); - - // automatically decrypt if it's the selected email - if (email === currentMessage()) { - emailDao.decryptBody({ - message: email - }, $scope.onError); - } - }); - }; - - /** - * Called when clicking on an email list item - */ - $scope.select = function(email) { - // unselect an item - if (!email) { - $scope.state.mailList.selected = undefined; + $scope.getBody = function(email) { + emailDao.getBody({ + folder: currentFolder(), + message: email + }, function(err) { + if (err && err.code !== 42) { + $scope.onError(err); return; } - $scope.state.mailList.selected = email; - - if (!firstSelect) { - // only toggle to read view on 2nd select in mobile mode - $scope.state.read.toggle(true); - } - firstSelect = false; - - keychainDao.refreshKeyForUserId(email.from[0].address, onKeyRefreshed); - - function onKeyRefreshed(err) { - if (err) { - $scope.onError(err); - } + // display fetched body + $scope.$digest(); + // automatically decrypt if it's the selected email + if (email === currentMessage()) { emailDao.decryptBody({ message: email }, $scope.onError); - - // if the email is unread, please sync the new state. - // otherweise forget about it. - if (!email.unread) { - return; - } - - // let's close pending notifications for unread messages in the inbox - if (currentFolder().type === FOLDER_TYPE_INBOX) { - while ($scope.pendingNotifications.length) { - notification.close($scope.pendingNotifications.shift()); - } - } - - $scope.toggleUnread(email); } - }; + }); + }; - /** - * Mark an email as unread or read, respectively - */ - $scope.toggleUnread = function(message) { - updateStatus('Updating unread flag...'); + /** + * Called when clicking on an email list item + */ + $scope.select = function(email) { + // unselect an item + if (!email) { + $scope.state.mailList.selected = undefined; + return; + } - message.unread = !message.unread; - emailDao.setFlags({ + $scope.state.mailList.selected = email; + + if (!firstSelect) { + // only toggle to read view on 2nd select in mobile mode + $scope.state.read.toggle(true); + } + firstSelect = false; + + keychainDao.refreshKeyForUserId(email.from[0].address, onKeyRefreshed); + + function onKeyRefreshed(err) { + if (err) { + $scope.onError(err); + } + + emailDao.decryptBody({ + message: email + }, $scope.onError); + + // if the email is unread, please sync the new state. + // otherweise forget about it. + if (!email.unread) { + return; + } + + // let's close pending notifications for unread messages in the inbox + if (currentFolder().type === FOLDER_TYPE_INBOX) { + while ($scope.pendingNotifications.length) { + notification.close($scope.pendingNotifications.shift()); + } + } + + $scope.toggleUnread(email); + } + }; + + /** + * Mark an email as unread or read, respectively + */ + $scope.toggleUnread = function(message) { + updateStatus('Updating unread flag...'); + + message.unread = !message.unread; + emailDao.setFlags({ + folder: currentFolder(), + message: message + }, function(err) { + if (err && err.code === 42) { + // offline, restore + message.unread = !message.unread; + updateStatus('Unable to mark unread flag in offline mode!'); + return; + } + + if (err) { + updateStatus('Error on sync!'); + $scope.onError(err); + return; + } + + updateStatus('Online'); + $scope.$apply(); + }); + }; + + /** + * Delete a message + */ + $scope.remove = function(message) { + if (!message) { + return; + } + + updateStatus('Deleting message...'); + remove(); + + function remove() { + emailDao.deleteMessage({ folder: currentFolder(), message: message }, function(err) { - if (err && err.code === 42) { - // offline, restore - message.unread = !message.unread; - updateStatus('Unable to mark unread flag in offline mode!'); - return; - } - if (err) { - updateStatus('Error on sync!'); + // show errors where appropriate + if (err.code === 42) { + $scope.select(message); + updateStatus('Unable to delete message in offline mode!'); + return; + } + updateStatus('Error during delete!'); $scope.onError(err); - return; } - - updateStatus('Online'); + updateStatus('Message deleted!'); $scope.$apply(); }); - }; - - /** - * Delete a message - */ - $scope.remove = function(message) { - if (!message) { - return; - } - - updateStatus('Deleting message...'); - remove(); - - function remove() { - emailDao.deleteMessage({ - folder: currentFolder(), - message: message - }, function(err) { - if (err) { - // show errors where appropriate - if (err.code === 42) { - $scope.select(message); - updateStatus('Unable to delete message in offline mode!'); - return; - } - updateStatus('Error during delete!'); - $scope.onError(err); - } - updateStatus('Message deleted!'); - $scope.$apply(); - }); - } - }; - - // share local scope functions with root state - $scope.state.mailList = { - remove: $scope.remove - }; - - // - // watch tasks - // - - /** - * List emails from folder when user changes folder - */ - $scope._stopWatchTask = $scope.$watch('state.nav.currentFolder', function() { - if (!currentFolder()) { - return; - } - - // reset searchFilter - $scope.searchText = undefined; - - // in development, display dummy mail objects - if ($routeParams.dev) { - updateStatus('Last update: ', new Date()); - currentFolder().messages = createDummyMails(); - return; - } - - // display and select first - openCurrentFolder(); - }); - - $scope.watchMessages = $scope.$watchCollection('state.nav.currentFolder.messages', function(messages) { - if (!messages) { - return; - } - - // sort message by uid - currentFolder().messages.sort(byUidDescending); - // set display buffer to first messages - $scope.displayMessages = currentFolder().messages.slice(0, INIT_DISPLAY_LEN); - - // Shows the next message based on the uid of the currently selected element - if (currentFolder().messages.indexOf(currentMessage()) === -1) { - firstSelect = true; // reset first selection - $scope.select($scope.displayMessages[0]); - } - }); - - /** - * display more items (for infinite scrolling) - */ - $scope.displayMore = function() { - if (!currentFolder() || !$scope.displayMessages) { - // folders not yet initialized - return; - } - - var len = currentFolder().messages.length, - dLen = $scope.displayMessages.length; - - if (dLen === len || $scope.searchText) { - // all messages are already displayed or we're in search mode - return; - } - - // copy next interval of messages to the end of the display messages array - var next = currentFolder().messages.slice(dLen, dLen + SCROLL_DISPLAY_LEN); - Array.prototype.push.apply($scope.displayMessages, next); - }; - - /** - * This method is called when the user changes the searchText - */ - $scope.displaySearchResults = function(searchText) { - if (searchTimeout) { - // remove timeout to wait for user typing query - clearTimeout(searchTimeout); - } - - if (!searchText) { - // set display buffer to first messages - $scope.displayMessages = currentFolder().messages.slice(0, INIT_DISPLAY_LEN); - $scope.searching = false; - updateStatus('Online'); - return; - } - - // display searching spinner - $scope.searching = true; - updateStatus('Searching ...'); - searchTimeout = setTimeout(function() { - $scope.$apply(function() { - // filter relevant messages - $scope.displayMessages = $scope.search(currentFolder().messages, searchText); - $scope.searching = false; - updateStatus('Matches in this folder'); - }); - }, 500); - }; - - /** - * Do full text search on messages. Parse meta data first - */ - $scope.search = function(messages, searchText) { - // don't filter on empty searchText - if (!searchText) { - return messages; - } - - // escape search string - searchText = searchText.replace(/([.*+?^${}()|\[\]\/\\])/g, "\\$1"); - // compare all strings (case insensitive) - var regex = new RegExp(searchText, 'i'); - - function contains(input) { - if (!input) { - return false; - } - return regex.test(input); - } - - function checkAddresses(header) { - if (!header || !header.length) { - return false; - } - - for (var i = 0; i < header.length; i++) { - if (contains(header[i].name) || contains(header[i].address)) { - return true; - } - } - - return false; - } - - /** - * Filter meta data first and then only look at plaintext and decrypted message bodies - */ - function matchMetaDataFirst(m) { - // compare subject - if (contains(m.subject)) { - return true; - } - // compares address headers - if (checkAddresses(m.from) || checkAddresses(m.to) || checkAddresses(m.cc) || checkAddresses(m.bcc)) { - return true; - } - // compare plaintext body - if (m.body && !m.encrypted && contains(m.body)) { - return true; - } - // compare decrypted body - if (m.body && m.encrypted && m.decrypted && contains(m.body)) { - return true; - } - // compare plaintex html body - if (m.html && !m.encrypted && contains(m.html)) { - return true; - } - // compare decrypted html body - if (m.html && m.encrypted && m.decrypted && contains(m.html)) { - return true; - } - return false; - } - - // user native js Array.filter - return messages.filter(matchMetaDataFirst); - }; - - /** - * Sync current folder when client comes back online - */ - $scope.watchOnline = $scope.$watch('account.online', function(isOnline) { - if (isOnline) { - updateStatus('Online'); - openCurrentFolder(); - } else { - updateStatus('Offline mode'); - } - }, true); - - // - // Helper Functions - // - - function openCurrentFolder() { - emailDao.openFolder({ - folder: currentFolder() - }, function(error) { - // dont wait until scroll to load visible mail bodies - $scope.loadVisibleBodies(); - - // don't display error for offline case - if (error && error.code === 42) { - return; - } - $scope.onError(error); - }); } + }; - function updateStatus(lbl, time) { - $scope.lastUpdateLbl = lbl; - $scope.lastUpdate = (time) ? time : ''; - } - - function currentFolder() { - return $scope.state.nav.currentFolder; - } - - function currentMessage() { - return $scope.state.mailList.selected; - } - - // - // Notification API - // - - (emailDao || {}).onIncomingMessage = function(msgs) { - var note, title, message, unreadMsgs; - - unreadMsgs = msgs.filter(function(msg) { - return msg.unread; - }); - - if (unreadMsgs.length === 0) { - return; - } - - if (unreadMsgs.length === 1) { - title = unreadMsgs[0].from[0].name || unreadMsgs[0].from[0].address; - message = unreadMsgs[0].subject; - } else { - title = unreadMsgs.length + ' new messages'; - message = _.pluck(unreadMsgs, 'subject').join('\n'); - } - - note = notification.create({ - title: title, - message: message, - onClick: function() { - // force toggle into read mode when notification is clicked - firstSelect = false; - - // remove from pending notificatiosn - var index = $scope.pendingNotifications.indexOf(note); - if (index !== -1) { - $scope.pendingNotifications.splice(index, 1); - } - - // mark message as read - $scope.select(_.findWhere(currentFolder().messages, { - uid: unreadMsgs[0].uid - })); - } - }); - $scope.pendingNotifications.push(note); - }; + // share local scope functions with root state + $scope.state.mailList = { + remove: $scope.remove }; // - // Directives + // watch tasks // - var ngModule = angular.module('mail-list', []); + /** + * List emails from folder when user changes folder + */ + $scope._stopWatchTask = $scope.$watch('state.nav.currentFolder', function() { + if (!currentFolder()) { + return; + } - ngModule.directive('woTouch', function($parse) { - return function(scope, elm, attrs) { - var handler = $parse(attrs.woTouch); + // reset searchFilter + $scope.searchText = undefined; - elm.on('touchstart', function() { - elm.addClass('active'); - }); - elm.on('touchleave touchcancel touchmove touchend', function() { - elm.removeClass('active'); - }); + // in development, display dummy mail objects + if ($routeParams.dev) { + updateStatus('Last update: ', new Date()); + currentFolder().messages = createDummyMails(); + return; + } - elm.on('click', function(event) { - elm.removeClass('active'); - scope.$apply(function() { - handler(scope, { - $event: event - }); - }); - }); - }; + // display and select first + openCurrentFolder(); }); - ngModule.directive('listScroll', function() { - return { - link: function(scope, elm, attrs) { - var model = attrs.listScroll, - listEl = elm[0], - scrollTimeout; + $scope.watchMessages = $scope.$watchCollection('state.nav.currentFolder.messages', function(messages) { + if (!messages) { + return; + } - /* - * iterates over the mails in the mail list and loads their bodies if they are visible in the viewport - */ - scope.loadVisibleBodies = function() { - var listBorder = listEl.getBoundingClientRect(), - top = listBorder.top, - bottom = listBorder.bottom, - listItems = listEl.children[0].children, - inViewport = false, - listItem, message, - isPartiallyVisibleTop, isPartiallyVisibleBottom, isVisible, - displayMessages = scope[model]; + // sort message by uid + currentFolder().messages.sort(byUidDescending); + // set display buffer to first messages + $scope.displayMessages = currentFolder().messages.slice(0, INIT_DISPLAY_LEN); - if (!top && !bottom) { - // list not visible - return; - } + // Shows the next message based on the uid of the currently selected element + if (currentFolder().messages.indexOf(currentMessage()) === -1) { + firstSelect = true; // reset first selection + $scope.select($scope.displayMessages[0]); + } + }); - for (var i = 0, len = listItems.length; i < len; i++) { - // the n-th list item (the dom representation of an email) corresponds to - // the n-th message model in the filteredMessages array - listItem = listItems.item(i).getBoundingClientRect(); + /** + * display more items (for infinite scrolling) + */ + $scope.displayMore = function() { + if (!currentFolder() || !$scope.displayMessages) { + // folders not yet initialized + return; + } - if (!displayMessages || displayMessages.length <= i) { - // stop if i get larger than the size of filtered messages - break; - } - message = displayMessages[i]; + var len = currentFolder().messages.length, + dLen = $scope.displayMessages.length; + if (dLen === len || $scope.searchText) { + // all messages are already displayed or we're in search mode + return; + } - 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 + // copy next interval of messages to the end of the display messages array + var next = currentFolder().messages.slice(dLen, dLen + SCROLL_DISPLAY_LEN); + Array.prototype.push.apply($scope.displayMessages, next); + }; - if (isPartiallyVisibleTop || isVisible || isPartiallyVisibleBottom) { - // we are now iterating over visible elements - inViewport = true; - // load mail body of visible - scope.getBody(message); - } else if (inViewport) { - // we are leaving the viewport, so stop iterating over the items - break; - } - } - }; + /** + * This method is called when the user changes the searchText + */ + $scope.displaySearchResults = function(searchText) { + if (searchTimeout) { + // remove timeout to wait for user typing query + clearTimeout(searchTimeout); + } - // load body when scrolling - listEl.onscroll = function() { - if (scrollTimeout) { - // remove timeout so that only scroll end - clearTimeout(scrollTimeout); - } - scrollTimeout = setTimeout(function() { - scope.loadVisibleBodies(); - }, 300); - }; + if (!searchText) { + // set display buffer to first messages + $scope.displayMessages = currentFolder().messages.slice(0, INIT_DISPLAY_LEN); + $scope.searching = false; + updateStatus('Online'); + return; + } - // load the visible message bodies, when the list is re-initialized and when scrolling stopped - scope.$watchCollection(model, function() { - scope.loadVisibleBodies(); - }); + // display searching spinner + $scope.searching = true; + updateStatus('Searching ...'); + searchTimeout = setTimeout(function() { + $scope.$apply(function() { + // filter relevant messages + $scope.displayMessages = $scope.search(currentFolder().messages, searchText); + $scope.searching = false; + updateStatus('Matches in this folder'); + }); + }, 500); + }; + + /** + * Do full text search on messages. Parse meta data first + */ + $scope.search = function(messages, searchText) { + // don't filter on empty searchText + if (!searchText) { + return messages; + } + + // escape search string + searchText = searchText.replace(/([.*+?^${}()|\[\]\/\\])/g, "\\$1"); + // compare all strings (case insensitive) + var regex = new RegExp(searchText, 'i'); + + function contains(input) { + if (!input) { + return false; } - }; - }); + return regex.test(input); + } - function byUidDescending(a, b) { - if (a.uid < b.uid) { - return 1; - } else if (b.uid < a.uid) { - return -1; + function checkAddresses(header) { + if (!header || !header.length) { + return false; + } + + for (var i = 0; i < header.length; i++) { + if (contains(header[i].name) || contains(header[i].address)) { + return true; + } + } + + return false; + } + + /** + * Filter meta data first and then only look at plaintext and decrypted message bodies + */ + function matchMetaDataFirst(m) { + // compare subject + if (contains(m.subject)) { + return true; + } + // compares address headers + if (checkAddresses(m.from) || checkAddresses(m.to) || checkAddresses(m.cc) || checkAddresses(m.bcc)) { + return true; + } + // compare plaintext body + if (m.body && !m.encrypted && contains(m.body)) { + return true; + } + // compare decrypted body + if (m.body && m.encrypted && m.decrypted && contains(m.body)) { + return true; + } + // compare plaintex html body + if (m.html && !m.encrypted && contains(m.html)) { + return true; + } + // compare decrypted html body + if (m.html && m.encrypted && m.decrypted && contains(m.html)) { + return true; + } + return false; + } + + // user native js Array.filter + return messages.filter(matchMetaDataFirst); + }; + + /** + * Sync current folder when client comes back online + */ + $scope.watchOnline = $scope.$watch('account.online', function(isOnline) { + if (isOnline) { + updateStatus('Online'); + openCurrentFolder(); } else { - return 0; + updateStatus('Offline mode'); } + }, true); + + // + // Helper Functions + // + + function openCurrentFolder() { + emailDao.openFolder({ + folder: currentFolder() + }, function(error) { + // dont wait until scroll to load visible mail bodies + $scope.loadVisibleBodies(); + + // don't display error for offline case + if (error && error.code === 42) { + return; + } + $scope.onError(error); + }); } - // Helper for development mode - - function createDummyMails() { - var uid = 1000000; - - var Email = function(unread, attachments, answered) { - this.uid = uid--; - this.from = [{ - name: 'Whiteout Support', - address: 'support@whiteout.io' - }]; // sender address - this.to = [{ - address: 'max.musterman@gmail.com' - }, { - address: 'max.musterman@gmail.com' - }]; // list of receivers - this.cc = [{ - address: 'john.doe@gmail.com' - }]; // list of receivers - this.attachments = attachments ? [{ - "filename": "a.md", - "filesize": 123, - "mimeType": "text/x-markdown", - "part": "2", - "content": null - }, { - "filename": "b.md", - "filesize": 456, - "mimeType": "text/x-markdown", - "part": "3", - "content": null - }, { - "filename": "c.md", - "filesize": 789, - "mimeType": "text/x-markdown", - "part": "4", - "content": null - }] : []; - this.unread = unread; - this.answered = answered; - this.sentDate = new Date('Thu Sep 19 2013 20:41:23 GMT+0200 (CEST)'); - this.subject = 'Getting started'; // Subject line - // this.body = 'And a good day to you too sir. \n' + - // '\n' + - // 'Thursday, Apr 24, 2014 3:33 PM safewithme.testuser@gmail.com wrote:\n' + - // '> adsfadfasdfasdfasfdasdfasdfas\n' + - // '\n' + - // 'http://example.com\n' + - // '\n' + - // '> Tuesday, Mar 25, 2014 4:19 PM gianniarcore@gmail.com wrote:\n' + - // '>> from 0.7.0.1\n' + - // '>>\n' + - // '>> God speed!'; // plaintext body - this.html = '

Hello there' + Math.random() + '

'; - this.encrypted = true; - this.decrypted = true; - }; - - var dummies = [], - i = 100; - while (i--) { - // every second/third/fourth dummy mail with unread/attachments/answered - dummies.push(new Email((i % 2 === 0), (i % 3 === 0), (i % 5 === 0))); - } - - return dummies; + function updateStatus(lbl, time) { + $scope.lastUpdateLbl = lbl; + $scope.lastUpdate = (time) ? time : ''; } - return MailListCtrl; -}); \ No newline at end of file + function currentFolder() { + return $scope.state.nav.currentFolder; + } + + function currentMessage() { + return $scope.state.mailList.selected; + } + + // + // Notification API + // + + (emailDao || {}).onIncomingMessage = function(msgs) { + var note, title, message, unreadMsgs; + + unreadMsgs = msgs.filter(function(msg) { + return msg.unread; + }); + + if (unreadMsgs.length === 0) { + return; + } + + if (unreadMsgs.length === 1) { + title = unreadMsgs[0].from[0].name || unreadMsgs[0].from[0].address; + message = unreadMsgs[0].subject; + } else { + title = unreadMsgs.length + ' new messages'; + message = _.pluck(unreadMsgs, 'subject').join('\n'); + } + + note = notification.create({ + title: title, + message: message, + onClick: function() { + // force toggle into read mode when notification is clicked + firstSelect = false; + + // remove from pending notificatiosn + var index = $scope.pendingNotifications.indexOf(note); + if (index !== -1) { + $scope.pendingNotifications.splice(index, 1); + } + + // mark message as read + $scope.select(_.findWhere(currentFolder().messages, { + uid: unreadMsgs[0].uid + })); + } + }); + $scope.pendingNotifications.push(note); + }; +}; + +// +// Directives +// + +var ngModule = angular.module('mail-list', []); + +ngModule.directive('woTouch', function($parse) { + return function(scope, elm, attrs) { + var handler = $parse(attrs.woTouch); + + elm.on('touchstart', function() { + elm.addClass('active'); + }); + elm.on('touchleave touchcancel touchmove touchend', function() { + elm.removeClass('active'); + }); + + elm.on('click', function(event) { + elm.removeClass('active'); + scope.$apply(function() { + handler(scope, { + $event: event + }); + }); + }); + }; +}); + +ngModule.directive('listScroll', function() { + return { + link: function(scope, elm, attrs) { + var model = attrs.listScroll, + listEl = elm[0], + scrollTimeout; + + /* + * iterates over the mails in the mail list and loads their bodies if they are visible in the viewport + */ + scope.loadVisibleBodies = function() { + var listBorder = listEl.getBoundingClientRect(), + top = listBorder.top, + bottom = listBorder.bottom, + listItems = listEl.children[0].children, + inViewport = false, + listItem, message, + isPartiallyVisibleTop, isPartiallyVisibleBottom, isVisible, + displayMessages = scope[model]; + + if (!top && !bottom) { + // list not visible + return; + } + + for (var i = 0, len = listItems.length; i < len; i++) { + // the n-th list item (the dom representation of an email) corresponds to + // the n-th message model in the filteredMessages array + listItem = listItems.item(i).getBoundingClientRect(); + + if (!displayMessages || displayMessages.length <= i) { + // stop if i get larger than the size of filtered messages + break; + } + 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 + + if (isPartiallyVisibleTop || isVisible || isPartiallyVisibleBottom) { + // we are now iterating over visible elements + inViewport = true; + // load mail body of visible + scope.getBody(message); + } else if (inViewport) { + // we are leaving the viewport, so stop iterating over the items + break; + } + } + }; + + // load body when scrolling + listEl.onscroll = function() { + if (scrollTimeout) { + // remove timeout so that only scroll end + clearTimeout(scrollTimeout); + } + scrollTimeout = setTimeout(function() { + scope.loadVisibleBodies(); + }, 300); + }; + + // load the visible message bodies, when the list is re-initialized and when scrolling stopped + scope.$watchCollection(model, function() { + scope.loadVisibleBodies(); + }); + } + }; +}); + +function byUidDescending(a, b) { + if (a.uid < b.uid) { + return 1; + } else if (b.uid < a.uid) { + return -1; + } else { + return 0; + } +} + +// Helper for development mode + +function createDummyMails() { + var uid = 1000000; + + var Email = function(unread, attachments, answered) { + this.uid = uid--; + this.from = [{ + name: 'Whiteout Support', + address: 'support@whiteout.io' + }]; // sender address + this.to = [{ + address: 'max.musterman@gmail.com' + }, { + address: 'max.musterman@gmail.com' + }]; // list of receivers + this.cc = [{ + address: 'john.doe@gmail.com' + }]; // list of receivers + this.attachments = attachments ? [{ + "filename": "a.md", + "filesize": 123, + "mimeType": "text/x-markdown", + "part": "2", + "content": null + }, { + "filename": "b.md", + "filesize": 456, + "mimeType": "text/x-markdown", + "part": "3", + "content": null + }, { + "filename": "c.md", + "filesize": 789, + "mimeType": "text/x-markdown", + "part": "4", + "content": null + }] : []; + this.unread = unread; + this.answered = answered; + this.sentDate = new Date('Thu Sep 19 2013 20:41:23 GMT+0200 (CEST)'); + this.subject = 'Getting started'; // Subject line + // this.body = 'And a good day to you too sir. \n' + + // '\n' + + // 'Thursday, Apr 24, 2014 3:33 PM safewithme.testuser@gmail.com wrote:\n' + + // '> adsfadfasdfasdfasfdasdfasdfas\n' + + // '\n' + + // 'http://example.com\n' + + // '\n' + + // '> Tuesday, Mar 25, 2014 4:19 PM gianniarcore@gmail.com wrote:\n' + + // '>> from 0.7.0.1\n' + + // '>>\n' + + // '>> God speed!'; // plaintext body + this.html = '

Hello there' + Math.random() + '

'; + this.encrypted = true; + this.decrypted = true; + }; + + var dummies = [], + i = 100; + while (i--) { + // every second/third/fourth dummy mail with unread/attachments/answered + dummies.push(new Email((i % 2 === 0), (i % 3 === 0), (i % 5 === 0))); + } + + return dummies; +} + +exports = MailListCtrl; \ No newline at end of file diff --git a/src/js/controller/navigation.js b/src/js/controller/navigation.js index 70bd878..ccccaa2 100644 --- a/src/js/controller/navigation.js +++ b/src/js/controller/navigation.js @@ -1,210 +1,206 @@ -define(function(require) { - 'use strict'; +'use strict'; - var angular = require('angular'), - appController = require('js/app-controller'), - config = require('js/app-config').config, - notification = require('js/util/notification'), - backBtnHandler = require('js/util/backbutton-handler'), - _ = require('underscore'), - emailDao, outboxBo; +var appController = require('../app-controller'), + config = require('../app-config').config, + notification = require('../util/notification'), + backBtnHandler = require('../util/backbutton-handler'), + emailDao, outboxBo; + +// +// Controller +// + +var NavigationCtrl = function($scope, $routeParams, $location) { + if (!appController._emailDao && !$routeParams.dev) { + $location.path('/'); // init app + return; + } + + emailDao = appController._emailDao; + outboxBo = appController._outboxBo; // - // Controller + // scope functions // - var NavigationCtrl = function($scope, $routeParams, $location) { - if (!appController._emailDao && !$routeParams.dev) { - $location.path('/'); // init app - return; - } - - emailDao = appController._emailDao; - outboxBo = appController._outboxBo; - - // - // scope functions - // - - $scope.state.nav = { - open: false, - toggle: function(to) { - this.open = to; - } - }; - - $scope.openFolder = function(folder) { - $scope.state.nav.currentFolder = folder; - $scope.state.nav.toggle(false); - }; - - $scope.onOutboxUpdate = function(err, count) { - if (err) { - $scope.onError(err); - return; - } - - // update the outbox mail count - var outbox = _.findWhere($scope.account.folders, { - type: config.outboxMailboxType - }); - outbox.count = count; - $scope.$apply(); - - emailDao.refreshFolder({ - folder: outbox - }, $scope.onError); - }; - - $scope.logout = function() { - $scope.onError({ - title: 'Logout', - message: 'Are you sure you want to logout?', - callback: function(confirm) { - if (confirm) { - appController.logout(); - } - }, - sync: true - }); - }; - - // - // Start - // - - // handle back button - backBtnHandler.start(); - // init folders - initializeFolders(); - - // select inbox as the current folder on init - if ($scope.account.folders && $scope.account.folders.length > 0) { - $scope.openFolder($scope.account.folders[0]); - } - // connect imap/smtp clients on first startup - appController.onConnect(function(err) { - if (err) { - $scope.onError(err); - return; - } - - // select inbox if not yet selected - if (!$scope.state.nav.currentFolder) { - $scope.openFolder($scope.account.folders[0]); - $scope.$apply(); - } - }); - - // - // helper functions - // - - function initializeFolders() { - // create dummy folder in dev environment only - if ($routeParams.dev) { - createDummyFolders(); - return; - } - - // get pointer to account/folder/message tree on root scope - $scope.$root.account = emailDao._account; - - // set notificatio handler for sent messages - outboxBo.onSent = sentNotification; - // start checking outbox periodically - outboxBo.startChecking($scope.onOutboxUpdate); - - } - - function sentNotification(email) { - notification.create({ - title: 'Message sent', - message: email.subject, - timeout: 2000 - }, function() {}); - } - - - // attach dummy folders for development - function createDummyFolders() { - $scope.$root.account = {}; - $scope.account.folders = [{ - type: 'Inbox', - count: 2, - path: 'INBOX' - }, { - type: 'Sent', - count: 0, - path: 'SENT' - }, { - type: config.outboxMailboxType, - count: 0, - path: config.outboxMailboxPath - }, { - type: 'Drafts', - count: 0, - path: 'DRAFTS' - }, { - type: 'Trash', - count: 0, - path: 'TRASH' - }]; + $scope.state.nav = { + open: false, + toggle: function(to) { + this.open = to; } }; + $scope.openFolder = function(folder) { + $scope.state.nav.currentFolder = folder; + $scope.state.nav.toggle(false); + }; + + $scope.onOutboxUpdate = function(err, count) { + if (err) { + $scope.onError(err); + return; + } + + // update the outbox mail count + var outbox = _.findWhere($scope.account.folders, { + type: config.outboxMailboxType + }); + outbox.count = count; + $scope.$apply(); + + emailDao.refreshFolder({ + folder: outbox + }, $scope.onError); + }; + + $scope.logout = function() { + $scope.onError({ + title: 'Logout', + message: 'Are you sure you want to logout?', + callback: function(confirm) { + if (confirm) { + appController.logout(); + } + }, + sync: true + }); + }; + // - // Directives + // Start // - var ngModule = angular.module('navigation', []); - ngModule.directive('keyShortcuts', function($timeout) { - return function(scope, elm) { - elm.bind('keydown', function(e) { - // global state is not yet set, ignore keybaord shortcuts - if (!scope.state) { - return; - } + // handle back button + backBtnHandler.start(); + // init folders + initializeFolders(); - var modifier = e.ctrlKey || e.metaKey; + // select inbox as the current folder on init + if ($scope.account.folders && $scope.account.folders.length > 0) { + $scope.openFolder($scope.account.folders[0]); + } + // connect imap/smtp clients on first startup + appController.onConnect(function(err) { + if (err) { + $scope.onError(err); + return; + } - if (modifier && e.keyCode === 78 && scope.state.lightbox !== 'write') { - // n -> new mail - e.preventDefault(); - scope.state.writer.write(); - scope.$apply(); - - } else if (modifier && e.keyCode === 70 && scope.state.lightbox !== 'write') { - // f -> find - e.preventDefault(); - scope.state.mailList.searching = true; - $timeout(function() { - scope.state.mailList.searching = false; - }, 200); - scope.$apply(); - - } else if (modifier && e.keyCode === 82 && scope.state.lightbox !== 'write' && scope.state.mailList.selected) { - // r -> reply - e.preventDefault(); - scope.state.writer.write(scope.state.mailList.selected); - scope.$apply(); - - } else if (e.keyCode === 27 && scope.state.lightbox !== undefined) { - // escape -> close current lightbox - e.preventDefault(); - scope.state.lightbox = undefined; - scope.$apply(); - - } else if (e.keyCode === 27 && scope.state.nav.open) { - // escape -> close nav view - e.preventDefault(); - scope.state.nav.toggle(false); - scope.$apply(); - } - - }); - }; + // select inbox if not yet selected + if (!$scope.state.nav.currentFolder) { + $scope.openFolder($scope.account.folders[0]); + $scope.$apply(); + } }); - return NavigationCtrl; -}); \ No newline at end of file + // + // helper functions + // + + function initializeFolders() { + // create dummy folder in dev environment only + if ($routeParams.dev) { + createDummyFolders(); + return; + } + + // get pointer to account/folder/message tree on root scope + $scope.$root.account = emailDao._account; + + // set notificatio handler for sent messages + outboxBo.onSent = sentNotification; + // start checking outbox periodically + outboxBo.startChecking($scope.onOutboxUpdate); + + } + + function sentNotification(email) { + notification.create({ + title: 'Message sent', + message: email.subject, + timeout: 2000 + }, function() {}); + } + + + // attach dummy folders for development + function createDummyFolders() { + $scope.$root.account = {}; + $scope.account.folders = [{ + type: 'Inbox', + count: 2, + path: 'INBOX' + }, { + type: 'Sent', + count: 0, + path: 'SENT' + }, { + type: config.outboxMailboxType, + count: 0, + path: config.outboxMailboxPath + }, { + type: 'Drafts', + count: 0, + path: 'DRAFTS' + }, { + type: 'Trash', + count: 0, + path: 'TRASH' + }]; + } +}; + +// +// Directives +// + +var ngModule = angular.module('navigation', []); +ngModule.directive('keyShortcuts', function($timeout) { + return function(scope, elm) { + elm.bind('keydown', function(e) { + // global state is not yet set, ignore keybaord shortcuts + if (!scope.state) { + return; + } + + var modifier = e.ctrlKey || e.metaKey; + + if (modifier && e.keyCode === 78 && scope.state.lightbox !== 'write') { + // n -> new mail + e.preventDefault(); + scope.state.writer.write(); + scope.$apply(); + + } else if (modifier && e.keyCode === 70 && scope.state.lightbox !== 'write') { + // f -> find + e.preventDefault(); + scope.state.mailList.searching = true; + $timeout(function() { + scope.state.mailList.searching = false; + }, 200); + scope.$apply(); + + } else if (modifier && e.keyCode === 82 && scope.state.lightbox !== 'write' && scope.state.mailList.selected) { + // r -> reply + e.preventDefault(); + scope.state.writer.write(scope.state.mailList.selected); + scope.$apply(); + + } else if (e.keyCode === 27 && scope.state.lightbox !== undefined) { + // escape -> close current lightbox + e.preventDefault(); + scope.state.lightbox = undefined; + scope.$apply(); + + } else if (e.keyCode === 27 && scope.state.nav.open) { + // escape -> close nav view + e.preventDefault(); + scope.state.nav.toggle(false); + scope.$apply(); + } + + }); + }; +}); + +exports = NavigationCtrl; \ No newline at end of file diff --git a/src/js/controller/popover.js b/src/js/controller/popover.js index 32148eb..ed6654a 100644 --- a/src/js/controller/popover.js +++ b/src/js/controller/popover.js @@ -1,47 +1,43 @@ -define(function(require) { - 'use strict'; +'use strict'; - var angular = require('angular'); +// +// Controller +// - // - // Controller - // +var PopoverCtrl = function($scope) { + $scope.state.popover = {}; +}; - var PopoverCtrl = function($scope) { - $scope.state.popover = {}; +// +// Directives +// + +var ngModule = angular.module('popover', []); +ngModule.directive('popover', function() { + return function(scope, elm, attrs) { + var selector = attrs.popover; + var popover = angular.element(document.querySelector(selector)); + + elm.on('mouseover', function() { + // set popover position + var top = elm[0].offsetTop; + var left = elm[0].offsetLeft; + var width = elm[0].offsetWidth; + var height = elm[0].offsetHeight; + + popover[0].style.transition = 'opacity 0.3s linear'; + popover[0].style.top = (top + height / 2 - popover[0].offsetHeight / 2) + 'px'; + popover[0].style.left = (left + width) + 'px'; + popover[0].style.opacity = '1'; + }); + + elm.on('mouseout', function() { + popover[0].style.transition = 'opacity 0.3s linear, top 0.3s step-end, left 0.3s step-end'; + popover[0].style.opacity = '0'; + popover[0].style.top = '-9999px'; + popover[0].style.left = '-9999px'; + }); }; +}); - // - // Directives - // - - var ngModule = angular.module('popover', []); - ngModule.directive('popover', function() { - return function(scope, elm, attrs) { - var selector = attrs.popover; - var popover = angular.element(document.querySelector(selector)); - - elm.on('mouseover', function() { - // set popover position - var top = elm[0].offsetTop; - var left = elm[0].offsetLeft; - var width = elm[0].offsetWidth; - var height = elm[0].offsetHeight; - - popover[0].style.transition = 'opacity 0.3s linear'; - popover[0].style.top = (top + height / 2 - popover[0].offsetHeight / 2) + 'px'; - popover[0].style.left = (left + width) + 'px'; - popover[0].style.opacity = '1'; - }); - - elm.on('mouseout', function() { - popover[0].style.transition = 'opacity 0.3s linear, top 0.3s step-end, left 0.3s step-end'; - popover[0].style.opacity = '0'; - popover[0].style.top = '-9999px'; - popover[0].style.left = '-9999px'; - }); - }; - }); - - return PopoverCtrl; -}); \ No newline at end of file +exports = PopoverCtrl; \ No newline at end of file diff --git a/src/js/controller/privatekey-upload.js b/src/js/controller/privatekey-upload.js index 4ebe585..7c48dad 100644 --- a/src/js/controller/privatekey-upload.js +++ b/src/js/controller/privatekey-upload.js @@ -1,204 +1,201 @@ -define(function(require) { - 'use strict'; +'use strict'; - var angular = require('angular'), - appController = require('js/app-controller'), - util = require('js/crypto/util'), - keychain, pgp; +var appController = require('../app-controller'), + util = require('crypto-lib').util, + keychain, pgp; - var PrivateKeyUploadCtrl = function($scope) { - keychain = appController._keychain; - pgp = keychain._pgp; +var PrivateKeyUploadCtrl = function($scope) { + keychain = appController._keychain; + pgp = keychain._pgp; - $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 - $scope.checkServerForKey(function(privateKeySynced) { - if (privateKeySynced) { - // close lightbox - $scope.state.lightbox = undefined; - // show message - $scope.onError({ - title: 'Info', - message: 'Your PGP key has already been synced.' - }); - return; - } - - // show sync ui if key is not synced - $scope.displayUploadUi(); - }); - } - }; - - $scope.handlePaste = function(event) { - var evt = event; - if (evt.originalEvent) { - evt = evt.originalEvent; - } - - var value = evt.clipboardData.getData('text/plain'); - if (!value) { + $scope.state.privateKeyUpload = { + toggle: function(to) { + // open lightbox + $scope.state.lightbox = (to) ? 'privatekey-upload' : undefined; + if (!to) { return; } - value = value.replace(/-/g, ''); - $scope.code0 = value.slice(0, 4); - $scope.code1 = value.slice(4, 8); - $scope.code2 = value.slice(8, 12); - $scope.code3 = value.slice(12, 16); - $scope.code4 = value.slice(16, 20); - $scope.code5 = value.slice(20, 24); - }; - - $scope.checkServerForKey = function(callback) { - var keyParams = pgp.getKeyParams(); - keychain.hasPrivateKey({ - userId: keyParams.userId, - keyId: keyParams._id - }, function(err, privateKeySynced) { - if (err) { - $scope.onError(err); - return; - } - + // show syncing status + $scope.step = 4; + // check if key is already synced + $scope.checkServerForKey(function(privateKeySynced) { if (privateKeySynced) { - callback(privateKeySynced); + // close lightbox + $scope.state.lightbox = undefined; + // show message + $scope.onError({ + title: 'Info', + message: 'Your PGP key has already been synced.' + }); return; } - callback(); + // show sync ui if key is not synced + $scope.displayUploadUi(); }); - }; + } + }; - $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); + $scope.handlePaste = function(event) { + var evt = event; + if (evt.originalEvent) { + evt = evt.originalEvent; + } - // clear input fields of any previous artifacts - $scope.code0 = $scope.code1 = $scope.code2 = $scope.code3 = $scope.code4 = $scope.code5 = ''; - }; + var value = evt.clipboardData.getData('text/plain'); + if (!value) { + return; + } - $scope.verifyCode = function() { - var inputCode = '' + $scope.code0 + $scope.code1 + $scope.code2 + $scope.code3 + $scope.code4 + $scope.code5; + value = value.replace(/-/g, ''); + $scope.code0 = value.slice(0, 4); + $scope.code1 = value.slice(4, 8); + $scope.code2 = value.slice(8, 12); + $scope.code3 = value.slice(12, 16); + $scope.code4 = value.slice(16, 20); + $scope.code5 = value.slice(20, 24); + }; - if (inputCode.toUpperCase() !== $scope.code) { - var err = new Error('The code does not match. Please go back and check the generated code.'); - err.sync = true; + $scope.checkServerForKey = function(callback) { + var keyParams = pgp.getKeyParams(); + keychain.hasPrivateKey({ + userId: keyParams.userId, + keyId: keyParams._id + }, function(err, privateKeySynced) { + if (err) { $scope.onError(err); - return false; + return; } - return true; - }; + if (privateKeySynced) { + callback(privateKeySynced); + return; + } - $scope.setDeviceName = function(callback) { - keychain.setDeviceName($scope.deviceName, callback); - }; + callback(); + }); + }; - $scope.encryptAndUploadKey = function(callback) { - var userId = appController._emailDao._account.emailAddress; - var code = $scope.code; + $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); - // register device to keychain service - keychain.registerDevice({ - userId: userId - }, function(err) { + // clear input fields of any previous artifacts + $scope.code0 = $scope.code1 = $scope.code2 = $scope.code3 = $scope.code4 = $scope.code5 = ''; + }; + + $scope.verifyCode = function() { + var inputCode = '' + $scope.code0 + $scope.code1 + $scope.code2 + $scope.code3 + $scope.code4 + $scope.code5; + + if (inputCode.toUpperCase() !== $scope.code) { + var err = new Error('The code does not match. Please go back and check the generated code.'); + err.sync = true; + $scope.onError(err); + return false; + } + + return true; + }; + + $scope.setDeviceName = function(callback) { + keychain.setDeviceName($scope.deviceName, callback); + }; + + $scope.encryptAndUploadKey = function(callback) { + var userId = appController._emailDao._account.emailAddress; + var code = $scope.code; + + // register device to keychain service + keychain.registerDevice({ + userId: userId + }, function(err) { + if (err) { + $scope.onError(err); + return; + } + + // encrypt private PGP key using code and upload + keychain.uploadPrivateKey({ + userId: userId, + code: code + }, callback); + }); + }; + + $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 + $scope.setDeviceName(function(err) { if (err) { $scope.onError(err); return; } - // encrypt private PGP key using code and upload - keychain.uploadPrivateKey({ - userId: userId, - code: code - }, callback); - }); - }; - - $scope.goBack = function() { - if ($scope.step > 1) { - $scope.step--; - } - }; - - $scope.goForward = function() { - if ($scope.step < 2) { + // show spinner $scope.step++; - return; - } + $scope.$apply(); - if ($scope.step === 2 && $scope.verifyCode()) { - $scope.step++; - return; - } - - if ($scope.step === 3) { - // set device name to local storage - $scope.setDeviceName(function(err) { + // init key sync + $scope.encryptAndUploadKey(function(err) { if (err) { $scope.onError(err); return; } - // show spinner - $scope.step++; - $scope.$apply(); - - // init key sync - $scope.encryptAndUploadKey(function(err) { - if (err) { - $scope.onError(err); - return; - } - - // close sync dialog - $scope.state.privateKeyUpload.toggle(false); - // show success message - $scope.onError({ - title: 'Success', - message: 'Whiteout Keychain setup successful!' - }); + // close sync dialog + $scope.state.privateKeyUpload.toggle(false); + // show success message + $scope.onError({ + title: 'Success', + message: 'Whiteout Keychain setup successful!' }); }); - } - }; - + }); + } }; - // - // Directives - // +}; - var ngModule = angular.module('privatekey-upload', []); - ngModule.directive('focusNext', function() { - return { - link: function(scope, element, attr) { - var maxLen = element[0].maxLength; +// +// Directives +// - scope.$watch(attr.ngModel, function(val) { - if (val && val.length === maxLen) { - var nextinput = element.next('input'); - if (nextinput.length) { - nextinput[0].focus(); - } +var ngModule = angular.module('privatekey-upload', []); +ngModule.directive('focusNext', function() { + return { + link: function(scope, element, attr) { + var maxLen = element[0].maxLength; + + scope.$watch(attr.ngModel, function(val) { + if (val && val.length === maxLen) { + var nextinput = element.next('input'); + if (nextinput.length) { + nextinput[0].focus(); } - }); - } - }; - }); + } + }); + } + }; +}); - return PrivateKeyUploadCtrl; -}); \ No newline at end of file +exports = PrivateKeyUploadCtrl; \ No newline at end of file diff --git a/src/js/controller/read-sandbox.js b/src/js/controller/read-sandbox.js index 7a778e1..ca38807 100644 --- a/src/js/controller/read-sandbox.js +++ b/src/js/controller/read-sandbox.js @@ -1,218 +1,215 @@ -(function() { - 'use strict'; +'use strict'; - // set listener for event from main window - window.onmessage = function(e) { - var html = ''; +// set listener for event from main window +window.onmessage = function(e) { + var html = ''; - if (e.data.html) { - // display html mail body - html = '
' + e.data.html + '
'; - } else if (e.data.text) { - // diplay text mail body by with colored conversation nodes - html = renderNodes(parseConversation(e.data.text)); - } + if (e.data.html) { + // display html mail body + 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(/]+\b)src=['"][^'">]+['"]/ig, function(match, prefix) { - return prefix; + // remove sources where necessary + if (e.data.removeImages) { + html = html.replace(/(]+\b)src=['"][^'">]+['"]/ig, function(match, prefix) { + return prefix; + }); + } + + document.body.innerHTML = html; + + scaleToFit(); +}; + +window.addEventListener('resize', scaleToFit); + +/** + * Parse email body and generate conversation nodes + * @param {Object} email The email object + * @return {Node} The root node of the conversion + */ +function parseConversation(textBody) { + var nodes; + + function parseLines(body) { + var lines = []; + body.split('\n').forEach(parseLine); + + function parseLine(line) { + var regex = /^>*/; + var result = regex.exec(line); + + lines.push({ + text: line.replace(regex, '').trim(), + level: (result && result.length > 0) ? result[0].length : 0 }); } - document.body.innerHTML = html; + return lines; + } - scaleToFit(); + function buildTextNodes(lines) { + var i, j, root, currentLevel, currentNode, levelDelta; + + root = new Node(); + currentLevel = 0; + currentNode = root; + + // iterate over text lines + for (i = 0; i < lines.length; i++) { + levelDelta = lines[i].level - currentLevel; + + if (levelDelta === 0) { + // we are at the desired node ... no traversal required + } else if (levelDelta > 0) { + // traverse to child node(s) + for (j = 0; j < levelDelta; j++) { + var newChild = new Node(currentNode); + // create new child node + currentNode.children.push(newChild); + // go to last child node + currentNode = newChild; + // increase current level by one + currentLevel++; + } + } else { + // traverse to parent(s) + for (j = levelDelta; j < 0; j++) { + currentNode = currentNode.parent; + currentLevel--; + } + } + + // add text to the current node + currentNode.addLine(lines[i].text); + } + + return root; + } + + function Node(parent) { + this.parent = parent; + this.children = []; + } + Node.prototype.addLine = function(lineText) { + var c, l; + + c = this.children; + l = c.length; + + // append text node to children if last child is not a text node + if (l < 1 || typeof c[l - 1] !== 'string') { + c[l] = ''; + l = c.length; + } + + // append line to last child (add newline between lines) + c[l - 1] += lineText + '\n'; }; - window.addEventListener('resize', scaleToFit); - - /** - * Parse email body and generate conversation nodes - * @param {Object} email The email object - * @return {Node} The root node of the conversion - */ - function parseConversation(textBody) { - var nodes; - - function parseLines(body) { - var lines = []; - body.split('\n').forEach(parseLine); - - function parseLine(line) { - var regex = /^>*/; - var result = regex.exec(line); - - lines.push({ - text: line.replace(regex, '').trim(), - level: (result && result.length > 0) ? result[0].length : 0 - }); - } - - return lines; - } - - function buildTextNodes(lines) { - var i, j, root, currentLevel, currentNode, levelDelta; - - root = new Node(); - currentLevel = 0; - currentNode = root; - - // iterate over text lines - for (i = 0; i < lines.length; i++) { - levelDelta = lines[i].level - currentLevel; - - if (levelDelta === 0) { - // we are at the desired node ... no traversal required - } else if (levelDelta > 0) { - // traverse to child node(s) - for (j = 0; j < levelDelta; j++) { - var newChild = new Node(currentNode); - // create new child node - currentNode.children.push(newChild); - // go to last child node - currentNode = newChild; - // increase current level by one - currentLevel++; - } - } else { - // traverse to parent(s) - for (j = levelDelta; j < 0; j++) { - currentNode = currentNode.parent; - currentLevel--; - } - } - - // add text to the current node - currentNode.addLine(lines[i].text); - } - - return root; - } - - function Node(parent) { - this.parent = parent; - this.children = []; - } - Node.prototype.addLine = function(lineText) { - var c, l; - - c = this.children; - l = c.length; - - // append text node to children if last child is not a text node - if (l < 1 || typeof c[l - 1] !== 'string') { - c[l] = ''; - l = c.length; - } - - // append line to last child (add newline between lines) - c[l - 1] += lineText + '\n'; - }; - - function removeParentReference(node) { - if (!node.children) { - // this is a text leaf ... terminate recursion - return; - } - - // remove parent node to prevent infinite loop in JSON stringify - delete node.parent; - - for (var i = 0; i < node.children.length; i++) { - if (typeof node.children[i] === 'string') { - // remove trailing newline in string - node.children[i] = node.children[i].replace(/\n$/, ''); - } else { - // I used recursion ... - removeParentReference(node.children[i]); - } - } - } - - nodes = buildTextNodes(parseLines(textBody.replace(/ >/g, '>'))); - removeParentReference(nodes); - - return nodes; - } - - /** - * Render the conversation nodes as markup. This is not injected directly into the DOM, but rather send to a sandboxed iframe to be rendered - * @param {Node} root The conversation root node - * @return {Strin} The conversation as markup - */ - function renderNodes(root) { - var body = ''; - - function render(node) { - var i, html = ''; - if (!node.children) { - // this is a text leaf - 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); - // wrap line into an element for easier styling - html += '
' + lines[i] + '
'; - } - return html; - } - - for (i = 0; i < node.children.length; i++) { - html += '
' + render(node.children[i]) + '
'; - } - - return html; - } - - function createArchor(url) { - return '
' + url + ''; - } - - function isLineEmpty(line) { - return line.replace(/>/g, '').trim().length === 0; - } - - for (var j = 0; j < root.children.length; j++) { - // start by rendering the root nodes children - body += render(root.children[j]); - } - - return '
' + body + '
'; - } - - /** - * Transform scale content to fit iframe width - */ - function scaleToFit() { - var view = document.getElementsByClassName('scale-body').item(0); - if(!view) { + function removeParentReference(node) { + if (!node.children) { + // this is a text leaf ... terminate recursion return; } - var parentWidth = view.parentNode.offsetWidth; - var w = view.offsetWidth; - var scale = ''; + // remove parent node to prevent infinite loop in JSON stringify + delete node.parent; - if(w > parentWidth) { - scale = parentWidth / w; - scale = 'scale(' + scale + ',' + scale + ')'; + for (var i = 0; i < node.children.length; i++) { + if (typeof node.children[i] === 'string') { + // remove trailing newline in string + node.children[i] = node.children[i].replace(/\n$/, ''); + } else { + // I used recursion ... + removeParentReference(node.children[i]); + } } - - view.style['-webkit-transform-origin'] = '0 0'; - view.style.transformOrigin = '0 0'; - view.style['-webkit-transform'] = scale; - view.style.transform = scale; } -})(); \ No newline at end of file + nodes = buildTextNodes(parseLines(textBody.replace(/ >/g, '>'))); + removeParentReference(nodes); + + return nodes; +} + +/** + * Render the conversation nodes as markup. This is not injected directly into the DOM, but rather send to a sandboxed iframe to be rendered + * @param {Node} root The conversation root node + * @return {Strin} The conversation as markup + */ +function renderNodes(root) { + var body = ''; + + function render(node) { + var i, html = ''; + if (!node.children) { + // this is a text leaf + 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); + // wrap line into an element for easier styling + html += '
' + lines[i] + '
'; + } + return html; + } + + for (i = 0; i < node.children.length; i++) { + html += '
' + render(node.children[i]) + '
'; + } + + return html; + } + + function createArchor(url) { + return '' + url + ''; + } + + function isLineEmpty(line) { + return line.replace(/>/g, '').trim().length === 0; + } + + for (var j = 0; j < root.children.length; j++) { + // start by rendering the root nodes children + body += render(root.children[j]); + } + + return '
' + body + '
'; +} + +/** + * 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; +} \ No newline at end of file diff --git a/src/js/controller/read.js b/src/js/controller/read.js index dc3ea9c..acbfcb8 100644 --- a/src/js/controller/read.js +++ b/src/js/controller/read.js @@ -1,286 +1,283 @@ -define(function(require) { - 'use strict'; +'use strict'; - var appController = require('js/app-controller'), - download = require('js/util/download'), - angular = require('angular'), - str = require('js/app-config').string, - emailDao, invitationDao, outbox, pgp, keychain; +var appController = require('../app-controller'), + download = require('../util/download'), + str = require('../app-config').string, + emailDao, invitationDao, outbox, pgp, keychain; - // - // Controller - // +// +// Controller +// - var ReadCtrl = function($scope) { +var ReadCtrl = function($scope) { - emailDao = appController._emailDao; - invitationDao = appController._invitationDao; - outbox = appController._outboxBo; - pgp = appController._pgp; - keychain = appController._keychain; + emailDao = appController._emailDao; + invitationDao = appController._invitationDao; + outbox = appController._outboxBo; + pgp = appController._pgp; + keychain = appController._keychain; - // set default value so that the popover height is correct on init - $scope.keyId = 'No key found.'; + // set default value so that the popover height is correct on init + $scope.keyId = 'No key found.'; - $scope.state.read = { - open: false, - toggle: function(to) { - this.open = to; - } - }; - - $scope.getKeyId = function(address) { - $scope.keyId = 'Searching...'; - keychain.getReceiverPublicKey(address, function(err, pubkey) { - if (err) { - $scope.onError(err); - return; - } - - if (!pubkey) { - $scope.keyId = 'User has no key. Click to invite.'; - $scope.$apply(); - return; - } - - var fpr = pgp.getFingerprint(pubkey.publicKey); - var formatted = fpr.slice(32); - - $scope.keyId = 'PGP key: ' + formatted; - $scope.$apply(); - }); - }; - - $scope.$watch('state.mailList.selected', function(mail) { - if (!mail) { - return; - } - - // display sender security status - mail.from.forEach(checkPublicKey); - // display recipient security status - mail.to.forEach(checkPublicKey); - // display recipient security status - Array.isArray(mail.cc) && mail.cc.forEach(checkPublicKey); - }); - - function checkPublicKey(user) { - user.secure = undefined; - - if (!keychain) { - return; - } - - keychain.getReceiverPublicKey(user.address, function(err, pubkey) { - if (err) { - $scope.onError(err); - return; - } - - if (pubkey && pubkey.publicKey) { - user.secure = true; - } else { - user.secure = false; - } - - $scope.$apply(); - }); + $scope.state.read = { + open: false, + toggle: function(to) { + this.open = to; } - - $scope.download = function(attachment) { - // download file to disk if content is available - if (attachment.content) { - download.createDownload({ - content: attachment.content, - filename: attachment.filename, - contentType: attachment.mimeType - }); - return; - } - - var folder = $scope.state.nav.currentFolder; - var email = $scope.state.mailList.selected; - emailDao.getAttachment({ - folder: folder, - uid: email.uid, - attachment: attachment - }, $scope.onError); - }; - - $scope.invite = function(user) { - // only invite non-pgp users - if (user.secure) { - return; - } - - $scope.keyId = 'Sending invitation...'; - - var sender = emailDao._account.emailAddress, - recipient = user.address; - - invitationDao.invite({ - recipient: recipient, - sender: sender - }, function(err) { - if (err) { - $scope.onError(err); - return; - } - - var invitationMail = { - from: [{ - address: sender - }], - to: [{ - address: recipient - }], - cc: [], - bcc: [], - subject: str.invitationSubject, - body: str.invitationMessage - }; - - // send invitation mail - outbox.put(invitationMail, $scope.onError); - }); - }; }; - // - // Directives - // - - var ngModule = angular.module('read', []); - - ngModule.directive('replySelection', function() { - return function(scope, elm) { - var popover, visible; - - popover = angular.element(document.querySelector('.reply-selection')); - visible = false; - - elm.on('touchstart click', appear); - elm.parent().parent().on('touchstart click', disappear); - popover.on('touchstart click', disappear); - - function appear(e) { - e.preventDefault(); - e.stopPropagation(); - - visible = true; - - // set popover position - var top = elm[0].offsetTop; - var left = elm[0].offsetLeft; - var width = elm[0].offsetWidth; - var height = elm[0].offsetHeight; - - popover[0].style.transition = 'opacity 0.1s linear'; - popover[0].style.top = (top + height) + 'px'; - popover[0].style.left = (left + width / 2 - popover[0].offsetWidth / 2) + 'px'; - popover[0].style.opacity = '1'; + $scope.getKeyId = function(address) { + $scope.keyId = 'Searching...'; + keychain.getReceiverPublicKey(address, function(err, pubkey) { + if (err) { + $scope.onError(err); + return; } - function disappear() { - if (!visible) { - return; - } - - popover[0].style.transition = 'opacity 0.25s linear, top 0.25s step-end, left 0.25s step-end'; - popover[0].style.opacity = '0'; - popover[0].style.top = '-9999px'; - popover[0].style.left = '-9999px'; - visible = false; + if (!pubkey) { + $scope.keyId = 'User has no key. Click to invite.'; + $scope.$apply(); + return; } - }; + + var fpr = pgp.getFingerprint(pubkey.publicKey); + var formatted = fpr.slice(32); + + $scope.keyId = 'PGP key: ' + formatted; + $scope.$apply(); + }); + }; + + $scope.$watch('state.mailList.selected', function(mail) { + if (!mail) { + return; + } + + // display sender security status + mail.from.forEach(checkPublicKey); + // display recipient security status + mail.to.forEach(checkPublicKey); + // display recipient security status + Array.isArray(mail.cc) && mail.cc.forEach(checkPublicKey); }); - ngModule.directive('frameLoad', function($timeout, $window) { - return function(scope, elm) { - var iframe = elm[0]; + function checkPublicKey(user) { + user.secure = undefined; - scope.$watch('state.read.open', function(open) { - if (open) { - // trigger rendering of iframe - // otherwise scale to fit would not compute correct dimensions on mobile - displayText(scope.state.mailList.selected ? scope.state.mailList.selected.body : undefined); - displayHtml(scope.state.mailList.selected ? scope.state.mailList.selected.html : undefined); - } + if (!keychain) { + return; + } + + keychain.getReceiverPublicKey(user.address, function(err, pubkey) { + if (err) { + $scope.onError(err); + return; + } + + if (pubkey && pubkey.publicKey) { + user.secure = true; + } else { + user.secure = false; + } + + $scope.$apply(); + }); + } + + $scope.download = function(attachment) { + // download file to disk if content is available + if (attachment.content) { + download.createDownload({ + content: attachment.content, + filename: attachment.filename, + contentType: attachment.mimeType }); + return; + } - $window.addEventListener('resize', scaleToFit); + var folder = $scope.state.nav.currentFolder; + var email = $scope.state.mailList.selected; + emailDao.getAttachment({ + folder: folder, + uid: email.uid, + attachment: attachment + }, $scope.onError); + }; - iframe.onload = function() { - // set listeners - scope.$watch('state.mailList.selected.body', displayText); - scope.$watch('state.mailList.selected.html', displayHtml); - // display initial message body - scope.$apply(); + $scope.invite = function(user) { + // only invite non-pgp users + if (user.secure) { + return; + } + + $scope.keyId = 'Sending invitation...'; + + var sender = emailDao._account.emailAddress, + recipient = user.address; + + invitationDao.invite({ + recipient: recipient, + sender: sender + }, function(err) { + if (err) { + $scope.onError(err); + return; + } + + var invitationMail = { + from: [{ + address: sender + }], + to: [{ + address: recipient + }], + cc: [], + bcc: [], + subject: str.invitationSubject, + body: str.invitationMessage }; - function displayText(body) { - var mail = scope.state.mailList.selected; - if ((mail && mail.html) || (mail && mail.encrypted && !mail.decrypted)) { - return; - } + // send invitation mail + outbox.put(invitationMail, $scope.onError); + }); + }; +}; - // send text body for rendering in iframe - iframe.contentWindow.postMessage({ - text: body - }, '*'); +// +// Directives +// - $timeout(scaleToFit, 0); +var ngModule = angular.module('read', []); + +ngModule.directive('replySelection', function() { + return function(scope, elm) { + var popover, visible; + + popover = angular.element(document.querySelector('.reply-selection')); + visible = false; + + elm.on('touchstart click', appear); + elm.parent().parent().on('touchstart click', disappear); + popover.on('touchstart click', disappear); + + function appear(e) { + e.preventDefault(); + e.stopPropagation(); + + visible = true; + + // set popover position + var top = elm[0].offsetTop; + var left = elm[0].offsetLeft; + var width = elm[0].offsetWidth; + var height = elm[0].offsetHeight; + + popover[0].style.transition = 'opacity 0.1s linear'; + popover[0].style.top = (top + height) + 'px'; + popover[0].style.left = (left + width / 2 - popover[0].offsetWidth / 2) + 'px'; + popover[0].style.opacity = '1'; + } + + function disappear() { + if (!visible) { + return; } - function displayHtml(html) { - if (!html) { - return; - } + popover[0].style.transition = 'opacity 0.25s linear, top 0.25s step-end, left 0.25s step-end'; + popover[0].style.opacity = '0'; + popover[0].style.top = '-9999px'; + popover[0].style.left = '-9999px'; + visible = false; + } + }; +}); - // if there are image tags in the html? - var hasImages = /]+\bsrc=['"][^'">]+['"]/ig.test(html); - scope.showImageButton = hasImages; +ngModule.directive('frameLoad', function($timeout, $window) { + return function(scope, elm) { + var iframe = elm[0]; - iframe.contentWindow.postMessage({ - html: html, - removeImages: hasImages // avoids doing unnecessary work on the html - }, '*'); - - // only add a scope function to reload the html if there are images - if (hasImages) { - // reload WITH images - scope.displayImages = function() { - scope.showImageButton = false; - iframe.contentWindow.postMessage({ - html: html, - removeImages: false - }, '*'); - }; - } - - $timeout(scaleToFit, 0); + scope.$watch('state.read.open', function(open) { + if (open) { + // trigger rendering of iframe + // otherwise scale to fit would not compute correct dimensions on mobile + displayText(scope.state.mailList.selected ? scope.state.mailList.selected.body : undefined); + displayHtml(scope.state.mailList.selected ? scope.state.mailList.selected.html : undefined); } + }); - // transform scale iframe (necessary on iOS) to fit container width - function scaleToFit() { - var parentWidth = elm.parent().width(); - var w = elm.width(); - var scale = ''; + $window.addEventListener('resize', scaleToFit); - if (w > parentWidth) { - scale = parentWidth / w; - scale = 'scale(' + scale + ',' + scale + ')'; - } - - elm.css({ - '-webkit-transform-origin': '0 0', - 'transform-origin': '0 0', - '-webkit-transform': scale, - 'transform': scale - }); - } + iframe.onload = function() { + // set listeners + scope.$watch('state.mailList.selected.body', displayText); + scope.$watch('state.mailList.selected.html', displayHtml); + // display initial message body + scope.$apply(); }; - }); - return ReadCtrl; -}); \ No newline at end of file + function displayText(body) { + var mail = scope.state.mailList.selected; + if ((mail && mail.html) || (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; + } + + // if there are image tags in the html? + var hasImages = /]+\bsrc=['"][^'">]+['"]/ig.test(html); + scope.showImageButton = hasImages; + + iframe.contentWindow.postMessage({ + html: html, + removeImages: hasImages // avoids doing unnecessary work on the html + }, '*'); + + // only add a scope function to reload the html if there are images + if (hasImages) { + // reload WITH images + scope.displayImages = function() { + scope.showImageButton = false; + iframe.contentWindow.postMessage({ + html: html, + removeImages: false + }, '*'); + }; + } + + $timeout(scaleToFit, 0); + } + + // transform scale iframe (necessary on iOS) to fit container width + function scaleToFit() { + var parentWidth = elm.parent().width(); + var w = elm.width(); + var scale = ''; + + if (w > parentWidth) { + scale = parentWidth / w; + scale = 'scale(' + scale + ',' + scale + ')'; + } + + elm.css({ + '-webkit-transform-origin': '0 0', + 'transform-origin': '0 0', + '-webkit-transform': scale, + 'transform': scale + }); + } + }; +}); + +exports = ReadCtrl; \ No newline at end of file diff --git a/src/js/controller/set-passphrase.js b/src/js/controller/set-passphrase.js index 726c78b..9dceaf4 100644 --- a/src/js/controller/set-passphrase.js +++ b/src/js/controller/set-passphrase.js @@ -1,138 +1,136 @@ -define(function(require) { - 'use strict'; +'use strict'; - var appController = require('js/app-controller'), - pgp, keychain; +var appController = require('../app-controller'), + pgp, keychain; - // - // Controller - // +// +// Controller +// - var SetPassphraseCtrl = function($scope) { - keychain = appController._keychain; - pgp = appController._pgp; +var SetPassphraseCtrl = function($scope) { + keychain = appController._keychain; + pgp = appController._pgp; - $scope.state.setPassphrase = { - toggle: function(to) { - $scope.state.lightbox = (to) ? 'set-passphrase' : undefined; + $scope.state.setPassphrase = { + toggle: function(to) { + $scope.state.lightbox = (to) ? 'set-passphrase' : undefined; - $scope.newPassphrase = undefined; - $scope.oldPassphrase = undefined; - $scope.confirmation = undefined; - $scope.passphraseMsg = undefined; - } - }; - - // - // scope variables - // - - // - // 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; - keychain.lookupPrivateKey(keyId, function(err, savedKey) { - if (err) { - $scope.onError(err); - return; - } - - pgp.changePassphrase({ - privateKeyArmored: savedKey.encryptedKey, - oldPassphrase: $scope.oldPassphrase, - newPassphrase: $scope.newPassphrase - }, onPassphraseChanged); - }); - }; - - function onPassphraseChanged(err, newPrivateKeyArmored) { - if (err) { - err.showBugReporter = false; - $scope.onError(err); - return; - } - - // persist new armored key - var keyParams = pgp.getKeyParams(newPrivateKeyArmored); - var privateKey = { - _id: keyParams._id, - userId: keyParams.userId, - userIds: keyParams.userIds, - encryptedKey: newPrivateKeyArmored - }; - - keychain.saveLocalPrivateKey(privateKey, onKeyPersisted); - } - - function onKeyPersisted(err) { - if (err) { - $scope.onError(err); - return; - } - - $scope.state.setPassphrase.toggle(false); - $scope.$apply(); - $scope.onError({ - title: 'Success', - message: 'Passphrase change complete.' - }); + $scope.newPassphrase = undefined; + $scope.oldPassphrase = undefined; + $scope.confirmation = undefined; + $scope.passphraseMsg = undefined; } }; - return SetPassphraseCtrl; -}); \ No newline at end of file + // + // scope variables + // + + // + // 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; + keychain.lookupPrivateKey(keyId, function(err, savedKey) { + if (err) { + $scope.onError(err); + return; + } + + pgp.changePassphrase({ + privateKeyArmored: savedKey.encryptedKey, + oldPassphrase: $scope.oldPassphrase, + newPassphrase: $scope.newPassphrase + }, onPassphraseChanged); + }); + }; + + function onPassphraseChanged(err, newPrivateKeyArmored) { + if (err) { + err.showBugReporter = false; + $scope.onError(err); + return; + } + + // persist new armored key + var keyParams = pgp.getKeyParams(newPrivateKeyArmored); + var privateKey = { + _id: keyParams._id, + userId: keyParams.userId, + userIds: keyParams.userIds, + encryptedKey: newPrivateKeyArmored + }; + + keychain.saveLocalPrivateKey(privateKey, onKeyPersisted); + } + + function onKeyPersisted(err) { + if (err) { + $scope.onError(err); + return; + } + + $scope.state.setPassphrase.toggle(false); + $scope.$apply(); + $scope.onError({ + title: 'Success', + message: 'Passphrase change complete.' + }); + } +}; + +exports = SetPassphraseCtrl; \ No newline at end of file diff --git a/src/js/controller/write.js b/src/js/controller/write.js index 41d0c12..81f901d 100644 --- a/src/js/controller/write.js +++ b/src/js/controller/write.js @@ -1,534 +1,530 @@ -define(function(require) { - 'use strict'; +'use strict'; - var angular = require('angular'), - _ = require('underscore'), - appController = require('js/app-controller'), - axe = require('axe'), - util = require('js/crypto/util'), - str = require('js/app-config').string, - pgp, emailDao, outbox, keychainDao, auth; +var appController = require('../app-controller'), + axe = require('axe-logger'), + util = require('crypto-lib').util, + str = require('../app-config').string, + pgp, emailDao, outbox, keychainDao, auth; + +// +// Controller +// + +var WriteCtrl = function($scope, $filter, $q) { + pgp = appController._pgp; + auth = appController._auth; + emailDao = appController._emailDao; + outbox = appController._outboxBo; + keychainDao = appController._keychain; + + // set default value so that the popover height is correct on init + $scope.keyId = 'XXXXXXXX'; // - // Controller + // Init // - var WriteCtrl = function($scope, $filter, $q) { - pgp = appController._pgp; - auth = appController._auth; - emailDao = appController._emailDao; - outbox = appController._outboxBo; - keychainDao = appController._keychain; + $scope.state.writer = { + write: function(replyTo, replyAll, forward) { + $scope.state.lightbox = 'write'; + $scope.replyTo = replyTo; - // set default value so that the popover height is correct on init - $scope.keyId = 'XXXXXXXX'; + resetFields(); - // - // Init - // + // fill fields depending on replyTo + fillFields(replyTo, replyAll, forward); - $scope.state.writer = { - write: function(replyTo, replyAll, forward) { - $scope.state.lightbox = 'write'; - $scope.replyTo = replyTo; + $scope.verify($scope.to[0]); + }, + reportBug: function() { + $scope.state.lightbox = 'write'; + resetFields(); + reportBug(); + $scope.verify($scope.to[0]); + }, + close: function() { + $scope.state.lightbox = undefined; + } + }; - resetFields(); + function resetFields() { + $scope.writerTitle = 'New email'; + $scope.to = []; + $scope.showCC = false; + $scope.cc = []; + $scope.showBCC = false; + $scope.bcc = []; + $scope.subject = ''; + $scope.body = ''; + $scope.attachments = []; + $scope.addressBookCache = undefined; + } - // fill fields depending on replyTo - fillFields(replyTo, replyAll, forward); + function reportBug() { + var dump = ''; + var appender = { + log: function(level, date, component, log) { + // add a tag for the log level + if (level === axe.DEBUG) { + dump += '[DEBUG]'; + } else if (level === axe.INFO) { + dump += '[INFO]'; + } else if (level === axe.WARN) { + dump += '[WARN]'; + } else if (level === axe.ERROR) { + dump += '[ERROR]'; + } - $scope.verify($scope.to[0]); - }, - reportBug: function() { - $scope.state.lightbox = 'write'; - resetFields(); - reportBug(); - $scope.verify($scope.to[0]); - }, - close: function() { - $scope.state.lightbox = undefined; + dump += '[' + date.toISOString() + ']'; + + // component is optional + if (component) { + dump += '[' + component + ']'; + } + + // log may be an error or a string + dump += ' ' + (log || '').toString(); + + // if an error it is, a stack trace it has. print it, we should. + if (log.stack) { + dump += ' . Stack: ' + log.stack; + } + + dump += '\n'; } }; + axe.dump(appender); - function resetFields() { - $scope.writerTitle = 'New email'; - $scope.to = []; - $scope.showCC = false; - $scope.cc = []; - $scope.showBCC = false; - $scope.bcc = []; - $scope.subject = ''; - $scope.body = ''; - $scope.attachments = []; - $scope.addressBookCache = undefined; + $scope.to = [{ + address: str.supportAddress + }]; + $scope.writerTitle = str.bugReportTitle; + $scope.subject = str.bugReportSubject; + $scope.body = str.bugReportBody + dump; + + } + + function fillFields(re, replyAll, forward) { + var replyTo, from, sentDate, body; + + if (!re) { + return; } - function reportBug() { - var dump = ''; - var appender = { - log: function(level, date, component, log) { - // add a tag for the log level - if (level === axe.DEBUG) { - dump += '[DEBUG]'; - } else if (level === axe.INFO) { - dump += '[INFO]'; - } else if (level === axe.WARN) { - dump += '[WARN]'; - } else if (level === axe.ERROR) { - dump += '[ERROR]'; - } + $scope.writerTitle = (forward) ? 'Forward' : 'Reply'; - dump += '[' + date.toISOString() + ']'; + replyTo = re.replyTo && re.replyTo[0] && re.replyTo[0].address || re.from[0].address; - // component is optional - if (component) { - dump += '[' + component + ']'; - } + // fill recipient field and references + if (!forward) { + $scope.to.unshift({ + address: replyTo + }); + $scope.to.forEach($scope.verify); - // log may be an error or a string - dump += ' ' + (log || '').toString(); - - // if an error it is, a stack trace it has. print it, we should. - if (log.stack) { - dump += ' . Stack: ' + log.stack; - } - - dump += '\n'; - } - }; - axe.dump(appender); - - $scope.to = [{ - address: str.supportAddress - }]; - $scope.writerTitle = str.bugReportTitle; - $scope.subject = str.bugReportSubject; - $scope.body = str.bugReportBody + dump; - - } - - function fillFields(re, replyAll, forward) { - var replyTo, from, sentDate, body; - - if (!re) { - return; + $scope.references = (re.references || []); + if (re.id && $scope.references.indexOf(re.id) < 0) { + // references might not exist yet, so use the double concat + $scope.references = $scope.references.concat(re.id); } - - $scope.writerTitle = (forward) ? 'Forward' : 'Reply'; - - replyTo = re.replyTo && re.replyTo[0] && re.replyTo[0].address || re.from[0].address; - - // fill recipient field and references - if (!forward) { - $scope.to.unshift({ - address: replyTo - }); - $scope.to.forEach($scope.verify); - - $scope.references = (re.references || []); - if (re.id && $scope.references.indexOf(re.id) < 0) { - // references might not exist yet, so use the double concat - $scope.references = $scope.references.concat(re.id); - } - if (re.id) { - $scope.inReplyTo = re.id; - } - } - if (replyAll) { - re.to.concat(re.cc).forEach(function(recipient) { - var me = emailDao._account.emailAddress; - if (recipient.address === me && replyTo !== me) { - // don't reply to yourself - return; - } - $scope.cc.unshift({ - address: recipient.address - }); - }); - - // filter duplicates - $scope.cc = _.uniq($scope.cc, function(recipient) { - return recipient.address; - }); - $scope.showCC = true; - $scope.cc.forEach($scope.verify); - } - - // fill attachments and references on forward - if (forward) { - // create a new array, otherwise removing an attachment will also - // remove it from the original in the mail list as a side effect - $scope.attachments = [].concat(re.attachments); - if (re.id) { - $scope.references = [re.id]; - } - } - - // fill subject - if (forward) { - $scope.subject = 'Fwd: ' + re.subject; - } else { - $scope.subject = 'Re: ' + ((re.subject) ? re.subject.replace('Re: ', '') : ''); - } - - // fill text body - from = re.from[0].name || replyTo; - sentDate = $filter('date')(re.sentDate, 'EEEE, MMM d, yyyy h:mm a'); - - function createString(array) { - var str = ''; - array.forEach(function(to) { - str += (str) ? ', ' : ''; - str += ((to.name) ? to.name : to.address) + ' <' + to.address + '>'; - }); - return str; - } - - if (forward) { - body = '\n\n' + - '---------- Forwarded message ----------\n' + - 'From: ' + re.from[0].name + ' <' + re.from[0].address + '>\n' + - 'Date: ' + sentDate + '\n' + - 'Subject: ' + re.subject + '\n' + - 'To: ' + createString(re.to) + '\n' + - ((re.cc && re.cc.length > 0) ? 'Cc: ' + createString(re.cc) + '\n' : '') + - '\n\n'; - - } else { - body = '\n\n' + sentDate + ' ' + from + ' wrote:\n> '; - } - - if (re.body) { - body += re.body.trim().split('\n').join('\n> ').replace(/ >/g, '>'); - $scope.body = body; + if (re.id) { + $scope.inReplyTo = re.id; } } - - // - // Editing headers - // - - /** - * Verify email address and fetch its public key - */ - $scope.verify = function(recipient) { - if (!recipient) { - return; - } - - // set display to insecure while fetching keys - recipient.key = undefined; - recipient.secure = false; - $scope.checkSendStatus(); - - // verify email address - if (!util.validateEmailAddress(recipient.address)) { - recipient.secure = undefined; - $scope.checkSendStatus(); - return; - } - - // keychainDao is undefined in local dev environment - if (keychainDao) { - // check if to address is contained in known public keys - // when we write an email, we always need to work with the latest keys available - keychainDao.refreshKeyForUserId(recipient.address, function(err, key) { - if (err) { - $scope.onError(err); - return; - } - - if (key) { - // compare again since model could have changed during the roundtrip - var matchingUserId = _.findWhere(key.userIds, { - emailAddress: recipient.address - }); - // compare either primary userId or (if available) multiple IDs - if (key.userId === recipient.address || matchingUserId) { - recipient.key = key; - recipient.secure = true; - } - } - - $scope.checkSendStatus(); - $scope.$digest(); - }); - } - }; - - /** - * Check if it is ok to send an email depending on the invitation state of the addresses - */ - $scope.checkSendStatus = function() { - $scope.okToSend = false; - $scope.sendBtnText = undefined; - $scope.sendBtnSecure = undefined; - - var allSecure = true; - var numReceivers = 0; - - // count number of receivers and check security - $scope.to.forEach(check); - $scope.cc.forEach(check); - $scope.bcc.forEach(check); - - function check(recipient) { - // validate address - if (!util.validateEmailAddress(recipient.address)) { + if (replyAll) { + re.to.concat(re.cc).forEach(function(recipient) { + var me = emailDao._account.emailAddress; + if (recipient.address === me && replyTo !== me) { + // don't reply to yourself return; } - numReceivers++; - if (!recipient.secure) { - allSecure = false; - } + $scope.cc.unshift({ + address: recipient.address + }); + }); + + // filter duplicates + $scope.cc = _.uniq($scope.cc, function(recipient) { + return recipient.address; + }); + $scope.showCC = true; + $scope.cc.forEach($scope.verify); + } + + // fill attachments and references on forward + if (forward) { + // create a new array, otherwise removing an attachment will also + // remove it from the original in the mail list as a side effect + $scope.attachments = [].concat(re.attachments); + if (re.id) { + $scope.references = [re.id]; } + } - // only allow sending if receviers exist - if (numReceivers < 1) { - return; - } + // fill subject + if (forward) { + $scope.subject = 'Fwd: ' + re.subject; + } else { + $scope.subject = 'Re: ' + ((re.subject) ? re.subject.replace('Re: ', '') : ''); + } - // bcc automatically disables secure sending - if ($scope.bcc.filter(filterEmptyAddresses).length > 0) { - allSecure = false; - } + // fill text body + from = re.from[0].name || replyTo; + sentDate = $filter('date')(re.sentDate, 'EEEE, MMM d, yyyy h:mm a'); - if (allSecure) { - // send encrypted if all secure - $scope.okToSend = true; - $scope.sendBtnText = str.sendBtnSecure; - $scope.sendBtnSecure = true; - } else { - // send plaintext - $scope.okToSend = true; - $scope.sendBtnText = str.sendBtnClear; - $scope.sendBtnSecure = false; - } - }; + function createString(array) { + var str = ''; + array.forEach(function(to) { + str += (str) ? ', ' : ''; + str += ((to.name) ? to.name : to.address) + ' <' + to.address + '>'; + }); + return str; + } - // - // Editing attachments - // + if (forward) { + body = '\n\n' + + '---------- Forwarded message ----------\n' + + 'From: ' + re.from[0].name + ' <' + re.from[0].address + '>\n' + + 'Date: ' + sentDate + '\n' + + 'Subject: ' + re.subject + '\n' + + 'To: ' + createString(re.to) + '\n' + + ((re.cc && re.cc.length > 0) ? 'Cc: ' + createString(re.cc) + '\n' : '') + + '\n\n'; - $scope.remove = function(attachment) { - $scope.attachments.splice($scope.attachments.indexOf(attachment), 1); - }; + } else { + body = '\n\n' + sentDate + ' ' + from + ' wrote:\n> '; + } - // - // Editing email body - // + if (re.body) { + body += re.body.trim().split('\n').join('\n> ').replace(/ >/g, '>'); + $scope.body = body; + } + } - $scope.sendToOutbox = function() { - var email; + // + // Editing headers + // - // build email model for smtp-client - email = { - from: [{ - name: emailDao._account.realname, - address: emailDao._account.emailAddress - }], - to: $scope.to.filter(filterEmptyAddresses), - cc: $scope.cc.filter(filterEmptyAddresses), - bcc: $scope.bcc.filter(filterEmptyAddresses), - subject: $scope.subject.trim() ? $scope.subject.trim() : str.fallbackSubject, // Subject line, or the fallback subject, if nothing valid was entered - body: $scope.body.trim(), // use parsed plaintext body - attachments: $scope.attachments, - sentDate: new Date(), - headers: {} - }; + /** + * Verify email address and fetch its public key + */ + $scope.verify = function(recipient) { + if (!recipient) { + return; + } - if ($scope.inReplyTo) { - email.headers['in-reply-to'] = '<' + $scope.inReplyTo + '>'; - } + // set display to insecure while fetching keys + recipient.key = undefined; + recipient.secure = false; + $scope.checkSendStatus(); - if ($scope.references && $scope.references.length) { - email.headers.references = $scope.references.map(function(reference) { - return '<' + reference + '>'; - }).join(' '); - } + // verify email address + if (!util.validateEmailAddress(recipient.address)) { + recipient.secure = undefined; + $scope.checkSendStatus(); + return; + } - // close the writer - $scope.state.writer.close(); - - // persist the email to disk for later sending - outbox.put(email, function(err) { + // keychainDao is undefined in local dev environment + if (keychainDao) { + // check if to address is contained in known public keys + // when we write an email, we always need to work with the latest keys available + keychainDao.refreshKeyForUserId(recipient.address, function(err, key) { if (err) { $scope.onError(err); return; } - // if we need to synchronize replyTo.answered = true to imap, - // let's do that. otherwise, we're done - if (!$scope.replyTo || $scope.replyTo.answered) { - return; + if (key) { + // compare again since model could have changed during the roundtrip + var matchingUserId = _.findWhere(key.userIds, { + emailAddress: recipient.address + }); + // compare either primary userId or (if available) multiple IDs + if (key.userId === recipient.address || matchingUserId) { + recipient.key = key; + recipient.secure = true; + } } - $scope.replyTo.answered = true; - emailDao.setFlags({ - folder: currentFolder(), - message: $scope.replyTo - }, function(err) { - if (err && err.code !== 42) { - $scope.onError(err); - return; - } - - // offline or no error, let's apply the ui changes - $scope.$apply(); - }); + $scope.checkSendStatus(); + $scope.$digest(); }); - - }; - - // - // Tag input & Autocomplete - // - - $scope.tagStyle = function(recipient) { - var classes = ['label']; - if (recipient.secure === false) { - classes.push('label-primary'); - } - return classes; - }; - - $scope.lookupAddressBook = function(query) { - var deferred = $q.defer(); - - if (!$scope.addressBookCache) { - // populate address book cache - keychainDao.listLocalPublicKeys(function(err, keys) { - if (err) { - $scope.onError(err); - return; - } - - $scope.addressBookCache = keys.map(function(key) { - return { - address: key.userId - }; - }); - filter(); - }); - - } else { - filter(); - } - - // query address book cache - function filter() { - var addresses = $scope.addressBookCache.filter(function(i) { - return i.address.indexOf(query) !== -1; - }); - deferred.resolve(addresses); - } - - return deferred.promise; - }; - - // - // Helpers - // - - function currentFolder() { - return $scope.state.nav.currentFolder; - } - - /* - * Visitor to filter out objects without an address property, i.e. empty addresses - */ - function filterEmptyAddresses(addr) { - return !!addr.address; } }; + /** + * Check if it is ok to send an email depending on the invitation state of the addresses + */ + $scope.checkSendStatus = function() { + $scope.okToSend = false; + $scope.sendBtnText = undefined; + $scope.sendBtnSecure = undefined; + + var allSecure = true; + var numReceivers = 0; + + // count number of receivers and check security + $scope.to.forEach(check); + $scope.cc.forEach(check); + $scope.bcc.forEach(check); + + function check(recipient) { + // validate address + if (!util.validateEmailAddress(recipient.address)) { + return; + } + numReceivers++; + if (!recipient.secure) { + allSecure = false; + } + } + + // only allow sending if receviers exist + if (numReceivers < 1) { + return; + } + + // bcc automatically disables secure sending + if ($scope.bcc.filter(filterEmptyAddresses).length > 0) { + allSecure = false; + } + + if (allSecure) { + // send encrypted if all secure + $scope.okToSend = true; + $scope.sendBtnText = str.sendBtnSecure; + $scope.sendBtnSecure = true; + } else { + // send plaintext + $scope.okToSend = true; + $scope.sendBtnText = str.sendBtnClear; + $scope.sendBtnSecure = false; + } + }; // - // Directives + // Editing attachments // - var ngModule = angular.module('write', []); + $scope.remove = function(attachment) { + $scope.attachments.splice($scope.attachments.indexOf(attachment), 1); + }; - ngModule.directive('focusMe', function($timeout, $parse) { - return { - //scope: true, // optionally create a child scope - link: function(scope, element, attrs) { - var model = $parse(attrs.focusMe); - scope.$watch(model, function(value) { - if (value === true) { - $timeout(function() { - var el = element[0]; - el.focus(); - // set cursor to start of textarea - if (el.type === 'textarea') { - el.selectionStart = 0; - el.selectionEnd = 0; - } - }, 100); - } - }); - } + // + // Editing email body + // + + $scope.sendToOutbox = function() { + var email; + + // build email model for smtp-client + email = { + from: [{ + name: emailDao._account.realname, + address: emailDao._account.emailAddress + }], + to: $scope.to.filter(filterEmptyAddresses), + cc: $scope.cc.filter(filterEmptyAddresses), + bcc: $scope.bcc.filter(filterEmptyAddresses), + subject: $scope.subject.trim() ? $scope.subject.trim() : str.fallbackSubject, // Subject line, or the fallback subject, if nothing valid was entered + body: $scope.body.trim(), // use parsed plaintext body + attachments: $scope.attachments, + sentDate: new Date(), + headers: {} }; - }); - ngModule.directive('focusInput', function($timeout, $parse) { - return { - //scope: true, // optionally create a child scope - link: function(scope, element, attrs) { - var model = $parse(attrs.focusInput); - scope.$watch(model, function(value) { - if (value === true) { - $timeout(function() { - element.find('input').first().focus(); - }, 100); - } - }); + if ($scope.inReplyTo) { + email.headers['in-reply-to'] = '<' + $scope.inReplyTo + '>'; + } + + if ($scope.references && $scope.references.length) { + email.headers.references = $scope.references.map(function(reference) { + return '<' + reference + '>'; + }).join(' '); + } + + // close the writer + $scope.state.writer.close(); + + // persist the email to disk for later sending + outbox.put(email, function(err) { + if (err) { + $scope.onError(err); + return; } - }; - }); - ngModule.directive('focusInputOnClick', function() { - return { - //scope: true, // optionally create a child scope - link: function(scope, element) { - element.on('click', function() { - element.find('input').first().focus(); - }); + // if we need to synchronize replyTo.answered = true to imap, + // let's do that. otherwise, we're done + if (!$scope.replyTo || $scope.replyTo.answered) { + return; } - }; - }); - ngModule.directive('attachmentInput', function() { - return function(scope, elm) { - elm.on('change', function(e) { - for (var i = 0; i < e.target.files.length; i++) { - addAttachment(e.target.files.item(i)); + $scope.replyTo.answered = true; + emailDao.setFlags({ + folder: currentFolder(), + message: $scope.replyTo + }, function(err) { + if (err && err.code !== 42) { + $scope.onError(err); + return; + } + + // offline or no error, let's apply the ui changes + $scope.$apply(); + }); + }); + + }; + + // + // Tag input & Autocomplete + // + + $scope.tagStyle = function(recipient) { + var classes = ['label']; + if (recipient.secure === false) { + classes.push('label-primary'); + } + return classes; + }; + + $scope.lookupAddressBook = function(query) { + var deferred = $q.defer(); + + if (!$scope.addressBookCache) { + // populate address book cache + keychainDao.listLocalPublicKeys(function(err, keys) { + if (err) { + $scope.onError(err); + return; + } + + $scope.addressBookCache = keys.map(function(key) { + return { + address: key.userId + }; + }); + filter(); + }); + + } else { + filter(); + } + + // query address book cache + function filter() { + var addresses = $scope.addressBookCache.filter(function(i) { + return i.address.indexOf(query) !== -1; + }); + deferred.resolve(addresses); + } + + return deferred.promise; + }; + + // + // Helpers + // + + function currentFolder() { + return $scope.state.nav.currentFolder; + } + + /* + * Visitor to filter out objects without an address property, i.e. empty addresses + */ + function filterEmptyAddresses(addr) { + return !!addr.address; + } +}; + + +// +// Directives +// + +var ngModule = angular.module('write', []); + +ngModule.directive('focusMe', function($timeout, $parse) { + return { + //scope: true, // optionally create a child scope + link: function(scope, element, attrs) { + var model = $parse(attrs.focusMe); + scope.$watch(model, function(value) { + if (value === true) { + $timeout(function() { + var el = element[0]; + el.focus(); + // set cursor to start of textarea + if (el.type === 'textarea') { + el.selectionStart = 0; + el.selectionEnd = 0; + } + }, 100); } }); + } + }; +}); - function addAttachment(file) { - var reader = new FileReader(); - reader.onload = function(e) { - scope.attachments.push({ - filename: file.name, - mimeType: file.type, - content: new Uint8Array(e.target.result) - }); - scope.$digest(); - }; - reader.readAsArrayBuffer(file); - } - }; - }); - - ngModule.directive('attachmentBtn', function() { - return function(scope, elm) { - elm.on('click touchstart', function(e) { - e.preventDefault(); - document.querySelector('#attachment-input').click(); +ngModule.directive('focusInput', function($timeout, $parse) { + return { + //scope: true, // optionally create a child scope + link: function(scope, element, attrs) { + var model = $parse(attrs.focusInput); + scope.$watch(model, function(value) { + if (value === true) { + $timeout(function() { + element.find('input').first().focus(); + }, 100); + } }); - }; - }); + } + }; +}); - return WriteCtrl; -}); \ No newline at end of file +ngModule.directive('focusInputOnClick', function() { + return { + //scope: true, // optionally create a child scope + link: function(scope, element) { + element.on('click', function() { + element.find('input').first().focus(); + }); + } + }; +}); + +ngModule.directive('attachmentInput', function() { + return function(scope, elm) { + elm.on('change', function(e) { + for (var i = 0; i < e.target.files.length; i++) { + addAttachment(e.target.files.item(i)); + } + }); + + function addAttachment(file) { + var reader = new FileReader(); + reader.onload = function(e) { + scope.attachments.push({ + filename: file.name, + mimeType: file.type, + content: new Uint8Array(e.target.result) + }); + scope.$digest(); + }; + reader.readAsArrayBuffer(file); + } + }; +}); + +ngModule.directive('attachmentBtn', function() { + return function(scope, elm) { + elm.on('click touchstart', function(e) { + e.preventDefault(); + document.querySelector('#attachment-input').click(); + }); + }; +}); + +exports = WriteCtrl; \ No newline at end of file diff --git a/src/js/crypto/crypto.js b/src/js/crypto/crypto.js index 7b09f53..a2d964e 100644 --- a/src/js/crypto/crypto.js +++ b/src/js/crypto/crypto.js @@ -2,122 +2,121 @@ * High level crypto api that invokes native crypto (if available) and * gracefully degrades to JS crypto (if unavailable) */ -define(function(require) { - 'use strict'; - var aes = require('js/crypto/aes-gcm'), - pbkdf2 = require('js/crypto/pbkdf2'), - config = require('js/app-config').config, - axe = require('axe'); +'use strict'; - var PBKDF2_WORKER = '/crypto/pbkdf2-worker.js'; +var aes = require('crypto-lib').aes, + pbkdf2 = require('./pbkdf2'), + config = require('../app-config').config, + axe = require('axe-logger'); - var Crypto = function() {}; +var PBKDF2_WORKER = '/crypto/pbkdf2-worker.js'; - /** - * Encrypt plaintext using AES-GCM. - * @param {String} plaintext The input string in UTF-16 - * @param {String} key The base64 encoded key - * @param {String} iv The base64 encoded IV - * @param {Function} callback(error, ciphertext) - * @return {String} The base64 encoded ciphertext - */ - Crypto.prototype.encrypt = function(plaintext, key, iv, callback) { - var ct; +var Crypto = function() {}; - try { - ct = aes.encrypt(plaintext, key, iv); - } catch (err) { - callback(err); - return; +/** + * Encrypt plaintext using AES-GCM. + * @param {String} plaintext The input string in UTF-16 + * @param {String} key The base64 encoded key + * @param {String} iv The base64 encoded IV + * @param {Function} callback(error, ciphertext) + * @return {String} The base64 encoded ciphertext + */ +Crypto.prototype.encrypt = function(plaintext, key, iv, callback) { + var ct; + + try { + ct = aes.encrypt(plaintext, key, iv); + } catch (err) { + callback(err); + return; + } + + callback(null, ct); +}; + +/** + * Decrypt ciphertext suing AES-GCM + * @param {String} ciphertext The base64 encoded ciphertext + * @param {String} key The base64 encoded key + * @param {String} iv The base64 encoded IV + * @param {Function} callback(error, plaintext) + * @return {String} The decrypted plaintext in UTF-16 + */ +Crypto.prototype.decrypt = function(ciphertext, key, iv, callback) { + var pt; + + try { + pt = aes.decrypt(ciphertext, key, iv); + } catch (err) { + callback(err); + return; + } + + callback(null, pt); +}; + +/** + * Do PBKDF2 key derivation in a WebWorker thread + */ +Crypto.prototype.deriveKey = function(password, salt, keySize, callback) { + startWorker({ + script: PBKDF2_WORKER, + args: { + password: password, + salt: salt, + keySize: keySize + }, + callback: callback, + noWorker: function() { + return pbkdf2.getKey(password, salt, keySize); } + }); +}; - callback(null, ct); - }; +// +// helper functions +// - /** - * Decrypt ciphertext suing AES-GCM - * @param {String} ciphertext The base64 encoded ciphertext - * @param {String} key The base64 encoded key - * @param {String} iv The base64 encoded IV - * @param {Function} callback(error, plaintext) - * @return {String} The decrypted plaintext in UTF-16 - */ - Crypto.prototype.decrypt = function(ciphertext, key, iv, callback) { - var pt; - - try { - pt = aes.decrypt(ciphertext, key, iv); - } catch (err) { - callback(err); - return; - } - - callback(null, pt); - }; - - /** - * Do PBKDF2 key derivation in a WebWorker thread - */ - Crypto.prototype.deriveKey = function(password, salt, keySize, callback) { - startWorker({ - script: PBKDF2_WORKER, - args: { - password: password, - salt: salt, - keySize: keySize - }, - callback: callback, - noWorker: function() { - return pbkdf2.getKey(password, salt, keySize); - } - }); - }; - - // - // helper functions - // - - function startWorker(options) { - // check for WebWorker support - if (window.Worker) { - // init webworker thread - var worker = new Worker(config.workerPath + options.script); - worker.onmessage = function(e) { - if (e.data.err) { - options.callback(e.data.err); - return; - } - // return result from the worker - options.callback(null, e.data); - }; - worker.onerror = function(e) { - // show error message in logger - axe.error('Error handling web worker: Line ' + e.lineno + ' in ' + e.filename + ': ' + e.message); - // return error - options.callback({ - errMsg: (e.message) ? e.message : e - }); +function startWorker(options) { + // check for WebWorker support + if (window.Worker) { + // init webworker thread + var worker = new Worker(config.workerPath + options.script); + worker.onmessage = function(e) { + if (e.data.err) { + options.callback(e.data.err); return; - }; - // send data to the worker - worker.postMessage(options.args); - return; - } - - // no WebWorker support... do synchronous call - var result; - try { - result = options.noWorker(); - } catch (e) { + } + // return result from the worker + options.callback(null, e.data); + }; + worker.onerror = function(e) { + // show error message in logger + axe.error('Error handling web worker: Line ' + e.lineno + ' in ' + e.filename + ': ' + e.message); // return error options.callback({ errMsg: (e.message) ? e.message : e }); return; - } - options.callback(null, result); + }; + // send data to the worker + worker.postMessage(options.args); + return; } - return Crypto; -}); \ No newline at end of file + // no WebWorker support... do synchronous call + var result; + try { + result = options.noWorker(); + } catch (e) { + // return error + options.callback({ + errMsg: (e.message) ? e.message : e + }); + return; + } + options.callback(null, result); +} + +exports = Crypto; \ No newline at end of file diff --git a/src/js/crypto/pbkdf2-worker.js b/src/js/crypto/pbkdf2-worker.js index fd71a41..665ddf0 100644 --- a/src/js/crypto/pbkdf2-worker.js +++ b/src/js/crypto/pbkdf2-worker.js @@ -1,38 +1,23 @@ -(function() { - 'use strict'; +'use strict'; - // import web worker dependencies - importScripts('../../lib/require.js'); +var pbkdf2 = require('./pbkdf2'); - /** - * In the web worker thread context, 'this' and 'self' can be used as a global - * variable namespace similar to the 'window' object in the main thread - */ - self.onmessage = function(e) { - // fetch dependencies via require.js - require(['../../require-config'], function() { - require.config({ - baseUrl: '../../lib' - }); +/** + * In the web worker thread context, 'this' and 'self' can be used as a global + * variable namespace similar to the 'window' object in the main thread + */ +self.onmessage = function(e) { + var i = e.data, + key = null; - require(['js/crypto/pbkdf2'], function(pbkdf2) { + if (i.password && i.salt && i.keySize) { + // start deriving key + key = pbkdf2.getKey(i.password, i.salt, i.keySize); - var i = e.data, - key = null; + } else { + throw 'Not all arguments for web worker crypto are defined!'; + } - if (i.password && i.salt && i.keySize) { - // start deriving key - key = pbkdf2.getKey(i.password, i.salt, i.keySize); - - } else { - throw 'Not all arguments for web worker crypto are defined!'; - } - - // pass output back to main thread - self.postMessage(key); - - }); - }); - }; - -}()); \ No newline at end of file + // pass output back to main thread + self.postMessage(key); +}; \ No newline at end of file diff --git a/src/js/crypto/pbkdf2.js b/src/js/crypto/pbkdf2.js index 3a0b12a..e2d31f4 100644 --- a/src/js/crypto/pbkdf2.js +++ b/src/js/crypto/pbkdf2.js @@ -1,25 +1,24 @@ /** * A Wrapper for Forge's PBKDF2 function */ -define(['forge'], function(forge) { - 'use strict'; - var self = {}; +'use strict'; - /** - * PBKDF2-HMAC-SHA256 key derivation with a random salt and 10000 iterations - * @param {String} password The password in UTF8 - * @param {String} salt The base64 encoded salt - * @param {String} keySize The key size in bits - * @return {String} The base64 encoded key - */ - self.getKey = function(password, salt, keySize) { - var saltUtf8 = forge.util.decode64(salt); - var md = forge.md.sha256.create(); - var key = forge.pkcs5.pbkdf2(password, saltUtf8, 10000, keySize / 8, md); +var self = {}; - return forge.util.encode64(key); - }; +/** + * PBKDF2-HMAC-SHA256 key derivation with a random salt and 10000 iterations + * @param {String} password The password in UTF8 + * @param {String} salt The base64 encoded salt + * @param {String} keySize The key size in bits + * @return {String} The base64 encoded key + */ +self.getKey = function(password, salt, keySize) { + var saltUtf8 = forge.util.decode64(salt); + var md = forge.md.sha256.create(); + var key = forge.pkcs5.pbkdf2(password, saltUtf8, 10000, keySize / 8, md); - return self; -}); \ No newline at end of file + return forge.util.encode64(key); +}; + +exports = self; \ No newline at end of file diff --git a/src/js/crypto/pgp.js b/src/js/crypto/pgp.js index 2a50b80..63a84b7 100644 --- a/src/js/crypto/pgp.js +++ b/src/js/crypto/pgp.js @@ -1,452 +1,450 @@ /** * High level crypto api that handles all calls to OpenPGP.js */ -define(function(require) { - 'use strict'; - var openpgp = require('openpgp'), - util = require('openpgp').util, - config = require('js/app-config').config; +'use strict'; - var PGP = function() { - openpgp.config.prefer_hash_algorithm = openpgp.enums.hash.sha256; - openpgp.initWorker(config.workerPath + '/../lib/openpgp/openpgp.worker.js'); - }; +var util = openpgp.util, + config = require('../app-config').config; - /** - * Generate a key pair for the user - */ - PGP.prototype.generateKeys = function(options, callback) { - var userId, passphrase; +var PGP = function() { + openpgp.config.prefer_hash_algorithm = openpgp.enums.hash.sha256; + openpgp.initWorker(config.workerPath + '/../lib/openpgp/openpgp.worker.js'); +}; - if (!util.emailRegEx.test(options.emailAddress) || !options.keySize) { - callback(new Error('Crypto init failed. Not all options set!')); - return; - } +/** + * Generate a key pair for the user + */ +PGP.prototype.generateKeys = function(options, callback) { + var userId, passphrase; - // generate keypair - userId = 'Whiteout User <' + options.emailAddress + '>'; - passphrase = (options.passphrase) ? options.passphrase : undefined; - openpgp.generateKeyPair({ - keyType: 1, // (keytype 1=RSA) - numBits: options.keySize, - userId: userId, - passphrase: passphrase - }, onGenerated); + if (!util.emailRegEx.test(options.emailAddress) || !options.keySize) { + callback(new Error('Crypto init failed. Not all options set!')); + return; + } - function onGenerated(err, keys) { - if (err) { - callback(new Error('Keygeneration failed!')); - return; - } + // generate keypair + userId = 'Whiteout User <' + options.emailAddress + '>'; + passphrase = (options.passphrase) ? options.passphrase : undefined; + openpgp.generateKeyPair({ + keyType: 1, // (keytype 1=RSA) + numBits: options.keySize, + userId: userId, + passphrase: passphrase + }, onGenerated); - callback(null, { - keyId: keys.key.primaryKey.getKeyId().toHex().toUpperCase(), - privateKeyArmored: keys.privateKeyArmored, - publicKeyArmored: keys.publicKeyArmored - }); - } - }; - - /** - * Show a user's fingerprint - */ - PGP.prototype.getFingerprint = function(keyArmored) { - function fingerprint(key) { - return key.primaryKey.getFingerprint().toUpperCase(); - } - - // process armored key input - if (keyArmored) { - return fingerprint(openpgp.key.readArmored(keyArmored).keys[0]); - } - - if (!this._publicKey) { - throw new Error('No public key set for fingerprint generation!'); - } - - // get local fingerpring - return fingerprint(this._publicKey); - }; - - /** - * Show a user's key id. - */ - PGP.prototype.getKeyId = function(keyArmored) { - var key, pubKeyId, privKeyId; - - // process armored key input - if (keyArmored) { - key = openpgp.key.readArmored(keyArmored).keys[0]; - return key.primaryKey.getKeyId().toHex().toUpperCase(); - } - - // check already imported keys - if (!this._privateKey || !this._publicKey) { - throw new Error('Cannot read key IDs... keys not set!'); - } - - pubKeyId = this._publicKey.primaryKey.getKeyId().toHex().toUpperCase(); - privKeyId = this._privateKey.primaryKey.getKeyId().toHex().toUpperCase(); - - if (!pubKeyId || !privKeyId || pubKeyId !== privKeyId) { - throw new Error('Key IDs do not match!'); - } - - return pubKeyId; - }; - - /** - * Read all relevant params of an armored key. - */ - PGP.prototype.getKeyParams = function(keyArmored) { - var key, packet, userIds; - - // process armored key input - if (keyArmored) { - key = openpgp.key.readArmored(keyArmored).keys[0]; - } else if (this._publicKey) { - key = this._publicKey; - } else { - throw new Error('Cannot read key params... keys not set!'); - } - - packet = key.primaryKey; - - // read user names and email addresses - userIds = []; - key.getUserIds().forEach(function(userId) { - userIds.push({ - name: userId.split('<')[0].trim(), - emailAddress: userId.split('<')[1].split('>')[0].trim() - }); - }); - - return { - _id: packet.getKeyId().toHex().toUpperCase(), - userId: userIds[0].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, - bitSize: packet.getBitSize(), - created: packet.created, - }; - }; - - /** - * Extract a public key from a private key - * @param {String} privateKeyArmored The private PGP key block - * @return {String} The publick PGP key block - */ - PGP.prototype.extractPublicKey = function(privateKeyArmored) { - var privkey = openpgp.key.readArmored(privateKeyArmored).keys[0]; - var pubkey = privkey.toPublic(); - return pubkey.armor(); - }; - - /** - * Import the user's key pair - */ - PGP.prototype.importKeys = function(options, callback) { - var pubKeyId, privKeyId, self = this; - - // check options - if (!options.privateKeyArmored || !options.publicKeyArmored) { - callback(new Error('Importing keys failed. Not all options set!')); - return; - } - - function resetKeys() { - self._publicKey = undefined; - self._privateKey = undefined; - } - - // read armored keys - try { - this._publicKey = openpgp.key.readArmored(options.publicKeyArmored).keys[0]; - this._privateKey = openpgp.key.readArmored(options.privateKeyArmored).keys[0]; - } catch (e) { - resetKeys(); - callback(new Error('Importing keys failed. Parsing error!')); - return; - } - - // decrypt private key with passphrase - if (!this._privateKey.decrypt(options.passphrase)) { - resetKeys(); - callback(new Error('Incorrect passphrase!')); - return; - } - - // check if keys have the same id - pubKeyId = this._publicKey.primaryKey.getKeyId().toHex(); - privKeyId = this._privateKey.primaryKey.getKeyId().toHex(); - if (!pubKeyId || !privKeyId || pubKeyId !== privKeyId) { - resetKeys(); - callback(new Error('Key IDs dont match!')); - return; - } - - callback(); - }; - - /** - * Export the user's key pair - */ - PGP.prototype.exportKeys = function(callback) { - if (!this._publicKey || !this._privateKey) { - callback(new Error('Could not export keys!')); + function onGenerated(err, keys) { + if (err) { + callback(new Error('Keygeneration failed!')); return; } callback(null, { - keyId: this._publicKey.primaryKey.getKeyId().toHex().toUpperCase(), - privateKeyArmored: this._privateKey.armor(), - publicKeyArmored: this._publicKey.armor() + keyId: keys.key.primaryKey.getKeyId().toHex().toUpperCase(), + privateKeyArmored: keys.privateKeyArmored, + publicKeyArmored: keys.publicKeyArmored }); - }; + } +}; - /** - * Change the passphrase of an ascii armored private key. - */ - PGP.prototype.changePassphrase = function(options, callback) { - var privKey, packets, newPassphrase, newKeyArmored; - - // set undefined instead of empty string as passphrase - newPassphrase = (options.newPassphrase) ? options.newPassphrase : undefined; - - if (!options.privateKeyArmored) { - callback(new Error('Private key must be specified to change passphrase!')); - return; - } - - if (options.oldPassphrase === newPassphrase || - (!options.oldPassphrase && !newPassphrase)) { - callback(new Error('New and old passphrase are the same!')); - return; - } - - // read armored key - try { - privKey = openpgp.key.readArmored(options.privateKeyArmored).keys[0]; - } catch (e) { - callback(new Error('Importing key failed. Parsing error!')); - return; - } - - // decrypt private key with passphrase - if (!privKey.decrypt(options.oldPassphrase)) { - callback(new Error('Old passphrase incorrect!')); - return; - } - - // encrypt key with new passphrase - try { - packets = privKey.getAllKeyPackets(); - for (var i = 0; i < packets.length; i++) { - packets[i].encrypt(newPassphrase); - } - newKeyArmored = privKey.armor(); - } catch (e) { - callback(new Error('Setting new passphrase failed!')); - return; - } - - // check if new passphrase really works - if (!privKey.decrypt(newPassphrase)) { - callback(new Error('Decrypting key with new passphrase failed!')); - return; - } - - callback(null, newKeyArmored); - }; - - /** - * Encrypt and sign a pgp message for a list of receivers - */ - PGP.prototype.encrypt = function(plaintext, publicKeysArmored, callback) { - var publicKeys; - - // check keys - if (!this._privateKey) { - callback(new Error('Error encrypting. Keys must be set!')); - return; - } - - // parse armored public keys - try { - if (publicKeysArmored && publicKeysArmored.length) { - publicKeys = []; - publicKeysArmored.forEach(function(pubkeyArmored) { - publicKeys = publicKeys.concat(openpgp.key.readArmored(pubkeyArmored).keys); - }); - } - } catch (err) { - callback(new Error('Error encrypting plaintext!')); - return; - } - - if (publicKeys) { - // encrypt and sign the plaintext - openpgp.signAndEncryptMessage(publicKeys, this._privateKey, plaintext, callback); - } else { - // if no public keys are available encrypt for myself - openpgp.signAndEncryptMessage([this._publicKey], this._privateKey, plaintext, callback); - } - }; - - /** - * Decrypts a ciphertext - * @param {String} ciphertext The encrypted PGP message block - * @param {String} publicKeyArmored The public key used to sign the message - * @param {Function} callback(error, plaintext, signaturesValid) signaturesValid is undefined in case there are no signature, null in case there are signatures but the wrong public key or no key was used to verify, true if the signature was successfully verified, or false if the signataure verification failed. - */ - PGP.prototype.decrypt = function(ciphertext, publicKeyArmored, callback) { - var publicKeys, message; - - // check keys - if (!this._privateKey) { - callback(new Error('Error decrypting. Keys must be set!')); - return; - } - - // read keys and ciphertext message - try { - if (publicKeyArmored) { - // parse public keys if available ... - publicKeys = openpgp.key.readArmored(publicKeyArmored).keys; - } else { - // use own public key to know if signatures are available - publicKeys = [this._publicKey]; - } - message = openpgp.message.readArmored(ciphertext); - } catch (err) { - callback(new Error('Error parsing encrypted PGP message!')); - return; - } - - // decrypt and verify pgp message - openpgp.decryptAndVerifyMessage(this._privateKey, publicKeys, message, onDecrypted); - - function onDecrypted(err, decrypted) { - if (err) { - callback(new Error('Error decrypting and verifying PGP message!')); - return; - } - - // return decrypted plaintext - callback(null, decrypted.text, checkSignatureValidity(decrypted.signatures)); - } - }; - - /** - * Verifies a clearsigned message - * @param {String} clearSignedText The clearsigned text, usually from a signed pgp/inline message - * @param {String} publicKeyArmored The public key used to signed the message - * @param {Function} callback(error, signaturesValid) signaturesValid is undefined in case there are no signature, null in case there are signatures but the wrong public key or no key was used to verify, true if the signature was successfully verified, or false if the signataure verification failed. - */ - PGP.prototype.verifyClearSignedMessage = function(clearSignedText, publicKeyArmored, callback) { - var publicKeys, - message; - - // check keys - if (!this._privateKey) { - callback(new Error('Error verifying signed PGP message. Keys must be set!')); - return; - } - - // read keys and ciphertext message - try { - if (publicKeyArmored) { - // parse public keys if available ... - publicKeys = openpgp.key.readArmored(publicKeyArmored).keys; - } else { - // use own public key to know if signatures are available - publicKeys = [this._publicKey]; - } - message = openpgp.cleartext.readArmored(clearSignedText); - } catch (err) { - callback(new Error('Error verifying signed PGP message!')); - return; - } - - openpgp.verifyClearSignedMessage(publicKeys, message, function(err, result) { - if (err) { - callback(new Error('Error verifying PGP message!')); - return; - } - - callback(null, checkSignatureValidity(result.signatures)); - }); - }; - - /** - * Verifies a message with a detached signature - * @param {String} message The signed text, usually from a signed pgp/mime message - * @param {String} pgpSignature The detached signature, usually from a signed pgp/mime message - * @param {String} publicKeyArmored The public key used to signed the message - * @param {Function} callback(error, signaturesValid) signaturesValid is undefined in case there are no signature, null in case there are signatures but the wrong public key or no key was used to verify, true if the signature was successfully verified, or false if the signataure verification failed. - */ - PGP.prototype.verifySignedMessage = function(message, pgpSignature, publicKeyArmored, callback) { - var publicKeys; - - // check keys - if (!this._privateKey) { - callback(new Error('Error verifying signed PGP message. Keys must be set!')); - return; - } - - // read keys and ciphertext message - try { - if (publicKeyArmored) { - // parse public keys if available ... - publicKeys = openpgp.key.readArmored(publicKeyArmored).keys; - } else { - // use own public key to know if signatures are available - publicKeys = [this._publicKey]; - } - } catch (err) { - callback(new Error('Error verifying signed PGP message!')); - return; - } - - var signatures; - try { - var msg = openpgp.message.readSignedContent(message, pgpSignature); - signatures = msg.verify(publicKeys); - } catch (err) { - callback(new Error('Error verifying signed PGP message!')); - return; - } - - callback(null, checkSignatureValidity(signatures)); - }; - - /** - * Checks signature validity - * @param {Object} decrypted OpenPGP.js Signature array - * @return {undefined|null|true|false} - * If signatures array is empty (the message was not signed), returns undefined - * If you're using the wrong public key, returns null. - * If signatures are invalid, returns false. - * If everything is in order, returns true - */ - function checkSignatureValidity(signatures) { - if (!signatures.length) { - // signatures array is empty (the message was not signed) - return; - } - - for (var i = 0; i < signatures.length; i++) { - if (signatures[i].valid !== true) { // null | false - // you're using the wrong public key or signatures are invalid - return signatures[i].valid; - } - } - - // everything is in order - return true; +/** + * Show a user's fingerprint + */ +PGP.prototype.getFingerprint = function(keyArmored) { + function fingerprint(key) { + return key.primaryKey.getFingerprint().toUpperCase(); } - return PGP; -}); \ No newline at end of file + // process armored key input + if (keyArmored) { + return fingerprint(openpgp.key.readArmored(keyArmored).keys[0]); + } + + if (!this._publicKey) { + throw new Error('No public key set for fingerprint generation!'); + } + + // get local fingerpring + return fingerprint(this._publicKey); +}; + +/** + * Show a user's key id. + */ +PGP.prototype.getKeyId = function(keyArmored) { + var key, pubKeyId, privKeyId; + + // process armored key input + if (keyArmored) { + key = openpgp.key.readArmored(keyArmored).keys[0]; + return key.primaryKey.getKeyId().toHex().toUpperCase(); + } + + // check already imported keys + if (!this._privateKey || !this._publicKey) { + throw new Error('Cannot read key IDs... keys not set!'); + } + + pubKeyId = this._publicKey.primaryKey.getKeyId().toHex().toUpperCase(); + privKeyId = this._privateKey.primaryKey.getKeyId().toHex().toUpperCase(); + + if (!pubKeyId || !privKeyId || pubKeyId !== privKeyId) { + throw new Error('Key IDs do not match!'); + } + + return pubKeyId; +}; + +/** + * Read all relevant params of an armored key. + */ +PGP.prototype.getKeyParams = function(keyArmored) { + var key, packet, userIds; + + // process armored key input + if (keyArmored) { + key = openpgp.key.readArmored(keyArmored).keys[0]; + } else if (this._publicKey) { + key = this._publicKey; + } else { + throw new Error('Cannot read key params... keys not set!'); + } + + packet = key.primaryKey; + + // read user names and email addresses + userIds = []; + key.getUserIds().forEach(function(userId) { + userIds.push({ + name: userId.split('<')[0].trim(), + emailAddress: userId.split('<')[1].split('>')[0].trim() + }); + }); + + return { + _id: packet.getKeyId().toHex().toUpperCase(), + userId: userIds[0].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, + bitSize: packet.getBitSize(), + created: packet.created, + }; +}; + +/** + * Extract a public key from a private key + * @param {String} privateKeyArmored The private PGP key block + * @return {String} The publick PGP key block + */ +PGP.prototype.extractPublicKey = function(privateKeyArmored) { + var privkey = openpgp.key.readArmored(privateKeyArmored).keys[0]; + var pubkey = privkey.toPublic(); + return pubkey.armor(); +}; + +/** + * Import the user's key pair + */ +PGP.prototype.importKeys = function(options, callback) { + var pubKeyId, privKeyId, self = this; + + // check options + if (!options.privateKeyArmored || !options.publicKeyArmored) { + callback(new Error('Importing keys failed. Not all options set!')); + return; + } + + function resetKeys() { + self._publicKey = undefined; + self._privateKey = undefined; + } + + // read armored keys + try { + this._publicKey = openpgp.key.readArmored(options.publicKeyArmored).keys[0]; + this._privateKey = openpgp.key.readArmored(options.privateKeyArmored).keys[0]; + } catch (e) { + resetKeys(); + callback(new Error('Importing keys failed. Parsing error!')); + return; + } + + // decrypt private key with passphrase + if (!this._privateKey.decrypt(options.passphrase)) { + resetKeys(); + callback(new Error('Incorrect passphrase!')); + return; + } + + // check if keys have the same id + pubKeyId = this._publicKey.primaryKey.getKeyId().toHex(); + privKeyId = this._privateKey.primaryKey.getKeyId().toHex(); + if (!pubKeyId || !privKeyId || pubKeyId !== privKeyId) { + resetKeys(); + callback(new Error('Key IDs dont match!')); + return; + } + + callback(); +}; + +/** + * Export the user's key pair + */ +PGP.prototype.exportKeys = function(callback) { + if (!this._publicKey || !this._privateKey) { + callback(new Error('Could not export keys!')); + return; + } + + callback(null, { + keyId: this._publicKey.primaryKey.getKeyId().toHex().toUpperCase(), + privateKeyArmored: this._privateKey.armor(), + publicKeyArmored: this._publicKey.armor() + }); +}; + +/** + * Change the passphrase of an ascii armored private key. + */ +PGP.prototype.changePassphrase = function(options, callback) { + var privKey, packets, newPassphrase, newKeyArmored; + + // set undefined instead of empty string as passphrase + newPassphrase = (options.newPassphrase) ? options.newPassphrase : undefined; + + if (!options.privateKeyArmored) { + callback(new Error('Private key must be specified to change passphrase!')); + return; + } + + if (options.oldPassphrase === newPassphrase || + (!options.oldPassphrase && !newPassphrase)) { + callback(new Error('New and old passphrase are the same!')); + return; + } + + // read armored key + try { + privKey = openpgp.key.readArmored(options.privateKeyArmored).keys[0]; + } catch (e) { + callback(new Error('Importing key failed. Parsing error!')); + return; + } + + // decrypt private key with passphrase + if (!privKey.decrypt(options.oldPassphrase)) { + callback(new Error('Old passphrase incorrect!')); + return; + } + + // encrypt key with new passphrase + try { + packets = privKey.getAllKeyPackets(); + for (var i = 0; i < packets.length; i++) { + packets[i].encrypt(newPassphrase); + } + newKeyArmored = privKey.armor(); + } catch (e) { + callback(new Error('Setting new passphrase failed!')); + return; + } + + // check if new passphrase really works + if (!privKey.decrypt(newPassphrase)) { + callback(new Error('Decrypting key with new passphrase failed!')); + return; + } + + callback(null, newKeyArmored); +}; + +/** + * Encrypt and sign a pgp message for a list of receivers + */ +PGP.prototype.encrypt = function(plaintext, publicKeysArmored, callback) { + var publicKeys; + + // check keys + if (!this._privateKey) { + callback(new Error('Error encrypting. Keys must be set!')); + return; + } + + // parse armored public keys + try { + if (publicKeysArmored && publicKeysArmored.length) { + publicKeys = []; + publicKeysArmored.forEach(function(pubkeyArmored) { + publicKeys = publicKeys.concat(openpgp.key.readArmored(pubkeyArmored).keys); + }); + } + } catch (err) { + callback(new Error('Error encrypting plaintext!')); + return; + } + + if (publicKeys) { + // encrypt and sign the plaintext + openpgp.signAndEncryptMessage(publicKeys, this._privateKey, plaintext, callback); + } else { + // if no public keys are available encrypt for myself + openpgp.signAndEncryptMessage([this._publicKey], this._privateKey, plaintext, callback); + } +}; + +/** + * Decrypts a ciphertext + * @param {String} ciphertext The encrypted PGP message block + * @param {String} publicKeyArmored The public key used to sign the message + * @param {Function} callback(error, plaintext, signaturesValid) signaturesValid is undefined in case there are no signature, null in case there are signatures but the wrong public key or no key was used to verify, true if the signature was successfully verified, or false if the signataure verification failed. + */ +PGP.prototype.decrypt = function(ciphertext, publicKeyArmored, callback) { + var publicKeys, message; + + // check keys + if (!this._privateKey) { + callback(new Error('Error decrypting. Keys must be set!')); + return; + } + + // read keys and ciphertext message + try { + if (publicKeyArmored) { + // parse public keys if available ... + publicKeys = openpgp.key.readArmored(publicKeyArmored).keys; + } else { + // use own public key to know if signatures are available + publicKeys = [this._publicKey]; + } + message = openpgp.message.readArmored(ciphertext); + } catch (err) { + callback(new Error('Error parsing encrypted PGP message!')); + return; + } + + // decrypt and verify pgp message + openpgp.decryptAndVerifyMessage(this._privateKey, publicKeys, message, onDecrypted); + + function onDecrypted(err, decrypted) { + if (err) { + callback(new Error('Error decrypting and verifying PGP message!')); + return; + } + + // return decrypted plaintext + callback(null, decrypted.text, checkSignatureValidity(decrypted.signatures)); + } +}; + +/** + * Verifies a clearsigned message + * @param {String} clearSignedText The clearsigned text, usually from a signed pgp/inline message + * @param {String} publicKeyArmored The public key used to signed the message + * @param {Function} callback(error, signaturesValid) signaturesValid is undefined in case there are no signature, null in case there are signatures but the wrong public key or no key was used to verify, true if the signature was successfully verified, or false if the signataure verification failed. + */ +PGP.prototype.verifyClearSignedMessage = function(clearSignedText, publicKeyArmored, callback) { + var publicKeys, + message; + + // check keys + if (!this._privateKey) { + callback(new Error('Error verifying signed PGP message. Keys must be set!')); + return; + } + + // read keys and ciphertext message + try { + if (publicKeyArmored) { + // parse public keys if available ... + publicKeys = openpgp.key.readArmored(publicKeyArmored).keys; + } else { + // use own public key to know if signatures are available + publicKeys = [this._publicKey]; + } + message = openpgp.cleartext.readArmored(clearSignedText); + } catch (err) { + callback(new Error('Error verifying signed PGP message!')); + return; + } + + openpgp.verifyClearSignedMessage(publicKeys, message, function(err, result) { + if (err) { + callback(new Error('Error verifying PGP message!')); + return; + } + + callback(null, checkSignatureValidity(result.signatures)); + }); +}; + +/** + * Verifies a message with a detached signature + * @param {String} message The signed text, usually from a signed pgp/mime message + * @param {String} pgpSignature The detached signature, usually from a signed pgp/mime message + * @param {String} publicKeyArmored The public key used to signed the message + * @param {Function} callback(error, signaturesValid) signaturesValid is undefined in case there are no signature, null in case there are signatures but the wrong public key or no key was used to verify, true if the signature was successfully verified, or false if the signataure verification failed. + */ +PGP.prototype.verifySignedMessage = function(message, pgpSignature, publicKeyArmored, callback) { + var publicKeys; + + // check keys + if (!this._privateKey) { + callback(new Error('Error verifying signed PGP message. Keys must be set!')); + return; + } + + // read keys and ciphertext message + try { + if (publicKeyArmored) { + // parse public keys if available ... + publicKeys = openpgp.key.readArmored(publicKeyArmored).keys; + } else { + // use own public key to know if signatures are available + publicKeys = [this._publicKey]; + } + } catch (err) { + callback(new Error('Error verifying signed PGP message!')); + return; + } + + var signatures; + try { + var msg = openpgp.message.readSignedContent(message, pgpSignature); + signatures = msg.verify(publicKeys); + } catch (err) { + callback(new Error('Error verifying signed PGP message!')); + return; + } + + callback(null, checkSignatureValidity(signatures)); +}; + +/** + * Checks signature validity + * @param {Object} decrypted OpenPGP.js Signature array + * @return {undefined|null|true|false} + * If signatures array is empty (the message was not signed), returns undefined + * If you're using the wrong public key, returns null. + * If signatures are invalid, returns false. + * If everything is in order, returns true + */ +function checkSignatureValidity(signatures) { + if (!signatures.length) { + // signatures array is empty (the message was not signed) + return; + } + + for (var i = 0; i < signatures.length; i++) { + if (signatures[i].valid !== true) { // null | false + // you're using the wrong public key or signatures are invalid + return signatures[i].valid; + } + } + + // everything is in order + return true; +} + +exports = PGP; \ No newline at end of file diff --git a/src/js/dao/admin-dao.js b/src/js/dao/admin-dao.js index dfd7d4f..0a60e9e 100644 --- a/src/js/dao/admin-dao.js +++ b/src/js/dao/admin-dao.js @@ -1,63 +1,61 @@ -define(function() { - 'use strict'; +'use strict'; - var AdminDAO = function(restDao) { - this._restDao = restDao; - }; +var AdminDAO = function(restDao) { + this._restDao = restDao; +}; - /** - * Create a new email account. - * @param {String} options.emailAddress The desired email address - * @param {String} options.password The password to be used for the account. - * @param {String} options.phone The user's mobile phone number (required for verification and password reset). - * @param {Function} callback(error) - */ - AdminDAO.prototype.createUser = function(options, callback) { - var uri; +/** + * Create a new email account. + * @param {String} options.emailAddress The desired email address + * @param {String} options.password The password to be used for the account. + * @param {String} options.phone The user's mobile phone number (required for verification and password reset). + * @param {Function} callback(error) + */ +AdminDAO.prototype.createUser = function(options, callback) { + var uri; - if (!options.emailAddress || !options.password || !options.phone) { - callback(new Error('Incomplete arguments!')); + if (!options.emailAddress || !options.password || !options.phone) { + callback(new Error('Incomplete arguments!')); + return; + } + + uri = '/user'; + this._restDao.post(options, uri, function(err) { + if (err && err.code === 409) { + callback(new Error('User name is already taken!')); + return; + } else if (err) { + callback(new Error('Error creating new user!')); return; } - uri = '/user'; - this._restDao.post(options, uri, function(err) { - if (err && err.code === 409) { - callback(new Error('User name is already taken!')); - return; - } else if (err) { - callback(new Error('Error creating new user!')); - return; - } + callback(); + }); +}; +/** + * Verify a user's phone number by confirming a token to the server. + * @param {String} options.emailAddress The desired email address + * @param {String} options.token The validation token. + * @param {Function} callback(error) + */ +AdminDAO.prototype.validateUser = function(options, callback) { + var uri; + + if (!options.emailAddress || !options.token) { + callback(new Error('Incomplete arguments!')); + return; + } + + uri = '/user/validate'; + this._restDao.post(options, uri, function(err) { + if (!err || (err && err.code === 202)) { + // success callback(); - }); - }; - - /** - * Verify a user's phone number by confirming a token to the server. - * @param {String} options.emailAddress The desired email address - * @param {String} options.token The validation token. - * @param {Function} callback(error) - */ - AdminDAO.prototype.validateUser = function(options, callback) { - var uri; - - if (!options.emailAddress || !options.token) { - callback(new Error('Incomplete arguments!')); - return; + } else { + callback(new Error('Validation failed!')); } + }); +}; - uri = '/user/validate'; - this._restDao.post(options, uri, function(err) { - if (!err || (err && err.code === 202)) { - // success - callback(); - } else { - callback(new Error('Validation failed!')); - } - }); - }; - - return AdminDAO; -}); \ No newline at end of file +exports = AdminDAO; \ No newline at end of file diff --git a/src/js/dao/devicestorage-dao.js b/src/js/dao/devicestorage-dao.js index 9522ead..0b2b9a0 100644 --- a/src/js/dao/devicestorage-dao.js +++ b/src/js/dao/devicestorage-dao.js @@ -1,97 +1,93 @@ /** - * High level storage api that handles all persistence on the device. If - * SQLcipher/SQLite is available, all data is securely persisted there, - * through transparent encryption. If not, the crypto API is - * used to encrypt data on the fly before persisting via a JSON store. + * High level storage api that handles all persistence on the device. */ -define(function() { - 'use strict'; - var DeviceStorageDAO = function(localDbDao) { - this._localDbDao = localDbDao; - }; +'use strict'; - DeviceStorageDAO.prototype.init = function(emailAddress, callback) { - this._localDbDao.init(emailAddress, callback); - }; +var DeviceStorageDAO = function(localDbDao) { + this._localDbDao = localDbDao; +}; - /** - * Stores a list of encrypted items in the object store - * @param list [Array] The list of items to be persisted - * @param type [String] The type of item to be persisted e.g. 'email' - */ - DeviceStorageDAO.prototype.storeList = function(list, type, callback) { - var key, items = []; +DeviceStorageDAO.prototype.init = function(emailAddress, callback) { + this._localDbDao.init(emailAddress, callback); +}; - // nothing to store - if (!list || list.length === 0) { - callback(); - return; - } - // validate type - if (!type) { - callback({ - errMsg: 'Type is not set!' - }); - return; - } +/** + * Stores a list of encrypted items in the object store + * @param list [Array] The list of items to be persisted + * @param type [String] The type of item to be persisted e.g. 'email' + */ +DeviceStorageDAO.prototype.storeList = function(list, type, callback) { + var key, items = []; - // format items for batch storing in dao - list.forEach(function(i) { - key = createKey(i, type); - - items.push({ - key: key, - object: i - }); + // nothing to store + if (!list || list.length === 0) { + callback(); + return; + } + // validate type + if (!type) { + callback({ + errMsg: 'Type is not set!' }); - - this._localDbDao.batch(items, callback); - }; - - /** - * Deletes items of a certain type from storage - */ - DeviceStorageDAO.prototype.removeList = function(type, callback) { - this._localDbDao.removeList(type, callback); - }; - - /** - * 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) - */ - DeviceStorageDAO.prototype.listItems = function(type, offset, num, callback) { - // fetch all items of a certain type from the data-store - this._localDbDao.list(type, offset, num, callback); - }; - - /** - * Clear the whole device data-store - */ - DeviceStorageDAO.prototype.clear = function(callback) { - this._localDbDao.clear(callback); - }; - - // - // helper functions - // - - function createKey(i, type) { - var key; - - // put uid in key if available... for easy querying - if (i.uid) { - key = type + '_' + i.uid; - } else if (i.id) { - key = type + '_' + i.id; - } else { - key = type; - } - - return key; + return; } - return DeviceStorageDAO; -}); \ No newline at end of file + // format items for batch storing in dao + list.forEach(function(i) { + key = createKey(i, type); + + items.push({ + key: key, + object: i + }); + }); + + this._localDbDao.batch(items, callback); +}; + +/** + * Deletes items of a certain type from storage + */ +DeviceStorageDAO.prototype.removeList = function(type, callback) { + this._localDbDao.removeList(type, callback); +}; + +/** + * 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) + */ +DeviceStorageDAO.prototype.listItems = function(type, offset, num, callback) { + // fetch all items of a certain type from the data-store + this._localDbDao.list(type, offset, num, callback); +}; + +/** + * Clear the whole device data-store + */ +DeviceStorageDAO.prototype.clear = function(callback) { + this._localDbDao.clear(callback); +}; + +// +// helper functions +// + +function createKey(i, type) { + var key; + + // put uid in key if available... for easy querying + if (i.uid) { + key = type + '_' + i.uid; + } else if (i.id) { + key = type + '_' + i.id; + } else { + key = type; + } + + return key; +} + +exports = DeviceStorageDAO; \ No newline at end of file diff --git a/src/js/dao/email-dao.js b/src/js/dao/email-dao.js index 3207882..c7ef1f9 100644 --- a/src/js/dao/email-dao.js +++ b/src/js/dao/email-dao.js @@ -1,190 +1,224 @@ -define(function(require) { - 'use strict'; +'use strict'; - var util = require('js/crypto/util'), - _ = require('underscore'), - config = require('js/app-config').config, - str = require('js/app-config').string; +var util = require('crypto-lib').util, + config = require('../app-config').config, + str = require('../app-config').string; + +// +// +// Constants +// +// + +var FOLDER_DB_TYPE = 'folders'; + +var SYNC_TYPE_NEW = 'new'; +var SYNC_TYPE_DELETED = 'deleted'; +var SYNC_TYPE_MSGS = 'messages'; + +var FOLDER_TYPE_INBOX = 'Inbox'; +var FOLDER_TYPE_SENT = 'Sent'; +var FOLDER_TYPE_DRAFTS = 'Drafts'; +var FOLDER_TYPE_TRASH = 'Trash'; + +var MSG_ATTR_UID = 'uid'; +var MSG_PART_ATTR_CONTENT = 'content'; +var MSG_PART_TYPE_ATTACHMENT = 'attachment'; +var MSG_PART_TYPE_ENCRYPTED = 'encrypted'; +var MSG_PART_TYPE_SIGNED = 'signed'; +var MSG_PART_TYPE_TEXT = 'text'; +var MSG_PART_TYPE_HTML = 'html'; + +// +// +// Email Dao +// +// + +/** + * High-level data access object that orchestrates everything around the handling of encrypted mails: + * PGP de-/encryption, receiving via IMAP, sending via SMTP, MIME parsing, local db persistence + * + * @param {Object} keychain The keychain DAO handles keys transparently + * @param {Object} pgp Orchestrates decryption + * @param {Object} devicestorage Handles persistence to the local indexed db + * @param {Object} pgpbuilder Generates and encrypts MIME and SMTP messages + * @param {Object} mailreader Parses MIME messages received from IMAP + */ +var EmailDAO = function(keychain, pgp, devicestorage, pgpbuilder, mailreader) { + this._keychain = keychain; + this._pgp = pgp; + this._devicestorage = devicestorage; + this._pgpbuilder = pgpbuilder; + this._mailreader = mailreader; +}; - // - // - // Constants - // - // - - var FOLDER_DB_TYPE = 'folders'; - - var SYNC_TYPE_NEW = 'new'; - var SYNC_TYPE_DELETED = 'deleted'; - var SYNC_TYPE_MSGS = 'messages'; - - var FOLDER_TYPE_INBOX = 'Inbox'; - var FOLDER_TYPE_SENT = 'Sent'; - var FOLDER_TYPE_DRAFTS = 'Drafts'; - var FOLDER_TYPE_TRASH = 'Trash'; - - var MSG_ATTR_UID = 'uid'; - var MSG_PART_ATTR_CONTENT = 'content'; - var MSG_PART_TYPE_ATTACHMENT = 'attachment'; - var MSG_PART_TYPE_ENCRYPTED = 'encrypted'; - var MSG_PART_TYPE_SIGNED = 'signed'; - var MSG_PART_TYPE_TEXT = 'text'; - var MSG_PART_TYPE_HTML = 'html'; - - // - // - // Email Dao - // - // - - /** - * High-level data access object that orchestrates everything around the handling of encrypted mails: - * PGP de-/encryption, receiving via IMAP, sending via SMTP, MIME parsing, local db persistence - * - * @param {Object} keychain The keychain DAO handles keys transparently - * @param {Object} pgp Orchestrates decryption - * @param {Object} devicestorage Handles persistence to the local indexed db - * @param {Object} pgpbuilder Generates and encrypts MIME and SMTP messages - * @param {Object} mailreader Parses MIME messages received from IMAP - */ - var EmailDAO = function(keychain, pgp, devicestorage, pgpbuilder, mailreader) { - this._keychain = keychain; - this._pgp = pgp; - this._devicestorage = devicestorage; - this._pgpbuilder = pgpbuilder; - this._mailreader = mailreader; - }; +// +// +// Public API +// +// - // - // - // Public API - // - // +/** + * Initializes the email dao: + * - validates the email address + * - retrieves the user's key pair (if available) + * - initializes _account.folders with the content from memory + * + * @param {Object} options.account The account + * @param {String} options.account.emailAddress The user's id + * @param {Function} callback(error, keypair) Invoked with the keypair or error information when the email dao is initialized + */ +EmailDAO.prototype.init = function(options, callback) { + var self = this, + keypair; + self._account = options.account; + self._account.busy = 0; // triggers the spinner + self._account.online = false; + self._account.loggingIn = false; - /** - * Initializes the email dao: - * - validates the email address - * - retrieves the user's key pair (if available) - * - initializes _account.folders with the content from memory - * - * @param {Object} options.account The account - * @param {String} options.account.emailAddress The user's id - * @param {Function} callback(error, keypair) Invoked with the keypair or error information when the email dao is initialized - */ - EmailDAO.prototype.init = function(options, callback) { - var self = this, - keypair; + // validate email address + var emailAddress = self._account.emailAddress; + if (!util.validateEmailAddress(emailAddress)) { + callback({ + errMsg: 'The user email address must be specified!' + }); + return; + } - self._account = options.account; - self._account.busy = 0; // triggers the spinner - self._account.online = false; - self._account.loggingIn = false; + // init keychain and then crypto module + initKeychain(); - // validate email address - var emailAddress = self._account.emailAddress; - if (!util.validateEmailAddress(emailAddress)) { - callback({ - errMsg: 'The user email address must be specified!' - }); - return; - } - - // init keychain and then crypto module - initKeychain(); - - function initKeychain() { - // call getUserKeyPair to read/sync keypair with devicestorage/cloud - self._keychain.getUserKeyPair(emailAddress, function(err, storedKeypair) { - if (err) { - callback(err); - return; - } - - keypair = storedKeypair; - initFolders(); - }); - } - - function initFolders() { - // try init folders from memory, since imap client not initiated yet - self._initFoldersFromDisk(function(err) { - // dont handle offline case this time - if (err && err.code !== 42) { - callback(err); - return; - } - - callback(null, keypair); - }); - } - }; - - /** - * Unlocks the keychain by either decrypting an existing private key or generating a new keypair - * @param {String} options.passphrase The passphrase to decrypt the private key - * @param {Function} callback(error) Invoked when the the keychain is unlocked or when an error occurred buring unlocking - */ - EmailDAO.prototype.unlock = function(options, callback) { - var self = this; - - if (options.keypair) { - // import existing key pair into crypto module - handleExistingKeypair(options.keypair); - return; - } - - // no keypair for is stored for the user... generate a new one - self._pgp.generateKeys({ - emailAddress: self._account.emailAddress, - keySize: self._account.asymKeySize, - passphrase: options.passphrase - }, function(err, generatedKeypair) { + function initKeychain() { + // call getUserKeyPair to read/sync keypair with devicestorage/cloud + self._keychain.getUserKeyPair(emailAddress, function(err, storedKeypair) { if (err) { callback(err); return; } - handleGenerated(generatedKeypair); + keypair = storedKeypair; + initFolders(); + }); + } + + function initFolders() { + // try init folders from memory, since imap client not initiated yet + self._initFoldersFromDisk(function(err) { + // dont handle offline case this time + if (err && err.code !== 42) { + callback(err); + return; + } + + callback(null, keypair); + }); + } +}; + +/** + * Unlocks the keychain by either decrypting an existing private key or generating a new keypair + * @param {String} options.passphrase The passphrase to decrypt the private key + * @param {Function} callback(error) Invoked when the the keychain is unlocked or when an error occurred buring unlocking + */ +EmailDAO.prototype.unlock = function(options, callback) { + var self = this; + + if (options.keypair) { + // import existing key pair into crypto module + handleExistingKeypair(options.keypair); + return; + } + + // no keypair for is stored for the user... generate a new one + self._pgp.generateKeys({ + emailAddress: self._account.emailAddress, + keySize: self._account.asymKeySize, + passphrase: options.passphrase + }, function(err, generatedKeypair) { + if (err) { + callback(err); + return; + } + + handleGenerated(generatedKeypair); + }); + + function handleExistingKeypair(keypair) { + var privKeyParams, pubKeyParams; + try { + privKeyParams = self._pgp.getKeyParams(keypair.privateKey.encryptedKey); + pubKeyParams = self._pgp.getKeyParams(keypair.publicKey.publicKey); + } catch (e) { + callback(new Error('Error reading key params!')); + return; + } + + // check if key IDs match + if (!keypair.privateKey._id || keypair.privateKey._id !== keypair.publicKey._id || keypair.privateKey._id !== privKeyParams._id || keypair.publicKey._id !== pubKeyParams._id) { + callback(new Error('Key IDs dont match!')); + return; + } + + // check that key userIds contain email address of user account + var matchingPrivUserId = _.findWhere(privKeyParams.userIds, { + emailAddress: self._account.emailAddress + }); + var matchingPubUserId = _.findWhere(pubKeyParams.userIds, { + emailAddress: self._account.emailAddress }); - function handleExistingKeypair(keypair) { - var privKeyParams, pubKeyParams; - try { - privKeyParams = self._pgp.getKeyParams(keypair.privateKey.encryptedKey); - pubKeyParams = self._pgp.getKeyParams(keypair.publicKey.publicKey); - } catch (e) { - callback(new Error('Error reading key params!')); + if (!matchingPrivUserId || !matchingPubUserId || keypair.privateKey.userId !== self._account.emailAddress || keypair.publicKey.userId !== self._account.emailAddress) { + callback(new Error('User IDs dont match!')); + return; + } + + // import existing key pair into crypto module + self._pgp.importKeys({ + passphrase: options.passphrase, + privateKeyArmored: keypair.privateKey.encryptedKey, + publicKeyArmored: keypair.publicKey.publicKey + }, function(err) { + if (err) { + callback(err); return; } - // check if key IDs match - if (!keypair.privateKey._id || keypair.privateKey._id !== keypair.publicKey._id || keypair.privateKey._id !== privKeyParams._id || keypair.publicKey._id !== pubKeyParams._id) { - callback(new Error('Key IDs dont match!')); + // set decrypted privateKey to pgpMailer + self._pgpbuilder._privateKey = self._pgp._privateKey; + callback(); + }); + } + + function handleGenerated(generatedKeypair) { + // import the new key pair into crypto module + self._pgp.importKeys({ + passphrase: options.passphrase, + privateKeyArmored: generatedKeypair.privateKeyArmored, + publicKeyArmored: generatedKeypair.publicKeyArmored + }, function(err) { + if (err) { + callback(err); return; } - // check that key userIds contain email address of user account - var matchingPrivUserId = _.findWhere(privKeyParams.userIds, { - emailAddress: self._account.emailAddress - }); - var matchingPubUserId = _.findWhere(pubKeyParams.userIds, { - emailAddress: self._account.emailAddress - }); - - if (!matchingPrivUserId || !matchingPubUserId || keypair.privateKey.userId !== self._account.emailAddress || keypair.publicKey.userId !== self._account.emailAddress) { - callback(new Error('User IDs dont match!')); - return; - } - - // import existing key pair into crypto module - self._pgp.importKeys({ - passphrase: options.passphrase, - privateKeyArmored: keypair.privateKey.encryptedKey, - publicKeyArmored: keypair.publicKey.publicKey - }, function(err) { + // persist newly generated keypair + var newKeypair = { + publicKey: { + _id: generatedKeypair.keyId, + userId: self._account.emailAddress, + publicKey: generatedKeypair.publicKeyArmored + }, + privateKey: { + _id: generatedKeypair.keyId, + userId: self._account.emailAddress, + encryptedKey: generatedKeypair.privateKeyArmored + } + }; + self._keychain.putUserKeyPair(newKeypair, function(err) { if (err) { callback(err); return; @@ -194,382 +228,275 @@ define(function(require) { self._pgpbuilder._privateKey = self._pgp._privateKey; callback(); }); - } + }); + } +}; - function handleGenerated(generatedKeypair) { - // import the new key pair into crypto module - self._pgp.importKeys({ - passphrase: options.passphrase, - privateKeyArmored: generatedKeypair.privateKeyArmored, - publicKeyArmored: generatedKeypair.publicKeyArmored - }, function(err) { - if (err) { - callback(err); - return; - } +/** + * Opens a folder in IMAP so that we can receive updates for it. + * Please note that this is a no-op if you try to open the outbox, since it is not an IMAP folder + * but a virtual folder that only exists on disk. + * + * @param {Object} options.folder The folder to be opened + * @param {Function} callback(error) Invoked when the folder has been opened + */ +EmailDAO.prototype.openFolder = function(options, callback) { + var self = this, + err; - // persist newly generated keypair - var newKeypair = { - publicKey: { - _id: generatedKeypair.keyId, - userId: self._account.emailAddress, - publicKey: generatedKeypair.publicKeyArmored - }, - privateKey: { - _id: generatedKeypair.keyId, - userId: self._account.emailAddress, - encryptedKey: generatedKeypair.privateKeyArmored - } - }; - self._keychain.putUserKeyPair(newKeypair, function(err) { - if (err) { - callback(err); - return; - } + if (!self._account.online) { + err = new Error('Client is currently offline!'); + err.code = 42; + callback(err); + return; + } - // set decrypted privateKey to pgpMailer - self._pgpbuilder._privateKey = self._pgp._privateKey; - callback(); - }); - }); - } - }; + if (options.folder.path === config.outboxMailboxPath) { + return; + } - /** - * Opens a folder in IMAP so that we can receive updates for it. - * Please note that this is a no-op if you try to open the outbox, since it is not an IMAP folder - * but a virtual folder that only exists on disk. - * - * @param {Object} options.folder The folder to be opened - * @param {Function} callback(error) Invoked when the folder has been opened - */ - EmailDAO.prototype.openFolder = function(options, callback) { - var self = this, - err; + this._imapClient.selectMailbox({ + path: options.folder.path + }, callback); +}; - if (!self._account.online) { - err = new Error('Client is currently offline!'); - err.code = 42; - callback(err); +/** + * Synchronizes a folder's contents from disk to memory, i.e. if + * a message has disappeared from the disk, this method will remove it from folder.messages, and + * it adds any messages from disk to memory the are not yet in folder.messages + * + * @param {Object} options.folder The folder to synchronize + * @param {Function} callback [description] + */ +EmailDAO.prototype.refreshFolder = function(options, callback) { + var self = this, + folder = options.folder; + + self.busy(); + folder.messages = folder.messages || []; + self._localListMessages({ + folder: folder + }, function(err, storedMessages) { + if (err) { + done(err); return; } - if (options.folder.path === config.outboxMailboxPath) { - return; - } + var storedUids = _.pluck(storedMessages, MSG_ATTR_UID), + memoryUids = _.pluck(folder.messages, MSG_ATTR_UID), + newUids = _.difference(storedUids, memoryUids), // uids of messages that are not yet in memory + removedUids = _.difference(memoryUids, storedUids); // uids of messages that are no longer stored on the disk - this._imapClient.selectMailbox({ - path: options.folder.path - }, callback); - }; - - /** - * Synchronizes a folder's contents from disk to memory, i.e. if - * a message has disappeared from the disk, this method will remove it from folder.messages, and - * it adds any messages from disk to memory the are not yet in folder.messages - * - * @param {Object} options.folder The folder to synchronize - * @param {Function} callback [description] - */ - EmailDAO.prototype.refreshFolder = function(options, callback) { - var self = this, - folder = options.folder; - - self.busy(); - folder.messages = folder.messages || []; - self._localListMessages({ - folder: folder - }, function(err, storedMessages) { - if (err) { - done(err); - return; + // which messages are new on the disk that are not yet in memory? + _.filter(storedMessages, function(msg) { + return _.contains(newUids, msg.uid); + }).forEach(function(newMessage) { + // remove the body parts to not load unnecessary data to memory + // however, don't do that for the outbox. load the full message there. + if (folder.path !== config.outboxMailboxPath) { + delete newMessage.bodyParts; } - var storedUids = _.pluck(storedMessages, MSG_ATTR_UID), - memoryUids = _.pluck(folder.messages, MSG_ATTR_UID), - newUids = _.difference(storedUids, memoryUids), // uids of messages that are not yet in memory - removedUids = _.difference(memoryUids, storedUids); // uids of messages that are no longer stored on the disk - - // which messages are new on the disk that are not yet in memory? - _.filter(storedMessages, function(msg) { - return _.contains(newUids, msg.uid); - }).forEach(function(newMessage) { - // remove the body parts to not load unnecessary data to memory - // however, don't do that for the outbox. load the full message there. - if (folder.path !== config.outboxMailboxPath) { - delete newMessage.bodyParts; - } - - folder.messages.push(newMessage); - }); - - // which messages are no longer on disk, i.e. have been removed/sent/... - _.filter(folder.messages, function(msg) { - return _.contains(removedUids, msg.uid); - }).forEach(function(removedMessage) { - // remove the message - var index = folder.messages.indexOf(removedMessage); - folder.messages.splice(index, 1); - }); - - done(); + folder.messages.push(newMessage); }); - function done(err) { - self.done(); // stop the spinner - updateUnreadCount(folder); // update the unread count - callback(err); - } - }; + // which messages are no longer on disk, i.e. have been removed/sent/... + _.filter(folder.messages, function(msg) { + return _.contains(removedUids, msg.uid); + }).forEach(function(removedMessage) { + // remove the message + var index = folder.messages.indexOf(removedMessage); + folder.messages.splice(index, 1); + }); - /** - * Fetches a message's headers from IMAP. - * - * NB! If we fetch a message whose subject line correspond's to that of a verification message, - * we try to verify that, and if that worked, we delete the verified message from IMAP. - * - * @param {Object} options.folder The folder for which to fetch the message - * @param {Function} callback(error) Invoked when the message is persisted and added to folder.messages - */ - EmailDAO.prototype.fetchMessages = function(options, callback) { - var self = this, - folder = options.folder; + done(); + }); - self.busy(); + function done(err) { + self.done(); // stop the spinner + updateUnreadCount(folder); // update the unread count + callback(err); + } +}; - if (!self._account.online) { - done({ - errMsg: 'Client is currently offline!', - code: 42 - }); +/** + * Fetches a message's headers from IMAP. + * + * NB! If we fetch a message whose subject line correspond's to that of a verification message, + * we try to verify that, and if that worked, we delete the verified message from IMAP. + * + * @param {Object} options.folder The folder for which to fetch the message + * @param {Function} callback(error) Invoked when the message is persisted and added to folder.messages + */ +EmailDAO.prototype.fetchMessages = function(options, callback) { + var self = this, + folder = options.folder; + + self.busy(); + + if (!self._account.online) { + done({ + errMsg: 'Client is currently offline!', + code: 42 + }); + return; + } + + // list the messages starting from the lowest new uid to the highest new uid + self._imapListMessages(options, function(err, messages) { + if (err) { + done(err); return; } - // list the messages starting from the lowest new uid to the highest new uid - self._imapListMessages(options, function(err, messages) { - if (err) { - done(err); - return; - } + // if there are verification messages in the synced messages, handle it + var verificationMessages = _.filter(messages, function(message) { + return message.subject === str.verificationSubject; + }); - // if there are verification messages in the synced messages, handle it - var verificationMessages = _.filter(messages, function(message) { - return message.subject === str.verificationSubject; - }); + // if there are verification messages, continue after we've tried to verify + if (verificationMessages.length > 0) { + var after = _.after(verificationMessages.length, storeHeaders); - // if there are verification messages, continue after we've tried to verify - if (verificationMessages.length > 0) { - var after = _.after(verificationMessages.length, storeHeaders); - - verificationMessages.forEach(function(verificationMessage) { - handleVerification(verificationMessage, function(err, isValid) { - // if it was NOT a valid verification mail, do nothing - // if an error occurred and the mail was a valid verification mail, - // keep the mail in the list so the user can see it and verify manually - if (!isValid || err) { - after(); - return; - } - - // if verification worked, we remove the mail from the list. - messages.splice(messages.indexOf(verificationMessage), 1); + verificationMessages.forEach(function(verificationMessage) { + handleVerification(verificationMessage, function(err, isValid) { + // if it was NOT a valid verification mail, do nothing + // if an error occurred and the mail was a valid verification mail, + // keep the mail in the list so the user can see it and verify manually + if (!isValid || err) { after(); - }); - }); - return; - } - - // no verification messages, just proceed as usual - storeHeaders(); - - function storeHeaders() { - if (_.isEmpty(messages)) { - // nothing to do, we're done here - done(); - return; - } - - // persist the encrypted message to the local storage - self._localStoreMessages({ - folder: folder, - emails: messages - }, function(err) { - if (err) { - done(err); return; } - // this enables us to already show the attachment clip in the message list ui - messages.forEach(function(message) { - message.attachments = message.bodyParts.filter(function(bodyPart) { - return bodyPart.type === MSG_PART_TYPE_ATTACHMENT; - }); - }); - - [].unshift.apply(folder.messages, messages); // add the new messages to the folder - updateUnreadCount(folder); // update the unread count - - // notify about new messages only for the inbox - if (folder.type === FOLDER_TYPE_INBOX) { - self.onIncomingMessage(messages); - } - done(); - }); - } - }); - - function done(err) { - self.done(); // stop the spinner - callback(err); - } - - // Handles verification of public keys, deletion of messages with verified keys - function handleVerification(message, localCallback) { - self._getBodyParts({ - folder: folder, - uid: message.uid, - bodyParts: message.bodyParts - }, function(error, parsedBodyParts) { - // we could not stream the text to determine if the verification was valid or not - // so handle it as if it were valid - if (error) { - localCallback(error, true); - return; - } - - var body = _.pluck(filterBodyParts(parsedBodyParts, MSG_PART_TYPE_TEXT), MSG_PART_ATTR_CONTENT).join('\n'), - verificationUrlPrefix = config.cloudUrl + config.verificationUrl, - uuid = body.split(verificationUrlPrefix).pop().substr(0, 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)) { - localCallback(null, false); - return; - } - - // there's a valid uuid in the message, so try to verify it - self._keychain.verifyPublicKey(uuid, function(err) { - if (err) { - localCallback({ - errMsg: 'Verifying your public key failed: ' + err.errMsg - }, true); - return; - } - - // public key has been verified, delete the message - self._imapDeleteMessage({ - folder: folder, - uid: message.uid - }, function() { - // if we could successfully not delete the message or not doesn't matter. - // just don't show it in whiteout and keep quiet about it - localCallback(null, true); - }); + // if verification worked, we remove the mail from the list. + messages.splice(messages.indexOf(verificationMessage), 1); + after(); }); }); - } - }; - - /** - * Delete a message from IMAP, disk and folder.messages. - * - * Please note that this deletes from disk only if you delete from the outbox, - * since it is not an IMAP folder but a virtual folder that only exists on disk. - * - * @param {Object} options.folder The folder from which to delete the messages - * @param {Object} options.message The message that should be deleted - * @param {Boolean} options.localOnly Indicated if the message should not be removed from IMAP - * @param {Function} callback(error) Invoked when the message was delete, or an error occurred - */ - EmailDAO.prototype.deleteMessage = function(options, callback) { - var self = this, - folder = options.folder, - message = options.message; - - self.busy(); - - folder.messages.splice(folder.messages.indexOf(message), 1); - - // delete only locally - if (options.localOnly || options.folder.path === config.outboxMailboxPath) { - deleteLocal(); return; } - deleteImap(); + // no verification messages, just proceed as usual + storeHeaders(); - function deleteImap() { - if (!self._account.online) { - // no action if we're not online - done({ - errMsg: 'Client is currently offline!', - code: 42 - }); + function storeHeaders() { + if (_.isEmpty(messages)) { + // nothing to do, we're done here + done(); return; } - // delete from IMAP - self._imapDeleteMessage({ + // persist the encrypted message to the local storage + self._localStoreMessages({ folder: folder, - uid: message.uid + emails: messages }, function(err) { if (err) { done(err); return; } - deleteLocal(); + // this enables us to already show the attachment clip in the message list ui + messages.forEach(function(message) { + message.attachments = message.bodyParts.filter(function(bodyPart) { + return bodyPart.type === MSG_PART_TYPE_ATTACHMENT; + }); + }); + + [].unshift.apply(folder.messages, messages); // add the new messages to the folder + updateUnreadCount(folder); // update the unread count + + // notify about new messages only for the inbox + if (folder.type === FOLDER_TYPE_INBOX) { + self.onIncomingMessage(messages); + } + done(); }); } + }); - function deleteLocal() { - // delete from indexed db - self._localDeleteMessage({ - folder: folder, - uid: message.uid - }, done); - } + function done(err) { + self.done(); // stop the spinner + callback(err); + } - function done(err) { - self.done(); // stop the spinner - if (err) { - folder.messages.unshift(message); // re-add the message to the folder in case of an error + // Handles verification of public keys, deletion of messages with verified keys + function handleVerification(message, localCallback) { + self._getBodyParts({ + folder: folder, + uid: message.uid, + bodyParts: message.bodyParts + }, function(error, parsedBodyParts) { + // we could not stream the text to determine if the verification was valid or not + // so handle it as if it were valid + if (error) { + localCallback(error, true); + return; } - updateUnreadCount(folder); // update the unread count, if necessary - callback(err); - } - }; - /** - * Updates a message's 'unread' and 'answered' flags - * - * Please note if you set flags on disk only if you delete from the outbox, - * since it is not an IMAP folder but a virtual folder that only exists on disk. - * - * @param {[type]} options [description] - * @param {Function} callback [description] - */ - EmailDAO.prototype.setFlags = function(options, callback) { - var self = this, - folder = options.folder, - message = options.message; + var body = _.pluck(filterBodyParts(parsedBodyParts, MSG_PART_TYPE_TEXT), MSG_PART_ATTR_CONTENT).join('\n'), + verificationUrlPrefix = config.cloudUrl + config.verificationUrl, + uuid = body.split(verificationUrlPrefix).pop().substr(0, 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}/; - self.busy(); // start the spinner + // there's no valid uuid in the message, so forget about it + if (!uuidRegex.test(uuid)) { + localCallback(null, false); + return; + } - // no-op if the message if not present anymore (for whatever reason) - if (folder.messages.indexOf(message) < 0) { - self.done(); // stop the spinner - return; - } + // there's a valid uuid in the message, so try to verify it + self._keychain.verifyPublicKey(uuid, function(err) { + if (err) { + localCallback({ + errMsg: 'Verifying your public key failed: ' + err.errMsg + }, true); + return; + } - // don't do a roundtrip to IMAP, - // especially if you want to mark outbox messages - if (options.localOnly || options.folder.path === config.outboxMailboxPath) { - markStorage(); - return; - } + // public key has been verified, delete the message + self._imapDeleteMessage({ + folder: folder, + uid: message.uid + }, function() { + // if we could successfully not delete the message or not doesn't matter. + // just don't show it in whiteout and keep quiet about it + localCallback(null, true); + }); + }); + }); + } +}; +/** + * Delete a message from IMAP, disk and folder.messages. + * + * Please note that this deletes from disk only if you delete from the outbox, + * since it is not an IMAP folder but a virtual folder that only exists on disk. + * + * @param {Object} options.folder The folder from which to delete the messages + * @param {Object} options.message The message that should be deleted + * @param {Boolean} options.localOnly Indicated if the message should not be removed from IMAP + * @param {Function} callback(error) Invoked when the message was delete, or an error occurred + */ +EmailDAO.prototype.deleteMessage = function(options, callback) { + var self = this, + folder = options.folder, + message = options.message; + + self.busy(); + + folder.messages.splice(folder.messages.indexOf(message), 1); + + // delete only locally + if (options.localOnly || options.folder.path === config.outboxMailboxPath) { + deleteLocal(); + return; + } + + deleteImap(); + + function deleteImap() { if (!self._account.online) { // no action if we're not online done({ @@ -579,1153 +506,1222 @@ define(function(require) { return; } - markImap(); + // delete from IMAP + self._imapDeleteMessage({ + folder: folder, + uid: message.uid + }, function(err) { + if (err) { + done(err); + return; + } - function markImap() { - // mark a message unread/answered on IMAP - self._imapMark({ - folder: folder, - uid: options.message.uid, - unread: options.message.unread, - answered: options.message.answered - }, function(err) { - if (err) { - done(err); - return; - } + deleteLocal(); + }); + } - markStorage(); - }); + function deleteLocal() { + // delete from indexed db + self._localDeleteMessage({ + folder: folder, + uid: message.uid + }, done); + } + + function done(err) { + self.done(); // stop the spinner + if (err) { + folder.messages.unshift(message); // re-add the message to the folder in case of an error } + updateUnreadCount(folder); // update the unread count, if necessary + callback(err); + } +}; - function markStorage() { - // angular pollutes that data transfer objects with helper properties (e.g. $$hashKey), - // which we do not want to persist to disk. in order to avoid that, we load the pristine - // message from disk, change the flags and re-persist it to disk - self._localListMessages({ +/** + * Updates a message's 'unread' and 'answered' flags + * + * Please note if you set flags on disk only if you delete from the outbox, + * since it is not an IMAP folder but a virtual folder that only exists on disk. + * + * @param {[type]} options [description] + * @param {Function} callback [description] + */ +EmailDAO.prototype.setFlags = function(options, callback) { + var self = this, + folder = options.folder, + message = options.message; + + self.busy(); // start the spinner + + // no-op if the message if not present anymore (for whatever reason) + if (folder.messages.indexOf(message) < 0) { + self.done(); // stop the spinner + return; + } + + // don't do a roundtrip to IMAP, + // especially if you want to mark outbox messages + if (options.localOnly || options.folder.path === config.outboxMailboxPath) { + markStorage(); + return; + } + + if (!self._account.online) { + // no action if we're not online + done({ + errMsg: 'Client is currently offline!', + code: 42 + }); + return; + } + + markImap(); + + function markImap() { + // mark a message unread/answered on IMAP + self._imapMark({ + folder: folder, + uid: options.message.uid, + unread: options.message.unread, + answered: options.message.answered + }, function(err) { + if (err) { + done(err); + return; + } + + markStorage(); + }); + } + + function markStorage() { + // angular pollutes that data transfer objects with helper properties (e.g. $$hashKey), + // which we do not want to persist to disk. in order to avoid that, we load the pristine + // message from disk, change the flags and re-persist it to disk + self._localListMessages({ + folder: folder, + uid: options.message.uid, + }, function(err, storedMessages) { + if (err) { + done(err); + return; + } + + // set the flags + var storedMessage = storedMessages[0]; + storedMessage.unread = options.message.unread; + storedMessage.answered = options.message.answered; + storedMessage.modseq = options.message.modseq || storedMessage.modseq; + + // store + self._localStoreMessages({ folder: folder, - uid: options.message.uid, - }, function(err, storedMessages) { + emails: [storedMessage] + }, done); + }); + } + + function done(err) { + self.done(); // stop the spinner + updateUnreadCount(folder); // update the unread count + callback(err); + } +}; + +/** + * Streams message content + * @param {Object} options.message The message for which to retrieve the body + * @param {Object} options.folder The IMAP folder + * @param {Function} callback(error, message) Invoked when the message is streamed, or provides information if an error occurred + */ +EmailDAO.prototype.getBody = function(options, callback) { + var self = this, + message = options.message, + folder = options.folder; + + // the message either already has a body or is fetching it right now, so no need to become active here + if (message.loadingBody || typeof message.body !== 'undefined') { + return; + } + + message.loadingBody = true; + + self.busy(); + + /* + * read this before inspecting the method! + * + * you will wonder about the round trip to the disk where we load the persisted object. there are two reasons for this behavior: + * 1) if you work with a message that was loaded from the disk, we strip the message.bodyParts array, + * because it is not really necessary to keep everything in memory + * 2) the message in memory is polluted by angular. angular tracks ordering of a list by adding a property + * to the model. this property is auto generated and must not be persisted. + */ + + retrieveContent(); + + function retrieveContent() { + // load the local message from memory + self._localListMessages({ + folder: folder, + uid: message.uid + }, function(err, localMessages) { + if (err || localMessages.length === 0) { + done(err); + return; + } + + var localMessage = localMessages[0]; + + // treat attachment and non-attachment body parts separately: + // we need to fetch the content for non-attachment body parts (encrypted, signed, text, html, resources referenced from the html) + // but we spare the effort and fetch attachment content later upon explicit user request. + var contentParts = localMessage.bodyParts.filter(function(bodyPart) { + return bodyPart.type !== MSG_PART_TYPE_ATTACHMENT || (bodyPart.type === MSG_PART_TYPE_ATTACHMENT && bodyPart.id); + }); + var attachmentParts = localMessage.bodyParts.filter(function(bodyPart) { + return bodyPart.type === MSG_PART_TYPE_ATTACHMENT && !bodyPart.id; + }); + + // do we need to fetch content from the imap server? + var needsFetch = false; + contentParts.forEach(function(part) { + needsFetch = (typeof part.content === 'undefined'); + }); + + if (!needsFetch) { + // if we have all the content we need, + // we can extract the content + message.bodyParts = localMessage.bodyParts; + extractContent(); + return; + } + + // get the raw content from the imap server + self._getBodyParts({ + folder: folder, + uid: localMessage.uid, + bodyParts: contentParts + }, function(err, parsedBodyParts) { if (err) { done(err); return; } - // set the flags - var storedMessage = storedMessages[0]; - storedMessage.unread = options.message.unread; - storedMessage.answered = options.message.answered; - storedMessage.modseq = options.message.modseq || storedMessage.modseq; + // piece together the parsed bodyparts and the empty attachments which have not been parsed + message.bodyParts = parsedBodyParts.concat(attachmentParts); + localMessage.bodyParts = parsedBodyParts.concat(attachmentParts); - // store + // persist it to disk self._localStoreMessages({ folder: folder, - emails: [storedMessage] - }, done); + emails: [localMessage] + }, function(error) { + if (error) { + done(error); + return; + } + + // extract the content + extractContent(); + }); }); + }); + } + + function extractContent() { + if (message.encrypted) { + // show the encrypted message + message.body = filterBodyParts(message.bodyParts, MSG_PART_TYPE_ENCRYPTED)[0].content; + return done(); } - function done(err) { - self.done(); // stop the spinner - updateUnreadCount(folder); // update the unread count - callback(err); - } - }; + var root = message.bodyParts; - /** - * Streams message content - * @param {Object} options.message The message for which to retrieve the body - * @param {Object} options.folder The IMAP folder - * @param {Function} callback(error, message) Invoked when the message is streamed, or provides information if an error occurred - */ - EmailDAO.prototype.getBody = function(options, callback) { - var self = this, - message = options.message, - folder = options.folder; - - // the message either already has a body or is fetching it right now, so no need to become active here - if (message.loadingBody || typeof message.body !== 'undefined') { - return; + if (message.signed) { + // PGP/MIME signed + var signedRoot = filterBodyParts(message.bodyParts, MSG_PART_TYPE_SIGNED)[0]; // in case of a signed message, you only want to show the signed content and ignore the rest + message.signedMessage = signedRoot.signedMessage; + message.signature = signedRoot.signature; + root = signedRoot.content; } - message.loadingBody = true; - - self.busy(); + var body = _.pluck(filterBodyParts(root, MSG_PART_TYPE_TEXT), MSG_PART_ATTR_CONTENT).join('\n'); /* - * read this before inspecting the method! - * - * you will wonder about the round trip to the disk where we load the persisted object. there are two reasons for this behavior: - * 1) if you work with a message that was loaded from the disk, we strip the message.bodyParts array, - * because it is not really necessary to keep everything in memory - * 2) the message in memory is polluted by angular. angular tracks ordering of a list by adding a property - * to the model. this property is auto generated and must not be persisted. + * if the message is plain text and contains pgp/inline, we are only interested in the encrypted + * content, the rest (corporate mail footer, attachments, etc.) is discarded. + * "-----BEGIN/END (...)-----" must be at the start/end of a line, + * the regex must not match a pgp block in a plain text reply or forward of a pgp/inline message, + * the encryption will break for replies/forward, because "> " corrupts the PGP block with non-radix-64 characters, */ + var pgpInlineMatch = /^-{5}BEGIN PGP MESSAGE-{5}[\s\S]*-{5}END PGP MESSAGE-{5}$/im.exec(body); + if (pgpInlineMatch) { + message.body = pgpInlineMatch[0]; // show the plain text content + message.encrypted = true; // signal the ui that we're handling encrypted content - retrieveContent(); - - function retrieveContent() { - // load the local message from memory - self._localListMessages({ - folder: folder, - uid: message.uid - }, function(err, localMessages) { - if (err || localMessages.length === 0) { - done(err); - return; - } - - var localMessage = localMessages[0]; - - // treat attachment and non-attachment body parts separately: - // we need to fetch the content for non-attachment body parts (encrypted, signed, text, html, resources referenced from the html) - // but we spare the effort and fetch attachment content later upon explicit user request. - var contentParts = localMessage.bodyParts.filter(function(bodyPart) { - return bodyPart.type !== MSG_PART_TYPE_ATTACHMENT || (bodyPart.type === MSG_PART_TYPE_ATTACHMENT && bodyPart.id); - }); - var attachmentParts = localMessage.bodyParts.filter(function(bodyPart) { - return bodyPart.type === MSG_PART_TYPE_ATTACHMENT && !bodyPart.id; - }); - - // do we need to fetch content from the imap server? - var needsFetch = false; - contentParts.forEach(function(part) { - needsFetch = (typeof part.content === 'undefined'); - }); - - if (!needsFetch) { - // if we have all the content we need, - // we can extract the content - message.bodyParts = localMessage.bodyParts; - extractContent(); - return; - } - - // get the raw content from the imap server - self._getBodyParts({ - folder: folder, - uid: localMessage.uid, - bodyParts: contentParts - }, function(err, parsedBodyParts) { - if (err) { - done(err); - return; - } - - // piece together the parsed bodyparts and the empty attachments which have not been parsed - message.bodyParts = parsedBodyParts.concat(attachmentParts); - localMessage.bodyParts = parsedBodyParts.concat(attachmentParts); - - // persist it to disk - self._localStoreMessages({ - folder: folder, - emails: [localMessage] - }, function(error) { - if (error) { - done(error); - return; - } - - // extract the content - extractContent(); - }); - }); - }); + // replace the bodyParts info with an artificial bodyPart of type "encrypted" + message.bodyParts = [{ + type: MSG_PART_TYPE_ENCRYPTED, + content: pgpInlineMatch[0], + _isPgpInline: true // used internally to avoid trying to parse non-MIME text with the mailreader + }]; + return done(); } - function extractContent() { - if (message.encrypted) { - // show the encrypted message - message.body = filterBodyParts(message.bodyParts, MSG_PART_TYPE_ENCRYPTED)[0].content; - return done(); - } - - var root = message.bodyParts; - - if (message.signed) { - // PGP/MIME signed - var signedRoot = filterBodyParts(message.bodyParts, MSG_PART_TYPE_SIGNED)[0]; // in case of a signed message, you only want to show the signed content and ignore the rest - message.signedMessage = signedRoot.signedMessage; - message.signature = signedRoot.signature; - root = signedRoot.content; - } - - var body = _.pluck(filterBodyParts(root, MSG_PART_TYPE_TEXT), MSG_PART_ATTR_CONTENT).join('\n'); - - /* - * if the message is plain text and contains pgp/inline, we are only interested in the encrypted - * content, the rest (corporate mail footer, attachments, etc.) is discarded. - * "-----BEGIN/END (...)-----" must be at the start/end of a line, - * the regex must not match a pgp block in a plain text reply or forward of a pgp/inline message, - * the encryption will break for replies/forward, because "> " corrupts the PGP block with non-radix-64 characters, - */ - var pgpInlineMatch = /^-{5}BEGIN PGP MESSAGE-{5}[\s\S]*-{5}END PGP MESSAGE-{5}$/im.exec(body); - if (pgpInlineMatch) { - message.body = pgpInlineMatch[0]; // show the plain text content - message.encrypted = true; // signal the ui that we're handling encrypted content - - // replace the bodyParts info with an artificial bodyPart of type "encrypted" - message.bodyParts = [{ - type: MSG_PART_TYPE_ENCRYPTED, - content: pgpInlineMatch[0], - _isPgpInline: true // used internally to avoid trying to parse non-MIME text with the mailreader - }]; - return done(); - } - - /* - * any content before/after the PGP block will be discarded, - * "-----BEGIN/END (...)-----" must be at the start/end of a line, - * after the hash (and possibly other) arbitrary headers, the signed payload begins, - * the text is followed by a final \n and then the pgp signature begins - * untrusted attachments and html is ignored - */ - var clearSignedMatch = /^-{5}BEGIN PGP SIGNED MESSAGE-{5}\nHash:[ ][^\n]+\n(?:[A-Za-z]+:[ ][^\n]+\n)*\n([\s\S]*)\n-{5}BEGIN PGP SIGNATURE-{5}[\S\s]*-{5}END PGP SIGNATURE-{5}$/im.exec(body); - if (clearSignedMatch) { - // PGP/INLINE signed - message.signed = true; - message.clearSignedMessage = clearSignedMatch[0]; - body = clearSignedMatch[1]; - } - - if (!message.signed) { - // message is not signed, so we're done here - return setBody(); - } - - // check the signatures for signed messages - self._checkSignatures(message, function(err, signaturesValid) { - if (err) { - return done(err); - } - - message.signaturesValid = signaturesValid; - setBody(); - }); - - function setBody() { - message.body = body; - if (!message.clearSignedMessage) { - message.attachments = filterBodyParts(root, MSG_PART_TYPE_ATTACHMENT); - message.html = _.pluck(filterBodyParts(root, MSG_PART_TYPE_HTML), MSG_PART_ATTR_CONTENT).join('\n'); - inlineExternalImages(message); - } - - done(); - } + /* + * any content before/after the PGP block will be discarded, + * "-----BEGIN/END (...)-----" must be at the start/end of a line, + * after the hash (and possibly other) arbitrary headers, the signed payload begins, + * the text is followed by a final \n and then the pgp signature begins + * untrusted attachments and html is ignored + */ + var clearSignedMatch = /^-{5}BEGIN PGP SIGNED MESSAGE-{5}\nHash:[ ][^\n]+\n(?:[A-Za-z]+:[ ][^\n]+\n)*\n([\s\S]*)\n-{5}BEGIN PGP SIGNATURE-{5}[\S\s]*-{5}END PGP SIGNATURE-{5}$/im.exec(body); + if (clearSignedMatch) { + // PGP/INLINE signed + message.signed = true; + message.clearSignedMessage = clearSignedMatch[0]; + body = clearSignedMatch[1]; } - - function done(err) { - self.done(); - message.loadingBody = false; - callback(err, err ? undefined : message); - } - }; - - EmailDAO.prototype._checkSignatures = function(message, callback) { - var self = this; - - self._keychain.getReceiverPublicKey(message.from[0].address, function(err, senderPublicKey) { - if (err) { - return callback(err); - } - - // get the receiver's public key to check the message signature - var senderKey = senderPublicKey ? senderPublicKey.publicKey : undefined; - - if (message.clearSignedMessage) { - self._pgp.verifyClearSignedMessage(message.clearSignedMessage, senderKey, callback); - } else if (message.signedMessage && message.signature) { - self._pgp.verifySignedMessage(message.signedMessage, message.signature, senderKey, callback); - } else { - callback(null, undefined); - } - }); - }; - - /** - * Retrieves an attachment matching a body part for a given uid and a folder - * - * @param {Object} options.folder The folder where to find the attachment - * @param {Number} options.uid The uid for the message the attachment body part belongs to - * @param {Object} options.attachment The attachment body part to fetch and parse from IMAP - * @param {Function} callback(error, attachment) Invoked when the attachment body part was retrieved and parsed, or an error occurred - */ - EmailDAO.prototype.getAttachment = function(options, callback) { - var self = this, - attachment = options.attachment; - - self.busy(); - attachment.busy = true; - self._getBodyParts({ - folder: options.folder, - uid: options.uid, - bodyParts: [attachment] - }, function(err, parsedBodyParts) { - attachment.busy = false; - if (err) { - callback(err); - return; - } - self.done(); - // add the content to the original object - attachment.content = parsedBodyParts[0].content; - callback(err, err ? undefined : attachment); - }); - }; - - /** - * Decrypts a message and replaces sets the decrypted plaintext as the message's body, html, or attachment, respectively. - * The first encrypted body part's ciphertext (in the content property) will be decrypted. - * - * @param {Object} options.message The message - * @param {Function} callback(error, message) - */ - EmailDAO.prototype.decryptBody = function(options, callback) { - var self = this, - message = options.message; - - // the message is decrypting has no body, is not encrypted or has already been decrypted - if (!message.bodyParts || message.decryptingBody || !message.body || !message.encrypted || message.decrypted) { - callback(null, message); - return; + if (!message.signed) { + // message is not signed, so we're done here + return setBody(); } - message.decryptingBody = true; - - self.busy(); - // get the sender's public key for signature checking - self._keychain.getReceiverPublicKey(message.from[0].address, function(err, senderPublicKey) { + // check the signatures for signed messages + self._checkSignatures(message, function(err, signaturesValid) { if (err) { return done(err); } - // get the receiver's public key to check the message signature - var encryptedNode = filterBodyParts(message.bodyParts, MSG_PART_TYPE_ENCRYPTED)[0]; - var senderKey = senderPublicKey ? senderPublicKey.publicKey : undefined; - self._pgp.decrypt(encryptedNode.content, senderKey, function(err, decrypted, signaturesValid) { - if (err || !decrypted) { - return showError(err.message || 'An error occurred during the decryption.'); - } - - // if the decryption worked and signatures are present, everything's fine. - // no error is thrown if signatures are not present - message.signed = typeof signaturesValid !== 'undefined'; - message.signaturesValid = signaturesValid; - - // if the encrypted node contains pgp/inline, we must not parse it - // with the mailreader as it is not well-formed MIME - if (encryptedNode._isPgpInline) { - message.body = decrypted; - message.decrypted = true; - return done(); - } - - // the mailparser works on the .raw property - encryptedNode.raw = decrypted; - - // parse the decrypted raw content in the mailparser - self._mailreader.parse({ - bodyParts: [encryptedNode] - }, function(err, root) { - if (err) { - return showError(err.errMsg || err.message); - } - - if (!message.signed) { - // message had no signature in the ciphertext, so there's a little extra effort to be done here - // is there a signed MIME node? - var signedRoot = filterBodyParts(root, MSG_PART_TYPE_SIGNED)[0]; - if (!signedRoot) { - // no signed MIME node, obviously an unsigned PGP/MIME message - return setBody(); - } - - // if there is something signed in here, we're only interested in the signed content - message.signedMessage = signedRoot.signedMessage; - message.signature = signedRoot.signature; - root = signedRoot.content; - - // check the signatures for encrypted messages - self._checkSignatures(message, function(err, signaturesValid) { - if (err) { - return done(err); - } - - message.signed = typeof signaturesValid !== 'undefined'; - message.signaturesValid = signaturesValid; - setBody(); - }); - return; - } - - // message had a signature in the ciphertext, so we're done here - setBody(); - - function setBody() { - // we have successfully interpreted the descrypted message, - // so let's update the views on the message parts - message.body = _.pluck(filterBodyParts(root, MSG_PART_TYPE_TEXT), MSG_PART_ATTR_CONTENT).join('\n'); - message.html = _.pluck(filterBodyParts(root, MSG_PART_TYPE_HTML), MSG_PART_ATTR_CONTENT).join('\n'); - message.attachments = _.reject(filterBodyParts(root, MSG_PART_TYPE_ATTACHMENT), function(attmt) { - // remove the pgp-signature from the attachments - return attmt.mimeType === "application/pgp-signature"; - }); - inlineExternalImages(message); - - message.decrypted = true; - - // we're done here! - done(); - } - }); - }); + message.signaturesValid = signaturesValid; + setBody(); }); - function showError(msg) { - message.body = msg; - message.decrypted = true; // display error msg in body + function setBody() { + message.body = body; + if (!message.clearSignedMessage) { + message.attachments = filterBodyParts(root, MSG_PART_TYPE_ATTACHMENT); + message.html = _.pluck(filterBodyParts(root, MSG_PART_TYPE_HTML), MSG_PART_ATTR_CONTENT).join('\n'); + inlineExternalImages(message); + } + done(); } + } - function done(err) { - self.done(); - message.decryptingBody = false; - callback(err, err ? undefined : message); - } - }; - /** - * Encrypted (if necessary) and sends a message with a predefined clear text greeting. - * - * @param {Object} options.email The message to be sent - * @param {Function} callback(error) Invoked when the message was sent, or an error occurred - */ - EmailDAO.prototype.sendEncrypted = function(options, callback) { - var self = this; + function done(err) { + self.done(); + message.loadingBody = false; + callback(err, err ? undefined : message); + } +}; - if (!self._account.online) { - callback({ - errMsg: 'Client is currently offline!', - code: 42 - }); - return; +EmailDAO.prototype._checkSignatures = function(message, callback) { + var self = this; + + self._keychain.getReceiverPublicKey(message.from[0].address, function(err, senderPublicKey) { + if (err) { + return callback(err); } - self.busy(); - // mime encode, sign, encrypt and send email via smtp - self._pgpMailer.send({ - encrypt: true, - smtpclient: options.smtpclient, // filled solely in the integration test, undefined in normal usage - mail: options.email, - publicKeysArmored: options.email.publicKeysArmored - }, function(err, rfcText) { - if (err) { - return callback(err); - } + // get the receiver's public key to check the message signature + var senderKey = senderPublicKey ? senderPublicKey.publicKey : undefined; - // upload the sent message to the sent folder if necessary - var sentFolder = _.findWhere(self._account.folders, { - type: FOLDER_TYPE_SENT - }); - - if (self.ignoreUploadOnSent || !sentFolder || !rfcText) { - self.done(); - return callback(); - } - - self._imapClient.uploadMessage({ - path: sentFolder.path, - message: rfcText - }, function(err) { - self.done(); - callback(err); - }); - }); - }; - - /** - * Sends a signed message in the plain - * - * @param {Object} options.email The message to be sent - * @param {Function} callback(error) Invoked when the message was sent, or an error occurred - */ - EmailDAO.prototype.sendPlaintext = function(options, callback) { - var self = this; - - if (!self._account.online) { - callback({ - errMsg: 'Client is currently offline!', - code: 42 - }); - return; + if (message.clearSignedMessage) { + self._pgp.verifyClearSignedMessage(message.clearSignedMessage, senderKey, callback); + } else if (message.signedMessage && message.signature) { + self._pgp.verifySignedMessage(message.signedMessage, message.signature, senderKey, callback); + } else { + callback(null, undefined); } - self.busy(); + }); +}; - // add suffix to plaintext mail - options.email.body += str.signature + config.cloudUrl + '/' + self._account.emailAddress; +/** + * Retrieves an attachment matching a body part for a given uid and a folder + * + * @param {Object} options.folder The folder where to find the attachment + * @param {Number} options.uid The uid for the message the attachment body part belongs to + * @param {Object} options.attachment The attachment body part to fetch and parse from IMAP + * @param {Function} callback(error, attachment) Invoked when the attachment body part was retrieved and parsed, or an error occurred + */ +EmailDAO.prototype.getAttachment = function(options, callback) { + var self = this, + attachment = options.attachment; - // mime encode, sign and send email via smtp - self._pgpMailer.send({ - smtpclient: options.smtpclient, // filled solely in the integration test, undefined in normal usage - mail: options.email - }, function(err, rfcText) { - if (err) { - return callback(err); - } - - // upload the sent message to the sent folder if necessary - var sentFolder = _.findWhere(self._account.folders, { - type: FOLDER_TYPE_SENT - }); - - if (self.ignoreUploadOnSent || !sentFolder || !rfcText) { - self.done(); - return callback(); - } - - self._imapClient.uploadMessage({ - path: sentFolder.path, - message: rfcText - }, function(err) { - self.done(); - callback(err); - }); - }); - }; - - /** - * Signs and encrypts a message - * - * @param {Object} options.email The message to be encrypted - * @param {Function} callback(error, message) Invoked when the message was encrypted, or an error occurred - */ - EmailDAO.prototype.encrypt = function(options, callback) { - var self = this; - - self.busy(); - self._pgpbuilder.encrypt(options, function(err) { - self.done(); + self.busy(); + attachment.busy = true; + self._getBodyParts({ + folder: options.folder, + uid: options.uid, + bodyParts: [attachment] + }, function(err, parsedBodyParts) { + attachment.busy = false; + if (err) { callback(err); - }); + return; + } + self.done(); + // add the content to the original object + attachment.content = parsedBodyParts[0].content; + callback(err, err ? undefined : attachment); + }); +}; - }; +/** + * Decrypts a message and replaces sets the decrypted plaintext as the message's body, html, or attachment, respectively. + * The first encrypted body part's ciphertext (in the content property) will be decrypted. + * + * @param {Object} options.message The message + * @param {Function} callback(error, message) + */ +EmailDAO.prototype.decryptBody = function(options, callback) { + var self = this, + message = options.message; + // the message is decrypting has no body, is not encrypted or has already been decrypted + if (!message.bodyParts || message.decryptingBody || !message.body || !message.encrypted || message.decrypted) { + callback(null, message); + return; + } - // - // - // Event Handlers - // - // + message.decryptingBody = true; + self.busy(); + // get the sender's public key for signature checking + self._keychain.getReceiverPublicKey(message.from[0].address, function(err, senderPublicKey) { + if (err) { + return done(err); + } - /** - * This handler should be invoked when navigator.onLine === true. It will try to connect a - * given instance of the imap client. If the connection attempt was successful, it will - * update the locally available folders with the newly received IMAP folder listing. - * - * @param {Object} options.imapClient The IMAP client used to receive messages - * @param {Object} options.pgpMailer The SMTP client used to send messages - * @param {Function} callback [description] - */ - EmailDAO.prototype.onConnect = function(options, callback) { - var self = this; - - self._account.loggingIn = true; - - self._imapClient = options.imapClient; - self._pgpMailer = options.pgpMailer; - - self._imapClient.login(function(err) { - self._account.loggingIn = false; - - if (err) { - callback(err); - return; + // get the receiver's public key to check the message signature + var encryptedNode = filterBodyParts(message.bodyParts, MSG_PART_TYPE_ENCRYPTED)[0]; + var senderKey = senderPublicKey ? senderPublicKey.publicKey : undefined; + self._pgp.decrypt(encryptedNode.content, senderKey, function(err, decrypted, signaturesValid) { + if (err || !decrypted) { + return showError(err.message || 'An error occurred during the decryption.'); } - // init folders - self._initFoldersFromImap(function(err) { + // if the decryption worked and signatures are present, everything's fine. + // no error is thrown if signatures are not present + message.signed = typeof signaturesValid !== 'undefined'; + message.signaturesValid = signaturesValid; + + // if the encrypted node contains pgp/inline, we must not parse it + // with the mailreader as it is not well-formed MIME + if (encryptedNode._isPgpInline) { + message.body = decrypted; + message.decrypted = true; + return done(); + } + + // the mailparser works on the .raw property + encryptedNode.raw = decrypted; + + // parse the decrypted raw content in the mailparser + self._mailreader.parse({ + bodyParts: [encryptedNode] + }, function(err, root) { if (err) { - callback(err); - return; + return showError(err.errMsg || err.message); } - // attach sync update handler - self._imapClient.onSyncUpdate = self._onSyncUpdate.bind(self); - - // fill the imap mailboxCache with information we have locally available: - // - highest locally available moseq - // - list of locally available uids - // - highest locally available uid - // - next expected uid - var mailboxCache = {}; - self._account.folders.forEach(function(folder) { - if (folder.messages.length === 0) { - return; + if (!message.signed) { + // message had no signature in the ciphertext, so there's a little extra effort to be done here + // is there a signed MIME node? + var signedRoot = filterBodyParts(root, MSG_PART_TYPE_SIGNED)[0]; + if (!signedRoot) { + // no signed MIME node, obviously an unsigned PGP/MIME message + return setBody(); } - var uids, highestModseq, lastUid; + // if there is something signed in here, we're only interested in the signed content + message.signedMessage = signedRoot.signedMessage; + message.signature = signedRoot.signature; + root = signedRoot.content; - uids = _.pluck(folder.messages, MSG_ATTR_UID).sort(function(a, b) { - return a - b; + // check the signatures for encrypted messages + self._checkSignatures(message, function(err, signaturesValid) { + if (err) { + return done(err); + } + + message.signed = typeof signaturesValid !== 'undefined'; + message.signaturesValid = signaturesValid; + setBody(); }); - lastUid = uids[uids.length - 1]; - - highestModseq = _.pluck(folder.messages, 'modseq').sort(function(a, b) { - return a - b; - }).pop(); - - mailboxCache[folder.path] = { - exists: lastUid, - uidNext: lastUid + 1, - uidlist: uids, - highestModseq: highestModseq - }; - }); - self._imapClient.mailboxCache = mailboxCache; - - // set status to online after setting cache to prevent race condition - self._account.online = true; - - // set up the imap client to listen for changes in the inbox - var inbox = _.findWhere(self._account.folders, { - type: FOLDER_TYPE_INBOX - }); - - if (!inbox) { - return callback(); - } - - self._imapClient.listenForChanges({ - path: inbox.path - }, callback); - }); - }); - }; - - /** - * This handler should be invoked when navigator.onLine === false. - * It will discard the imap client and pgp mailer - */ - EmailDAO.prototype.onDisconnect = function(callback) { - var self = this; - - // logout of imap-client - self._imapClient.logout(function() { - // ignore error, because it's not problem if logout fails - if (callback) { - callback(); - } - }); - - // discard clients - self._account.online = false; - self._imapClient = undefined; - self._pgpMailer = undefined; - }; - - /** - * The are updates in the IMAP folder of the following type - * - 'new': a list of uids that are newly available - * - 'deleted': a list of uids that were deleted from IMAP available - * - 'messages': a list of messages (uid + flags) that where changes are available - * - * @param {String} options.type The type of the update - * @param {String} options.path The mailbox for which updates are available - * @param {Array} options.list Array containing update information. Number (uid) or mail with Object (uid and flags), respectively - */ - EmailDAO.prototype._onSyncUpdate = function(options) { - var self = this; - - var folder = _.findWhere(self._account.folders, { - path: options.path - }); - - if (!folder) { - // ignore updates for an unknown folder - return; - } - - if (options.type === SYNC_TYPE_NEW) { - // new messages available on imap, fetch from imap and store to disk and memory - self.fetchMessages({ - folder: folder, - firstUid: Math.min.apply(null, options.list), - lastUid: Math.max.apply(null, options.list) - }, self.onError.bind(self)); - } else if (options.type === SYNC_TYPE_DELETED) { - // messages have been deleted, remove from local storage and memory - options.list.forEach(function(uid) { - var message = _.findWhere(folder.messages, { - uid: uid - }); - - if (!message) { return; } - self.deleteMessage({ - folder: folder, - message: message, - localOnly: true - }, self.onError.bind(self)); - }); - } else if (options.type === SYNC_TYPE_MSGS) { - // NB! several possible reasons why this could be called. - // if a message in the array has uid value and flag array, it had a possible flag update - options.list.forEach(function(changedMsg) { - if (!changedMsg.uid || !changedMsg.flags) { - return; - } + // message had a signature in the ciphertext, so we're done here + setBody(); - var message = _.findWhere(folder.messages, { - uid: changedMsg.uid - }); - - if (!message) { - return; - } - - // update unread, answered, modseq to the latest info - message.answered = changedMsg.flags.indexOf('\\Answered') > -1; - message.unread = changedMsg.flags.indexOf('\\Seen') === -1; - message.modseq = changedMsg.modseq; - - self.setFlags({ - folder: folder, - message: message, - localOnly: true - }, self.onError.bind(self)); - }); - } - }; - - - // - // - // Internal API - // - // - - - /** - * Updates the folder information from memory, and adds/removes folders in account.folders. - * The locally available messages are loaded from memory - * - * @param {Function} callback Invoked when the folders are up to date - */ - EmailDAO.prototype._initFoldersFromDisk = function(callback) { - var self = this; - - self.busy(); // start the spinner - - // fetch list from local cache - self._devicestorage.listItems(FOLDER_DB_TYPE, 0, null, function(err, stored) { - if (err) { - return done(err); - } - - self._account.folders = stored[0] || []; - self._initMessagesFromDisk(done); - }); - - function done(err) { - self.done(); // stop the spinner - callback(err); - } - }; - - /** - * Updates the folder information from imap (if we're online). Adds/removes folders in account.folders, - * if we added/removed folder in IMAP. If we have an uninitialized folder that lacks folder.messages, - * all the locally available messages are loaded from memory. - * - * @param {Function} callback Invoked when the folders are up to date - */ - EmailDAO.prototype._initFoldersFromImap = function(callback) { - var self = this; - - self.busy(); // start the spinner - - // fetch list from imap server - self._imapClient.listWellKnownFolders(function(err, wellKnownFolders) { - if (err) { - return done(err); - } - - // initialize the folders to something meaningful if that hasn't already happened - self._account.folders = self._account.folders || []; - - // smuggle the outbox into the well known folders, which is obv not present on imap... - wellKnownFolders[config.outboxMailboxType] = [{ - name: config.outboxMailboxName, - type: config.outboxMailboxType, - path: config.outboxMailboxPath - }]; - - // indicates if we need to persist anything to disk - var foldersChanged = false; - - // the folders listed in the navigation pane - [FOLDER_TYPE_INBOX, FOLDER_TYPE_SENT, config.outboxMailboxType, FOLDER_TYPE_DRAFTS, FOLDER_TYPE_TRASH].forEach(function(mbxType) { - var localFolderWithType, imapFolderWithPath; - - // check if there is a folder of this type locally available - localFolderWithType = _.findWhere(self._account.folders, { - type: mbxType - }); - - if (localFolderWithType) { - // we have a local folder available, so let's check if this folder still exists on imap - - imapFolderWithPath = _.findWhere(wellKnownFolders[mbxType], { - path: localFolderWithType.path + function setBody() { + // we have successfully interpreted the descrypted message, + // so let's update the views on the message parts + message.body = _.pluck(filterBodyParts(root, MSG_PART_TYPE_TEXT), MSG_PART_ATTR_CONTENT).join('\n'); + message.html = _.pluck(filterBodyParts(root, MSG_PART_TYPE_HTML), MSG_PART_ATTR_CONTENT).join('\n'); + message.attachments = _.reject(filterBodyParts(root, MSG_PART_TYPE_ATTACHMENT), function(attmt) { + // remove the pgp-signature from the attachments + return attmt.mimeType === "application/pgp-signature"; }); + inlineExternalImages(message); - if (imapFolderWithPath) { - // folder present on imap, no need to update. - return; - } + message.decrypted = true; - // folder not present on imap, so remove the folder and see if there are any updates for this folder type - self._account.folders.splice(self._account.folders.indexOf(localFolderWithType), 1); - foldersChanged = true; + // we're done here! + done(); } - - if (!wellKnownFolders[mbxType] || !wellKnownFolders[mbxType].length) { - // no imap folders of the respective mailbox type, so nothing to do here - return; - } - - /** - * we have no local folder of the type, so do something intelligent, - * i.e. take the first folder of the respective type - */ - self._account.folders.push(wellKnownFolders[mbxType][0]); - foldersChanged = true; - }); - - // if folders have not changed, can fill them with messages directly - if (!foldersChanged) { - return self._initMessagesFromDisk(done); - } - - // persist encrypted list in device storage - // note: the folders in the ui also include the messages array, so let's create a clean array here - var folders = self._account.folders.map(function(folder) { - return { - name: folder.name, - path: folder.path, - type: folder.type - }; - }); - self._devicestorage.storeList([folders], FOLDER_DB_TYPE, function(err) { - if (err) { - return done(err); - } - - self._initMessagesFromDisk(done); }); }); + }); - function done(err) { - self.done(); // stop the spinner - callback(err); + function showError(msg) { + message.body = msg; + message.decrypted = true; // display error msg in body + done(); + } + + function done(err) { + self.done(); + message.decryptingBody = false; + callback(err, err ? undefined : message); + } +}; + +/** + * Encrypted (if necessary) and sends a message with a predefined clear text greeting. + * + * @param {Object} options.email The message to be sent + * @param {Function} callback(error) Invoked when the message was sent, or an error occurred + */ +EmailDAO.prototype.sendEncrypted = function(options, callback) { + var self = this; + + if (!self._account.online) { + callback({ + errMsg: 'Client is currently offline!', + code: 42 + }); + return; + } + + self.busy(); + // mime encode, sign, encrypt and send email via smtp + self._pgpMailer.send({ + encrypt: true, + smtpclient: options.smtpclient, // filled solely in the integration test, undefined in normal usage + mail: options.email, + publicKeysArmored: options.email.publicKeysArmored + }, function(err, rfcText) { + if (err) { + return callback(err); } - }; - /** - * Fill uninitialized folders with the locally available messages. - * - * @param {Function} callback Invoked when the folders are filled with messages - */ - EmailDAO.prototype._initMessagesFromDisk = function(callback) { - var self = this; + // upload the sent message to the sent folder if necessary + var sentFolder = _.findWhere(self._account.folders, { + type: FOLDER_TYPE_SENT + }); - if (!self._account.folders || self._account.folders.length === 0) { + if (self.ignoreUploadOnSent || !sentFolder || !rfcText) { + self.done(); return callback(); } - var after = _.after(self._account.folders.length, callback); - - self._account.folders.forEach(function(folder) { - if (folder.messages) { - // the folder is already initialized - return after(); - } - - // sync messages from disk to the folder model - self.refreshFolder({ - folder: folder - }, function(err) { - if (err) { - return callback(err); - } - - after(); - }); + self._imapClient.uploadMessage({ + path: sentFolder.path, + message: rfcText + }, function(err) { + self.done(); + callback(err); }); - }; + }); +}; - EmailDAO.prototype.busy = function() { - this._account.busy++; - }; +/** + * Sends a signed message in the plain + * + * @param {Object} options.email The message to be sent + * @param {Function} callback(error) Invoked when the message was sent, or an error occurred + */ +EmailDAO.prototype.sendPlaintext = function(options, callback) { + var self = this; - EmailDAO.prototype.done = function() { - if (this._account.busy > 0) { - this._account.busy--; - } - }; + if (!self._account.online) { + callback({ + errMsg: 'Client is currently offline!', + code: 42 + }); + return; + } + self.busy(); + // add suffix to plaintext mail + options.email.body += str.signature + config.cloudUrl + '/' + self._account.emailAddress; - - // - // - // IMAP API - // - // - - /** - * Mark messages as un-/read or un-/answered on IMAP - * - * @param {Object} options.folder The folder where to find the message - * @param {Number} options.uid The uid for which to change the flags - * @param {Number} options.unread Un-/Read flag - * @param {Number} options.answered Un-/Answered flag - */ - EmailDAO.prototype._imapMark = function(options, callback) { - if (!this._account.online) { - callback({ - errMsg: 'Client is currently offline!', - code: 42 - }); - return; + // mime encode, sign and send email via smtp + self._pgpMailer.send({ + smtpclient: options.smtpclient, // filled solely in the integration test, undefined in normal usage + mail: options.email + }, function(err, rfcText) { + if (err) { + return callback(err); } - options.path = options.folder.path; - this._imapClient.updateFlags(options, callback); - }; - - /** - * If we're in the trash folder or no trash folder is available, this deletes a message from IMAP. - * Otherwise, it moves a message to the trash folder. - * - * @param {Object} options.folder The folder where to find the message - * @param {Number} options.uid The uid of the message - * @param {Function} callback(error) Callback with an error object in case something went wrong. - */ - EmailDAO.prototype._imapDeleteMessage = function(options, callback) { - if (!this._account.online) { - callback({ - errMsg: 'Client is currently offline!', - code: 42 - }); - return; - } - - var trash = _.findWhere(this._account.folders, { - type: FOLDER_TYPE_TRASH + // upload the sent message to the sent folder if necessary + var sentFolder = _.findWhere(self._account.folders, { + type: FOLDER_TYPE_SENT }); - // there's no known trash folder to move the mail to or we're in the trash folder, so we can purge the message - if (!trash || options.folder === trash) { - this._imapClient.deleteMessage({ - path: options.folder.path, - uid: options.uid - }, callback); + if (self.ignoreUploadOnSent || !sentFolder || !rfcText) { + self.done(); + return callback(); + } + self._imapClient.uploadMessage({ + path: sentFolder.path, + message: rfcText + }, function(err) { + self.done(); + callback(err); + }); + }); +}; + +/** + * Signs and encrypts a message + * + * @param {Object} options.email The message to be encrypted + * @param {Function} callback(error, message) Invoked when the message was encrypted, or an error occurred + */ +EmailDAO.prototype.encrypt = function(options, callback) { + var self = this; + + self.busy(); + self._pgpbuilder.encrypt(options, function(err) { + self.done(); + callback(err); + }); + +}; + + +// +// +// Event Handlers +// +// + + +/** + * This handler should be invoked when navigator.onLine === true. It will try to connect a + * given instance of the imap client. If the connection attempt was successful, it will + * update the locally available folders with the newly received IMAP folder listing. + * + * @param {Object} options.imapClient The IMAP client used to receive messages + * @param {Object} options.pgpMailer The SMTP client used to send messages + * @param {Function} callback [description] + */ +EmailDAO.prototype.onConnect = function(options, callback) { + var self = this; + + self._account.loggingIn = true; + + self._imapClient = options.imapClient; + self._pgpMailer = options.pgpMailer; + + self._imapClient.login(function(err) { + self._account.loggingIn = false; + + if (err) { + callback(err); return; } - // move the message to the trash folder - this._imapClient.moveMessage({ - path: options.folder.path, - destination: trash.path, - uid: options.uid - }, callback); - }; - - /** - * Get list messsage headers without the body - * - * @param {String} options.folder The folder - * @param {Number} options.firstUid The lower bound of the uid (inclusive) - * @param {Number} options.lastUid The upper bound of the uid range (inclusive) - * @param {Function} callback (error, messages) The callback when the imap client is done fetching message metadata - */ - EmailDAO.prototype._imapListMessages = function(options, callback) { - var self = this; - - if (!this._account.online) { - callback({ - errMsg: 'Client is currently offline!', - code: 42 - }); - return; - } - - options.path = options.folder.path; - self._imapClient.listMessages(options, callback); - }; - - /** - * Stream an email messsage's body - * @param {String} options.folder The folder - * @param {String} options.uid the message's uid - * @param {Object} options.bodyParts The message, as retrieved by _imapListMessages - * @param {Function} callback (error, message) The callback when the imap client is done streaming message text content - */ - EmailDAO.prototype._getBodyParts = function(options, callback) { - var self = this; - - if (!self._account.online) { - callback({ - errMsg: 'Client is currently offline!', - code: 42 - }); - return; - } - - options.path = options.folder.path; - self._imapClient.getBodyParts(options, function(err) { + // init folders + self._initFoldersFromImap(function(err) { if (err) { callback(err); return; } - // interpret the raw content of the email - self._mailreader.parse(options, callback); - }); - }; + // attach sync update handler + self._imapClient.onSyncUpdate = self._onSyncUpdate.bind(self); - // - // - // Local Storage API - // - // - - - /** - * List the locally available items form the indexed db stored under "email_[FOLDER PATH]_[MESSAGE UID]" (if a message was provided), - * or "email_[FOLDER PATH]", respectively - * - * @param {Object} options.folder The folder for which to list the content - * @param {Object} options.uid A specific uid to look up locally in the folder - * @param {Function} callback(error, list) Invoked with the results of the query, or further information, if an error occurred - */ - EmailDAO.prototype._localListMessages = function(options, callback) { - var dbType = 'email_' + options.folder.path + (options.uid ? '_' + options.uid : ''); - this._devicestorage.listItems(dbType, 0, null, callback); - }; - - /** - * Stores a bunch of messages to the indexed db. The messages are stored under "email_[FOLDER PATH]_[MESSAGE UID]" - * - * @param {Object} options.folder The folder for which to list the content - * @param {Array} options.messages The messages to store - * @param {Function} callback(error, list) Invoked with the results of the query, or further information, if an error occurred - */ - EmailDAO.prototype._localStoreMessages = function(options, callback) { - var dbType = 'email_' + options.folder.path; - this._devicestorage.storeList(options.emails, dbType, callback); - }; - - /** - * Stores a bunch of messages to the indexed db. The messages are stored under "email_[FOLDER PATH]_[MESSAGE UID]" - * - * @param {Object} options.folder The folder for which to list the content - * @param {Array} options.messages The messages to store - * @param {Function} callback(error, list) Invoked with the results of the query, or further information, if an error occurred - */ - EmailDAO.prototype._localDeleteMessage = function(options, callback) { - var path = options.folder.path, - uid = options.uid, - id = options.id; - - if (!path || !(uid || id)) { - callback({ - errMsg: 'Invalid options!' - }); - return; - } - - var dbType = 'email_' + path + '_' + (uid || id); - this._devicestorage.removeList(dbType, callback); - }; - - - // - // - // Helper Functions - // - // - - - /** - * Updates a folder's unread count: - * - For the outbox, that's the total number of messages, - * - For every other folder, it's the number of unread messages - */ - function updateUnreadCount(folder) { - var allMsgs = folder.messages.length, - unreadMsgs = _.filter(folder.messages, function(msg) { - return msg.unread; - }).length; - - folder.count = folder.path === config.outboxMailboxPath ? allMsgs : unreadMsgs; - } - - /** - * 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; - } - - /** - * Helper function that looks through the HTML content for and - * inlines the images linked internally. Manipulates message.html as a side-effect. - * If no attachment matching the internal reference is found, or constructing a data - * uri fails, just remove the source. - * - * @param {Object} message DTO - */ - function inlineExternalImages(message) { - message.html = message.html.replace(/(]+\bsrc=['"])cid:([^'">]+)(['"])/ig, function(match, prefix, src, suffix) { - var localSource = '', - payload = ''; - - var internalReference = _.findWhere(message.attachments, { - id: src - }); - - if (internalReference) { - for (var i = 0; i < internalReference.content.byteLength; i++) { - payload += String.fromCharCode(internalReference.content[i]); + // fill the imap mailboxCache with information we have locally available: + // - highest locally available moseq + // - list of locally available uids + // - highest locally available uid + // - next expected uid + var mailboxCache = {}; + self._account.folders.forEach(function(folder) { + if (folder.messages.length === 0) { + return; } - try { - localSource = 'data:application/octet-stream;base64,' + btoa(payload); // try to replace the source - } catch (e) {} + var uids, highestModseq, lastUid; + + uids = _.pluck(folder.messages, MSG_ATTR_UID).sort(function(a, b) { + return a - b; + }); + lastUid = uids[uids.length - 1]; + + highestModseq = _.pluck(folder.messages, 'modseq').sort(function(a, b) { + return a - b; + }).pop(); + + mailboxCache[folder.path] = { + exists: lastUid, + uidNext: lastUid + 1, + uidlist: uids, + highestModseq: highestModseq + }; + }); + self._imapClient.mailboxCache = mailboxCache; + + // set status to online after setting cache to prevent race condition + self._account.online = true; + + // set up the imap client to listen for changes in the inbox + var inbox = _.findWhere(self._account.folders, { + type: FOLDER_TYPE_INBOX + }); + + if (!inbox) { + return callback(); } - return prefix + localSource + suffix; + self._imapClient.listenForChanges({ + path: inbox.path + }, callback); }); + }); +}; + +/** + * This handler should be invoked when navigator.onLine === false. + * It will discard the imap client and pgp mailer + */ +EmailDAO.prototype.onDisconnect = function(callback) { + var self = this; + + // logout of imap-client + self._imapClient.logout(function() { + // ignore error, because it's not problem if logout fails + if (callback) { + callback(); + } + }); + + // discard clients + self._account.online = false; + self._imapClient = undefined; + self._pgpMailer = undefined; +}; + +/** + * The are updates in the IMAP folder of the following type + * - 'new': a list of uids that are newly available + * - 'deleted': a list of uids that were deleted from IMAP available + * - 'messages': a list of messages (uid + flags) that where changes are available + * + * @param {String} options.type The type of the update + * @param {String} options.path The mailbox for which updates are available + * @param {Array} options.list Array containing update information. Number (uid) or mail with Object (uid and flags), respectively + */ +EmailDAO.prototype._onSyncUpdate = function(options) { + var self = this; + + var folder = _.findWhere(self._account.folders, { + path: options.path + }); + + if (!folder) { + // ignore updates for an unknown folder + return; } - return EmailDAO; -}); \ No newline at end of file + if (options.type === SYNC_TYPE_NEW) { + // new messages available on imap, fetch from imap and store to disk and memory + self.fetchMessages({ + folder: folder, + firstUid: Math.min.apply(null, options.list), + lastUid: Math.max.apply(null, options.list) + }, self.onError.bind(self)); + } else if (options.type === SYNC_TYPE_DELETED) { + // messages have been deleted, remove from local storage and memory + options.list.forEach(function(uid) { + var message = _.findWhere(folder.messages, { + uid: uid + }); + + if (!message) { + return; + } + + self.deleteMessage({ + folder: folder, + message: message, + localOnly: true + }, self.onError.bind(self)); + }); + } else if (options.type === SYNC_TYPE_MSGS) { + // NB! several possible reasons why this could be called. + // if a message in the array has uid value and flag array, it had a possible flag update + options.list.forEach(function(changedMsg) { + if (!changedMsg.uid || !changedMsg.flags) { + return; + } + + var message = _.findWhere(folder.messages, { + uid: changedMsg.uid + }); + + if (!message) { + return; + } + + // update unread, answered, modseq to the latest info + message.answered = changedMsg.flags.indexOf('\\Answered') > -1; + message.unread = changedMsg.flags.indexOf('\\Seen') === -1; + message.modseq = changedMsg.modseq; + + self.setFlags({ + folder: folder, + message: message, + localOnly: true + }, self.onError.bind(self)); + }); + } +}; + + +// +// +// Internal API +// +// + + +/** + * Updates the folder information from memory, and adds/removes folders in account.folders. + * The locally available messages are loaded from memory + * + * @param {Function} callback Invoked when the folders are up to date + */ +EmailDAO.prototype._initFoldersFromDisk = function(callback) { + var self = this; + + self.busy(); // start the spinner + + // fetch list from local cache + self._devicestorage.listItems(FOLDER_DB_TYPE, 0, null, function(err, stored) { + if (err) { + return done(err); + } + + self._account.folders = stored[0] || []; + self._initMessagesFromDisk(done); + }); + + function done(err) { + self.done(); // stop the spinner + callback(err); + } +}; + +/** + * Updates the folder information from imap (if we're online). Adds/removes folders in account.folders, + * if we added/removed folder in IMAP. If we have an uninitialized folder that lacks folder.messages, + * all the locally available messages are loaded from memory. + * + * @param {Function} callback Invoked when the folders are up to date + */ +EmailDAO.prototype._initFoldersFromImap = function(callback) { + var self = this; + + self.busy(); // start the spinner + + // fetch list from imap server + self._imapClient.listWellKnownFolders(function(err, wellKnownFolders) { + if (err) { + return done(err); + } + + // initialize the folders to something meaningful if that hasn't already happened + self._account.folders = self._account.folders || []; + + // smuggle the outbox into the well known folders, which is obv not present on imap... + wellKnownFolders[config.outboxMailboxType] = [{ + name: config.outboxMailboxName, + type: config.outboxMailboxType, + path: config.outboxMailboxPath + }]; + + // indicates if we need to persist anything to disk + var foldersChanged = false; + + // the folders listed in the navigation pane + [FOLDER_TYPE_INBOX, FOLDER_TYPE_SENT, config.outboxMailboxType, FOLDER_TYPE_DRAFTS, FOLDER_TYPE_TRASH].forEach(function(mbxType) { + var localFolderWithType, imapFolderWithPath; + + // check if there is a folder of this type locally available + localFolderWithType = _.findWhere(self._account.folders, { + type: mbxType + }); + + if (localFolderWithType) { + // we have a local folder available, so let's check if this folder still exists on imap + + imapFolderWithPath = _.findWhere(wellKnownFolders[mbxType], { + path: localFolderWithType.path + }); + + if (imapFolderWithPath) { + // folder present on imap, no need to update. + return; + } + + // folder not present on imap, so remove the folder and see if there are any updates for this folder type + self._account.folders.splice(self._account.folders.indexOf(localFolderWithType), 1); + foldersChanged = true; + } + + if (!wellKnownFolders[mbxType] || !wellKnownFolders[mbxType].length) { + // no imap folders of the respective mailbox type, so nothing to do here + return; + } + + /** + * we have no local folder of the type, so do something intelligent, + * i.e. take the first folder of the respective type + */ + self._account.folders.push(wellKnownFolders[mbxType][0]); + foldersChanged = true; + }); + + // if folders have not changed, can fill them with messages directly + if (!foldersChanged) { + return self._initMessagesFromDisk(done); + } + + // persist encrypted list in device storage + // note: the folders in the ui also include the messages array, so let's create a clean array here + var folders = self._account.folders.map(function(folder) { + return { + name: folder.name, + path: folder.path, + type: folder.type + }; + }); + self._devicestorage.storeList([folders], FOLDER_DB_TYPE, function(err) { + if (err) { + return done(err); + } + + self._initMessagesFromDisk(done); + }); + }); + + function done(err) { + self.done(); // stop the spinner + callback(err); + } +}; + +/** + * Fill uninitialized folders with the locally available messages. + * + * @param {Function} callback Invoked when the folders are filled with messages + */ +EmailDAO.prototype._initMessagesFromDisk = function(callback) { + var self = this; + + if (!self._account.folders || self._account.folders.length === 0) { + return callback(); + } + + var after = _.after(self._account.folders.length, callback); + + self._account.folders.forEach(function(folder) { + if (folder.messages) { + // the folder is already initialized + return after(); + } + + // sync messages from disk to the folder model + self.refreshFolder({ + folder: folder + }, function(err) { + if (err) { + return callback(err); + } + + after(); + }); + }); +}; + +EmailDAO.prototype.busy = function() { + this._account.busy++; +}; + +EmailDAO.prototype.done = function() { + if (this._account.busy > 0) { + this._account.busy--; + } +}; + + + +// +// +// IMAP API +// +// + +/** + * Mark messages as un-/read or un-/answered on IMAP + * + * @param {Object} options.folder The folder where to find the message + * @param {Number} options.uid The uid for which to change the flags + * @param {Number} options.unread Un-/Read flag + * @param {Number} options.answered Un-/Answered flag + */ +EmailDAO.prototype._imapMark = function(options, callback) { + if (!this._account.online) { + callback({ + errMsg: 'Client is currently offline!', + code: 42 + }); + return; + } + + options.path = options.folder.path; + this._imapClient.updateFlags(options, callback); +}; + +/** + * If we're in the trash folder or no trash folder is available, this deletes a message from IMAP. + * Otherwise, it moves a message to the trash folder. + * + * @param {Object} options.folder The folder where to find the message + * @param {Number} options.uid The uid of the message + * @param {Function} callback(error) Callback with an error object in case something went wrong. + */ +EmailDAO.prototype._imapDeleteMessage = function(options, callback) { + if (!this._account.online) { + callback({ + errMsg: 'Client is currently offline!', + code: 42 + }); + return; + } + + var trash = _.findWhere(this._account.folders, { + type: FOLDER_TYPE_TRASH + }); + + // there's no known trash folder to move the mail to or we're in the trash folder, so we can purge the message + if (!trash || options.folder === trash) { + this._imapClient.deleteMessage({ + path: options.folder.path, + uid: options.uid + }, callback); + + return; + } + + // move the message to the trash folder + this._imapClient.moveMessage({ + path: options.folder.path, + destination: trash.path, + uid: options.uid + }, callback); +}; + +/** + * Get list messsage headers without the body + * + * @param {String} options.folder The folder + * @param {Number} options.firstUid The lower bound of the uid (inclusive) + * @param {Number} options.lastUid The upper bound of the uid range (inclusive) + * @param {Function} callback (error, messages) The callback when the imap client is done fetching message metadata + */ +EmailDAO.prototype._imapListMessages = function(options, callback) { + var self = this; + + if (!this._account.online) { + callback({ + errMsg: 'Client is currently offline!', + code: 42 + }); + return; + } + + options.path = options.folder.path; + self._imapClient.listMessages(options, callback); +}; + +/** + * Stream an email messsage's body + * @param {String} options.folder The folder + * @param {String} options.uid the message's uid + * @param {Object} options.bodyParts The message, as retrieved by _imapListMessages + * @param {Function} callback (error, message) The callback when the imap client is done streaming message text content + */ +EmailDAO.prototype._getBodyParts = function(options, callback) { + var self = this; + + if (!self._account.online) { + callback({ + errMsg: 'Client is currently offline!', + code: 42 + }); + return; + } + + options.path = options.folder.path; + self._imapClient.getBodyParts(options, function(err) { + if (err) { + callback(err); + return; + } + // interpret the raw content of the email + self._mailreader.parse(options, callback); + }); +}; + + +// +// +// Local Storage API +// +// + + +/** + * List the locally available items form the indexed db stored under "email_[FOLDER PATH]_[MESSAGE UID]" (if a message was provided), + * or "email_[FOLDER PATH]", respectively + * + * @param {Object} options.folder The folder for which to list the content + * @param {Object} options.uid A specific uid to look up locally in the folder + * @param {Function} callback(error, list) Invoked with the results of the query, or further information, if an error occurred + */ +EmailDAO.prototype._localListMessages = function(options, callback) { + var dbType = 'email_' + options.folder.path + (options.uid ? '_' + options.uid : ''); + this._devicestorage.listItems(dbType, 0, null, callback); +}; + +/** + * Stores a bunch of messages to the indexed db. The messages are stored under "email_[FOLDER PATH]_[MESSAGE UID]" + * + * @param {Object} options.folder The folder for which to list the content + * @param {Array} options.messages The messages to store + * @param {Function} callback(error, list) Invoked with the results of the query, or further information, if an error occurred + */ +EmailDAO.prototype._localStoreMessages = function(options, callback) { + var dbType = 'email_' + options.folder.path; + this._devicestorage.storeList(options.emails, dbType, callback); +}; + +/** + * Stores a bunch of messages to the indexed db. The messages are stored under "email_[FOLDER PATH]_[MESSAGE UID]" + * + * @param {Object} options.folder The folder for which to list the content + * @param {Array} options.messages The messages to store + * @param {Function} callback(error, list) Invoked with the results of the query, or further information, if an error occurred + */ +EmailDAO.prototype._localDeleteMessage = function(options, callback) { + var path = options.folder.path, + uid = options.uid, + id = options.id; + + if (!path || !(uid || id)) { + callback({ + errMsg: 'Invalid options!' + }); + return; + } + + var dbType = 'email_' + path + '_' + (uid || id); + this._devicestorage.removeList(dbType, callback); +}; + + +// +// +// Helper Functions +// +// + + +/** + * Updates a folder's unread count: + * - For the outbox, that's the total number of messages, + * - For every other folder, it's the number of unread messages + */ +function updateUnreadCount(folder) { + var allMsgs = folder.messages.length, + unreadMsgs = _.filter(folder.messages, function(msg) { + return msg.unread; + }).length; + + folder.count = folder.path === config.outboxMailboxPath ? allMsgs : unreadMsgs; +} + +/** + * 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; +} + +/** + * Helper function that looks through the HTML content for and + * inlines the images linked internally. Manipulates message.html as a side-effect. + * If no attachment matching the internal reference is found, or constructing a data + * uri fails, just remove the source. + * + * @param {Object} message DTO + */ +function inlineExternalImages(message) { + message.html = message.html.replace(/(]+\bsrc=['"])cid:([^'">]+)(['"])/ig, function(match, prefix, src, suffix) { + var localSource = '', + payload = ''; + + var internalReference = _.findWhere(message.attachments, { + id: src + }); + + if (internalReference) { + for (var i = 0; i < internalReference.content.byteLength; i++) { + payload += String.fromCharCode(internalReference.content[i]); + } + + try { + localSource = 'data:application/octet-stream;base64,' + btoa(payload); // try to replace the source + } catch (e) {} + } + + return prefix + localSource + suffix; + }); +} + +exports = EmailDAO; \ No newline at end of file diff --git a/src/js/dao/invitation-dao.js b/src/js/dao/invitation-dao.js index f7013de..3243ae1 100644 --- a/src/js/dao/invitation-dao.js +++ b/src/js/dao/invitation-dao.js @@ -1,62 +1,60 @@ -define(function() { - 'use strict'; +'use strict'; - /** - * The InvitationDAO is a high level Data Access Object that access the invitation service REST endpoint. - * @param {Object} restDao The REST Data Access Object abstraction - */ - var InvitationDAO = function(restDao) { - this._restDao = restDao; - }; +/** + * The InvitationDAO is a high level Data Access Object that access the invitation service REST endpoint. + * @param {Object} restDao The REST Data Access Object abstraction + */ +var InvitationDAO = function(restDao) { + this._restDao = restDao; +}; - // - // Constants - // +// +// Constants +// - InvitationDAO.INVITE_MISSING = 1; - InvitationDAO.INVITE_PENDING = 2; - InvitationDAO.INVITE_SUCCESS = 4; +InvitationDAO.INVITE_MISSING = 1; +InvitationDAO.INVITE_PENDING = 2; +InvitationDAO.INVITE_SUCCESS = 4; - // - // API - // +// +// API +// - /** - * Notes an invite for the recipient by the sender in the invitation web service - * @param {String} options.recipient User ID of the recipient - * @param {String} options.sender User ID of the sender - * @param {Function} callback(error, status) Returns information if the invitation worked (INVITE_SUCCESS), if an invitation is already pendin (INVITE_PENDING), or information if an error occurred. - */ - InvitationDAO.prototype.invite = function(options, callback) { - if (typeof options !== 'object' || typeof options.recipient !== 'string' || typeof options.recipient !== 'string') { - callback({ - errMsg: 'erroneous usage of api: incorrect parameters!' - }); +/** + * Notes an invite for the recipient by the sender in the invitation web service + * @param {String} options.recipient User ID of the recipient + * @param {String} options.sender User ID of the sender + * @param {Function} callback(error, status) Returns information if the invitation worked (INVITE_SUCCESS), if an invitation is already pendin (INVITE_PENDING), or information if an error occurred. + */ +InvitationDAO.prototype.invite = function(options, callback) { + if (typeof options !== 'object' || typeof options.recipient !== 'string' || typeof options.recipient !== 'string') { + callback({ + errMsg: 'erroneous usage of api: incorrect parameters!' + }); + return; + } + + var uri = '/invitation/recipient/' + options.recipient + '/sender/' + options.sender; + this._restDao.put({}, uri, completed); + + function completed(error, res, status) { + if (error) { + callback(error); return; } - var uri = '/invitation/recipient/' + options.recipient + '/sender/' + options.sender; - this._restDao.put({}, uri, completed); - - function completed(error, res, status) { - if (error) { - callback(error); - return; - } - - if (status === 201) { - callback(null, InvitationDAO.INVITE_SUCCESS); - return; - } else if (status === 304) { - callback(null, InvitationDAO.INVITE_PENDING); - return; - } - - callback({ - errMsg: 'unexpected invitation state' - }); + if (status === 201) { + callback(null, InvitationDAO.INVITE_SUCCESS); + return; + } else if (status === 304) { + callback(null, InvitationDAO.INVITE_PENDING); + return; } - }; - return InvitationDAO; -}); \ No newline at end of file + callback({ + errMsg: 'unexpected invitation state' + }); + } +}; + +exports = InvitationDAO; \ No newline at end of file diff --git a/src/js/dao/keychain-dao.js b/src/js/dao/keychain-dao.js index 7b070c1..f847c63 100644 --- a/src/js/dao/keychain-dao.js +++ b/src/js/dao/keychain-dao.js @@ -2,241 +2,896 @@ * A high-level Data-Access Api for handling Keypair synchronization * between the cloud service and the device's local storage */ -define(function(require) { - 'use strict'; - var _ = require('underscore'), - util = require('js/crypto/util'), - config = require('js/app-config').config; +'use strict'; - var DB_PUBLICKEY = 'publickey', - DB_PRIVATEKEY = 'privatekey', - DB_DEVICENAME = 'devicename', - DB_DEVICE_SECRET = 'devicesecret'; +var util = require('crypto-lib').util, + config = require('../app-config').config; - var KeychainDAO = function(localDbDao, publicKeyDao, privateKeyDao, crypto, pgp) { - this._localDbDao = localDbDao; - this._publicKeyDao = publicKeyDao; - this._privateKeyDao = privateKeyDao; - this._crypto = crypto; - this._pgp = pgp; - }; +var DB_PUBLICKEY = 'publickey', + DB_PRIVATEKEY = 'privatekey', + DB_DEVICENAME = 'devicename', + DB_DEVICE_SECRET = 'devicesecret'; - // - // Public key functions - // +var KeychainDAO = function(localDbDao, publicKeyDao, privateKeyDao, crypto, pgp) { + this._localDbDao = localDbDao; + this._publicKeyDao = publicKeyDao; + this._privateKeyDao = privateKeyDao; + this._crypto = crypto; + this._pgp = pgp; +}; - /** - * Verifies the public key of a user o nthe public key store - * @param {String} uuid The uuid to verify the key - * @param {Function} callback(error) Callback with an optional error object when the verification is done. If the was an error, the error object contains the information for it. - */ - KeychainDAO.prototype.verifyPublicKey = function(uuid, callback) { - this._publicKeyDao.verify(uuid, callback); - }; +// +// Public key functions +// - /** - * 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 - */ - KeychainDAO.prototype.getPublicKeys = function(ids, callback) { - var self = this, - after, already, pubkeys = []; +/** + * Verifies the public key of a user o nthe public key store + * @param {String} uuid The uuid to verify the key + * @param {Function} callback(error) Callback with an optional error object when the verification is done. If the was an error, the error object contains the information for it. + */ +KeychainDAO.prototype.verifyPublicKey = function(uuid, callback) { + this._publicKeyDao.verify(uuid, callback); +}; - // return empty array if key ids are emtpy - if (ids.length < 1) { - callback(null, pubkeys); - return; - } +/** + * 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 + */ +KeychainDAO.prototype.getPublicKeys = function(ids, callback) { + var self = this, + after, already, pubkeys = []; - after = _.after(ids.length, function() { - callback(null, pubkeys); - }); + // return empty array if key ids are emtpy + if (ids.length < 1) { + callback(null, pubkeys); + return; + } - _.each(ids, function(i) { - // lookup locally and in storage - self.lookupPublicKey(i._id, function(err, pubkey) { - if (err || !pubkey) { - callback({ - errMsg: 'Error looking up public key!', - err: err - }); - return; - } + after = _.after(ids.length, function() { + callback(null, pubkeys); + }); - // check if public key with that id has already been fetched - already = null; - already = _.findWhere(pubkeys, { - _id: i._id + _.each(ids, function(i) { + // lookup locally and in storage + self.lookupPublicKey(i._id, function(err, pubkey) { + if (err || !pubkey) { + callback({ + errMsg: 'Error looking up public key!', + err: err }); - if (!already) { - pubkeys.push(pubkey); - } - - after(); // asynchronously iterate through objects - }); - }); - }; - - /** - * Checks for public key updates of a given user id - * @param {String} userId The user id (email address) for which to check the key - * @param {Function} callback(error, key) Invoked when the key has been updated or an error occurred - */ - KeychainDAO.prototype.refreshKeyForUserId = function(userId, callback) { - var self = this; - - // get the public key corresponding to the userId - self.getReceiverPublicKey(userId, function(err, localKey) { - if (!localKey || !localKey._id) { - // there is no key available, no need to refresh - callback(); return; } - // no need to refresh manually imported public keys - if (localKey.imported) { + // check if public key with that id has already been fetched + already = null; + already = _.findWhere(pubkeys, { + _id: i._id + }); + if (!already) { + pubkeys.push(pubkey); + } + + after(); // asynchronously iterate through objects + }); + }); +}; + +/** + * Checks for public key updates of a given user id + * @param {String} userId The user id (email address) for which to check the key + * @param {Function} callback(error, key) Invoked when the key has been updated or an error occurred + */ +KeychainDAO.prototype.refreshKeyForUserId = function(userId, callback) { + var self = this; + + // get the public key corresponding to the userId + self.getReceiverPublicKey(userId, function(err, localKey) { + if (!localKey || !localKey._id) { + // there is no key available, no need to refresh + callback(); + return; + } + + // no need to refresh manually imported public keys + if (localKey.imported) { + callback(null, localKey); + return; + } + + // check if the key id still exists on the key server + checkKeyExists(localKey); + }); + + // checks if the user's key has been revoked by looking up the key id + function checkKeyExists(localKey) { + self._publicKeyDao.get(localKey._id, function(err, cloudKey) { + if (err && err.code === 42) { + // we're offline, we're done checking the key callback(null, localKey); return; } - // check if the key id still exists on the key server - checkKeyExists(localKey); + if (err) { + // there was an error, exit and inform + callback(err); + return; + } + + if (cloudKey && cloudKey._id === localKey._id) { + // the key is present on the server, all is well + callback(null, localKey); + return; + } + + // the key has changed, update the key + updateKey(localKey); }); + } - // checks if the user's key has been revoked by looking up the key id - function checkKeyExists(localKey) { - self._publicKeyDao.get(localKey._id, function(err, cloudKey) { - if (err && err.code === 42) { - // we're offline, we're done checking the key + function updateKey(localKey) { + // look for an updated key for the user id + self._publicKeyDao.getByUserId(userId, function(err, newKey) { + // offline? + if (err && err.code === 42) { + callback(null, localKey); + return; + } + + if (err) { + callback(err); + return; + } + + // the public key has changed, we need to ask for permission to update the key + self.requestPermissionForKeyUpdate({ + userId: userId, + newKey: newKey + }, function(granted) { + if (!granted) { + // permission was not given to update the key, so don't overwrite the old one! callback(null, localKey); return; } - if (err) { - // there was an error, exit and inform - callback(err); - return; - } - - if (cloudKey && cloudKey._id === localKey._id) { - // the key is present on the server, all is well - callback(null, localKey); - return; - } - - // the key has changed, update the key - updateKey(localKey); - }); - } - - function updateKey(localKey) { - // look for an updated key for the user id - self._publicKeyDao.getByUserId(userId, function(err, newKey) { - // offline? - if (err && err.code === 42) { - callback(null, localKey); - return; - } - - if (err) { - callback(err); - return; - } - - // the public key has changed, we need to ask for permission to update the key - self.requestPermissionForKeyUpdate({ - userId: userId, - newKey: newKey - }, function(granted) { - if (!granted) { - // permission was not given to update the key, so don't overwrite the old one! - callback(null, localKey); + // permission to update the key was given, so delete the old one and persist the new one + self.removeLocalPublicKey(localKey._id, function(err) { + if (err || !newKey) { + // error or no new key to save + callback(err); return; } - // permission to update the key was given, so delete the old one and persist the new one - self.removeLocalPublicKey(localKey._id, function(err) { - if (err || !newKey) { - // error or no new key to save - callback(err); - return; - } - - // persist the new key and return it - self.saveLocalPublicKey(newKey, function(err) { - callback(err, err ? undefined : newKey); - }); + // persist the new key and return it + self.saveLocalPublicKey(newKey, function(err) { + callback(err, err ? undefined : newKey); }); }); - }); + + }); + } +}; + +/** + * Look up a reveiver's public key by user id + * @param userId [String] the receiver's email address + */ +KeychainDAO.prototype.getReceiverPublicKey = function(userId, callback) { + var self = this; + + // search local keyring for public key + self._localDbDao.list(DB_PUBLICKEY, 0, null, function(err, allPubkeys) { + if (err) { + callback(err); + return; } - }; - /** - * Look up a reveiver's public key by user id - * @param userId [String] the receiver's email address - */ - KeychainDAO.prototype.getReceiverPublicKey = function(userId, callback) { - var self = this; - - // search local keyring for public key - self._localDbDao.list(DB_PUBLICKEY, 0, null, function(err, allPubkeys) { - if (err) { - callback(err); - return; - } - - // query primary email address - var pubkey = _.findWhere(allPubkeys, { - userId: userId - }); - - // query mutliple userIds (for imported public keys) - if (!pubkey) { - for (var i = 0, match; i < allPubkeys.length; i++) { - match = _.findWhere(allPubkeys[i].userIds, { - emailAddress: userId - }); - if (match) { - pubkey = allPubkeys[i]; - break; - } - } - } - - if (pubkey && pubkey._id) { - // that user's public key is already in local storage - callback(null, pubkey); - return; - } - - // no public key by that user id in storage - // find from cloud by email address - self._publicKeyDao.getByUserId(userId, onKeyReceived); + // query primary email address + var pubkey = _.findWhere(allPubkeys, { + userId: userId }); - function onKeyReceived(err, cloudPubkey) { - if (err && err.code === 42) { - // offline - callback(); - return; + // query mutliple userIds (for imported public keys) + if (!pubkey) { + for (var i = 0, match; i < allPubkeys.length; i++) { + match = _.findWhere(allPubkeys[i].userIds, { + emailAddress: userId + }); + if (match) { + pubkey = allPubkeys[i]; + break; + } } + } + if (pubkey && pubkey._id) { + // that user's public key is already in local storage + callback(null, pubkey); + return; + } + + // no public key by that user id in storage + // find from cloud by email address + self._publicKeyDao.getByUserId(userId, onKeyReceived); + }); + + function onKeyReceived(err, cloudPubkey) { + if (err && err.code === 42) { + // offline + callback(); + return; + } + + if (err) { + callback(err); + return; + } + + if (!cloudPubkey) { + // public key has been deleted without replacement + callback(); + return; + } + + self.saveLocalPublicKey(cloudPubkey, function(err) { if (err) { callback(err); return; } - if (!cloudPubkey) { - // public key has been deleted without replacement - callback(); + callback(null, cloudPubkey); + }); + } +}; + +// +// Device registration functions +// + +/** + * Set the device's memorable name e.g 'iPhone Work' + * @param {String} deviceName The device name + * @param {Function} callback(error) + */ +KeychainDAO.prototype.setDeviceName = function(deviceName, callback) { + if (!deviceName) { + callback(new Error('Please set a device name!')); + return; + } + + this._localDbDao.persist(DB_DEVICENAME, deviceName, callback); +}; + +/** + * Get the device' memorable name from local storage. Throws an error if not set + * @param {Function} callback(error, deviceName) + * @return {String} The device name + */ +KeychainDAO.prototype.getDeviceName = function(callback) { + // check if deviceName is already persisted in storage + this._localDbDao.read(DB_DEVICENAME, function(err, deviceName) { + if (err) { + callback(err); + return; + } + + if (!deviceName) { + callback(new Error('Device name not set!')); + return; + } + + callback(null, deviceName); + }); +}; + +/** + * Geneate a device specific key and secret to authenticate to the private key service. + * @param {Function} callback(error, deviceSecret:[base64 encoded string]) + */ +KeychainDAO.prototype.getDeviceSecret = function(callback) { + var self = this; + + // generate random deviceSecret or get from storage + self._localDbDao.read(DB_DEVICE_SECRET, function(err, storedDevSecret) { + if (err) { + callback(err); + return; + } + + if (storedDevSecret) { + // a device key is already available locally + callback(null, storedDevSecret); + return; + } + + // generate random deviceSecret + var deviceSecret = util.random(config.symKeySize); + // persist deviceSecret to local storage (in plaintext) + self._localDbDao.persist(DB_DEVICE_SECRET, deviceSecret, function(err) { + if (err) { + callback(err); return; } + callback(null, 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 + * @param {Function} callback(error) + */ +KeychainDAO.prototype.registerDevice = function(options, callback) { + var self = this, + devName; + + // check if deviceName is already persisted in storage + self.getDeviceName(function(err, deviceName) { + if (err) { + callback(err); + return; + } + + requestDeviceRegistration(deviceName); + }); + + function requestDeviceRegistration(deviceName) { + devName = deviceName; + + // request device registration session key + self._privateKeyDao.requestDeviceRegistration({ + userId: options.userId, + deviceName: deviceName + }, function(err, regSessionKey) { + if (err) { + callback(err); + return; + } + + if (!regSessionKey.encryptedRegSessionKey) { + callback(new Error('Invalid format for session key!')); + return; + } + + decryptSessionKey(regSessionKey); + }); + } + + function decryptSessionKey(regSessionKey) { + self.lookupPublicKey(config.serverPrivateKeyId, function(err, serverPubkey) { + if (err) { + callback(err); + return; + } + + if (!serverPubkey || !serverPubkey.publicKey) { + callback(new Error('Server public key for device registration not found!')); + return; + } + + // decrypt the session key + var ct = regSessionKey.encryptedRegSessionKey; + self._pgp.decrypt(ct, serverPubkey.publicKey, function(err, decrypedSessionKey, signaturesValid) { + if (err || !signaturesValid) { + return callback(err || new Error('Verifying PGP signature failed!')); + } + + uploadDeviceSecret(decrypedSessionKey); + }); + }); + } + + function uploadDeviceSecret(regSessionKey) { + // read device secret from local storage + self.getDeviceSecret(function(err, deviceSecret) { + if (err) { + callback(err); + return; + } + + // generate iv + var iv = util.random(config.symIvSize); + // encrypt deviceSecret + self._crypto.encrypt(deviceSecret, regSessionKey, iv, function(err, encryptedDeviceSecret) { + if (err) { + callback(err); + return; + } + + // upload encryptedDeviceSecret + self._privateKeyDao.uploadDeviceSecret({ + userId: options.userId, + deviceName: devName, + encryptedDeviceSecret: encryptedDeviceSecret, + iv: iv + }, callback); + }); + }); + } +}; + +// +// Private key functions +// + +/** + * Authenticate to the private key server (required before private PGP key upload). + * @param {String} userId The user's email address + * @param {Function} callback(error, authSessionKey) + * @return {Object} {sessionId:String, sessionKey:[base64 encoded]} + */ +KeychainDAO.prototype._authenticateToPrivateKeyServer = function(userId, callback) { + var self = this, + sessionId; + + // request auth session key required for upload + self._privateKeyDao.requestAuthSessionKey({ + userId: userId + }, function(err, authSessionKey) { + if (err) { + callback(err); + return; + } + + if (!authSessionKey.encryptedAuthSessionKey || !authSessionKey.encryptedChallenge || !authSessionKey.sessionId) { + callback(new Error('Invalid format for session key!')); + return; + } + + // remember session id for verification + sessionId = authSessionKey.sessionId; + + decryptSessionKey(authSessionKey); + }); + + function decryptSessionKey(authSessionKey) { + self.lookupPublicKey(config.serverPrivateKeyId, function(err, serverPubkey) { + if (err) { + callback(err); + return; + } + + if (!serverPubkey || !serverPubkey.publicKey) { + callback(new Error('Server public key for authentication not found!')); + return; + } + + // decrypt the session key + var ct1 = authSessionKey.encryptedAuthSessionKey; + self._pgp.decrypt(ct1, serverPubkey.publicKey, function(err, decryptedSessionKey, signaturesValid) { + if (err || !signaturesValid) { + return callback(err || new Error('Verifying PGP signature failed!')); + } + + // decrypt the challenge + var ct2 = authSessionKey.encryptedChallenge; + self._pgp.decrypt(ct2, serverPubkey.publicKey, function(err, decryptedChallenge, signaturesValid) { + if (err || !signaturesValid) { + return callback(err || new Error('Verifying PGP signature failed!')); + } + + encryptChallenge(decryptedSessionKey, decryptedChallenge); + }); + }); + }); + } + + function encryptChallenge(sessionKey, challenge) { + // get device secret + self.getDeviceSecret(function(err, deviceSecret) { + if (err) { + callback(err); + return; + } + + var iv = util.random(config.symIvSize); + // encrypt the challenge + self._crypto.encrypt(challenge, sessionKey, iv, function(err, encryptedChallenge) { + if (err) { + callback(err); + return; + } + + // encrypt the device secret + self._crypto.encrypt(deviceSecret, sessionKey, iv, function(err, encryptedDeviceSecret) { + if (err) { + callback(err); + 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 + self._privateKeyDao.verifyAuthentication({ + userId: userId, + sessionId: sessionId, + encryptedChallenge: response.encryptedChallenge, + encryptedDeviceSecret: response.encryptedDeviceSecret, + iv: response.iv + }, function(err) { + if (err) { + callback(err); + return; + } + + callback(null, { + 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 + * @param {Function} callback(error) + */ +KeychainDAO.prototype.uploadPrivateKey = function(options, callback) { + var self = this, + keySize = config.symKeySize, + salt; + + if (!options.userId || !options.code) { + callback(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 + self._crypto.deriveKey(code, salt, keySize, function(err, key) { + if (err) { + callback(err); + return; + } + + encryptPrivateKey(key); + }); + } + + function encryptPrivateKey(encryptionKey) { + // get private key from local storage + self.getUserKeyPair(options.userId, function(err, keypair) { + if (err) { + callback(err); + return; + } + + var privkeyId = keypair.privateKey._id, + pgpBlock = keypair.privateKey.encryptedKey; + + // encrypt the private key with the derived key + var iv = util.random(config.symIvSize); + self._crypto.encrypt(pgpBlock, encryptionKey, iv, function(err, ct) { + if (err) { + callback(err); + return; + } + + var payload = { + _id: privkeyId, + userId: options.userId, + encryptedPrivateKey: ct, + salt: salt, + iv: iv + }; + + uploadPrivateKey(payload); + }); + }); + } + + function uploadPrivateKey(payload) { + // authenticate to server for upload + self._authenticateToPrivateKeyServer(options.userId, function(err, authSessionKey) { + if (err) { + callback(err); + return; + } + + // encrypt encryptedPrivateKey again using authSessionKey + var pt = payload.encryptedPrivateKey, + iv = payload.iv, + key = authSessionKey.sessionKey; + self._crypto.encrypt(pt, key, iv, function(err, ct) { + if (err) { + callback(err); + return; + } + + // replace the encryptedPrivateKey with the double wrapped ciphertext + payload.encryptedPrivateKey = ct; + // set sessionId + payload.sessionId = authSessionKey.sessionId; + + // upload the encrypted priavet key + self._privateKeyDao.upload(payload, callback); + }); + }); + } +}; + +/** + * 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 + * @param {Function} callback(error) + */ +KeychainDAO.prototype.requestPrivateKeyDownload = function(options, callback) { + this._privateKeyDao.requestDownload(options, callback); +}; + +/** + * 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 + * @param {Function} callback(error) + */ +KeychainDAO.prototype.hasPrivateKey = function(options, callback) { + this._privateKeyDao.hasPrivateKey(options, callback); +}; + +/** + * 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 + * @param {Function} callback(error, encryptedPrivateKey) + */ +KeychainDAO.prototype.downloadPrivateKey = function(options, callback) { + this._privateKeyDao.download(options, callback); +}; + +/** + * 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 + * @param {Function} callback(error, keyObject) + */ +KeychainDAO.prototype.decryptAndStorePrivateKeyLocally = function(options, callback) { + var self = this, + code = options.code, + salt = options.salt, + keySize = config.symKeySize; + + if (!options._id || !options.userId || !options.code || !options.salt || !options.encryptedPrivateKey || !options.iv) { + callback(new Error('Incomplete arguments!')); + return; + } + + // derive key from the code and the salt using PBKDF2 + self._crypto.deriveKey(code, salt, keySize, function(err, key) { + if (err) { + callback(err); + return; + } + + decryptAndStore(key); + }); + + function decryptAndStore(derivedKey) { + // decrypt the private key with the derived key + var ct = options.encryptedPrivateKey, + iv = options.iv; + + self._crypto.decrypt(ct, derivedKey, iv, function(err, privateKeyArmored) { + if (err) { + callback(new Error('Invalid keychain code!')); + return; + } + + // validate pgp key + var keyParams; + try { + keyParams = self._pgp.getKeyParams(privateKeyArmored); + } catch (e) { + callback(new Error('Error parsing private PGP key!')); + return; + } + + if (keyParams._id !== options._id || keyParams.userId !== options.userId) { + callback(new Error('Private key parameters don\'t match with public key\'s!')); + return; + } + + var keyObject = { + _id: options._id, + userId: options.userId, + encryptedKey: privateKeyArmored + }; + + // store private key locally + self.saveLocalPrivateKey(keyObject, function(err) { + if (err) { + callback(err); + return; + } + + callback(null, keyObject); + }); + }); + } +}; + +// +// Keypair functions +// + +/** + * Gets the local user's key either from local storage + * or fetches it from the cloud. The private key is encrypted. + * If no key pair exists, null is returned. + * return [Object] The user's key pair {publicKey, privateKey} + */ +KeychainDAO.prototype.getUserKeyPair = function(userId, callback) { + var self = this; + + // search for user's public key locally + self._localDbDao.list(DB_PUBLICKEY, 0, null, function(err, allPubkeys) { + if (err) { + callback(err); + return; + } + + var pubkey = _.findWhere(allPubkeys, { + userId: userId + }); + + if (pubkey && pubkey._id) { + // that user's public key is already in local storage... + // sync keypair to the cloud + syncKeypair(pubkey._id); + return; + } + + // no public key by that user id in storage + // find from cloud by email address + self._publicKeyDao.getByUserId(userId, function(err, cloudPubkey) { + if (err) { + callback(err); + return; + } + + if (cloudPubkey && cloudPubkey._id) { + // there is a public key for that user already in the cloud... + // sync keypair to local storage + syncKeypair(cloudPubkey._id); + return; + } + + // continue without keypair... generate in crypto.js + callback(); + }); + }); + + function syncKeypair(keypairId) { + // persist key pair in local storage + self.lookupPublicKey(keypairId, function(err, savedPubkey) { + if (err) { + callback(err); + return; + } + + // persist private key in local storage + self.lookupPrivateKey(keypairId, function(err, savedPrivkey) { + var keys = {}; + + if (err) { + callback(err); + return; + } + + if (savedPubkey && savedPubkey.publicKey) { + keys.publicKey = savedPubkey; + } + + if (savedPrivkey && savedPrivkey.encryptedKey) { + keys.privateKey = savedPrivkey; + } + + callback(null, keys); + }); + }); + } +}; + +/** + * Checks to see if the user's key pair is stored both + * locally and in the cloud and persist arccordingly + * @param [Object] The user's key pair {publicKey, privateKey} + */ +KeychainDAO.prototype.putUserKeyPair = function(keypair, callback) { + var self = this; + + // validate input + if (!keypair || !keypair.publicKey || !keypair.privateKey || !keypair.publicKey.userId || keypair.publicKey.userId !== keypair.privateKey.userId) { + callback({ + errMsg: 'Incorrect input!' + }); + return; + } + + // don't check the user's own public key for deletion in refreshKeyForUserId + keypair.publicKey.imported = true; + + // store public key locally + self.saveLocalPublicKey(keypair.publicKey, function(err) { + if (err) { + callback(err); + return; + } + + // persist public key in cloud storage + self._publicKeyDao.put(keypair.publicKey, function(err) { + // validate result + if (err) { + callback(err); + return; + } + + // store private key locally + self.saveLocalPrivateKey(keypair.privateKey, callback); + }); + }); +}; + +// +// Helper functions +// + +KeychainDAO.prototype.lookupPublicKey = function(id, callback) { + var self = this; + + if (!id) { + callback({ + errMsg: 'ID must be set for public key query!' + }); + return; + } + + // lookup in local storage + self._localDbDao.read(DB_PUBLICKEY + '_' + id, function(err, pubkey) { + if (err) { + callback(err); + return; + } + + if (pubkey) { + callback(null, pubkey); + return; + } + + // fetch from cloud storage + self._publicKeyDao.get(id, function(err, cloudPubkey) { + if (err) { + callback(err); + return; + } + + // cache public key in cache self.saveLocalPublicKey(cloudPubkey, function(err) { if (err) { callback(err); @@ -245,694 +900,37 @@ define(function(require) { callback(null, cloudPubkey); }); - } - }; - - // - // Device registration functions - // - - /** - * Set the device's memorable name e.g 'iPhone Work' - * @param {String} deviceName The device name - * @param {Function} callback(error) - */ - KeychainDAO.prototype.setDeviceName = function(deviceName, callback) { - if (!deviceName) { - callback(new Error('Please set a device name!')); - return; - } - - this._localDbDao.persist(DB_DEVICENAME, deviceName, callback); - }; - - /** - * Get the device' memorable name from local storage. Throws an error if not set - * @param {Function} callback(error, deviceName) - * @return {String} The device name - */ - KeychainDAO.prototype.getDeviceName = function(callback) { - // check if deviceName is already persisted in storage - this._localDbDao.read(DB_DEVICENAME, function(err, deviceName) { - if (err) { - callback(err); - return; - } - - if (!deviceName) { - callback(new Error('Device name not set!')); - return; - } - - callback(null, deviceName); }); - }; - - /** - * Geneate a device specific key and secret to authenticate to the private key service. - * @param {Function} callback(error, deviceSecret:[base64 encoded string]) - */ - KeychainDAO.prototype.getDeviceSecret = function(callback) { - var self = this; - - // generate random deviceSecret or get from storage - self._localDbDao.read(DB_DEVICE_SECRET, function(err, storedDevSecret) { - if (err) { - callback(err); - return; - } - - if (storedDevSecret) { - // a device key is already available locally - callback(null, storedDevSecret); - return; - } - - // generate random deviceSecret - var deviceSecret = util.random(config.symKeySize); - // persist deviceSecret to local storage (in plaintext) - self._localDbDao.persist(DB_DEVICE_SECRET, deviceSecret, function(err) { - if (err) { - callback(err); - return; - } - - callback(null, 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 - * @param {Function} callback(error) - */ - KeychainDAO.prototype.registerDevice = function(options, callback) { - var self = this, - devName; - - // check if deviceName is already persisted in storage - self.getDeviceName(function(err, deviceName) { - if (err) { - callback(err); - return; - } - - requestDeviceRegistration(deviceName); - }); - - function requestDeviceRegistration(deviceName) { - devName = deviceName; - - // request device registration session key - self._privateKeyDao.requestDeviceRegistration({ - userId: options.userId, - deviceName: deviceName - }, function(err, regSessionKey) { - if (err) { - callback(err); - return; - } - - if (!regSessionKey.encryptedRegSessionKey) { - callback(new Error('Invalid format for session key!')); - return; - } - - decryptSessionKey(regSessionKey); - }); - } - - function decryptSessionKey(regSessionKey) { - self.lookupPublicKey(config.serverPrivateKeyId, function(err, serverPubkey) { - if (err) { - callback(err); - return; - } - - if (!serverPubkey || !serverPubkey.publicKey) { - callback(new Error('Server public key for device registration not found!')); - return; - } - - // decrypt the session key - var ct = regSessionKey.encryptedRegSessionKey; - self._pgp.decrypt(ct, serverPubkey.publicKey, function(err, decrypedSessionKey, signaturesValid) { - if (err || !signaturesValid) { - return callback(err || new Error('Verifying PGP signature failed!')); - } - - uploadDeviceSecret(decrypedSessionKey); - }); - }); - } - - function uploadDeviceSecret(regSessionKey) { - // read device secret from local storage - self.getDeviceSecret(function(err, deviceSecret) { - if (err) { - callback(err); - return; - } - - // generate iv - var iv = util.random(config.symIvSize); - // encrypt deviceSecret - self._crypto.encrypt(deviceSecret, regSessionKey, iv, function(err, encryptedDeviceSecret) { - if (err) { - callback(err); - return; - } - - // upload encryptedDeviceSecret - self._privateKeyDao.uploadDeviceSecret({ - userId: options.userId, - deviceName: devName, - encryptedDeviceSecret: encryptedDeviceSecret, - iv: iv - }, callback); - }); - }); - } - }; - - // - // Private key functions - // - - /** - * Authenticate to the private key server (required before private PGP key upload). - * @param {String} userId The user's email address - * @param {Function} callback(error, authSessionKey) - * @return {Object} {sessionId:String, sessionKey:[base64 encoded]} - */ - KeychainDAO.prototype._authenticateToPrivateKeyServer = function(userId, callback) { - var self = this, - sessionId; - - // request auth session key required for upload - self._privateKeyDao.requestAuthSessionKey({ - userId: userId - }, function(err, authSessionKey) { - if (err) { - callback(err); - return; - } - - if (!authSessionKey.encryptedAuthSessionKey || !authSessionKey.encryptedChallenge || !authSessionKey.sessionId) { - callback(new Error('Invalid format for session key!')); - return; - } - - // remember session id for verification - sessionId = authSessionKey.sessionId; - - decryptSessionKey(authSessionKey); - }); - - function decryptSessionKey(authSessionKey) { - self.lookupPublicKey(config.serverPrivateKeyId, function(err, serverPubkey) { - if (err) { - callback(err); - return; - } - - if (!serverPubkey || !serverPubkey.publicKey) { - callback(new Error('Server public key for authentication not found!')); - return; - } - - // decrypt the session key - var ct1 = authSessionKey.encryptedAuthSessionKey; - self._pgp.decrypt(ct1, serverPubkey.publicKey, function(err, decryptedSessionKey, signaturesValid) { - if (err || !signaturesValid) { - return callback(err || new Error('Verifying PGP signature failed!')); - } - - // decrypt the challenge - var ct2 = authSessionKey.encryptedChallenge; - self._pgp.decrypt(ct2, serverPubkey.publicKey, function(err, decryptedChallenge, signaturesValid) { - if (err || !signaturesValid) { - return callback(err || new Error('Verifying PGP signature failed!')); - } - - encryptChallenge(decryptedSessionKey, decryptedChallenge); - }); - }); - }); - } - - function encryptChallenge(sessionKey, challenge) { - // get device secret - self.getDeviceSecret(function(err, deviceSecret) { - if (err) { - callback(err); - return; - } - - var iv = util.random(config.symIvSize); - // encrypt the challenge - self._crypto.encrypt(challenge, sessionKey, iv, function(err, encryptedChallenge) { - if (err) { - callback(err); - return; - } - - // encrypt the device secret - self._crypto.encrypt(deviceSecret, sessionKey, iv, function(err, encryptedDeviceSecret) { - if (err) { - callback(err); - 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 - self._privateKeyDao.verifyAuthentication({ - userId: userId, - sessionId: sessionId, - encryptedChallenge: response.encryptedChallenge, - encryptedDeviceSecret: response.encryptedDeviceSecret, - iv: response.iv - }, function(err) { - if (err) { - callback(err); - return; - } - - callback(null, { - 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 - * @param {Function} callback(error) - */ - KeychainDAO.prototype.uploadPrivateKey = function(options, callback) { - var self = this, - keySize = config.symKeySize, - salt; - - if (!options.userId || !options.code) { - callback(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 - self._crypto.deriveKey(code, salt, keySize, function(err, key) { - if (err) { - callback(err); - return; - } - - encryptPrivateKey(key); - }); - } - - function encryptPrivateKey(encryptionKey) { - // get private key from local storage - self.getUserKeyPair(options.userId, function(err, keypair) { - if (err) { - callback(err); - return; - } - - var privkeyId = keypair.privateKey._id, - pgpBlock = keypair.privateKey.encryptedKey; - - // encrypt the private key with the derived key - var iv = util.random(config.symIvSize); - self._crypto.encrypt(pgpBlock, encryptionKey, iv, function(err, ct) { - if (err) { - callback(err); - return; - } - - var payload = { - _id: privkeyId, - userId: options.userId, - encryptedPrivateKey: ct, - salt: salt, - iv: iv - }; - - uploadPrivateKey(payload); - }); - }); - } - - function uploadPrivateKey(payload) { - // authenticate to server for upload - self._authenticateToPrivateKeyServer(options.userId, function(err, authSessionKey) { - if (err) { - callback(err); - return; - } - - // encrypt encryptedPrivateKey again using authSessionKey - var pt = payload.encryptedPrivateKey, - iv = payload.iv, - key = authSessionKey.sessionKey; - self._crypto.encrypt(pt, key, iv, function(err, ct) { - if (err) { - callback(err); - return; - } - - // replace the encryptedPrivateKey with the double wrapped ciphertext - payload.encryptedPrivateKey = ct; - // set sessionId - payload.sessionId = authSessionKey.sessionId; - - // upload the encrypted priavet key - self._privateKeyDao.upload(payload, callback); - }); - }); - } - }; - - /** - * 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 - * @param {Function} callback(error) - */ - KeychainDAO.prototype.requestPrivateKeyDownload = function(options, callback) { - this._privateKeyDao.requestDownload(options, callback); - }; - - /** - * 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 - * @param {Function} callback(error) - */ - KeychainDAO.prototype.hasPrivateKey = function(options, callback) { - this._privateKeyDao.hasPrivateKey(options, callback); - }; - - /** - * 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 - * @param {Function} callback(error, encryptedPrivateKey) - */ - KeychainDAO.prototype.downloadPrivateKey = function(options, callback) { - this._privateKeyDao.download(options, callback); - }; - - /** - * 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 - * @param {Function} callback(error, keyObject) - */ - KeychainDAO.prototype.decryptAndStorePrivateKeyLocally = function(options, callback) { - var self = this, - code = options.code, - salt = options.salt, - keySize = config.symKeySize; - - if (!options._id || !options.userId || !options.code || !options.salt || !options.encryptedPrivateKey || !options.iv) { - callback(new Error('Incomplete arguments!')); - return; - } - - // derive key from the code and the salt using PBKDF2 - self._crypto.deriveKey(code, salt, keySize, function(err, key) { - if (err) { - callback(err); - return; - } - - decryptAndStore(key); - }); - - function decryptAndStore(derivedKey) { - // decrypt the private key with the derived key - var ct = options.encryptedPrivateKey, - iv = options.iv; - - self._crypto.decrypt(ct, derivedKey, iv, function(err, privateKeyArmored) { - if (err) { - callback(new Error('Invalid keychain code!')); - return; - } - - // validate pgp key - var keyParams; - try { - keyParams = self._pgp.getKeyParams(privateKeyArmored); - } catch (e) { - callback(new Error('Error parsing private PGP key!')); - return; - } - - if (keyParams._id !== options._id || keyParams.userId !== options.userId) { - callback(new Error('Private key parameters don\'t match with public key\'s!')); - return; - } - - var keyObject = { - _id: options._id, - userId: options.userId, - encryptedKey: privateKeyArmored - }; - - // store private key locally - self.saveLocalPrivateKey(keyObject, function(err) { - if (err) { - callback(err); - return; - } - - callback(null, keyObject); - }); - }); - } - }; - - // - // Keypair functions - // - - /** - * Gets the local user's key either from local storage - * or fetches it from the cloud. The private key is encrypted. - * If no key pair exists, null is returned. - * return [Object] The user's key pair {publicKey, privateKey} - */ - KeychainDAO.prototype.getUserKeyPair = function(userId, callback) { - var self = this; - - // search for user's public key locally - self._localDbDao.list(DB_PUBLICKEY, 0, null, function(err, allPubkeys) { - if (err) { - callback(err); - return; - } - - var pubkey = _.findWhere(allPubkeys, { - userId: userId - }); - - if (pubkey && pubkey._id) { - // that user's public key is already in local storage... - // sync keypair to the cloud - syncKeypair(pubkey._id); - return; - } - - // no public key by that user id in storage - // find from cloud by email address - self._publicKeyDao.getByUserId(userId, function(err, cloudPubkey) { - if (err) { - callback(err); - return; - } - - if (cloudPubkey && cloudPubkey._id) { - // there is a public key for that user already in the cloud... - // sync keypair to local storage - syncKeypair(cloudPubkey._id); - return; - } - - // continue without keypair... generate in crypto.js - callback(); - }); - }); - - function syncKeypair(keypairId) { - // persist key pair in local storage - self.lookupPublicKey(keypairId, function(err, savedPubkey) { - if (err) { - callback(err); - return; - } - - // persist private key in local storage - self.lookupPrivateKey(keypairId, function(err, savedPrivkey) { - var keys = {}; - - if (err) { - callback(err); - return; - } - - if (savedPubkey && savedPubkey.publicKey) { - keys.publicKey = savedPubkey; - } - - if (savedPrivkey && savedPrivkey.encryptedKey) { - keys.privateKey = savedPrivkey; - } - - callback(null, keys); - }); - }); - } - }; - - /** - * Checks to see if the user's key pair is stored both - * locally and in the cloud and persist arccordingly - * @param [Object] The user's key pair {publicKey, privateKey} - */ - KeychainDAO.prototype.putUserKeyPair = function(keypair, callback) { - var self = this; - - // validate input - if (!keypair || !keypair.publicKey || !keypair.privateKey || !keypair.publicKey.userId || keypair.publicKey.userId !== keypair.privateKey.userId) { - callback({ - errMsg: 'Incorrect input!' - }); - return; - } - - // don't check the user's own public key for deletion in refreshKeyForUserId - keypair.publicKey.imported = true; - - // store public key locally - self.saveLocalPublicKey(keypair.publicKey, function(err) { - if (err) { - callback(err); - return; - } - - // persist public key in cloud storage - self._publicKeyDao.put(keypair.publicKey, function(err) { - // validate result - if (err) { - callback(err); - return; - } - - // store private key locally - self.saveLocalPrivateKey(keypair.privateKey, callback); - }); - }); - }; - - // - // Helper functions - // - - KeychainDAO.prototype.lookupPublicKey = function(id, callback) { - var self = this; - - if (!id) { - callback({ - errMsg: 'ID must be set for public key query!' - }); - return; - } - - // lookup in local storage - self._localDbDao.read(DB_PUBLICKEY + '_' + id, function(err, pubkey) { - if (err) { - callback(err); - return; - } - - if (pubkey) { - callback(null, pubkey); - return; - } - - // fetch from cloud storage - self._publicKeyDao.get(id, function(err, cloudPubkey) { - if (err) { - callback(err); - return; - } - - // cache public key in cache - self.saveLocalPublicKey(cloudPubkey, function(err) { - if (err) { - callback(err); - return; - } - - callback(null, cloudPubkey); - }); - }); - }); - }; - - /** - * List all the locally stored public keys - */ - KeychainDAO.prototype.listLocalPublicKeys = function(callback) { - // search local keyring for public key - this._localDbDao.list(DB_PUBLICKEY, 0, null, callback); - }; - - KeychainDAO.prototype.removeLocalPublicKey = function(id, callback) { - this._localDbDao.remove(DB_PUBLICKEY + '_' + id, callback); - }; - - KeychainDAO.prototype.lookupPrivateKey = function(id, callback) { - // lookup in local storage - this._localDbDao.read(DB_PRIVATEKEY + '_' + id, callback); - }; - - KeychainDAO.prototype.saveLocalPublicKey = function(pubkey, callback) { - // persist public key (email, _id) - var pkLookupKey = DB_PUBLICKEY + '_' + pubkey._id; - this._localDbDao.persist(pkLookupKey, pubkey, callback); - }; - - KeychainDAO.prototype.saveLocalPrivateKey = function(privkey, callback) { - // persist private key (email, _id) - var prkLookupKey = DB_PRIVATEKEY + '_' + privkey._id; - this._localDbDao.persist(prkLookupKey, privkey, callback); - }; - - return KeychainDAO; -}); \ No newline at end of file + }); +}; + +/** + * List all the locally stored public keys + */ +KeychainDAO.prototype.listLocalPublicKeys = function(callback) { + // search local keyring for public key + this._localDbDao.list(DB_PUBLICKEY, 0, null, callback); +}; + +KeychainDAO.prototype.removeLocalPublicKey = function(id, callback) { + this._localDbDao.remove(DB_PUBLICKEY + '_' + id, callback); +}; + +KeychainDAO.prototype.lookupPrivateKey = function(id, callback) { + // lookup in local storage + this._localDbDao.read(DB_PRIVATEKEY + '_' + id, callback); +}; + +KeychainDAO.prototype.saveLocalPublicKey = function(pubkey, callback) { + // persist public key (email, _id) + var pkLookupKey = DB_PUBLICKEY + '_' + pubkey._id; + this._localDbDao.persist(pkLookupKey, pubkey, callback); +}; + +KeychainDAO.prototype.saveLocalPrivateKey = function(privkey, callback) { + // persist private key (email, _id) + var prkLookupKey = DB_PRIVATEKEY + '_' + privkey._id; + this._localDbDao.persist(prkLookupKey, privkey, callback); +}; + +exports = KeychainDAO; \ No newline at end of file diff --git a/src/js/dao/lawnchair-dao.js b/src/js/dao/lawnchair-dao.js index bee484f..d5812ae 100644 --- a/src/js/dao/lawnchair-dao.js +++ b/src/js/dao/lawnchair-dao.js @@ -1,222 +1,216 @@ /** * Handles generic caching of JSON objects in a lawnchair adapter */ -define(function(require) { - 'use strict'; - var _ = require('underscore'), - Lawnchair = require('lawnchair'); - require('lawnchairSQL'); - require('lawnchairIDB'); +'use strict'; - var LawnchairDAO = function() {}; +var LawnchairDAO = function() {}; - LawnchairDAO.prototype.init = function(dbName, callback) { - if (!dbName) { +LawnchairDAO.prototype.init = function(dbName, callback) { + if (!dbName) { + callback({ + errMsg: 'Lawnchair DB name must be specified!' + }); + return; + } + + this._db = new Lawnchair({ + name: dbName + }, function(lc) { + if (!lc) { callback({ - errMsg: 'Lawnchair DB name must be specified!' + errMsg: 'Lawnchair init failed!' }); return; } - this._db = new Lawnchair({ - name: dbName - }, function(lc) { - if (!lc) { - callback({ - errMsg: 'Lawnchair init failed!' - }); - return; - } + callback(); + }); +}; +/** + * Create or update an object + */ +LawnchairDAO.prototype.persist = function(key, object, callback) { + if (!key || !object) { + callback({ + errMsg: 'Key and Object must be set!' + }); + return; + } + + this._db.save({ + key: key, + object: object + }, function(persisted) { + if (persisted.key !== key) { + callback({ + errMsg: 'Persisting failed!' + }); + return; + } + + callback(); + }); +}; + +/** + * Persist a bunch of items at once + */ +LawnchairDAO.prototype.batch = function(list, callback) { + if (!(list instanceof Array)) { + callback({ + errMsg: 'Input must be of type Array!' + }); + return; + } + + this._db.batch(list, function(res) { + if (!res) { + callback({ + errMsg: 'Persisting batch failed!' + }); + return; + } + + callback(); + }); +}; + +/** + * Read a single item by its key + */ +LawnchairDAO.prototype.read = function(key, callback) { + if (!key) { + callback({ + errMsg: 'Key must be specified!' + }); + return; + } + + this._db.get(key, function(o) { + if (o) { + callback(null, o.object); + } else { callback(); - }); - }; + } + }); +}; - /** - * Create or update an object - */ - LawnchairDAO.prototype.persist = function(key, object, callback) { - if (!key || !object) { - callback({ - errMsg: 'Key and Object must be set!' - }); +/** + * 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) + */ +LawnchairDAO.prototype.list = function(type, offset, num, callback) { + var self = this, + i, from, to, + matchingKeys = [], + intervalKeys = [], + list = []; + + // validate input + if (!type || typeof offset === 'undefined' || typeof num === 'undefined') { + callback({ + errMsg: 'Args not is not set!' + }); + return; + } + + // 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); + } + }); + + // 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) { + callback(null, list); return; } - this._db.save({ - key: key, - object: object - }, function(persisted) { - if (persisted.key !== key) { - callback({ - errMsg: 'Persisting failed!' - }); - return; - } + // fetch all items from data-store with matching key + self._db.get(intervalKeys, function(intervalList) { + intervalList.forEach(function(item) { + list.push(item.object); + }); + // return only the interval between offset and num + callback(null, list); + }); + + }); +}; + +/** + * Removes an object liter from local storage by its key (delete) + */ +LawnchairDAO.prototype.remove = function(key, callback) { + this._db.remove(key, callback); +}; + +/** + * Removes an object liter from local storage by its key (delete) + */ +LawnchairDAO.prototype.removeList = function(type, callback) { + var self = this, + matchingKeys = [], + after; + + // validate type + if (!type) { + callback({ + errMsg: 'Type is not set!' + }); + return; + } + + // 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); + } + }); + + if (matchingKeys.length < 1) { callback(); - }); - }; - - /** - * Persist a bunch of items at once - */ - LawnchairDAO.prototype.batch = function(list, callback) { - if (!(list instanceof Array)) { - callback({ - errMsg: 'Input must be of type Array!' - }); return; } - this._db.batch(list, function(res) { - if (!res) { - callback({ - errMsg: 'Persisting batch failed!' - }); - return; - } - - callback(); + // remove all matching keys + after = _.after(matchingKeys.length, callback); + _.each(matchingKeys, function(key) { + self._db.remove(key, after); }); - }; + }); +}; - /** - * Read a single item by its key - */ - LawnchairDAO.prototype.read = function(key, callback) { - if (!key) { - callback({ - errMsg: 'Key must be specified!' - }); - return; - } +/** + * Clears the whole local storage cache + */ +LawnchairDAO.prototype.clear = function(callback) { + this._db.nuke(callback); +}; - this._db.get(key, function(o) { - if (o) { - callback(null, o.object); - } else { - callback(); - } - }); - }; - - /** - * 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) - */ - LawnchairDAO.prototype.list = function(type, offset, num, callback) { - var self = this, - i, from, to, - matchingKeys = [], - intervalKeys = [], - list = []; - - // validate input - if (!type || typeof offset === 'undefined' || typeof num === 'undefined') { - callback({ - errMsg: 'Args not is not set!' - }); - return; - } - - // 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); - } - }); - - // 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) { - callback(null, list); - return; - } - - // fetch all items from data-store with matching key - self._db.get(intervalKeys, function(intervalList) { - intervalList.forEach(function(item) { - list.push(item.object); - }); - - // return only the interval between offset and num - callback(null, list); - }); - - }); - }; - - /** - * Removes an object liter from local storage by its key (delete) - */ - LawnchairDAO.prototype.remove = function(key, callback) { - this._db.remove(key, callback); - }; - - /** - * Removes an object liter from local storage by its key (delete) - */ - LawnchairDAO.prototype.removeList = function(type, callback) { - var self = this, - matchingKeys = [], - after; - - // validate type - if (!type) { - callback({ - errMsg: 'Type is not set!' - }); - return; - } - - // 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); - } - }); - - if (matchingKeys.length < 1) { - callback(); - return; - } - - // remove all matching keys - after = _.after(matchingKeys.length, callback); - _.each(matchingKeys, function(key) { - self._db.remove(key, after); - }); - }); - }; - - /** - * Clears the whole local storage cache - */ - LawnchairDAO.prototype.clear = function(callback) { - this._db.nuke(callback); - }; - - return LawnchairDAO; -}); \ No newline at end of file +exports = LawnchairDAO; \ No newline at end of file diff --git a/src/js/dao/privatekey-dao.js b/src/js/dao/privatekey-dao.js index 5d8ba49..10979d7 100644 --- a/src/js/dao/privatekey-dao.js +++ b/src/js/dao/privatekey-dao.js @@ -1,198 +1,196 @@ -define(function() { - 'use strict'; +'use strict'; - var PrivateKeyDAO = function(restDao) { - this._restDao = restDao; - }; +var PrivateKeyDAO = function(restDao) { + this._restDao = restDao; +}; - // - // Device registration functions - // +// +// 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 - * @param {Function} callback(error, regSessionKey) - * @return {Object} {encryptedRegSessionKey:[base64]} - */ - PrivateKeyDAO.prototype.requestDeviceRegistration = function(options, callback) { - var uri; +/** + * 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 + * @param {Function} callback(error, regSessionKey) + * @return {Object} {encryptedRegSessionKey:[base64]} + */ +PrivateKeyDAO.prototype.requestDeviceRegistration = function(options, callback) { + var uri; - if (!options.userId || !options.deviceName) { - callback(new Error('Incomplete arguments!')); + if (!options.userId || !options.deviceName) { + callback(new Error('Incomplete arguments!')); + return; + } + + uri = '/device/user/' + options.userId + '/devicename/' + options.deviceName; + this._restDao.post(undefined, uri, callback); +}; + +/** + * 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 + * @param {Function} callback(error) + */ +PrivateKeyDAO.prototype.uploadDeviceSecret = function(options, callback) { + var uri; + + if (!options.userId || !options.deviceName || !options.encryptedDeviceSecret || !options.iv) { + callback(new Error('Incomplete arguments!')); + return; + } + + uri = '/device/user/' + options.userId + '/devicename/' + options.deviceName; + this._restDao.put(options, uri, callback); +}; + +// +// Private key functions +// + +/** + * Request authSessionKeys required for upload the encrypted private PGP key. + * @param {String} options.userId The user's email address + * @param {Function} callback(error, authSessionKey) + * @return {Object} {sessionId, encryptedAuthSessionKey:[base64 encoded], encryptedChallenge:[base64 encoded]} + */ +PrivateKeyDAO.prototype.requestAuthSessionKey = function(options, callback) { + var uri; + + if (!options.userId) { + callback(new Error('Incomplete arguments!')); + return; + } + + uri = '/auth/user/' + options.userId; + this._restDao.post(undefined, uri, callback); +}; + +/** + * 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 + * @param {Function} callback(error) + */ +PrivateKeyDAO.prototype.verifyAuthentication = function(options, callback) { + var uri; + + if (!options.userId || !options.sessionId || !options.encryptedChallenge || !options.encryptedDeviceSecret || !options.iv) { + callback(new Error('Incomplete arguments!')); + return; + } + + uri = '/auth/user/' + options.userId + '/session/' + options.sessionId; + this._restDao.put(options, uri, callback); +}; + +/** + * Upload the encrypted private PGP key. + * @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 + * @param {Function} callback(error) + */ +PrivateKeyDAO.prototype.upload = function(options, callback) { + var uri; + + if (!options._id || !options.userId || !options.encryptedPrivateKey || !options.sessionId || !options.salt || !options.iv) { + callback(new Error('Incomplete arguments!')); + return; + } + + uri = '/privatekey/user/' + options.userId + '/session/' + options.sessionId; + this._restDao.post(options, uri, callback); +}; + +/** + * 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 + * @param {Function} callback(error, found) + * @return {Boolean} whether the key was found on the server or not. + */ +PrivateKeyDAO.prototype.hasPrivateKey = function(options, callback) { + if (!options.userId || !options.keyId) { + callback(new Error('Incomplete arguments!')); + return; + } + + this._restDao.get({ + uri: '/privatekey/user/' + options.userId + '/key/' + options.keyId + '?ignoreRecovery=true', + }, function(err) { + // 404: there is no encrypted private key on the server + if (err && err.code !== 200) { + callback(null, false); return; } - uri = '/device/user/' + options.userId + '/devicename/' + options.deviceName; - this._restDao.post(undefined, uri, callback); - }; - - /** - * 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 - * @param {Function} callback(error) - */ - PrivateKeyDAO.prototype.uploadDeviceSecret = function(options, callback) { - var uri; - - if (!options.userId || !options.deviceName || !options.encryptedDeviceSecret || !options.iv) { - callback(new Error('Incomplete arguments!')); + if (err) { + callback(err); return; } - uri = '/device/user/' + options.userId + '/devicename/' + options.deviceName; - this._restDao.put(options, uri, callback); - }; + callback(null, true); + }); +}; - // - // Private key functions - // +/** + * 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 + * @param {Function} callback(error, found) + * @return {Boolean} whether the key was found on the server or not. + */ +PrivateKeyDAO.prototype.requestDownload = function(options, callback) { + if (!options.userId || !options.keyId) { + callback(new Error('Incomplete arguments!')); + return; + } - /** - * Request authSessionKeys required for upload the encrypted private PGP key. - * @param {String} options.userId The user's email address - * @param {Function} callback(error, authSessionKey) - * @return {Object} {sessionId, encryptedAuthSessionKey:[base64 encoded], encryptedChallenge:[base64 encoded]} - */ - PrivateKeyDAO.prototype.requestAuthSessionKey = function(options, callback) { - var uri; - - if (!options.userId) { - callback(new Error('Incomplete arguments!')); + this._restDao.get({ + uri: '/privatekey/user/' + options.userId + '/key/' + options.keyId + }, function(err) { + // 404: there is no encrypted private key on the server + if (err && err.code !== 200) { + callback(null, false); return; } - uri = '/auth/user/' + options.userId; - this._restDao.post(undefined, uri, callback); - }; - - /** - * 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 - * @param {Function} callback(error) - */ - PrivateKeyDAO.prototype.verifyAuthentication = function(options, callback) { - var uri; - - if (!options.userId || !options.sessionId || !options.encryptedChallenge || !options.encryptedDeviceSecret || !options.iv) { - callback(new Error('Incomplete arguments!')); + if (err) { + callback(err); return; } - uri = '/auth/user/' + options.userId + '/session/' + options.sessionId; - this._restDao.put(options, uri, callback); - }; + callback(null, true); + }); +}; - /** - * Upload the encrypted private PGP key. - * @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 - * @param {Function} callback(error) - */ - PrivateKeyDAO.prototype.upload = function(options, callback) { - var uri; +/** + * Verify the download request for the private PGP key using the recovery token sent via email. This downloads the actual encrypted private 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 + * @param {Function} callback(error, encryptedPrivateKey) + * @return {Object} {_id:[hex encoded capital 16 char key id], encryptedPrivateKey:[base64 encoded], encryptedUserId: [base64 encoded]} + */ +PrivateKeyDAO.prototype.download = function(options, callback) { + var uri; - if (!options._id || !options.userId || !options.encryptedPrivateKey || !options.sessionId || !options.salt || !options.iv) { - callback(new Error('Incomplete arguments!')); - return; - } + if (!options.userId || !options.keyId || !options.recoveryToken) { + callback(new Error('Incomplete arguments!')); + return; + } - uri = '/privatekey/user/' + options.userId + '/session/' + options.sessionId; - this._restDao.post(options, uri, callback); - }; + uri = '/privatekey/user/' + options.userId + '/key/' + options.keyId + '/recovery/' + options.recoveryToken; + this._restDao.get({ + uri: uri + }, callback); +}; - /** - * 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 - * @param {Function} callback(error, found) - * @return {Boolean} whether the key was found on the server or not. - */ - PrivateKeyDAO.prototype.hasPrivateKey = function(options, callback) { - if (!options.userId || !options.keyId) { - callback(new Error('Incomplete arguments!')); - return; - } - - this._restDao.get({ - uri: '/privatekey/user/' + options.userId + '/key/' + options.keyId + '?ignoreRecovery=true', - }, function(err) { - // 404: there is no encrypted private key on the server - if (err && err.code !== 200) { - callback(null, false); - return; - } - - if (err) { - callback(err); - return; - } - - callback(null, true); - }); - }; - - /** - * 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 - * @param {Function} callback(error, found) - * @return {Boolean} whether the key was found on the server or not. - */ - PrivateKeyDAO.prototype.requestDownload = function(options, callback) { - if (!options.userId || !options.keyId) { - callback(new Error('Incomplete arguments!')); - return; - } - - this._restDao.get({ - uri: '/privatekey/user/' + options.userId + '/key/' + options.keyId - }, function(err) { - // 404: there is no encrypted private key on the server - if (err && err.code !== 200) { - callback(null, false); - return; - } - - if (err) { - callback(err); - return; - } - - callback(null, true); - }); - }; - - /** - * Verify the download request for the private PGP key using the recovery token sent via email. This downloads the actual encrypted private 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 - * @param {Function} callback(error, encryptedPrivateKey) - * @return {Object} {_id:[hex encoded capital 16 char key id], encryptedPrivateKey:[base64 encoded], encryptedUserId: [base64 encoded]} - */ - PrivateKeyDAO.prototype.download = function(options, callback) { - var uri; - - if (!options.userId || !options.keyId || !options.recoveryToken) { - callback(new Error('Incomplete arguments!')); - return; - } - - uri = '/privatekey/user/' + options.userId + '/key/' + options.keyId + '/recovery/' + options.recoveryToken; - this._restDao.get({ - uri: uri - }, callback); - }; - - return PrivateKeyDAO; -}); \ No newline at end of file +exports = PrivateKeyDAO; \ No newline at end of file diff --git a/src/js/dao/publickey-dao.js b/src/js/dao/publickey-dao.js index a2ebba0..f4d6bff 100644 --- a/src/js/dao/publickey-dao.js +++ b/src/js/dao/publickey-dao.js @@ -1,105 +1,103 @@ -define(function() { - 'use strict'; +'use strict'; - var PublicKeyDAO = function(restDao) { - this._restDao = restDao; - }; +var PublicKeyDAO = function(restDao) { + this._restDao = restDao; +}; - /** - * Verify the public key behind the given uuid - */ - PublicKeyDAO.prototype.verify = function(uuid, callback) { - var uri = '/verify/' + uuid; +/** + * Verify the public key behind the given uuid + */ +PublicKeyDAO.prototype.verify = function(uuid, callback) { + var uri = '/verify/' + uuid; - this._restDao.get({ - uri: uri, - type: 'text' - }, function(err, res, status) { - if (err && err.code === 400) { - // there was an attempt to verify a non-existing public key - callback(); - return; - } + this._restDao.get({ + uri: uri, + type: 'text' + }, function(err, res, status) { + if (err && err.code === 400) { + // there was an attempt to verify a non-existing public key + callback(); + return; + } - callback(err, res, status); - }); - }; + callback(err, res, status); + }); +}; - /** - * Find the user's corresponding public key - */ - PublicKeyDAO.prototype.get = function(keyId, callback) { - var uri = '/publickey/key/' + keyId; +/** + * Find the user's corresponding public key + */ +PublicKeyDAO.prototype.get = function(keyId, callback) { + var uri = '/publickey/key/' + keyId; - this._restDao.get({ - uri: uri - }, function(err, key) { - if (err && err.code === 404) { - callback(); - return; - } + this._restDao.get({ + uri: uri + }, function(err, key) { + if (err && err.code === 404) { + callback(); + return; + } - if (err) { - callback(err); - return; - } + if (err) { + callback(err); + return; + } - callback(null, (key && key._id) ? key : undefined); - }); - }; + callback(null, (key && key._id) ? key : undefined); + }); +}; - /** - * Find the user's corresponding public key by email - */ - PublicKeyDAO.prototype.getByUserId = function(userId, callback) { - var uri = '/publickey/user/' + userId; +/** + * Find the user's corresponding public key by email + */ +PublicKeyDAO.prototype.getByUserId = function(userId, callback) { + var uri = '/publickey/user/' + userId; - this._restDao.get({ - uri: uri - }, function(err, keys) { - // not found - if (err && err.code === 404) { - callback(); - return; - } + this._restDao.get({ + uri: uri + }, function(err, keys) { + // not found + if (err && err.code === 404) { + callback(); + return; + } - if (err) { - callback(err); - return; - } + if (err) { + callback(err); + return; + } - if (!keys || keys.length < 1) { - // 'No public key for that user!' - callback(); - return; - } + if (!keys || keys.length < 1) { + // 'No public key for that user!' + callback(); + return; + } - if (keys.length > 1) { - callback({ - errMsg: 'That user has multiple public keys!' - }); - return; - } + if (keys.length > 1) { + callback({ + errMsg: 'That user has multiple public keys!' + }); + return; + } - callback(null, keys[0]); - }); - }; + callback(null, keys[0]); + }); +}; - /** - * Persist the user's publc key - */ - PublicKeyDAO.prototype.put = function(pubkey, callback) { - var uri = '/publickey/user/' + pubkey.userId + '/key/' + pubkey._id; - this._restDao.put(pubkey, uri, callback); - }; +/** + * Persist the user's publc key + */ +PublicKeyDAO.prototype.put = function(pubkey, callback) { + var uri = '/publickey/user/' + pubkey.userId + '/key/' + pubkey._id; + this._restDao.put(pubkey, uri, callback); +}; - /** - * Delete the public key from the cloud storage service - */ - PublicKeyDAO.prototype.remove = function(keyId, callback) { - var uri = '/publickey/key/' + keyId; - this._restDao.remove(uri, callback); - }; +/** + * Delete the public key from the cloud storage service + */ +PublicKeyDAO.prototype.remove = function(keyId, callback) { + var uri = '/publickey/key/' + keyId; + this._restDao.remove(uri, callback); +}; - return PublicKeyDAO; -}); \ No newline at end of file +exports = PublicKeyDAO; \ No newline at end of file diff --git a/src/js/dao/rest-dao.js b/src/js/dao/rest-dao.js index 686ebc1..17b3ce1 100644 --- a/src/js/dao/rest-dao.js +++ b/src/js/dao/rest-dao.js @@ -1,123 +1,121 @@ -define(function(require) { - 'use strict'; +'use strict'; - var config = require('js/app-config').config; +var config = require('../app-config').config; - var RestDAO = function(baseUri) { - if (baseUri) { - this._baseUri = baseUri; - } else { - this._baseUri = config.cloudUrl; - } - }; +var RestDAO = function(baseUri) { + if (baseUri) { + this._baseUri = baseUri; + } else { + this._baseUri = config.cloudUrl; + } +}; - /** - * GET (read) request - * @param {String} options.uri URI relative to the base uri to perform the GET request with. - * @param {String} options.type (optional) The type of data that you're expecting back from the server: json, xml, text. Default: json. - */ - RestDAO.prototype.get = function(options, callback) { - options.method = 'GET'; - this._processRequest(options, callback); - }; +/** + * GET (read) request + * @param {String} options.uri URI relative to the base uri to perform the GET request with. + * @param {String} options.type (optional) The type of data that you're expecting back from the server: json, xml, text. Default: json. + */ +RestDAO.prototype.get = function(options, callback) { + options.method = 'GET'; + this._processRequest(options, callback); +}; - /** - * POST (create) request - */ - RestDAO.prototype.post = function(item, uri, callback) { - this._processRequest({ - method: 'POST', - payload: item, - uri: uri - }, callback); - }; +/** + * POST (create) request + */ +RestDAO.prototype.post = function(item, uri, callback) { + this._processRequest({ + method: 'POST', + payload: item, + uri: uri + }, callback); +}; - /** - * PUT (update) request - */ - RestDAO.prototype.put = function(item, uri, callback) { - this._processRequest({ - method: 'PUT', - payload: item, - uri: uri - }, callback); - }; +/** + * PUT (update) request + */ +RestDAO.prototype.put = function(item, uri, callback) { + this._processRequest({ + method: 'PUT', + payload: item, + uri: uri + }, callback); +}; - /** - * DELETE (remove) request - */ - RestDAO.prototype.remove = function(uri, callback) { - this._processRequest({ - method: 'DELETE', - uri: uri - }, callback); - }; +/** + * DELETE (remove) request + */ +RestDAO.prototype.remove = function(uri, callback) { + this._processRequest({ + method: 'DELETE', + uri: uri + }, callback); +}; - // - // helper functions - // +// +// helper functions +// - RestDAO.prototype._processRequest = function(options, callback) { - var xhr, format; +RestDAO.prototype._processRequest = function(options, callback) { + var xhr, format; - if (typeof options.uri === 'undefined') { - callback({ - code: 400, - errMsg: 'Bad Request! URI is a mandatory parameter.' - }); - return; - } + if (typeof options.uri === 'undefined') { + callback({ + code: 400, + errMsg: 'Bad Request! URI is a mandatory parameter.' + }); + return; + } - options.type = options.type || 'json'; + options.type = options.type || 'json'; - if (options.type === 'json') { - format = 'application/json'; - } else if (options.type === 'xml') { - format = 'application/xml'; - } else if (options.type === 'text') { - format = 'text/plain'; - } else { - callback({ - code: 400, - errMsg: 'Bad Request! Unhandled data type.' - }); - return; - } + if (options.type === 'json') { + format = 'application/json'; + } else if (options.type === 'xml') { + format = 'application/xml'; + } else if (options.type === 'text') { + format = 'text/plain'; + } else { + callback({ + code: 400, + errMsg: 'Bad Request! Unhandled data type.' + }); + return; + } - xhr = new XMLHttpRequest(); - xhr.open(options.method, this._baseUri + options.uri); - xhr.setRequestHeader('Accept', format); - xhr.setRequestHeader('Content-Type', format); + xhr = new XMLHttpRequest(); + xhr.open(options.method, this._baseUri + options.uri); + xhr.setRequestHeader('Accept', format); + xhr.setRequestHeader('Content-Type', format); - xhr.onload = function() { - var res; + 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; - } else { - res = xhr.responseText; - } - - callback(null, res, xhr.status); - return; + 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; + } else { + res = xhr.responseText; } - callback({ - code: xhr.status, - errMsg: xhr.statusText - }); - }; + callback(null, res, xhr.status); + return; + } - xhr.onerror = function() { - callback({ - code: 42, - errMsg: 'Error calling ' + options.method + ' on ' + options.uri - }); - }; - - xhr.send(options.payload ? JSON.stringify(options.payload) : undefined); + callback({ + code: xhr.status, + errMsg: xhr.statusText + }); }; - return RestDAO; -}); \ No newline at end of file + xhr.onerror = function() { + callback({ + code: 42, + errMsg: 'Error calling ' + options.method + ' on ' + options.uri + }); + }; + + xhr.send(options.payload ? JSON.stringify(options.payload) : undefined); +}; + +exports = RestDAO; \ No newline at end of file diff --git a/src/js/util/backbutton-handler.js b/src/js/util/backbutton-handler.js index 66b92a7..930e61a 100644 --- a/src/js/util/backbutton-handler.js +++ b/src/js/util/backbutton-handler.js @@ -1,57 +1,55 @@ -define(function(require) { - 'use strict'; +'use strict'; - var axe = require('axe'), - DEBUG_TAG = 'backbutton handler'; +var axe = require('axe-logger'), + DEBUG_TAG = 'backbutton handler'; - /** - * The back button handler introduces meaningful behavior fo rthe back button: - * if there's an open lightbox, close it; - * if the reader is open in mobile mode, close it; - * if the navigation is open, close it; - * if there's nothing else open, shut down the app; - * - * @type {Object} - */ - var backBtnHandler = { - attachHandler: function(scope) { - this.scope = scope; - }, - start: function() { - document.addEventListener("backbutton", handleBackButton, false); - }, - stop: function() { - document.removeEventListener("backbutton", handleBackButton, false); - } - }; - - function handleBackButton(event) { - axe.debug(DEBUG_TAG, 'back button pressed'); - - // this disarms the default behavior which we NEVER want - event.preventDefault(); - event.stopPropagation(); - - if (backBtnHandler.scope.state.lightbox) { - // closes the lightbox (error msgs, writer, ...) - backBtnHandler.scope.state.lightbox = undefined; - axe.debug(DEBUG_TAG, 'lightbox closed'); - backBtnHandler.scope.$apply(); - } else if (backBtnHandler.scope.state.read && backBtnHandler.scope.state.read.open) { - // closes the reader - backBtnHandler.scope.state.read.toggle(false); - axe.debug(DEBUG_TAG, 'reader closed'); - backBtnHandler.scope.$apply(); - } else if (backBtnHandler.scope.state.nav && backBtnHandler.scope.state.nav.open) { - // closes the navigation - backBtnHandler.scope.state.nav.toggle(false); - axe.debug(DEBUG_TAG, 'navigation closed'); - backBtnHandler.scope.$apply(); - } else { - // exits the app - navigator.app.exitApp(); - } +/** + * The back button handler introduces meaningful behavior fo rthe back button: + * if there's an open lightbox, close it; + * if the reader is open in mobile mode, close it; + * if the navigation is open, close it; + * if there's nothing else open, shut down the app; + * + * @type {Object} + */ +var backBtnHandler = { + attachHandler: function(scope) { + this.scope = scope; + }, + start: function() { + document.addEventListener("backbutton", handleBackButton, false); + }, + stop: function() { + document.removeEventListener("backbutton", handleBackButton, false); } +}; - return backBtnHandler; -}); \ No newline at end of file +function handleBackButton(event) { + axe.debug(DEBUG_TAG, 'back button pressed'); + + // this disarms the default behavior which we NEVER want + event.preventDefault(); + event.stopPropagation(); + + if (backBtnHandler.scope.state.lightbox) { + // closes the lightbox (error msgs, writer, ...) + backBtnHandler.scope.state.lightbox = undefined; + axe.debug(DEBUG_TAG, 'lightbox closed'); + backBtnHandler.scope.$apply(); + } else if (backBtnHandler.scope.state.read && backBtnHandler.scope.state.read.open) { + // closes the reader + backBtnHandler.scope.state.read.toggle(false); + axe.debug(DEBUG_TAG, 'reader closed'); + backBtnHandler.scope.$apply(); + } else if (backBtnHandler.scope.state.nav && backBtnHandler.scope.state.nav.open) { + // closes the navigation + backBtnHandler.scope.state.nav.toggle(false); + axe.debug(DEBUG_TAG, 'navigation closed'); + backBtnHandler.scope.$apply(); + } else { + // exits the app + navigator.app.exitApp(); + } +} + +exports = backBtnHandler; \ No newline at end of file diff --git a/src/js/util/connection-doctor.js b/src/js/util/connection-doctor.js index 43e047a..acd5a23 100644 --- a/src/js/util/connection-doctor.js +++ b/src/js/util/connection-doctor.js @@ -1,295 +1,293 @@ -define(function(require) { - 'use strict'; +'use strict'; - var TCPSocket = require('tcp-socket'), - appConfig = require('js/app-config'), - cfg = appConfig.config, - strings = appConfig.string, - ImapClient = require('imap-client'), - SmtpClient = require('smtpclient'); +var TCPSocket = require('tcp-socket'), + appConfig = require('../app-config'), + cfg = appConfig.config, + strings = appConfig.string, + ImapClient = require('imap-client'), + SmtpClient = require('wo-smtpclient'); - /** - * The connection doctor can check your connection. In essence, it reconstructs what happens when - * the app goes online in an abbreviated way. You need to configure() the instance with the IMAP/SMTP - * credentials before running check()! - * - * @constructor - */ - var ConnectionDoctor = function() {}; +/** + * The connection doctor can check your connection. In essence, it reconstructs what happens when + * the app goes online in an abbreviated way. You need to configure() the instance with the IMAP/SMTP + * credentials before running check()! + * + * @constructor + */ +var ConnectionDoctor = function() {}; - // - // Error codes - // +// +// Error codes +// - var OFFLINE = ConnectionDoctor.OFFLINE = 42; - var TLS_WRONG_CERT = ConnectionDoctor.TLS_WRONG_CERT = 43; - var HOST_UNREACHABLE = ConnectionDoctor.HOST_UNREACHABLE = 44; - var HOST_TIMEOUT = ConnectionDoctor.HOST_TIMEOUT = 45; - var AUTH_REJECTED = ConnectionDoctor.AUTH_REJECTED = 46; - var NO_INBOX = ConnectionDoctor.NO_INBOX = 47; - var GENERIC_ERROR = ConnectionDoctor.GENERIC_ERROR = 48; +var OFFLINE = ConnectionDoctor.OFFLINE = 42; +var TLS_WRONG_CERT = ConnectionDoctor.TLS_WRONG_CERT = 43; +var HOST_UNREACHABLE = ConnectionDoctor.HOST_UNREACHABLE = 44; +var HOST_TIMEOUT = ConnectionDoctor.HOST_TIMEOUT = 45; +var AUTH_REJECTED = ConnectionDoctor.AUTH_REJECTED = 46; +var NO_INBOX = ConnectionDoctor.NO_INBOX = 47; +var GENERIC_ERROR = ConnectionDoctor.GENERIC_ERROR = 48; - // - // Public API - // +// +// Public API +// - /** - * Configures the connection doctor - * - * @param {Object} credentials.imap IMAP configuration (host:string, port:number, secure:boolean, ignoreTLS:boolean) - * @param {Object} credentials.smtp SMTP configuration (host:string, port:number, secure:boolean, ignoreTLS:boolean) - * @param {String} credentials.username - * @param {String} credentials.password - */ - ConnectionDoctor.prototype.configure = function(credentials) { - this.credentials = credentials; +/** + * Configures the connection doctor + * + * @param {Object} credentials.imap IMAP configuration (host:string, port:number, secure:boolean, ignoreTLS:boolean) + * @param {Object} credentials.smtp SMTP configuration (host:string, port:number, secure:boolean, ignoreTLS:boolean) + * @param {String} credentials.username + * @param {String} credentials.password + */ +ConnectionDoctor.prototype.configure = function(credentials) { + this.credentials = credentials; - // internal members - this._imap = new ImapClient({ - host: this.credentials.imap.host, - port: this.credentials.imap.port, - secure: this.credentials.imap.secure, - ignoreTLS: this.credentials.imap.ignoreTLS, - ca: this.credentials.imap.ca, - auth: { - user: this.credentials.username, - pass: this.credentials.password, - xoauth2: this.credentials.xoauth2 - } - }); + // internal members + this._imap = new ImapClient({ + host: this.credentials.imap.host, + port: this.credentials.imap.port, + secure: this.credentials.imap.secure, + ignoreTLS: this.credentials.imap.ignoreTLS, + ca: this.credentials.imap.ca, + auth: { + user: this.credentials.username, + pass: this.credentials.password, + xoauth2: this.credentials.xoauth2 + } + }); - this._smtp = new SmtpClient(this.credentials.smtp.host, this.credentials.smtp.port, { - useSecureTransport: this.credentials.smtp.secure, - ignoreTLS: this.credentials.smtp.ignoreTLS, - ca: this.credentials.smtp.ca, - auth: { - user: this.credentials.username, - pass: this.credentials.password, - xoauth2: this.credentials.xoauth2 - } - }); - }; + this._smtp = new SmtpClient(this.credentials.smtp.host, this.credentials.smtp.port, { + useSecureTransport: this.credentials.smtp.secure, + ignoreTLS: this.credentials.smtp.ignoreTLS, + ca: this.credentials.smtp.ca, + auth: { + user: this.credentials.username, + pass: this.credentials.password, + xoauth2: this.credentials.xoauth2 + } + }); +}; - /** - * It conducts the following tests for IMAP and SMTP, respectively: - * 1) Check if browser is online - * 2) Connect to host:port via TCP/TLS - * 3) Login to the server - * 4) Perform some basic commands (e.g. list folders) - * 5) Exposes error codes - * - * @param {Function} callback(error) Invoked when the test suite passed, or with an error object if something went wrong - */ - ConnectionDoctor.prototype.check = function(callback) { - var self = this; +/** + * It conducts the following tests for IMAP and SMTP, respectively: + * 1) Check if browser is online + * 2) Connect to host:port via TCP/TLS + * 3) Login to the server + * 4) Perform some basic commands (e.g. list folders) + * 5) Exposes error codes + * + * @param {Function} callback(error) Invoked when the test suite passed, or with an error object if something went wrong + */ +ConnectionDoctor.prototype.check = function(callback) { + var self = this; - if (!self.credentials) { - return callback(new Error('You need to configure() the connection doctor first!')); + if (!self.credentials) { + return callback(new Error('You need to configure() the connection doctor first!')); + } + + self._checkOnline(function(error) { + if (error) { + return callback(error); } - self._checkOnline(function(error) { + self._checkReachable(self.credentials.imap, function(error) { if (error) { return callback(error); } - self._checkReachable(self.credentials.imap, function(error) { + self._checkReachable(self.credentials.smtp, function(error) { if (error) { return callback(error); } - self._checkReachable(self.credentials.smtp, function(error) { + self._checkImap(function(error) { if (error) { return callback(error); } - self._checkImap(function(error) { - if (error) { - return callback(error); - } - - self._checkSmtp(callback); - }); + self._checkSmtp(callback); }); }); }); - }; + }); +}; - // - // Internal API - // +// +// Internal API +// - /** - * Checks if the browser is online - * - * @param {Function} callback(error) Invoked when the test suite passed, or with an error object if browser is offline - */ - ConnectionDoctor.prototype._checkOnline = function(callback) { - if (navigator.onLine) { - callback(); - } else { - callback(createError(OFFLINE, strings.connDocOffline)); +/** + * Checks if the browser is online + * + * @param {Function} callback(error) Invoked when the test suite passed, or with an error object if browser is offline + */ +ConnectionDoctor.prototype._checkOnline = function(callback) { + if (navigator.onLine) { + callback(); + } else { + callback(createError(OFFLINE, strings.connDocOffline)); + } +}; + +/** + * Checks if a host is reachable via TCP + * + * @param {String} options.host + * @param {Number} options.port + * @param {Boolean} options.secure + * @param {Function} callback(error) Invoked when the test suite passed, or with an error object if something went wrong + */ +ConnectionDoctor.prototype._checkReachable = function(options, callback) { + var socket, + error, // remember the error message + timeout, // remember the timeout object + host = options.host + ':' + options.port, + hasTimedOut = false; // prevents multiple callbacks + + timeout = setTimeout(function() { + hasTimedOut = true; + callback(createError(HOST_TIMEOUT, strings.connDocHostTimeout.replace('{0}', host).replace('{1}', cfg.connDocTimeout))); + }, cfg.connDocTimeout); + + socket = TCPSocket.open(options.host, options.port, { + binaryType: 'arraybuffer', + useSecureTransport: options.secure, + ca: options.ca + }); + + socket.ondata = function() {}; // we don't actually care about the data + + // [WO-625] Mozilla forbids extensions to the TCPSocket object, + // throws an exception when assigned unexpected callback functions. + // The exception can be safely ignored since we need the callback + // for the other shims + try { + socket.oncert = function() { + if (options.ca) { + // the certificate we already have is outdated + error = createError(TLS_WRONG_CERT, strings.connDocTlsWrongCert.replace('{0}', host)); + } + }; + } catch (e) {} + + socket.onerror = function(e) { + if (!error) { + error = createError(HOST_UNREACHABLE, strings.connDocHostUnreachable.replace('{0}', host), e.data); } }; - /** - * Checks if a host is reachable via TCP - * - * @param {String} options.host - * @param {Number} options.port - * @param {Boolean} options.secure - * @param {Function} callback(error) Invoked when the test suite passed, or with an error object if something went wrong - */ - ConnectionDoctor.prototype._checkReachable = function(options, callback) { - var socket, - error, // remember the error message - timeout, // remember the timeout object - host = options.host + ':' + options.port, - hasTimedOut = false; // prevents multiple callbacks - - timeout = setTimeout(function() { - hasTimedOut = true; - callback(createError(HOST_TIMEOUT, strings.connDocHostTimeout.replace('{0}', host).replace('{1}', cfg.connDocTimeout))); - }, cfg.connDocTimeout); - - socket = TCPSocket.open(options.host, options.port, { - binaryType: 'arraybuffer', - useSecureTransport: options.secure, - ca: options.ca - }); - - socket.ondata = function() {}; // we don't actually care about the data - - // [WO-625] Mozilla forbids extensions to the TCPSocket object, - // throws an exception when assigned unexpected callback functions. - // The exception can be safely ignored since we need the callback - // for the other shims - try { - socket.oncert = function() { - if (options.ca) { - // the certificate we already have is outdated - error = createError(TLS_WRONG_CERT, strings.connDocTlsWrongCert.replace('{0}', host)); - } - }; - } catch (e) {} - - socket.onerror = function(e) { - if (!error) { - error = createError(HOST_UNREACHABLE, strings.connDocHostUnreachable.replace('{0}', host), e.data); - } - }; - - socket.onopen = function() { - socket.close(); - }; - - socket.onclose = function() { - if (!hasTimedOut) { - clearTimeout(timeout); - callback(error); - } - }; + socket.onopen = function() { + socket.close(); }; - /** - * Checks if an IMAP server is reachable, accepts the credentials, can list folders and has an inbox and logs out. - * Adds the certificate to the IMAP settings if not provided. - * - * @param {Function} callback(error) Invoked when the test suite passed, or with an error object if something went wrong - */ - ConnectionDoctor.prototype._checkImap = function(callback) { - var self = this, - loggedIn = false, - host = self.credentials.imap.host + ':' + self.credentials.imap.port; + socket.onclose = function() { + if (!hasTimedOut) { + clearTimeout(timeout); + callback(error); + } + }; +}; + +/** + * Checks if an IMAP server is reachable, accepts the credentials, can list folders and has an inbox and logs out. + * Adds the certificate to the IMAP settings if not provided. + * + * @param {Function} callback(error) Invoked when the test suite passed, or with an error object if something went wrong + */ +ConnectionDoctor.prototype._checkImap = function(callback) { + var self = this, + loggedIn = false, + host = self.credentials.imap.host + ':' + self.credentials.imap.port; - self._imap.onCert = function(pemEncodedCert) { - if (!self.credentials.imap.ca) { - self.credentials.imap.ca = pemEncodedCert; + self._imap.onCert = function(pemEncodedCert) { + if (!self.credentials.imap.ca) { + self.credentials.imap.ca = pemEncodedCert; + } + }; + + // login and logout do not use error objects in the callback, but rather invoke + // the global onError handler, so we need to track if login was successful + self._imap.onError = function(error) { + if (!loggedIn) { + callback(createError(AUTH_REJECTED, strings.connDocAuthRejected.replace('{0}', host), error)); + } else { + callback(createError(GENERIC_ERROR, strings.connDocGenericError.replace('{0}', host).replace('{1}', error.message), error)); + } + }; + + self._imap.login(function() { + loggedIn = true; + + self._imap.listWellKnownFolders(function(error, wellKnownFolders) { + if (error) { + return callback(createError(GENERIC_ERROR, strings.connDocGenericError.replace('{0}', host).replace('{1}', error.message), error)); } - }; - // login and logout do not use error objects in the callback, but rather invoke - // the global onError handler, so we need to track if login was successful - self._imap.onError = function(error) { - if (!loggedIn) { - callback(createError(AUTH_REJECTED, strings.connDocAuthRejected.replace('{0}', host), error)); - } else { - callback(createError(GENERIC_ERROR, strings.connDocGenericError.replace('{0}', host).replace('{1}', error.message), error)); + if (wellKnownFolders.Inbox.length === 0) { + // the client needs at least an inbox folder to work properly + return callback(createError(NO_INBOX, strings.connDocNoInbox.replace('{0}', host))); } - }; - self._imap.login(function() { - loggedIn = true; - - self._imap.listWellKnownFolders(function(error, wellKnownFolders) { - if (error) { - return callback(createError(GENERIC_ERROR, strings.connDocGenericError.replace('{0}', host).replace('{1}', error.message), error)); - } - - if (wellKnownFolders.Inbox.length === 0) { - // the client needs at least an inbox folder to work properly - return callback(createError(NO_INBOX, strings.connDocNoInbox.replace('{0}', host))); - } - - self._imap.logout(function() { - callback(); - }); + self._imap.logout(function() { + callback(); }); }); + }); +}; + +/** + * Checks if an SMTP server is reachable and accepts the credentials and logs out. + * Adds the certificate to the SMTP settings if not provided. + * + * @param {Function} callback(error) Invoked when the test suite passed, or with an error object if something went wrong + */ +ConnectionDoctor.prototype._checkSmtp = function(callback) { + var self = this, + host = self.credentials.smtp.host + ':' + self.credentials.smtp.port, + errored = false; // tracks if we need to invoke the callback at onclose or not + + self._smtp.oncert = function(pemEncodedCert) { + if (!self.credentials.smtp.ca) { + self.credentials.smtp.ca = pemEncodedCert; + } }; - /** - * Checks if an SMTP server is reachable and accepts the credentials and logs out. - * Adds the certificate to the SMTP settings if not provided. - * - * @param {Function} callback(error) Invoked when the test suite passed, or with an error object if something went wrong - */ - ConnectionDoctor.prototype._checkSmtp = function(callback) { - var self = this, - host = self.credentials.smtp.host + ':' + self.credentials.smtp.port, - errored = false; // tracks if we need to invoke the callback at onclose or not - - self._smtp.oncert = function(pemEncodedCert) { - if (!self.credentials.smtp.ca) { - self.credentials.smtp.ca = pemEncodedCert; - } - }; - - self._smtp.onerror = function(error) { - if (error) { - errored = true; - callback(createError(AUTH_REJECTED, strings.connDocAuthRejected.replace('{0}', host), error)); - } - }; - - self._smtp.onidle = function() { - self._smtp.quit(); - }; - - self._smtp.onclose = function() { - if (!errored) { - callback(); - } - }; - - self._smtp.connect(); + self._smtp.onerror = function(error) { + if (error) { + errored = true; + callback(createError(AUTH_REJECTED, strings.connDocAuthRejected.replace('{0}', host), error)); + } }; + self._smtp.onidle = function() { + self._smtp.quit(); + }; - // - // Helper Functions - // + self._smtp.onclose = function() { + if (!errored) { + callback(); + } + }; - function createError(code, message, underlyingError) { - var error = new Error(message); - error.code = code; - error.underlyingError = underlyingError; + self._smtp.connect(); +}; - return error; - } - return ConnectionDoctor; -}); \ No newline at end of file +// +// Helper Functions +// + +function createError(code, message, underlyingError) { + var error = new Error(message); + error.code = code; + error.underlyingError = underlyingError; + + return error; +} + +exports = ConnectionDoctor; \ No newline at end of file diff --git a/src/js/util/download.js b/src/js/util/download.js index 2ca1846..5d9d2c1 100644 --- a/src/js/util/download.js +++ b/src/js/util/download.js @@ -1,60 +1,58 @@ -define(function(require) { - 'use strict'; +'use strict'; - var util = require('js/crypto/util'); +var util = require('crypto-lib').util; - var dl = {}; +var dl = {}; - dl.createDownload = function(options) { - var contentType = options.contentType || 'application/octet-stream'; - var filename = options.filename || 'file'; - var content = options.content; - var a = document.createElement('a'); - var supportsBlob; +dl.createDownload = function(options) { + var contentType = options.contentType || 'application/octet-stream'; + var filename = options.filename || 'file'; + var content = options.content; + var a = document.createElement('a'); + var supportsBlob; - try { - supportsBlob = !!new Blob(); - } catch (e) {} + try { + supportsBlob = !!new Blob(); + } catch (e) {} - if (typeof a.download !== "undefined" && supportsBlob) { - // ff 30+, chrome 27+ (android: 37+) - document.body.appendChild(a); - a.style = "display: none"; - a.href = window.URL.createObjectURL(new Blob([content], { - type: contentType - })); - a.download = filename; - a.click(); - setTimeout(function() { - window.URL.revokeObjectURL(a.href); - document.body.removeChild(a); - }, 10); // arbitrary, just get it off the main thread - } else if (window.navigator.msSaveBlob) { - // ie 10+ - window.navigator.msSaveBlob(new Blob([content], { - type: contentType - }), filename); - } else if (supportsBlob) { - // safari actually makes no sense: - // - you can't open a new window - // - the file system api is dead - // - download attribute doesn't work - // - behaves randomly (opens a new tab or doesn't, downloads stuff or doesn't, ...) - var url = window.URL.createObjectURL(new Blob([content], { - type: contentType - })); - 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) { - content = util.arrBuf2BinStr(content.buffer); - } - window.open('data:' + contentType + ';base64,' + btoa(content), "_blank"); + if (typeof a.download !== "undefined" && supportsBlob) { + // ff 30+, chrome 27+ (android: 37+) + document.body.appendChild(a); + a.style = "display: none"; + a.href = window.URL.createObjectURL(new Blob([content], { + type: contentType + })); + a.download = filename; + a.click(); + setTimeout(function() { + window.URL.revokeObjectURL(a.href); + document.body.removeChild(a); + }, 10); // arbitrary, just get it off the main thread + } else if (window.navigator.msSaveBlob) { + // ie 10+ + window.navigator.msSaveBlob(new Blob([content], { + type: contentType + }), filename); + } else if (supportsBlob) { + // safari actually makes no sense: + // - you can't open a new window + // - the file system api is dead + // - download attribute doesn't work + // - behaves randomly (opens a new tab or doesn't, downloads stuff or doesn't, ...) + var url = window.URL.createObjectURL(new Blob([content], { + type: contentType + })); + 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) { + content = util.arrBuf2BinStr(content.buffer); + } + window.open('data:' + contentType + ';base64,' + btoa(content), "_blank"); + } +}; - return dl; -}); \ No newline at end of file +exports = dl; \ No newline at end of file diff --git a/src/js/util/error.js b/src/js/util/error.js index 965557b..7df0e6b 100644 --- a/src/js/util/error.js +++ b/src/js/util/error.js @@ -1,35 +1,33 @@ -define(function(require) { - 'use strict'; +'use strict'; - var axe = require('axe'); +var axe = require('axe-logger'); - var er = {}; - er.attachHandler = function(scope) { - scope.onError = function(options) { - if (!options) { - scope.$apply(); - return; - } +var er = {}; +er.attachHandler = function(scope) { + scope.onError = function(options) { + if (!options) { + scope.$apply(); + return; + } - axe.error((options.errMsg || options.message) + (options.stack ? ('\n' + options.stack) : '')); + axe.error((options.errMsg || options.message) + (options.stack ? ('\n' + options.stack) : '')); - scope.state.dialog = { - open: true, - title: options.title || 'Error', - message: options.errMsg || options.message, - faqLink: options.faqLink, - positiveBtnStr: options.positiveBtnStr || 'Ok', - negativeBtnStr: options.negativeBtnStr || 'Cancel', - showNegativeBtn: options.showNegativeBtn || false, - showBugReporter: (typeof options.showBugReporter !== 'undefined' ? options.showBugReporter : !options.title), // if title is set, presume it's not an error by default - callback: options.callback - }; - // don't call apply for synchronous calls - if (!options.sync) { - scope.$apply(); - } + scope.state.dialog = { + open: true, + title: options.title || 'Error', + message: options.errMsg || options.message, + faqLink: options.faqLink, + positiveBtnStr: options.positiveBtnStr || 'Ok', + negativeBtnStr: options.negativeBtnStr || 'Cancel', + showNegativeBtn: options.showNegativeBtn || false, + showBugReporter: (typeof options.showBugReporter !== 'undefined' ? options.showBugReporter : !options.title), // if title is set, presume it's not an error by default + callback: options.callback }; + // don't call apply for synchronous calls + if (!options.sync) { + scope.$apply(); + } }; +}; - return er; -}); \ No newline at end of file +exports = er; \ No newline at end of file diff --git a/src/js/util/notification.js b/src/js/util/notification.js index 8c4a7cc..e2cdd16 100644 --- a/src/js/util/notification.js +++ b/src/js/util/notification.js @@ -1,60 +1,58 @@ -define(function(require) { - 'use strict'; +'use strict'; - var cfg = require('js/app-config').config; +var cfg = require('../app-config').config; - var self = {}; +var self = {}; - if (window.Notification) { - self.hasPermission = Notification.permission === "granted"; +if (window.Notification) { + self.hasPermission = Notification.permission === "granted"; +} + +/** + * Creates a notification. Requests permission if not already granted + * + * @param {String} options.title The notification title + * @param {String} options.message The notification message + * @param {Number} options.timeout (optional) Timeout when the notification is closed in milliseconds + * @param {Function} options.onClick (optional) callback when the notification is clicked + * @returns {Notification} A notification instance + */ +self.create = function(options) { + options.onClick = options.onClick || function() {}; + + if (!window.Notification) { + return; } - /** - * Creates a notification. Requests permission if not already granted - * - * @param {String} options.title The notification title - * @param {String} options.message The notification message - * @param {Number} options.timeout (optional) Timeout when the notification is closed in milliseconds - * @param {Function} options.onClick (optional) callback when the notification is clicked - * @returns {Notification} A notification instance - */ - self.create = function(options) { - options.onClick = options.onClick || function() {}; - - if (!window.Notification) { - return; - } - - if (!self.hasPermission) { - // don't wait until callback returns - Notification.requestPermission(function(permission) { - if (permission === "granted") { - self.hasPermission = true; - } - }); - } - - var notification = new Notification(options.title, { - body: options.message, - icon: cfg.iconPath + if (!self.hasPermission) { + // don't wait until callback returns + Notification.requestPermission(function(permission) { + if (permission === "granted") { + self.hasPermission = true; + } }); - notification.onclick = function() { - window.focus(); - options.onClick(); - }; + } - if (options.timeout > 0) { - setTimeout(function() { - notification.close(); - }, options.timeout); - } - - return notification; + var notification = new Notification(options.title, { + body: options.message, + icon: cfg.iconPath + }); + notification.onclick = function() { + window.focus(); + options.onClick(); }; - self.close = function(notification) { - notification.close(); - }; + if (options.timeout > 0) { + setTimeout(function() { + notification.close(); + }, options.timeout); + } - return self; -}); \ No newline at end of file + return notification; +}; + +self.close = function(notification) { + notification.close(); +}; + +exports = self; \ No newline at end of file diff --git a/src/js/util/oauth.js b/src/js/util/oauth.js index 8762347..acc801a 100644 --- a/src/js/util/oauth.js +++ b/src/js/util/oauth.js @@ -1,101 +1,99 @@ -define(function() { - 'use strict'; +'use strict'; - var OAuth = function(googleApi) { - this._googleApi = googleApi; +var OAuth = function(googleApi) { + this._googleApi = googleApi; +}; + +/** + * Check if chrome.identity api is supported + * @return {Boolean} If is supported + */ +OAuth.prototype.isSupported = function() { + return !!(window.chrome && chrome.identity); +}; + +/** + * Request an OAuth token from chrome for gmail users + * @param {String} emailAddress The user's email address (optional) + */ +OAuth.prototype.getOAuthToken = function(emailAddress, callback) { + var idOptions = { + interactive: true }; - /** - * Check if chrome.identity api is supported - * @return {Boolean} If is supported - */ - OAuth.prototype.isSupported = function() { - return !!(window.chrome && chrome.identity); - }; - - /** - * Request an OAuth token from chrome for gmail users - * @param {String} emailAddress The user's email address (optional) - */ - OAuth.prototype.getOAuthToken = function(emailAddress, callback) { - var idOptions = { - interactive: true - }; - - // check which runtime the app is running under - chrome.runtime.getPlatformInfo(function(platformInfo) { - if (chrome.runtime.lastError || !platformInfo) { - callback(new Error('Error getting chrome platform info!')); - return; - } - - if (emailAddress && platformInfo.os.indexOf('android') !== -1) { - // set accountHint so that native Android account picker does not show up each time - idOptions.accountHint = emailAddress; - } - - // get OAuth Token from chrome - chrome.identity.getAuthToken(idOptions, function(token) { - if (chrome.runtime.lastError || !token) { - callback({ - errMsg: 'Error fetching an OAuth token for the user!' - }); - return; - } - - callback(null, token); - }); - }); - }; - - /** - * Remove an old OAuth token and get a new one. - * @param {String} options.oldToken The old token to be removed - * @param {String} options.emailAddress The user's email address (optional) - */ - OAuth.prototype.refreshToken = function(options, callback) { - var self = this; - - if (!options.oldToken) { - callback(new Error('oldToken option not set!')); + // check which runtime the app is running under + chrome.runtime.getPlatformInfo(function(platformInfo) { + if (chrome.runtime.lastError || !platformInfo) { + callback(new Error('Error getting chrome platform info!')); return; } - // remove cached token - chrome.identity.removeCachedAuthToken({ - token: options.oldToken - }, function() { - // get a new token - self.getOAuthToken(options.emailAddress, callback); - }); - }; - - /** - * Get email address from google api - * @param {String} token The oauth token - */ - OAuth.prototype.queryEmailAddress = function(token, callback) { - if (!token) { - callback({ - errMsg: 'Invalid OAuth token!' - }); - return; + if (emailAddress && platformInfo.os.indexOf('android') !== -1) { + // set accountHint so that native Android account picker does not show up each time + idOptions.accountHint = emailAddress; } - // fetch gmail user's email address from the Google Authorization Server - this._googleApi.get({ - uri: '/oauth2/v3/userinfo?access_token=' + token - }, function(err, info) { - if (err || !info || !info.email) { + // get OAuth Token from chrome + chrome.identity.getAuthToken(idOptions, function(token) { + if (chrome.runtime.lastError || !token) { callback({ - errMsg: 'Error looking up email address on google api!' + errMsg: 'Error fetching an OAuth token for the user!' }); return; } - callback(null, info.email); + callback(null, token); }); - }; + }); +}; - return OAuth; -}); \ No newline at end of file +/** + * Remove an old OAuth token and get a new one. + * @param {String} options.oldToken The old token to be removed + * @param {String} options.emailAddress The user's email address (optional) + */ +OAuth.prototype.refreshToken = function(options, callback) { + var self = this; + + if (!options.oldToken) { + callback(new Error('oldToken option not set!')); + return; + } + + // remove cached token + chrome.identity.removeCachedAuthToken({ + token: options.oldToken + }, function() { + // get a new token + self.getOAuthToken(options.emailAddress, callback); + }); +}; + +/** + * Get email address from google api + * @param {String} token The oauth token + */ +OAuth.prototype.queryEmailAddress = function(token, callback) { + if (!token) { + callback({ + errMsg: 'Invalid OAuth token!' + }); + return; + } + + // fetch gmail user's email address from the Google Authorization Server + this._googleApi.get({ + uri: '/oauth2/v3/userinfo?access_token=' + token + }, function(err, info) { + if (err || !info || !info.email) { + callback({ + errMsg: 'Error looking up email address on google api!' + }); + return; + } + + callback(null, info.email); + }); +}; + +exports = OAuth; \ No newline at end of file diff --git a/src/js/util/update/update-handler.js b/src/js/util/update/update-handler.js index a34252a..e070cb4 100644 --- a/src/js/util/update/update-handler.js +++ b/src/js/util/update/update-handler.js @@ -1,132 +1,130 @@ -define(function(require) { - 'use strict'; +'use strict'; - var axe = require('axe'), - cfg = require('js/app-config').config, - updateV1 = require('js/util/update/update-v1'), - updateV2 = require('js/util/update/update-v2'), - updateV3 = require('js/util/update/update-v3'), - updateV4 = require('js/util/update/update-v4'), - updateV5 = require('js/util/update/update-v5'); +var axe = require('axe-logger'), + cfg = require('../../app-config').config, + updateV1 = require('./update-v1'), + updateV2 = require('./update-v2'), + updateV3 = require('./update-v3'), + updateV4 = require('./update-v4'), + updateV5 = require('./update-v5'); - /** - * Handles database migration - */ - var UpdateHandler = function(appConfigStorage, userStorage, auth) { - this._appConfigStorage = appConfigStorage; - this._userStorage = userStorage; - this._updateScripts = [updateV1, updateV2, updateV3, updateV4, updateV5]; - this._auth = auth; +/** + * Handles database migration + */ +var UpdateHandler = function(appConfigStorage, userStorage, auth) { + this._appConfigStorage = appConfigStorage; + this._userStorage = userStorage; + this._updateScripts = [updateV1, updateV2, updateV3, updateV4, updateV5]; + this._auth = auth; +}; + +/** + * Executes all the necessary updates + * @param {Function} callback(error) Invoked when all the database updates were executed, or if an error occurred + */ +UpdateHandler.prototype.update = function(callback) { + var self = this, + currentVersion = 0, + targetVersion = cfg.dbVersion, + versionDbType = 'dbVersion'; + + self._appConfigStorage.listItems(versionDbType, 0, null, function(err, items) { + if (err) { + callback(err); + return; + } + + // parse the database version number + if (items && items.length > 0) { + currentVersion = parseInt(items[0], 10); + } + + self._applyUpdate({ + currentVersion: currentVersion, + targetVersion: targetVersion + }, callback); + }); +}; + +/** + * Schedules necessary updates and executes thom in order + */ +UpdateHandler.prototype._applyUpdate = function(options, callback) { + var self = this, + scriptOptions, + queue = []; + + if (options.currentVersion >= options.targetVersion) { + // the current database version is up to date + callback(); + return; + } + + scriptOptions = { + appConfigStorage: self._appConfigStorage, + userStorage: self._userStorage, + auth: self._auth }; - /** - * Executes all the necessary updates - * @param {Function} callback(error) Invoked when all the database updates were executed, or if an error occurred - */ - UpdateHandler.prototype.update = function(callback) { - var self = this, - currentVersion = 0, - targetVersion = cfg.dbVersion, - versionDbType = 'dbVersion'; + // add all the necessary database updates to the queue + for (var i = options.currentVersion; i < options.targetVersion; i++) { + queue.push(self._updateScripts[i]); + } - self._appConfigStorage.listItems(versionDbType, 0, null, function(err, items) { - if (err) { - callback(err); - return; - } + // takes the next update from the queue and executes it + function executeNextUpdate(err) { + if (err) { + callback(err); + return; + } - // parse the database version number - if (items && items.length > 0) { - currentVersion = parseInt(items[0], 10); - } - - self._applyUpdate({ - currentVersion: currentVersion, - targetVersion: targetVersion - }, callback); - }); - }; - - /** - * Schedules necessary updates and executes thom in order - */ - UpdateHandler.prototype._applyUpdate = function(options, callback) { - var self = this, - scriptOptions, - queue = []; - - if (options.currentVersion >= options.targetVersion) { - // the current database version is up to date + if (queue.length < 1) { + // we're done callback(); return; } - scriptOptions = { - appConfigStorage: self._appConfigStorage, - userStorage: self._userStorage, - auth: self._auth - }; + // process next update + var script = queue.shift(); + script(scriptOptions, executeNextUpdate); + } - // add all the necessary database updates to the queue - for (var i = options.currentVersion; i < options.targetVersion; i++) { - queue.push(self._updateScripts[i]); - } + executeNextUpdate(); +}; - // takes the next update from the queue and executes it - function executeNextUpdate(err) { - if (err) { - callback(err); - return; - } - - if (queue.length < 1) { - // we're done - callback(); - return; - } - - // process next update - var script = queue.shift(); - script(scriptOptions, executeNextUpdate); - } - - executeNextUpdate(); - }; - - /** - * Check application version and update correspondingly - */ - UpdateHandler.prototype.checkForUpdate = function(dialog) { - // Chrome Packaged App - if (typeof window.chrome !== 'undefined' && chrome.runtime && chrome.runtime.onUpdateAvailable) { - // check for Chrome app update and restart - chrome.runtime.onUpdateAvailable.addListener(function(details) { - axe.debug('New Chrome App update... requesting reload.'); - // Chrome downloaded a new app version - dialog({ - title: 'Update available', - message: 'A new version ' + details.version + ' of the app is available. Restart the app to update?', - positiveBtnStr: 'Restart', - negativeBtnStr: 'Not now', - showNegativeBtn: true, - callback: function(agree) { - if (agree) { - chrome.runtime.reload(); - } +/** + * Check application version and update correspondingly + */ +UpdateHandler.prototype.checkForUpdate = function(dialog) { + // Chrome Packaged App + if (typeof window.chrome !== 'undefined' && chrome.runtime && chrome.runtime.onUpdateAvailable) { + // check for Chrome app update and restart + chrome.runtime.onUpdateAvailable.addListener(function(details) { + axe.debug('New Chrome App update... requesting reload.'); + // Chrome downloaded a new app version + dialog({ + title: 'Update available', + message: 'A new version ' + details.version + ' of the app is available. Restart the app to update?', + positiveBtnStr: 'Restart', + negativeBtnStr: 'Not now', + showNegativeBtn: true, + callback: function(agree) { + if (agree) { + chrome.runtime.reload(); } - }); - }); - chrome.runtime.requestUpdateCheck(function(status) { - if (status === "update_found") { - axe.debug("Update pending..."); - } else if (status === "no_update") { - axe.debug("No update found."); - } else if (status === "throttled") { - axe.debug("Checking updates too frequently."); } }); - } - }; + }); + chrome.runtime.requestUpdateCheck(function(status) { + if (status === "update_found") { + axe.debug("Update pending..."); + } else if (status === "no_update") { + axe.debug("No update found."); + } else if (status === "throttled") { + axe.debug("Checking updates too frequently."); + } + }); + } +}; - return UpdateHandler; -}); \ No newline at end of file +exports = UpdateHandler; \ No newline at end of file diff --git a/src/js/util/update/update-v1.js b/src/js/util/update/update-v1.js index 942b9a6..77c2b61 100644 --- a/src/js/util/update/update-v1.js +++ b/src/js/util/update/update-v1.js @@ -1,29 +1,27 @@ -define(function() { - 'use strict'; +'use strict'; - /** - * Update handler for transition database version 0 -> 1 - * - * In database version 1, the stored email objects have to be purged, otherwise - * every non-prefixed mail in the IMAP folders would be nuked due to the implementation - * of the delta sync. - */ - function updateV1(options, callback) { - var emailDbType = 'email_', - versionDbType = 'dbVersion', - postUpdateDbVersion = 1; +/** + * Update handler for transition database version 0 -> 1 + * + * In database version 1, the stored email objects have to be purged, otherwise + * every non-prefixed mail in the IMAP folders would be nuked due to the implementation + * of the delta sync. + */ +function updateV1(options, callback) { + var emailDbType = 'email_', + versionDbType = 'dbVersion', + postUpdateDbVersion = 1; - // remove the emails - options.userStorage.removeList(emailDbType, function(err) { - if (err) { - callback(err); - return; - } + // remove the emails + options.userStorage.removeList(emailDbType, function(err) { + if (err) { + callback(err); + return; + } - // update the database version to postUpdateDbVersion - options.appConfigStorage.storeList([postUpdateDbVersion], versionDbType, callback); - }); - } + // update the database version to postUpdateDbVersion + options.appConfigStorage.storeList([postUpdateDbVersion], versionDbType, callback); + }); +} - return updateV1; -}); \ No newline at end of file +exports = updateV1; \ No newline at end of file diff --git a/src/js/util/update/update-v2.js b/src/js/util/update/update-v2.js index 03fdf50..74f62a7 100644 --- a/src/js/util/update/update-v2.js +++ b/src/js/util/update/update-v2.js @@ -1,28 +1,26 @@ -define(function() { - 'use strict'; +'use strict'; - /** - * Update handler for transition database version 1 -> 2 - * - * In database version 2, the stored email objects have to be purged, because the - * new data model stores information about the email structure in the property 'bodyParts'. - */ - function updateV2(options, callback) { - var emailDbType = 'email_', - versionDbType = 'dbVersion', - postUpdateDbVersion = 2; +/** + * Update handler for transition database version 1 -> 2 + * + * In database version 2, the stored email objects have to be purged, because the + * new data model stores information about the email structure in the property 'bodyParts'. + */ +function updateV2(options, callback) { + var emailDbType = 'email_', + versionDbType = 'dbVersion', + postUpdateDbVersion = 2; - // remove the emails - options.userStorage.removeList(emailDbType, function(err) { - if (err) { - callback(err); - return; - } + // remove the emails + options.userStorage.removeList(emailDbType, function(err) { + if (err) { + callback(err); + return; + } - // update the database version to postUpdateDbVersion - options.appConfigStorage.storeList([postUpdateDbVersion], versionDbType, callback); - }); - } + // update the database version to postUpdateDbVersion + options.appConfigStorage.storeList([postUpdateDbVersion], versionDbType, callback); + }); +} - return updateV2; -}); \ No newline at end of file +exports = updateV2; \ No newline at end of file diff --git a/src/js/util/update/update-v3.js b/src/js/util/update/update-v3.js index dfc49ff..82cbf01 100644 --- a/src/js/util/update/update-v3.js +++ b/src/js/util/update/update-v3.js @@ -1,28 +1,26 @@ -define(function() { - 'use strict'; +'use strict'; - /** - * Update handler for transition database version 2 -> 3 - * - * In database version 3, we introduced new flags to the messages, also - * the outbox uses artificial uids - */ - function update(options, callback) { - var emailDbType = 'email_', - versionDbType = 'dbVersion', - postUpdateDbVersion = 3; +/** + * Update handler for transition database version 2 -> 3 + * + * In database version 3, we introduced new flags to the messages, also + * the outbox uses artificial uids + */ +function update(options, callback) { + var emailDbType = 'email_', + versionDbType = 'dbVersion', + postUpdateDbVersion = 3; - // remove the emails - options.userStorage.removeList(emailDbType, function(err) { - if (err) { - callback(err); - return; - } + // remove the emails + options.userStorage.removeList(emailDbType, function(err) { + if (err) { + callback(err); + return; + } - // update the database version to postUpdateDbVersion - options.appConfigStorage.storeList([postUpdateDbVersion], versionDbType, callback); - }); - } + // update the database version to postUpdateDbVersion + options.appConfigStorage.storeList([postUpdateDbVersion], versionDbType, callback); + }); +} - return update; -}); \ No newline at end of file +exports = update; \ No newline at end of file diff --git a/src/js/util/update/update-v4.js b/src/js/util/update/update-v4.js index 54af103..b481aac 100644 --- a/src/js/util/update/update-v4.js +++ b/src/js/util/update/update-v4.js @@ -1,91 +1,89 @@ -define(function(require) { - 'use strict'; +'use strict'; - var config = require('js/app-config').config; +var config = require('../../app-config').config; - /** - * Update handler for transition database version 3 -> 4 - * - * In database version 4, we need to add a "provider" flag to the - * indexeddb. only gmail was allowed as a mail service provider before, - * so let's add this... - */ - function update(options, callback) { - var VERSION_DB_TYPE = 'dbVersion', - EMAIL_ADDR_DB_KEY = 'emailaddress', - USERNAME_DB_KEY = 'username', - PROVIDER_DB_KEY = 'provider', - IMAP_DB_KEY = 'imap', - SMTP_DB_KEY = 'smtp', - REALNAME_DB_KEY = 'realname', - POST_UPDATE_DB_VERSION = 4; +/** + * Update handler for transition database version 3 -> 4 + * + * In database version 4, we need to add a "provider" flag to the + * indexeddb. only gmail was allowed as a mail service provider before, + * so let's add this... + */ +function update(options, callback) { + var VERSION_DB_TYPE = 'dbVersion', + EMAIL_ADDR_DB_KEY = 'emailaddress', + USERNAME_DB_KEY = 'username', + PROVIDER_DB_KEY = 'provider', + IMAP_DB_KEY = 'imap', + SMTP_DB_KEY = 'smtp', + REALNAME_DB_KEY = 'realname', + POST_UPDATE_DB_VERSION = 4; - var imap = config.gmail.imap, - smtp = config.gmail.smtp; + var imap = config.gmail.imap, + smtp = config.gmail.smtp; - // load the email address (if existing) - loadFromDB(EMAIL_ADDR_DB_KEY, function(err, emailAddress) { + // load the email address (if existing) + loadFromDB(EMAIL_ADDR_DB_KEY, function(err, emailAddress) { + if (err) { + return callback(err); + } + + // load the provider (if existing) + loadFromDB(PROVIDER_DB_KEY, function(err, provider) { if (err) { return callback(err); } - // load the provider (if existing) - loadFromDB(PROVIDER_DB_KEY, function(err, provider) { + // if there is an email address without a provider, we need to add the missing provider entry + // for any other situation, we're good. + + if (!(emailAddress && !provider)) { + // update the database version to POST_UPDATE_DB_VERSION + return options.appConfigStorage.storeList([POST_UPDATE_DB_VERSION], VERSION_DB_TYPE, callback); + } + + // add the missing provider key + options.appConfigStorage.storeList(['gmail'], PROVIDER_DB_KEY, function(err) { if (err) { return callback(err); } - // if there is an email address without a provider, we need to add the missing provider entry - // for any other situation, we're good. - - if (!(emailAddress && !provider)) { - // update the database version to POST_UPDATE_DB_VERSION - return options.appConfigStorage.storeList([POST_UPDATE_DB_VERSION], VERSION_DB_TYPE, callback); - } - - // add the missing provider key - options.appConfigStorage.storeList(['gmail'], PROVIDER_DB_KEY, function(err) { + // add the missing user name key + options.appConfigStorage.storeList([emailAddress], USERNAME_DB_KEY, function(err) { if (err) { return callback(err); } - // add the missing user name key - options.appConfigStorage.storeList([emailAddress], USERNAME_DB_KEY, function(err) { + // add the missing imap host info key + options.appConfigStorage.storeList([imap], IMAP_DB_KEY, function(err) { if (err) { return callback(err); } - // add the missing imap host info key - options.appConfigStorage.storeList([imap], IMAP_DB_KEY, function(err) { + // add the missing empty real name + options.appConfigStorage.storeList([''], REALNAME_DB_KEY, function(err) { if (err) { return callback(err); } - // add the missing empty real name - options.appConfigStorage.storeList([''], REALNAME_DB_KEY, function(err) { + // add the missing smtp host info key + options.appConfigStorage.storeList([smtp], SMTP_DB_KEY, function(err) { if (err) { return callback(err); } - // add the missing smtp host info key - options.appConfigStorage.storeList([smtp], SMTP_DB_KEY, function(err) { + // reload the credentials + options.auth.initialized = false; + options.auth._loadCredentials(function(err) { if (err) { return callback(err); } - // reload the credentials - options.auth.initialized = false; - options.auth._loadCredentials(function(err) { - if (err) { - return callback(err); - } - - // update the database version to POST_UPDATE_DB_VERSION - options.appConfigStorage.storeList([POST_UPDATE_DB_VERSION], VERSION_DB_TYPE, callback); - }); + // update the database version to POST_UPDATE_DB_VERSION + options.appConfigStorage.storeList([POST_UPDATE_DB_VERSION], VERSION_DB_TYPE, callback); }); }); }); @@ -93,13 +91,13 @@ define(function(require) { }); }); }); + }); - function loadFromDB(key, callback) { - options.appConfigStorage.listItems(key, 0, null, function(err, cachedItems) { - callback(err, (!err && cachedItems && cachedItems[0])); - }); - } + function loadFromDB(key, callback) { + options.appConfigStorage.listItems(key, 0, null, function(err, cachedItems) { + callback(err, (!err && cachedItems && cachedItems[0])); + }); } +} - return update; -}); \ No newline at end of file +exports = update; \ No newline at end of file diff --git a/src/js/util/update/update-v5.js b/src/js/util/update/update-v5.js index ef3a035..338c470 100644 --- a/src/js/util/update/update-v5.js +++ b/src/js/util/update/update-v5.js @@ -1,56 +1,54 @@ -define(function() { - 'use strict'; +'use strict'; - var FOLDER_TYPE_INBOX = 'Inbox'; - var FOLDER_TYPE_SENT = 'Sent'; - var FOLDER_TYPE_DRAFTS = 'Drafts'; - var FOLDER_TYPE_TRASH = 'Trash'; +var FOLDER_TYPE_INBOX = 'Inbox'; +var FOLDER_TYPE_SENT = 'Sent'; +var FOLDER_TYPE_DRAFTS = 'Drafts'; +var FOLDER_TYPE_TRASH = 'Trash'; - var FOLDER_DB_TYPE = 'folders'; - var VERSION_DB_TYPE = 'dbVersion'; +var FOLDER_DB_TYPE = 'folders'; +var VERSION_DB_TYPE = 'dbVersion'; - var POST_UPDATE_DB_VERSION = 5; +var POST_UPDATE_DB_VERSION = 5; - /** - * Update handler for transition database version 4 -> 5 - * - * Due to an overlooked issue, there may be multiple folders, e.g. for sent mails. - * This removes the "duplicate" folders. - */ - function update(options, callback) { +/** + * Update handler for transition database version 4 -> 5 + * + * Due to an overlooked issue, there may be multiple folders, e.g. for sent mails. + * This removes the "duplicate" folders. + */ +function update(options, callback) { - // remove the emails - options.userStorage.listItems(FOLDER_DB_TYPE, 0, null, function(err, stored) { + // remove the emails + options.userStorage.listItems(FOLDER_DB_TYPE, 0, null, function(err, stored) { + if (err) { + return callback(err); + } + + 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) { + return mbx.type === mbxType; + }); + + if (foldersForType.length <= 1) { + return; // nothing to do here + } + + // remove duplicate folders + for (var i = 1; i < foldersForType.length; i++) { + folders.splice(folders.indexOf(foldersForType[i]), 1); + } + }); + + options.userStorage.storeList([folders], FOLDER_DB_TYPE, function(err) { if (err) { return callback(err); } - 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) { - return mbx.type === mbxType; - }); - - if (foldersForType.length <= 1) { - return; // nothing to do here - } - - // remove duplicate folders - for (var i = 1; i < foldersForType.length; i++) { - folders.splice(folders.indexOf(foldersForType[i]), 1); - } - }); - - options.userStorage.storeList([folders], FOLDER_DB_TYPE, function(err) { - if (err) { - return callback(err); - } - - // update the database version to POST_UPDATE_DB_VERSION - options.appConfigStorage.storeList([POST_UPDATE_DB_VERSION], VERSION_DB_TYPE, callback); - }); + // update the database version to POST_UPDATE_DB_VERSION + options.appConfigStorage.storeList([POST_UPDATE_DB_VERSION], VERSION_DB_TYPE, callback); }); - } + }); +} - return update; -}); \ No newline at end of file +exports = update; \ No newline at end of file diff --git a/src/lib/uuid/uuid.js b/src/lib/uuid/uuid.js deleted file mode 100644 index 4795b9d..0000000 --- a/src/lib/uuid/uuid.js +++ /dev/null @@ -1,245 +0,0 @@ -// uuid.js -// -// (c) 2010-2012 Robert Kieffer -// MIT License -// https://github.com/broofa/node-uuid -(function() { - var _global = this; - - // Unique ID creation requires a high quality random # generator. We feature - // detect to determine the best RNG source, normalizing to a function that - // returns 128-bits of randomness, since that's what's usually required - var _rng; - - // Node.js crypto-based RNG - http://nodejs.org/docs/v0.6.2/api/crypto.html - // - // Moderately fast, high quality - if (typeof(require) == 'function') { - try { - var _rb = require('crypto').randomBytes; - _rng = _rb && function() {return _rb(16);}; - } catch(e) {} - } - - if (!_rng && _global.crypto && crypto.getRandomValues) { - // WHATWG crypto-based RNG - http://wiki.whatwg.org/wiki/Crypto - // - // Moderately fast, high quality - var _rnds8 = new Uint8Array(16); - _rng = function whatwgRNG() { - crypto.getRandomValues(_rnds8); - return _rnds8; - }; - } - - if (!_rng) { - // Math.random()-based (RNG) - // - // If all else fails, use Math.random(). It's fast, but is of unspecified - // quality. - var _rnds = new Array(16); - _rng = function() { - for (var i = 0, r; i < 16; i++) { - if ((i & 0x03) === 0) r = Math.random() * 0x100000000; - _rnds[i] = r >>> ((i & 0x03) << 3) & 0xff; - } - - return _rnds; - }; - } - - // Buffer class to use - var BufferClass = typeof(Buffer) == 'function' ? Buffer : Array; - - // Maps for number <-> hex string conversion - var _byteToHex = []; - var _hexToByte = {}; - for (var i = 0; i < 256; i++) { - _byteToHex[i] = (i + 0x100).toString(16).substr(1); - _hexToByte[_byteToHex[i]] = i; - } - - // **`parse()` - Parse a UUID into it's component bytes** - function parse(s, buf, offset) { - var i = (buf && offset) || 0, ii = 0; - - buf = buf || []; - s.toLowerCase().replace(/[0-9a-f]{2}/g, function(oct) { - if (ii < 16) { // Don't overflow! - buf[i + ii++] = _hexToByte[oct]; - } - }); - - // Zero out remaining bytes if string was short - while (ii < 16) { - buf[i + ii++] = 0; - } - - return buf; - } - - // **`unparse()` - Convert UUID byte array (ala parse()) into a string** - function unparse(buf, offset) { - var i = offset || 0, bth = _byteToHex; - return bth[buf[i++]] + bth[buf[i++]] + - bth[buf[i++]] + bth[buf[i++]] + '-' + - bth[buf[i++]] + bth[buf[i++]] + '-' + - bth[buf[i++]] + bth[buf[i++]] + '-' + - bth[buf[i++]] + bth[buf[i++]] + '-' + - bth[buf[i++]] + bth[buf[i++]] + - bth[buf[i++]] + bth[buf[i++]] + - bth[buf[i++]] + bth[buf[i++]]; - } - - // **`v1()` - Generate time-based UUID** - // - // Inspired by https://github.com/LiosK/UUID.js - // and http://docs.python.org/library/uuid.html - - // random #'s we need to init node and clockseq - var _seedBytes = _rng(); - - // Per 4.5, create and 48-bit node id, (47 random bits + multicast bit = 1) - var _nodeId = [ - _seedBytes[0] | 0x01, - _seedBytes[1], _seedBytes[2], _seedBytes[3], _seedBytes[4], _seedBytes[5] - ]; - - // Per 4.2.2, randomize (14 bit) clockseq - var _clockseq = (_seedBytes[6] << 8 | _seedBytes[7]) & 0x3fff; - - // Previous uuid creation time - var _lastMSecs = 0, _lastNSecs = 0; - - // See https://github.com/broofa/node-uuid for API details - function v1(options, buf, offset) { - var i = buf && offset || 0; - var b = buf || []; - - options = options || {}; - - var clockseq = options.clockseq != null ? options.clockseq : _clockseq; - - // UUID timestamps are 100 nano-second units since the Gregorian epoch, - // (1582-10-15 00:00). JSNumbers aren't precise enough for this, so - // time is handled internally as 'msecs' (integer milliseconds) and 'nsecs' - // (100-nanoseconds offset from msecs) since unix epoch, 1970-01-01 00:00. - var msecs = options.msecs != null ? options.msecs : new Date().getTime(); - - // Per 4.2.1.2, use count of uuid's generated during the current clock - // cycle to simulate higher resolution clock - var nsecs = options.nsecs != null ? options.nsecs : _lastNSecs + 1; - - // Time since last uuid creation (in msecs) - var dt = (msecs - _lastMSecs) + (nsecs - _lastNSecs)/10000; - - // Per 4.2.1.2, Bump clockseq on clock regression - if (dt < 0 && options.clockseq == null) { - clockseq = clockseq + 1 & 0x3fff; - } - - // Reset nsecs if clock regresses (new clockseq) or we've moved onto a new - // time interval - if ((dt < 0 || msecs > _lastMSecs) && options.nsecs == null) { - nsecs = 0; - } - - // Per 4.2.1.2 Throw error if too many uuids are requested - if (nsecs >= 10000) { - throw new Error('uuid.v1(): Can\'t create more than 10M uuids/sec'); - } - - _lastMSecs = msecs; - _lastNSecs = nsecs; - _clockseq = clockseq; - - // Per 4.1.4 - Convert from unix epoch to Gregorian epoch - msecs += 12219292800000; - - // `time_low` - var tl = ((msecs & 0xfffffff) * 10000 + nsecs) % 0x100000000; - b[i++] = tl >>> 24 & 0xff; - b[i++] = tl >>> 16 & 0xff; - b[i++] = tl >>> 8 & 0xff; - b[i++] = tl & 0xff; - - // `time_mid` - var tmh = (msecs / 0x100000000 * 10000) & 0xfffffff; - b[i++] = tmh >>> 8 & 0xff; - b[i++] = tmh & 0xff; - - // `time_high_and_version` - b[i++] = tmh >>> 24 & 0xf | 0x10; // include version - b[i++] = tmh >>> 16 & 0xff; - - // `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant) - b[i++] = clockseq >>> 8 | 0x80; - - // `clock_seq_low` - b[i++] = clockseq & 0xff; - - // `node` - var node = options.node || _nodeId; - for (var n = 0; n < 6; n++) { - b[i + n] = node[n]; - } - - return buf ? buf : unparse(b); - } - - // **`v4()` - Generate random UUID** - - // See https://github.com/broofa/node-uuid for API details - function v4(options, buf, offset) { - // Deprecated - 'format' argument, as supported in v1.2 - var i = buf && offset || 0; - - if (typeof(options) == 'string') { - buf = options == 'binary' ? new BufferClass(16) : null; - options = null; - } - options = options || {}; - - var rnds = options.random || (options.rng || _rng)(); - - // Per 4.4, set bits for version and `clock_seq_hi_and_reserved` - rnds[6] = (rnds[6] & 0x0f) | 0x40; - rnds[8] = (rnds[8] & 0x3f) | 0x80; - - // Copy bytes to buffer, if provided - if (buf) { - for (var ii = 0; ii < 16; ii++) { - buf[i + ii] = rnds[ii]; - } - } - - return buf || unparse(rnds); - } - - // Export public API - var uuid = v4; - uuid.v1 = v1; - uuid.v4 = v4; - uuid.parse = parse; - uuid.unparse = unparse; - uuid.BufferClass = BufferClass; - - if (_global.define && define.amd) { - // Publish as AMD module - define(function() {return uuid;}); - } else if (typeof(module) != 'undefined' && module.exports) { - // Publish as node.js module - module.exports = uuid; - } else { - // Publish as global (in browsers) - var _previousRoot = _global.uuid; - - // **`noConflict()` - (browser only) to reset global 'uuid' var** - uuid.noConflict = function() { - _global.uuid = _previousRoot; - return uuid; - }; - - _global.uuid = uuid; - } -}()); diff --git a/src/package.json b/src/package.json deleted file mode 100644 index 637decf..0000000 --- a/src/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "Whiteout Mail", - "version": "0.0.1", - "main": "index.html", - "window": { - "toolbar": false, - "width": 1024, - "height": 768 - } -} \ No newline at end of file diff --git a/src/require-config.js b/src/require-config.js deleted file mode 100644 index 4a550a6..0000000 --- a/src/require-config.js +++ /dev/null @@ -1,68 +0,0 @@ -(function() { - 'use strict'; - - requirejs.config({ - nodeRequire: (typeof module !== 'undefined' && module.exports) ? require : undefined, - baseUrl: 'lib', - paths: { - js: '../js', - test: '../../test', - jquery: 'jquery.min', - underscore: 'underscore/underscore-min', - lawnchair: 'lawnchair/lawnchair-git', - lawnchairSQL: 'lawnchair/lawnchair-adapter-webkit-sqlite-git', - lawnchairIDB: 'lawnchair/lawnchair-adapter-indexed-db-git', - angular: 'angular/angular.min', - angularRoute: 'angular/angular-route.min', - angularAnimate: 'angular/angular-animate.min', - ngInfiniteScroll: 'ng-infinite-scroll.min', - ngTagsInput: 'ngtagsinput/ng-tags-input.min', - uuid: 'uuid/uuid', - forge: 'forge/forge.min', - punycode: 'punycode.min', - openpgp: 'openpgp/openpgp', - fastclick: 'fastclick/fastclick' - }, - shim: { - forge: { - exports: 'forge' - }, - jquery: { - exports: '$' - }, - angular: { - exports: 'angular', - deps: ['jquery'] - }, - angularRoute: { - exports: 'angular', - deps: ['angular'] - }, - angularAnimate: { - exports: 'angular', - deps: ['angular'] - }, - ngInfiniteScroll: { - exports: 'angular', - deps: ['jquery', 'angular'] - }, - ngTagsInput: { - exports: 'angular', - deps: ['angular'] - }, - lawnchair: { - exports: 'Lawnchair' - }, - lawnchairSQL: { - deps: ['lawnchair'] - }, - lawnchairIDB: { - deps: ['lawnchair', 'lawnchairSQL'] - }, - underscore: { - exports: '_' - } - } - }); - -}()); \ No newline at end of file From 9bfda7396981a03c3a6bee61ea7470a379a1467c Mon Sep 17 00:00:00 2001 From: Felix Hammerl Date: Tue, 7 Oct 2014 20:32:23 +0200 Subject: [PATCH 02/27] WIP add unit tests --- .gitignore | 2 + .jshintrc | 17 +- Gruntfile.js | 51 +- package.json | 140 +- test/unit/account-ctrl-test.js | 178 +- test/unit/add-account-ctrl-test.js | 408 +- test/unit/admin-dao-test.js | 250 +- test/unit/app-controller-test.js | 373 +- test/unit/auth-test.js | 631 ++- test/unit/backbutton-handler-test.js | 103 +- test/unit/connection-doctor-test.js | 695 ++- test/unit/contacts-ctrl-test.js | 360 +- test/unit/crypto-test.js | 79 +- test/unit/devicestorage-dao-test.js | 146 +- test/unit/dialog-ctrl-test.js | 82 +- test/unit/email-dao-test.js | 4239 ++++++++--------- test/unit/index.html | 63 +- test/unit/invitation-dao-test.js | 167 +- test/unit/keychain-dao-test.js | 2512 +++++----- test/unit/lawnchair-dao-test.js | 258 +- test/unit/login-ctrl-test.js | 408 +- test/unit/login-existing-ctrl-test.js | 174 +- test/unit/login-initial-ctrl-test.js | 347 +- test/unit/login-new-device-ctrl-test.js | 354 +- .../login-privatekey-download-ctrl-test.js | 486 +- test/unit/login-set-credentials-ctrl-test.js | 174 +- test/unit/mail-list-ctrl-test.js | 876 ++-- test/unit/main.js | 113 - test/unit/navigation-ctrl-test.js | 160 +- test/unit/oauth-test.js | 359 +- test/unit/outbox-bo-test.js | 519 +- test/unit/pgp-test.js | 864 ++-- test/unit/privatekey-dao-test.js | 352 +- test/unit/privatekey-upload-ctrl-test.js | 493 +- test/unit/publickey-dao-test.js | 258 +- test/unit/read-ctrl-test.js | 356 +- test/unit/rest-dao-test.js | 370 +- test/unit/set-passphrase-ctrl-test.js | 224 +- test/unit/update-handler-test.js | 811 ++-- test/unit/write-ctrl-test.js | 750 ++- 40 files changed, 9527 insertions(+), 9675 deletions(-) delete mode 100644 test/unit/main.js diff --git a/.gitignore b/.gitignore index 97d8d25..babcf49 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ dist/ release/ test/integration/src/ .elasticbeanstalk/ +test/unit/index.js +test/unit/index.js.map diff --git a/.jshintrc b/.jshintrc index 79d663e..a3064a8 100644 --- a/.jshintrc +++ b/.jshintrc @@ -18,19 +18,18 @@ "unused": true, "predef": [ + "self", "console", - "Notification", - "importScripts", - "process", - "Event", + "process", "chrome", - "define", - "self", + "Notification", + "Event", + "sinon", + "mocha", + "chai", + "expect", "describe", "it", - "chai", - "sinon", - "mocha", "before", "beforeEach", "after", diff --git a/Gruntfile.js b/Gruntfile.js index dacb48e..ce09983 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,6 +1,8 @@ module.exports = function(grunt) { 'use strict'; + require('time-grunt')(grunt); + var version = grunt.option('release'), zipName = (version) ? version : 'DEV'; @@ -24,7 +26,7 @@ module.exports = function(grunt) { }, jshint: { - all: ['Gruntfile.js', 'src/*.js', 'src/js/**/*.js', 'test/unit/*.js', 'test/integration/*.js'], + all: ['Gruntfile.js', 'src/*.js', 'src/js/**/*.js', 'test/unit/*-test.js', 'test/integration/*.js'], options: { jshintrc: '.jshintrc' } @@ -109,16 +111,22 @@ module.exports = function(grunt) { 'dist/js/app.min.js': ['src/js/app.js'] }, options: { - external: ['node-forge', 'net', 'tls'] // common.js apis not required at build time + external: [] } }, - /* TODO: - tls-worker: {}, - mailreader-worker: {}, - pbkdf2-worker: {}, - unitTest: {}, - unitTest: {}, - integrationTest: {} + unitTest: { + files: { + 'test/unit/index.js': ['test/unit/*-test.js'] + }, + options: { + external: [] + } + }, + /* + TODO: + mailreader-worker: {}, + pbkdf2-worker: {}, + integrationTest: {} */ }, @@ -126,6 +134,7 @@ module.exports = function(grunt) { all: { files: { 'dist/js/app.min.js': [ + 'src/lib/openpgp/openpgp.js', 'src/lib/underscore/underscore-min.js', 'node_modules/jquery/dist/jquery.min.js', 'src/lib/angular/angular.min.js', @@ -142,6 +151,28 @@ module.exports = function(grunt) { ] } }, + unitTest: { + files: { + 'test/unit/index.js': [ + 'src/lib/underscore/underscore-min.js', + 'node_modules/jquery/dist/jquery.min.js', + 'src/lib/openpgp/openpgp.js', + 'src/lib/angular/angular.min.js', + 'node_modules/angular-mocks/angular-mocks.js', + 'src/lib/angular/angular-route.min.js', + 'src/lib/angular/angular-animate.min.js', + 'src/lib/ngtagsinput/ng-tags-input.min.js', + 'src/lib/fastclick/fastclick.js', + 'node_modules/ng-infinite-scroll/build/ng-infinite-scroll.min.js', + 'src/lib/lawnchair/lawnchair-git.js', + 'src/lib/lawnchair/lawnchair-adapter-webkit-sqlite-git.js', + 'src/lib/lawnchair/lawnchair-adapter-indexed-db-git.js', + 'node_modules/dompurify/purify.js', + 'test/lib/angular-mocks.js', + 'test/unit/index.js' + ] + } + }, options: { banner: '/*! Copyright © <%= grunt.template.today("yyyy") %>, Whiteout Networks GmbH.*/\n' } @@ -152,7 +183,7 @@ module.exports = function(grunt) { expand: true, flatten: true, cwd: 'node_modules/', - src: ['requirejs/require.js', 'mocha/mocha.css', 'mocha/mocha.js', 'chai/chai.js', 'sinon/pkg/sinon.js', 'angularjs/src/ngMock/angular-mocks.js', 'browsercrow/src/*.js', 'browsersmtp/src/*.js'], + src: ['mocha/mocha.css', 'mocha/mocha.js', 'chai/chai.js', 'sinon/pkg/sinon.js', 'browsercrow/src/*.js', 'browsersmtp/src/*.js'], dest: 'test/lib/' }, font: { diff --git a/package.json b/package.json index a6e3162..614c632 100644 --- a/package.json +++ b/package.json @@ -1,70 +1,72 @@ { - "name": "whiteout-mail", - "version": "0.0.1", - "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" - }, - "scripts": { - "test": "grunt && grunt test", - "start": "node server.js" - }, - "dependencies": { - "axe-logger": "~0.0.2", - "compression": "^1.0.11", - "config": "^1.0.2", - "crypto-lib": "~0.2.1", - "dompurify": "~0.4.2", - "express": "^4.8.3", - "imap-client": "~0.4.3", - "jquery": "~2.1.1", - "mailreader": "~0.3.5", - "morgan": "^1.2.3", - "ng-infinite-scroll": "~1.1.2", - "npmlog": "^0.1.1", - "pgpbuilder": "~0.4.0", - "pgpmailer": "~0.4.0", - "socket.io": "^1.0.6", - "tcp-socket": "^0.3.9", - "wo-smtpclient": "^0.3.8" - }, - "devDependencies": { - "angularjs": "https://github.com/whiteout-io/angular.js/tarball/npm-version", - "browsercrow": "https://github.com/whiteout-io/browsercrow/tarball/master", - "browsersmtp": "https://github.com/whiteout-io/browsersmtp/tarball/master", - "chai": "~1.7.2", - "grunt": "~0.4.1", - "grunt-autoprefixer": "~0.7.2", - "grunt-browserify": "^3.0.1", - "grunt-contrib-clean": "~0.5.0", - "grunt-contrib-compress": "~0.5.2", - "grunt-contrib-connect": "~0.5.0", - "grunt-contrib-copy": "~0.4.1", - "grunt-contrib-jshint": "~0.6.4", - "grunt-contrib-sass": "~0.7.3", - "grunt-contrib-uglify": "^0.6.0", - "grunt-contrib-watch": "~0.5.3", - "grunt-csso": "~0.6.1", - "grunt-manifest": "^0.4.0", - "grunt-mocha": "~0.4.1", - "mocha": "~1.13.0", - "sinon": "~1.7.3" - } -} \ No newline at end of file + "name": "whiteout-mail", + "version": "0.0.1", + "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" + }, + "scripts": { + "test": "grunt && grunt test", + "start": "node server.js" + }, + "dependencies": { + "axe-logger": "~0.0.2", + "compression": "^1.0.11", + "config": "^1.0.2", + "crypto-lib": "~0.2.1", + "dompurify": "~0.4.2", + "express": "^4.8.3", + "imap-client": "~0.4.3", + "jquery": "~2.1.1", + "mailreader": "~0.3.5", + "morgan": "^1.2.3", + "ng-infinite-scroll": "~1.1.2", + "npmlog": "^0.1.1", + "pgpbuilder": "~0.4.0", + "pgpmailer": "~0.4.0", + "socket.io": "^1.0.6", + "tcp-socket": "^0.3.9", + "wo-smtpclient": "^0.3.8" + }, + "devDependencies": { + "angular-mocks": "^1.2.25", + "angularjs": "https://github.com/whiteout-io/angular.js/tarball/npm-version", + "browsercrow": "https://github.com/whiteout-io/browsercrow/tarball/master", + "browsersmtp": "https://github.com/whiteout-io/browsersmtp/tarball/master", + "chai": "~1.7.2", + "grunt": "~0.4.1", + "grunt-autoprefixer": "~0.7.2", + "grunt-browserify": "^3.0.1", + "grunt-contrib-clean": "~0.5.0", + "grunt-contrib-compress": "~0.5.2", + "grunt-contrib-connect": "~0.5.0", + "grunt-contrib-copy": "~0.4.1", + "grunt-contrib-jshint": "~0.6.4", + "grunt-contrib-sass": "~0.7.3", + "grunt-contrib-uglify": "^0.6.0", + "grunt-contrib-watch": "~0.5.3", + "grunt-csso": "~0.6.1", + "grunt-manifest": "^0.4.0", + "grunt-mocha": "~0.4.1", + "mocha": "~1.13.0", + "sinon": "~1.7.3", + "time-grunt": "^1.0.0" + } +} diff --git a/test/unit/account-ctrl-test.js b/test/unit/account-ctrl-test.js index 1902d0b..5deacd7 100644 --- a/test/unit/account-ctrl-test.js +++ b/test/unit/account-ctrl-test.js @@ -1,101 +1,97 @@ -define(function(require) { - 'use strict'; +'use strict'; - var expect = chai.expect, - angular = require('angular'), - mocks = require('angularMocks'), - AccountCtrl = require('js/controller/account'), - PGP = require('js/crypto/pgp'), - dl = require('js/util/download'), - appController = require('js/app-controller'), - KeychainDAO = require('js/dao/keychain-dao'); +var mocks = angular.mocks, + AccountCtrl = require('../../src/js/controller/account'), + PGP = require('../../src/js/crypto/pgp'), + dl = require('../../src/js/util/download'), + appController = require('../../src/js/app-controller'), + KeychainDAO = require('../../src/js/dao/keychain-dao'); - describe('Account Controller unit test', function() { - var scope, accountCtrl, - dummyFingerprint, expectedFingerprint, - dummyKeyId, expectedKeyId, - emailAddress, keySize, pgpMock, keychainMock; +describe('Account Controller unit test', function() { + var scope, accountCtrl, + dummyFingerprint, expectedFingerprint, + dummyKeyId, expectedKeyId, + emailAddress, keySize, pgpMock, keychainMock; - beforeEach(function() { - appController._pgp = pgpMock = sinon.createStubInstance(PGP); - appController._keychain = keychainMock = sinon.createStubInstance(KeychainDAO); + beforeEach(function() { + appController._pgp = pgpMock = sinon.createStubInstance(PGP); + appController._keychain = keychainMock = sinon.createStubInstance(KeychainDAO); - dummyFingerprint = '3A2D39B4E1404190B8B949DE7D7E99036E712926'; - expectedFingerprint = '3A2D 39B4 E140 4190 B8B9 49DE 7D7E 9903 6E71 2926'; - dummyKeyId = '9FEB47936E712926'; - expectedKeyId = '6E712926'; - pgpMock.getFingerprint.returns(dummyFingerprint); - pgpMock.getKeyId.returns(dummyKeyId); - emailAddress = 'fred@foo.com'; - keySize = 1234; - appController._emailDao = { - _account: { - emailAddress: emailAddress, - asymKeySize: keySize - } - }; - pgpMock.getKeyParams.returns({ - _id: dummyKeyId, - fingerprint: dummyFingerprint, - userId: emailAddress, - bitSize: keySize - }); - - angular.module('accounttest', []); - mocks.module('accounttest'); - mocks.inject(function($rootScope, $controller) { - scope = $rootScope.$new(); - scope.state = {}; - accountCtrl = $controller(AccountCtrl, { - $scope: scope - }); - }); + dummyFingerprint = '3A2D39B4E1404190B8B949DE7D7E99036E712926'; + expectedFingerprint = '3A2D 39B4 E140 4190 B8B9 49DE 7D7E 9903 6E71 2926'; + dummyKeyId = '9FEB47936E712926'; + expectedKeyId = '6E712926'; + pgpMock.getFingerprint.returns(dummyFingerprint); + pgpMock.getKeyId.returns(dummyKeyId); + emailAddress = 'fred@foo.com'; + keySize = 1234; + appController._emailDao = { + _account: { + emailAddress: emailAddress, + asymKeySize: keySize + } + }; + pgpMock.getKeyParams.returns({ + _id: dummyKeyId, + fingerprint: dummyFingerprint, + userId: emailAddress, + bitSize: keySize }); - afterEach(function() {}); - - describe('scope variables', function() { - it('should be set correctly', function() { - expect(scope.eMail).to.equal(emailAddress); - expect(scope.keyId).to.equal(expectedKeyId); - expect(scope.fingerprint).to.equal(expectedFingerprint); - expect(scope.keysize).to.equal(keySize); - }); - }); - describe('export to key file', function() { - it('should work', function() { - var createDownloadMock = sinon.stub(dl, 'createDownload'); - keychainMock.getUserKeyPair.withArgs(emailAddress).yields(null, { - publicKey: { - _id: dummyKeyId, - publicKey: 'a' - }, - privateKey: { - encryptedKey: 'b' - } - }); - createDownloadMock.withArgs(sinon.match(function(arg) { - return arg.content === 'a\r\nb' && arg.filename === 'whiteout_mail_' + emailAddress + '_' + expectedKeyId + '.asc' && arg.contentType === 'text/plain'; - })).returns(); - - scope.exportKeyFile(); - - expect(scope.state.lightbox).to.equal(undefined); - expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; - expect(dl.createDownload.calledOnce).to.be.true; - dl.createDownload.restore(); - }); - - it('should not work when key export failed', function(done) { - keychainMock.getUserKeyPair.yields(new Error('Boom!')); - scope.onError = function(err) { - expect(err.message).to.equal('Boom!'); - expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; - done(); - }; - - scope.exportKeyFile(); + angular.module('accounttest', []); + mocks.module('accounttest'); + mocks.inject(function($rootScope, $controller) { + scope = $rootScope.$new(); + scope.state = {}; + accountCtrl = $controller(AccountCtrl, { + $scope: scope }); }); }); + + afterEach(function() {}); + + describe('scope variables', function() { + it('should be set correctly', function() { + expect(scope.eMail).to.equal(emailAddress); + expect(scope.keyId).to.equal(expectedKeyId); + expect(scope.fingerprint).to.equal(expectedFingerprint); + expect(scope.keysize).to.equal(keySize); + }); + }); + describe('export to key file', function() { + it('should work', function() { + var createDownloadMock = sinon.stub(dl, 'createDownload'); + keychainMock.getUserKeyPair.withArgs(emailAddress).yields(null, { + publicKey: { + _id: dummyKeyId, + publicKey: 'a' + }, + privateKey: { + encryptedKey: 'b' + } + }); + createDownloadMock.withArgs(sinon.match(function(arg) { + return arg.content === 'a\r\nb' && arg.filename === 'whiteout_mail_' + emailAddress + '_' + expectedKeyId + '.asc' && arg.contentType === 'text/plain'; + })).returns(); + + scope.exportKeyFile(); + + expect(scope.state.lightbox).to.equal(undefined); + expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; + expect(dl.createDownload.calledOnce).to.be.true; + dl.createDownload.restore(); + }); + + it('should not work when key export failed', function(done) { + keychainMock.getUserKeyPair.yields(new Error('Boom!')); + scope.onError = function(err) { + expect(err.message).to.equal('Boom!'); + expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; + done(); + }; + + scope.exportKeyFile(); + }); + }); }); \ No newline at end of file diff --git a/test/unit/add-account-ctrl-test.js b/test/unit/add-account-ctrl-test.js index 4f42e92..69d2430 100644 --- a/test/unit/add-account-ctrl-test.js +++ b/test/unit/add-account-ctrl-test.js @@ -1,216 +1,212 @@ -define(function(require) { - 'use strict'; +'use strict'; - var expect = chai.expect, - angular = require('angular'), - mocks = require('angularMocks'), - AddAccountCtrl = require('js/controller/add-account'), - Auth = require('js/bo/auth'), - AdminDao = require('js/dao/admin-dao'), - appController = require('js/app-controller'); +var mocks = angular.mocks, + AddAccountCtrl = require('../../src/js/controller/add-account'), + Auth = require('../../src/js/bo/auth'), + AdminDao = require('../../src/js/dao/admin-dao'), + appController = require('../../src/js/app-controller'); - describe('Add Account Controller unit test', function() { - var scope, location, ctrl, authStub, origAuth, adminStub; +describe('Add Account Controller unit test', function() { + var scope, location, ctrl, authStub, origAuth, adminStub; - beforeEach(function() { - // remember original module to restore later, then replace it - origAuth = appController._auth; - appController._auth = authStub = sinon.createStubInstance(Auth); - appController._adminDao = adminStub = sinon.createStubInstance(AdminDao); + beforeEach(function() { + // remember original module to restore later, then replace it + origAuth = appController._auth; + appController._auth = authStub = sinon.createStubInstance(Auth); + appController._adminDao = adminStub = sinon.createStubInstance(AdminDao); - angular.module('addaccounttest', []); - mocks.module('addaccounttest'); - mocks.inject(function($controller, $rootScope, $location) { - location = $location; - scope = $rootScope.$new(); - scope.state = {}; - scope.form = {}; - scope.formValidate = {}; + angular.module('addaccounttest', []); + mocks.module('addaccounttest'); + mocks.inject(function($controller, $rootScope, $location) { + location = $location; + scope = $rootScope.$new(); + scope.state = {}; + scope.form = {}; + scope.formValidate = {}; - sinon.stub(location, 'path').returns(location); - sinon.stub(location, 'search').returns(location); - sinon.stub(scope, '$apply', function() {}); + sinon.stub(location, 'path').returns(location); + sinon.stub(location, 'search').returns(location); + sinon.stub(scope, '$apply', function() {}); - ctrl = $controller(AddAccountCtrl, { - $location: location, - $scope: scope, - $routeParams: {} - }); + ctrl = $controller(AddAccountCtrl, { + $location: location, + $scope: scope, + $routeParams: {} }); }); - - afterEach(function() { - // restore the app controller module - appController._auth = origAuth; - - location.path.restore(); - location.search.restore(); - if (scope.$apply.restore) { - scope.$apply.restore(); - } - }); - - describe('createWhiteoutAccount', function() { - it('should return early for invalid form', function() { - scope.form.$invalid = true; - scope.createWhiteoutAccount(); - expect(adminStub.createUser.called).to.be.false; - }); - - it('should fail to error creating user', function(done) { - scope.form.$invalid = false; - scope.betaCode = 'asfd'; - scope.phone = '12345'; - adminStub.createUser.yieldsAsync(new Error('asdf')); - - scope.$apply = function() { - expect(scope.busy).to.be.false; - expect(scope.errMsg).to.equal('asdf'); - expect(adminStub.createUser.calledOnce).to.be.true; - done(); - }; - - scope.createWhiteoutAccount(); - expect(scope.busy).to.be.true; - }); - - it('should work', function(done) { - scope.form.$invalid = false; - scope.betaCode = 'asfd'; - scope.phone = '12345'; - adminStub.createUser.yieldsAsync(); - - scope.$apply = function() { - expect(scope.busy).to.be.false; - expect(scope.errMsg).to.be.undefined; - expect(scope.step).to.equal(3); - expect(adminStub.createUser.calledOnce).to.be.true; - done(); - }; - - scope.createWhiteoutAccount(); - expect(scope.busy).to.be.true; - }); - }); - - describe('validateUser', function() { - it('should return early for invalid form', function() { - scope.formValidate.$invalid = true; - scope.validateUser(); - expect(adminStub.validateUser.called).to.be.false; - }); - - it('should fail to error creating user', function(done) { - scope.formValidate.$invalid = false; - scope.token = 'asfd'; - adminStub.validateUser.yieldsAsync(new Error('asdf')); - - scope.$apply = function() { - expect(scope.busyValidate).to.be.false; - expect(scope.errMsgValidate).to.equal('asdf'); - expect(adminStub.validateUser.calledOnce).to.be.true; - done(); - }; - - scope.validateUser(); - expect(scope.busyValidate).to.be.true; - }); - - it('should work', function(done) { - scope.formValidate.$invalid = false; - scope.token = 'asfd'; - adminStub.validateUser.yieldsAsync(); - - scope.login = function() { - expect(scope.busyValidate).to.be.true; - expect(scope.errMsgValidate).to.be.undefined; - expect(adminStub.validateUser.calledOnce).to.be.true; - done(); - }; - - scope.validateUser(); - expect(scope.busyValidate).to.be.true; - }); - }); - - describe('login', function() { - it('should work', function() { - scope.form.$invalid = false; - authStub.setCredentials.returns(); - - scope.login(); - expect(authStub.setCredentials.calledOnce).to.be.true; - expect(location.path.calledWith('/login')).to.be.true; - }); - }); - - describe('connectToGoogle', function() { - it('should forward to login', function() { - authStub._oauth = { - isSupported: function() { - return true; - } - }; - - authStub.getOAuthToken.yields(); - - scope.connectToGoogle(); - - expect(location.path.calledWith('/login-set-credentials')).to.be.true; - expect(location.search.calledWith({ - provider: 'gmail' - })).to.be.true; - expect(authStub.getOAuthToken.calledOnce).to.be.true; - }); - - it('should not use oauth for gmail', function() { - authStub._oauth = { - isSupported: function() { - return false; - } - }; - - scope.connectToGoogle(); - - expect(location.path.calledWith('/login-set-credentials')).to.be.true; - expect(location.search.calledWith({ - provider: 'gmail' - })).to.be.true; - expect(authStub.getOAuthToken.called).to.be.false; - }); - - it('should not forward to login when oauth fails', function(done) { - authStub._oauth = { - isSupported: function() { - return true; - } - }; - - authStub.getOAuthToken.yields(new Error()); - - scope.onError = function(err) { - expect(err).to.exist; - expect(location.path.called).to.be.false; - expect(location.search.called).to.be.false; - - done(); - }; - - scope.connectToGoogle(); - }); - }); - - describe('connectTo', function() { - it('should forward to login', function() { - var provider = 'wmail'; - scope.connectTo(provider); - - expect(location.path.calledWith('/login-set-credentials')).to.be.true; - expect(location.search.calledWith({ - provider: provider - })).to.be.true; - }); - }); - }); + + afterEach(function() { + // restore the app controller module + appController._auth = origAuth; + + location.path.restore(); + location.search.restore(); + if (scope.$apply.restore) { + scope.$apply.restore(); + } + }); + + describe('createWhiteoutAccount', function() { + it('should return early for invalid form', function() { + scope.form.$invalid = true; + scope.createWhiteoutAccount(); + expect(adminStub.createUser.called).to.be.false; + }); + + it('should fail to error creating user', function(done) { + scope.form.$invalid = false; + scope.betaCode = 'asfd'; + scope.phone = '12345'; + adminStub.createUser.yieldsAsync(new Error('asdf')); + + scope.$apply = function() { + expect(scope.busy).to.be.false; + expect(scope.errMsg).to.equal('asdf'); + expect(adminStub.createUser.calledOnce).to.be.true; + done(); + }; + + scope.createWhiteoutAccount(); + expect(scope.busy).to.be.true; + }); + + it('should work', function(done) { + scope.form.$invalid = false; + scope.betaCode = 'asfd'; + scope.phone = '12345'; + adminStub.createUser.yieldsAsync(); + + scope.$apply = function() { + expect(scope.busy).to.be.false; + expect(scope.errMsg).to.be.undefined; + expect(scope.step).to.equal(3); + expect(adminStub.createUser.calledOnce).to.be.true; + done(); + }; + + scope.createWhiteoutAccount(); + expect(scope.busy).to.be.true; + }); + }); + + describe('validateUser', function() { + it('should return early for invalid form', function() { + scope.formValidate.$invalid = true; + scope.validateUser(); + expect(adminStub.validateUser.called).to.be.false; + }); + + it('should fail to error creating user', function(done) { + scope.formValidate.$invalid = false; + scope.token = 'asfd'; + adminStub.validateUser.yieldsAsync(new Error('asdf')); + + scope.$apply = function() { + expect(scope.busyValidate).to.be.false; + expect(scope.errMsgValidate).to.equal('asdf'); + expect(adminStub.validateUser.calledOnce).to.be.true; + done(); + }; + + scope.validateUser(); + expect(scope.busyValidate).to.be.true; + }); + + it('should work', function(done) { + scope.formValidate.$invalid = false; + scope.token = 'asfd'; + adminStub.validateUser.yieldsAsync(); + + scope.login = function() { + expect(scope.busyValidate).to.be.true; + expect(scope.errMsgValidate).to.be.undefined; + expect(adminStub.validateUser.calledOnce).to.be.true; + done(); + }; + + scope.validateUser(); + expect(scope.busyValidate).to.be.true; + }); + }); + + describe('login', function() { + it('should work', function() { + scope.form.$invalid = false; + authStub.setCredentials.returns(); + + scope.login(); + expect(authStub.setCredentials.calledOnce).to.be.true; + expect(location.path.calledWith('/login')).to.be.true; + }); + }); + + describe('connectToGoogle', function() { + it('should forward to login', function() { + authStub._oauth = { + isSupported: function() { + return true; + } + }; + + authStub.getOAuthToken.yields(); + + scope.connectToGoogle(); + + expect(location.path.calledWith('/login-set-credentials')).to.be.true; + expect(location.search.calledWith({ + provider: 'gmail' + })).to.be.true; + expect(authStub.getOAuthToken.calledOnce).to.be.true; + }); + + it('should not use oauth for gmail', function() { + authStub._oauth = { + isSupported: function() { + return false; + } + }; + + scope.connectToGoogle(); + + expect(location.path.calledWith('/login-set-credentials')).to.be.true; + expect(location.search.calledWith({ + provider: 'gmail' + })).to.be.true; + expect(authStub.getOAuthToken.called).to.be.false; + }); + + it('should not forward to login when oauth fails', function(done) { + authStub._oauth = { + isSupported: function() { + return true; + } + }; + + authStub.getOAuthToken.yields(new Error()); + + scope.onError = function(err) { + expect(err).to.exist; + expect(location.path.called).to.be.false; + expect(location.search.called).to.be.false; + + done(); + }; + + scope.connectToGoogle(); + }); + }); + + describe('connectTo', function() { + it('should forward to login', function() { + var provider = 'wmail'; + scope.connectTo(provider); + + expect(location.path.calledWith('/login-set-credentials')).to.be.true; + expect(location.search.calledWith({ + provider: provider + })).to.be.true; + }); + }); + }); \ No newline at end of file diff --git a/test/unit/admin-dao-test.js b/test/unit/admin-dao-test.js index 06811b2..2550374 100644 --- a/test/unit/admin-dao-test.js +++ b/test/unit/admin-dao-test.js @@ -1,146 +1,142 @@ -define(function(require) { - 'use strict'; +'use strict'; - var RestDAO = require('js/dao/rest-dao'), - AdminDAO = require('js/dao/admin-dao'), - expect = chai.expect; +var RestDAO = require('../../src/js/dao/rest-dao'), + AdminDAO = require('../../src/js/dao/admin-dao'); - describe('Admin DAO unit tests', function() { +describe('Admin DAO unit tests', function() { - var adminDao, restDaoStub, - emailAddress = 'test@example.com', - password = 'secret'; + var adminDao, restDaoStub, + emailAddress = 'test@example.com', + password = 'secret'; - beforeEach(function() { - restDaoStub = sinon.createStubInstance(RestDAO); - adminDao = new AdminDAO(restDaoStub); - }); + beforeEach(function() { + restDaoStub = sinon.createStubInstance(RestDAO); + adminDao = new AdminDAO(restDaoStub); + }); - afterEach(function() {}); + afterEach(function() {}); - describe('createUser', function() { - it('should fail due to incomplete args', function(done) { - var opt = { - emailAddress: emailAddress - }; + describe('createUser', function() { + it('should fail due to incomplete args', function(done) { + var opt = { + emailAddress: emailAddress + }; - adminDao.createUser(opt, function(err) { - expect(err).to.exist; - done(); - }); - }); - - it('should fail if user already exists', function(done) { - var opt = { - emailAddress: emailAddress, - password: password, - phone: '12345' - }; - - restDaoStub.post.withArgs(opt, '/user').yields({ - code: 409 - }); - - adminDao.createUser(opt, function(err) { - expect(err.message).to.contain('already taken'); - expect(restDaoStub.post.calledOnce).to.be.true; - done(); - }); - }); - - it('should fail due to unknown error', function(done) { - var opt = { - emailAddress: emailAddress, - password: password, - phone: '12345' - }; - - restDaoStub.post.withArgs(opt, '/user').yields(new Error()); - - adminDao.createUser(opt, function(err) { - expect(err).to.exist; - expect(restDaoStub.post.calledOnce).to.be.true; - done(); - }); - }); - - it('should work', function(done) { - var opt = { - emailAddress: emailAddress, - password: password, - phone: '12345' - }; - - restDaoStub.post.withArgs(opt, '/user').yields(); - - adminDao.createUser(opt, function(err) { - expect(err).to.not.exist; - expect(restDaoStub.post.calledOnce).to.be.true; - done(); - }); + adminDao.createUser(opt, function(err) { + expect(err).to.exist; + done(); }); }); - describe('validateUser', function() { - it('should fail due to incomplete args', function(done) { - var opt = { - emailAddress: emailAddress - }; + it('should fail if user already exists', function(done) { + var opt = { + emailAddress: emailAddress, + password: password, + phone: '12345' + }; - adminDao.validateUser(opt, function(err) { - expect(err).to.exist; - done(); - }); + restDaoStub.post.withArgs(opt, '/user').yields({ + code: 409 }); - it('should fail due to error in rest api', function(done) { - var opt = { - emailAddress: emailAddress, - token: 'H45Z6D' - }; - - restDaoStub.post.withArgs(opt, '/user/validate').yields(new Error()); - - adminDao.validateUser(opt, function(err) { - expect(err).to.exist; - expect(restDaoStub.post.calledOnce).to.be.true; - done(); - }); - }); - - it('should work with no error object', function(done) { - var opt = { - emailAddress: emailAddress, - token: 'H45Z6D' - }; - - restDaoStub.post.withArgs(opt, '/user/validate').yields(); - - adminDao.validateUser(opt, function(err) { - expect(err).to.not.exist; - expect(restDaoStub.post.calledOnce).to.be.true; - done(); - }); - }); - - it('should work with 202', function(done) { - var opt = { - emailAddress: emailAddress, - token: 'H45Z6D' - }; - - restDaoStub.post.withArgs(opt, '/user/validate').yields({ - code: 202 - }); - - adminDao.validateUser(opt, function(err) { - expect(err).to.not.exist; - expect(restDaoStub.post.calledOnce).to.be.true; - done(); - }); + adminDao.createUser(opt, function(err) { + expect(err.message).to.contain('already taken'); + expect(restDaoStub.post.calledOnce).to.be.true; + done(); }); }); + it('should fail due to unknown error', function(done) { + var opt = { + emailAddress: emailAddress, + password: password, + phone: '12345' + }; + + restDaoStub.post.withArgs(opt, '/user').yields(new Error()); + + adminDao.createUser(opt, function(err) { + expect(err).to.exist; + expect(restDaoStub.post.calledOnce).to.be.true; + done(); + }); + }); + + it('should work', function(done) { + var opt = { + emailAddress: emailAddress, + password: password, + phone: '12345' + }; + + restDaoStub.post.withArgs(opt, '/user').yields(); + + adminDao.createUser(opt, function(err) { + expect(err).to.not.exist; + expect(restDaoStub.post.calledOnce).to.be.true; + done(); + }); + }); + }); + + describe('validateUser', function() { + it('should fail due to incomplete args', function(done) { + var opt = { + emailAddress: emailAddress + }; + + adminDao.validateUser(opt, function(err) { + expect(err).to.exist; + done(); + }); + }); + + it('should fail due to error in rest api', function(done) { + var opt = { + emailAddress: emailAddress, + token: 'H45Z6D' + }; + + restDaoStub.post.withArgs(opt, '/user/validate').yields(new Error()); + + adminDao.validateUser(opt, function(err) { + expect(err).to.exist; + expect(restDaoStub.post.calledOnce).to.be.true; + done(); + }); + }); + + it('should work with no error object', function(done) { + var opt = { + emailAddress: emailAddress, + token: 'H45Z6D' + }; + + restDaoStub.post.withArgs(opt, '/user/validate').yields(); + + adminDao.validateUser(opt, function(err) { + expect(err).to.not.exist; + expect(restDaoStub.post.calledOnce).to.be.true; + done(); + }); + }); + + it('should work with 202', function(done) { + var opt = { + emailAddress: emailAddress, + token: 'H45Z6D' + }; + + restDaoStub.post.withArgs(opt, '/user/validate').yields({ + code: 202 + }); + + adminDao.validateUser(opt, function(err) { + expect(err).to.not.exist; + expect(restDaoStub.post.calledOnce).to.be.true; + done(); + }); + }); }); }); \ No newline at end of file diff --git a/test/unit/app-controller-test.js b/test/unit/app-controller-test.js index 21f622b..b260ab2 100644 --- a/test/unit/app-controller-test.js +++ b/test/unit/app-controller-test.js @@ -1,212 +1,209 @@ -define(function(require) { - 'use strict'; +'use strict'; - var controller = require('js/app-controller'), - EmailDAO = require('js/dao/email-dao'), - OutboxBO = require('js/bo/outbox'), - DeviceStorageDAO = require('js/dao/devicestorage-dao'), - UpdateHandler = require('js/util/update/update-handler'), - Auth = require('js/bo/auth'), - expect = chai.expect; +var controller = require('../../src/js/app-controller'), + EmailDAO = require('../../src/js/dao/email-dao'), + OutboxBO = require('../../src/js/bo/outbox'), + DeviceStorageDAO = require('../../src/js/dao/devicestorage-dao'), + UpdateHandler = require('../../src/js/util/update/update-handler'), + Auth = require('../../src/js/bo/auth'); - describe('App Controller unit tests', function() { - var emailDaoStub, outboxStub, updateHandlerStub, appConfigStoreStub, devicestorageStub, isOnlineStub, authStub; +describe('App Controller unit tests', function() { + var emailDaoStub, outboxStub, updateHandlerStub, appConfigStoreStub, devicestorageStub, isOnlineStub, authStub; + + beforeEach(function() { + controller._emailDao = emailDaoStub = sinon.createStubInstance(EmailDAO); + controller._outboxBo = outboxStub = sinon.createStubInstance(OutboxBO); + controller._appConfigStore = appConfigStoreStub = sinon.createStubInstance(DeviceStorageDAO); + controller._userStorage = devicestorageStub = sinon.createStubInstance(DeviceStorageDAO); + controller._updateHandler = updateHandlerStub = sinon.createStubInstance(UpdateHandler); + controller._auth = authStub = sinon.createStubInstance(Auth); + + isOnlineStub = sinon.stub(controller, 'isOnline'); + }); + + afterEach(function() { + isOnlineStub.restore(); + }); + + describe('buildModules', function() { + it('should work', function() { + controller.buildModules({ + onError: function() {} + }); + expect(controller._appConfigStore).to.exist; + expect(controller._auth).to.exist; + expect(controller._userStorage).to.exist; + expect(controller._invitationDao).to.exist; + expect(controller._keychain).to.exist; + expect(controller._pgp).to.exist; + expect(controller._pgpbuilder).to.exist; + expect(controller._emailDao).to.exist; + expect(controller._outboxBo).to.exist; + expect(controller._updateHandler).to.exist; + }); + }); + + describe('start', function() { + it('should not explode', function(done) { + controller.start({ + onError: function() {} + }, function(err) { + expect(err).to.not.exist; + done(); + }); + }); + }); + + describe('onDisconnect', function() { + it('should work', function() { + controller.onDisconnect(); + + expect(emailDaoStub.onDisconnect.calledOnce).to.be.true; + }); + }); + + describe('logout', function() { + it('should work', function(done) { + authStub.logout.yields(); + emailDaoStub.onDisconnect.yields(new Error()); + + controller.onError = function(err) { + expect(err).to.exist; + expect(authStub.logout.calledOnce).to.be.true; + expect(emailDaoStub.onDisconnect.calledOnce).to.be.true; + done(); + }; + + controller.logout(); + }); + }); + + describe('onConnect', function() { + beforeEach(function() { + controller._emailDao._account = {}; + }); + + it('should not connect if offline', function(done) { + isOnlineStub.returns(false); + + controller.onConnect(function(err) { + expect(err).to.not.exist; + done(); + }); + }); + + it('should not connect if account is not initialized', function(done) { + controller._emailDao._account = null; + + controller.onConnect(function(err) { + expect(err).to.not.exist; + done(); + }); + }); + + it('should fail due to error in auth.getCredentials', function(done) { + isOnlineStub.returns(true); + authStub.getCredentials.yields(new Error()); + + controller.onConnect(function(err) { + expect(err).to.exist; + expect(authStub.getCredentials.calledOnce).to.be.true; + done(); + }); + }); + + it('should work', function(done) { + isOnlineStub.returns(true); + authStub.getCredentials.yields(null, { + emailAddress: 'asdf@example.com', + oauthToken: 'token', + sslCert: 'cert', + imap: {}, + smtp: {} + }); + emailDaoStub.onConnect.yields(); + + controller.onConnect(function(err) { + expect(err).to.not.exist; + expect(authStub.getCredentials.calledOnce).to.be.true; + expect(emailDaoStub.onConnect.calledOnce).to.be.true; + done(); + }); + }); + }); + + describe('init', function() { + var onConnectStub, emailAddress; beforeEach(function() { - controller._emailDao = emailDaoStub = sinon.createStubInstance(EmailDAO); - controller._outboxBo = outboxStub = sinon.createStubInstance(OutboxBO); - controller._appConfigStore = appConfigStoreStub = sinon.createStubInstance(DeviceStorageDAO); - controller._userStorage = devicestorageStub = sinon.createStubInstance(DeviceStorageDAO); - controller._updateHandler = updateHandlerStub = sinon.createStubInstance(UpdateHandler); - controller._auth = authStub = sinon.createStubInstance(Auth); + emailAddress = 'alice@bob.com'; - isOnlineStub = sinon.stub(controller, 'isOnline'); + // onConnect + onConnectStub = sinon.stub(controller, 'onConnect'); }); afterEach(function() { - isOnlineStub.restore(); + onConnectStub.restore(); }); - describe('buildModules', function() { - it('should work', function() { - controller.buildModules({ - onError: function() {} - }); - expect(controller._appConfigStore).to.exist; - expect(controller._auth).to.exist; - expect(controller._userStorage).to.exist; - expect(controller._invitationDao).to.exist; - expect(controller._keychain).to.exist; - expect(controller._pgp).to.exist; - expect(controller._pgpbuilder).to.exist; - expect(controller._emailDao).to.exist; - expect(controller._outboxBo).to.exist; - expect(controller._updateHandler).to.exist; + it('should fail due to error in storage initialization', function(done) { + devicestorageStub.init.withArgs(undefined).yields({}); + + controller.init({}, function(err, keypair) { + expect(err).to.exist; + expect(keypair).to.not.exist; + expect(devicestorageStub.init.calledOnce).to.be.true; + expect(updateHandlerStub.update.calledOnce).to.be.false; + done(); }); }); - describe('start', function() { - it('should not explode', function(done) { - controller.start({ - onError: function() {} - }, function(err) { - expect(err).to.not.exist; - done(); - }); + it('should fail due to error in update handler', function(done) { + devicestorageStub.init.yields(); + updateHandlerStub.update.yields({}); + + controller.init({ + emailAddress: emailAddress + }, function(err, keypair) { + expect(err).to.exist; + expect(keypair).to.not.exist; + expect(updateHandlerStub.update.calledOnce).to.be.true; + expect(devicestorageStub.init.calledOnce).to.be.true; + done(); }); }); - describe('onDisconnect', function() { - it('should work', function() { - controller.onDisconnect(); + it('should fail due to error in emailDao.init', function(done) { + devicestorageStub.init.yields(); + updateHandlerStub.update.yields(); + emailDaoStub.init.yields({}); - expect(emailDaoStub.onDisconnect.calledOnce).to.be.true; + controller.init({ + emailAddress: emailAddress + }, function(err, keypair) { + expect(err).to.exist; + expect(keypair).to.not.exist; + expect(updateHandlerStub.update.calledOnce).to.be.true; + expect(emailDaoStub.init.calledOnce).to.be.true; + expect(devicestorageStub.init.calledOnce).to.be.true; + done(); }); }); - describe('logout', function() { - it('should work', function(done) { - authStub.logout.yields(); - emailDaoStub.onDisconnect.yields(new Error()); + it('should work and return a keypair', function(done) { + devicestorageStub.init.withArgs(emailAddress).yields(); + emailDaoStub.init.yields(null, {}); + updateHandlerStub.update.yields(); - controller.onError = function(err) { - expect(err).to.exist; - expect(authStub.logout.calledOnce).to.be.true; - expect(emailDaoStub.onDisconnect.calledOnce).to.be.true; - done(); - }; - - controller.logout(); - }); - }); - - describe('onConnect', function() { - beforeEach(function() { - controller._emailDao._account = {}; - }); - - it('should not connect if offline', function(done) { - isOnlineStub.returns(false); - - controller.onConnect(function(err) { - expect(err).to.not.exist; - done(); - }); - }); - - it('should not connect if account is not initialized', function(done) { - controller._emailDao._account = null; - - controller.onConnect(function(err) { - expect(err).to.not.exist; - done(); - }); - }); - - it('should fail due to error in auth.getCredentials', function(done) { - isOnlineStub.returns(true); - authStub.getCredentials.yields(new Error()); - - controller.onConnect(function(err) { - expect(err).to.exist; - expect(authStub.getCredentials.calledOnce).to.be.true; - done(); - }); - }); - - it('should work', function(done) { - isOnlineStub.returns(true); - authStub.getCredentials.yields(null, { - emailAddress: 'asdf@example.com', - oauthToken: 'token', - sslCert: 'cert', - imap: {}, - smtp: {} - }); - emailDaoStub.onConnect.yields(); - - controller.onConnect(function(err) { - expect(err).to.not.exist; - expect(authStub.getCredentials.calledOnce).to.be.true; - expect(emailDaoStub.onConnect.calledOnce).to.be.true; - done(); - }); - }); - }); - - describe('init', function() { - var onConnectStub, emailAddress; - - beforeEach(function() { - emailAddress = 'alice@bob.com'; - - // onConnect - onConnectStub = sinon.stub(controller, 'onConnect'); - }); - - afterEach(function() { - onConnectStub.restore(); - }); - - it('should fail due to error in storage initialization', function(done) { - devicestorageStub.init.withArgs(undefined).yields({}); - - controller.init({}, function(err, keypair) { - expect(err).to.exist; - expect(keypair).to.not.exist; - expect(devicestorageStub.init.calledOnce).to.be.true; - expect(updateHandlerStub.update.calledOnce).to.be.false; - done(); - }); - }); - - it('should fail due to error in update handler', function(done) { - devicestorageStub.init.yields(); - updateHandlerStub.update.yields({}); - - controller.init({ - emailAddress: emailAddress - }, function(err, keypair) { - expect(err).to.exist; - expect(keypair).to.not.exist; - expect(updateHandlerStub.update.calledOnce).to.be.true; - expect(devicestorageStub.init.calledOnce).to.be.true; - done(); - }); - }); - - it('should fail due to error in emailDao.init', function(done) { - devicestorageStub.init.yields(); - updateHandlerStub.update.yields(); - emailDaoStub.init.yields({}); - - controller.init({ - emailAddress: emailAddress - }, function(err, keypair) { - expect(err).to.exist; - expect(keypair).to.not.exist; - expect(updateHandlerStub.update.calledOnce).to.be.true; - expect(emailDaoStub.init.calledOnce).to.be.true; - expect(devicestorageStub.init.calledOnce).to.be.true; - done(); - }); - }); - - it('should work and return a keypair', function(done) { - devicestorageStub.init.withArgs(emailAddress).yields(); - emailDaoStub.init.yields(null, {}); - updateHandlerStub.update.yields(); - - controller.init({ - emailAddress: emailAddress - }, function(err, keypair) { - expect(err).to.not.exist; - expect(keypair).to.exist; - expect(updateHandlerStub.update.calledOnce).to.be.true; - expect(emailDaoStub.init.calledOnce).to.be.true; - expect(devicestorageStub.init.calledOnce).to.be.true; - done(); - }); + controller.init({ + emailAddress: emailAddress + }, function(err, keypair) { + expect(err).to.not.exist; + expect(keypair).to.exist; + expect(updateHandlerStub.update.calledOnce).to.be.true; + expect(emailDaoStub.init.calledOnce).to.be.true; + expect(devicestorageStub.init.calledOnce).to.be.true; + done(); }); }); }); diff --git a/test/unit/auth-test.js b/test/unit/auth-test.js index 5df3c25..c3bea37 100644 --- a/test/unit/auth-test.js +++ b/test/unit/auth-test.js @@ -1,377 +1,374 @@ -define(function(require) { - 'use strict'; +'use strict'; - var Auth = require('js/bo/auth'), - OAuth = require('js/util/oauth'), - PGP = require('js/crypto/pgp'), - DeviceStorageDAO = require('js/dao/devicestorage-dao'), - expect = chai.expect; +var Auth = require('../../src/js/bo/auth'), + OAuth = require('../../src/js/util/oauth'), + PGP = require('../../src/js/crypto/pgp'), + DeviceStorageDAO = require('../../src/js/dao/devicestorage-dao'); - describe('Auth unit tests', function() { - // Constancts - var EMAIL_ADDR_DB_KEY = 'emailaddress'; - var USERNAME_DB_KEY = 'username'; - var REALNAME_DB_KEY = 'realname'; - var PASSWD_DB_KEY = 'password'; - var PROVIDER_DB_KEY = 'provider'; - var IMAP_DB_KEY = 'imap'; - var SMTP_DB_KEY = 'smtp'; - // SUT - var auth; +describe('Auth unit tests', function() { + // Constancts + var EMAIL_ADDR_DB_KEY = 'emailaddress'; + var USERNAME_DB_KEY = 'username'; + var REALNAME_DB_KEY = 'realname'; + var PASSWD_DB_KEY = 'password'; + var PROVIDER_DB_KEY = 'provider'; + var IMAP_DB_KEY = 'imap'; + var SMTP_DB_KEY = 'smtp'; + // SUT + var auth; - // Dependencies - var storageStub, oauthStub, pgpStub; + // Dependencies + var storageStub, oauthStub, pgpStub; - // test data - var emailAddress = 'bla@blubb.com'; - var password = 'passwordpasswordpassword'; - var encryptedPassword = 'pgppasswordpgppassword'; - var oauthToken = 'tokentokentokentoken'; - var provider = 'gmail'; - var realname = 'Bla Blubb'; - var username = 'bla'; - var imap = { - host: 'mail.blablubb.com', - port: 123, - secure: true, - ca: 'PEMPEMPEMPEMPEMPEMPEMPEMPEMPEM' - }; - var smtp = { - host: 'mail.blablubb.com', - port: 456, - secure: true, - ca: 'PEMPEMPEMPEMPEMPEMPEMPEMPEMPEM' - }; + // test data + var emailAddress = 'bla@blubb.com'; + var password = 'passwordpasswordpassword'; + var encryptedPassword = 'pgppasswordpgppassword'; + var oauthToken = 'tokentokentokentoken'; + var provider = 'gmail'; + var realname = 'Bla Blubb'; + var username = 'bla'; + var imap = { + host: 'mail.blablubb.com', + port: 123, + secure: true, + ca: 'PEMPEMPEMPEMPEMPEMPEMPEMPEMPEM' + }; + var smtp = { + host: 'mail.blablubb.com', + port: 456, + secure: true, + ca: 'PEMPEMPEMPEMPEMPEMPEMPEMPEMPEM' + }; - beforeEach(function() { - storageStub = sinon.createStubInstance(DeviceStorageDAO); - oauthStub = sinon.createStubInstance(OAuth); - pgpStub = sinon.createStubInstance(PGP); - auth = new Auth(storageStub, oauthStub, pgpStub); + beforeEach(function() { + storageStub = sinon.createStubInstance(DeviceStorageDAO); + oauthStub = sinon.createStubInstance(OAuth); + pgpStub = sinon.createStubInstance(PGP); + auth = new Auth(storageStub, oauthStub, pgpStub); + }); + + describe('#getCredentials', function() { + it('should load credentials and retrieve credentials from cfg', function(done) { + storageStub.listItems.withArgs(EMAIL_ADDR_DB_KEY, 0, null).yieldsAsync(null, [emailAddress]); + storageStub.listItems.withArgs(PASSWD_DB_KEY, 0, null).yieldsAsync(null, [encryptedPassword]); + storageStub.listItems.withArgs(PROVIDER_DB_KEY, 0, null).yieldsAsync(null, [provider]); + storageStub.listItems.withArgs(USERNAME_DB_KEY, 0, null).yieldsAsync(null, [username]); + storageStub.listItems.withArgs(REALNAME_DB_KEY, 0, null).yieldsAsync(null, [realname]); + storageStub.listItems.withArgs(IMAP_DB_KEY, 0, null).yieldsAsync(null, [imap]); + storageStub.listItems.withArgs(SMTP_DB_KEY, 0, null).yieldsAsync(null, [smtp]); + pgpStub.decrypt.withArgs(encryptedPassword, undefined).yields(null, password); + + auth.getCredentials(function(err, cred) { + expect(err).to.not.exist; + + expect(auth.provider).to.equal(provider); + expect(auth.emailAddress).to.equal(emailAddress); + expect(auth.password).to.equal(password); + + expect(cred.imap.host).to.equal(imap.host); + expect(cred.imap.port).to.equal(imap.port); + expect(cred.imap.secure).to.equal(imap.secure); + expect(cred.imap.ca).to.equal(imap.ca); + expect(cred.imap.auth.user).to.equal(username); + expect(cred.imap.auth.pass).to.equal(password); + + expect(cred.smtp.host).to.equal(smtp.host); + expect(cred.smtp.port).to.equal(smtp.port); + expect(cred.smtp.secure).to.equal(smtp.secure); + expect(cred.smtp.ca).to.equal(smtp.ca); + expect(cred.smtp.auth.user).to.equal(username); + expect(cred.smtp.auth.pass).to.equal(password); + + expect(storageStub.listItems.callCount).to.equal(7); + expect(pgpStub.decrypt.calledOnce).to.be.true; + + done(); + }); + }); + }); + + describe('#setCredentials', function() { + it('should set the credentials', function() { + auth.setCredentials({ + provider: 'albhsvadlbvsdalbsadflb', + emailAddress: emailAddress, + username: username, + realname: realname, + password: password, + imap: imap, + smtp: smtp + }); + + expect(auth.provider).to.equal('albhsvadlbvsdalbsadflb'); + expect(auth.emailAddress).to.equal(emailAddress); + expect(auth.username).to.equal(username); + expect(auth.realname).to.equal(realname); + expect(auth.password).to.equal(password); + expect(auth.smtp).to.equal(smtp); + expect(auth.imap).to.equal(imap); + expect(auth.credentialsDirty).to.be.true; }); - describe('#getCredentials', function() { - it('should load credentials and retrieve credentials from cfg', function(done) { - storageStub.listItems.withArgs(EMAIL_ADDR_DB_KEY, 0, null).yieldsAsync(null, [emailAddress]); - storageStub.listItems.withArgs(PASSWD_DB_KEY, 0, null).yieldsAsync(null, [encryptedPassword]); - storageStub.listItems.withArgs(PROVIDER_DB_KEY, 0, null).yieldsAsync(null, [provider]); - storageStub.listItems.withArgs(USERNAME_DB_KEY, 0, null).yieldsAsync(null, [username]); - storageStub.listItems.withArgs(REALNAME_DB_KEY, 0, null).yieldsAsync(null, [realname]); - storageStub.listItems.withArgs(IMAP_DB_KEY, 0, null).yieldsAsync(null, [imap]); - storageStub.listItems.withArgs(SMTP_DB_KEY, 0, null).yieldsAsync(null, [smtp]); - pgpStub.decrypt.withArgs(encryptedPassword, undefined).yields(null, password); + }); - auth.getCredentials(function(err, cred) { - expect(err).to.not.exist; + describe('#storeCredentials', function() { + it('should persist ALL the things!', function(done) { + auth.credentialsDirty = true; + auth.emailAddress = emailAddress; + auth.username = username; + auth.realname = realname; + auth.password = password; + auth.smtp = smtp; + auth.imap = imap; + auth.provider = provider; - expect(auth.provider).to.equal(provider); - expect(auth.emailAddress).to.equal(emailAddress); - expect(auth.password).to.equal(password); + storageStub.storeList.withArgs([encryptedPassword], PASSWD_DB_KEY).yieldsAsync(); + storageStub.storeList.withArgs([emailAddress], EMAIL_ADDR_DB_KEY).yieldsAsync(); + storageStub.storeList.withArgs([provider], PROVIDER_DB_KEY).yieldsAsync(); + storageStub.storeList.withArgs([username], USERNAME_DB_KEY).yieldsAsync(); + storageStub.storeList.withArgs([realname], REALNAME_DB_KEY).yieldsAsync(); + storageStub.storeList.withArgs([imap], IMAP_DB_KEY).yieldsAsync(); + storageStub.storeList.withArgs([smtp], SMTP_DB_KEY).yieldsAsync(); + pgpStub.encrypt.withArgs(password).yields(null, encryptedPassword); - expect(cred.imap.host).to.equal(imap.host); - expect(cred.imap.port).to.equal(imap.port); - expect(cred.imap.secure).to.equal(imap.secure); - expect(cred.imap.ca).to.equal(imap.ca); - expect(cred.imap.auth.user).to.equal(username); - expect(cred.imap.auth.pass).to.equal(password); + auth.storeCredentials(function(err) { + expect(err).to.not.exist; - expect(cred.smtp.host).to.equal(smtp.host); - expect(cred.smtp.port).to.equal(smtp.port); - expect(cred.smtp.secure).to.equal(smtp.secure); - expect(cred.smtp.ca).to.equal(smtp.ca); - expect(cred.smtp.auth.user).to.equal(username); - expect(cred.smtp.auth.pass).to.equal(password); + expect(storageStub.storeList.callCount).to.equal(7); + expect(pgpStub.encrypt.calledOnce).to.be.true; - expect(storageStub.listItems.callCount).to.equal(7); - expect(pgpStub.decrypt.calledOnce).to.be.true; + done(); + }); + }); + }); - done(); - }); + describe('#getOAuthToken', function() { + it('should refresh token with known email address', function(done) { + auth.emailAddress = emailAddress; + auth.oauthToken = 'oldToken'; + + oauthStub.refreshToken.withArgs({ + emailAddress: emailAddress, + oldToken: 'oldToken' + }).yieldsAsync(null, oauthToken); + + auth.getOAuthToken(function(err) { + expect(err).to.not.exist; + expect(auth.emailAddress).to.equal(emailAddress); + expect(auth.oauthToken).to.equal(oauthToken); + + expect(oauthStub.refreshToken.calledOnce).to.be.true; + + done(); }); }); - describe('#setCredentials', function() { - it('should set the credentials', function() { - auth.setCredentials({ - provider: 'albhsvadlbvsdalbsadflb', - emailAddress: emailAddress, - username: username, - realname: realname, - password: password, - imap: imap, - smtp: smtp - }); + it('should fetch token with known email address', function(done) { + auth.emailAddress = emailAddress; + oauthStub.getOAuthToken.withArgs(emailAddress).yieldsAsync(null, oauthToken); - expect(auth.provider).to.equal('albhsvadlbvsdalbsadflb'); + auth.getOAuthToken(function(err) { + expect(err).to.not.exist; expect(auth.emailAddress).to.equal(emailAddress); + expect(auth.oauthToken).to.equal(oauthToken); + + expect(oauthStub.getOAuthToken.calledOnce).to.be.true; + + done(); + }); + }); + + it('should fetch token with unknown email address', function(done) { + oauthStub.getOAuthToken.withArgs(undefined).yieldsAsync(null, oauthToken); + oauthStub.queryEmailAddress.withArgs(oauthToken).yieldsAsync(null, emailAddress); + + auth.getOAuthToken(function(err) { + expect(err).to.not.exist; + expect(auth.emailAddress).to.equal(emailAddress); + expect(auth.oauthToken).to.equal(oauthToken); + + expect(oauthStub.getOAuthToken.calledOnce).to.be.true; + expect(oauthStub.queryEmailAddress.calledOnce).to.be.true; + + done(); + }); + }); + + it('should fail when email address fetch fails', function(done) { + oauthStub.getOAuthToken.yieldsAsync(null, oauthToken); + oauthStub.queryEmailAddress.yieldsAsync(new Error()); + + auth.getOAuthToken(function(err) { + expect(err).to.exist; + expect(auth.emailAddress).to.not.exist; + expect(auth.oauthToken).to.not.exist; + + expect(oauthStub.getOAuthToken.calledOnce).to.be.true; + expect(oauthStub.queryEmailAddress.calledOnce).to.be.true; + + done(); + }); + }); + + it('should fail when oauth fetch fails', function(done) { + oauthStub.getOAuthToken.yieldsAsync(new Error()); + + auth.getOAuthToken(function(err) { + expect(err).to.exist; + expect(auth.emailAddress).to.not.exist; + expect(auth.oauthToken).to.not.exist; + + expect(oauthStub.getOAuthToken.calledOnce).to.be.true; + expect(oauthStub.queryEmailAddress.called).to.be.false; + + done(); + }); + }); + }); + + describe('#_loadCredentials', function() { + it('should work', function(done) { + storageStub.listItems.withArgs(EMAIL_ADDR_DB_KEY, 0, null).yieldsAsync(null, [emailAddress]); + storageStub.listItems.withArgs(PASSWD_DB_KEY, 0, null).yieldsAsync(null, [encryptedPassword]); + storageStub.listItems.withArgs(PROVIDER_DB_KEY, 0, null).yieldsAsync(null, [provider]); + storageStub.listItems.withArgs(USERNAME_DB_KEY, 0, null).yieldsAsync(null, [username]); + storageStub.listItems.withArgs(REALNAME_DB_KEY, 0, null).yieldsAsync(null, [realname]); + storageStub.listItems.withArgs(IMAP_DB_KEY, 0, null).yieldsAsync(null, [imap]); + storageStub.listItems.withArgs(SMTP_DB_KEY, 0, null).yieldsAsync(null, [smtp]); + + auth._loadCredentials(function(err) { + expect(err).to.not.exist; + expect(auth.emailAddress).to.equal(emailAddress); + expect(auth.password).to.equal(encryptedPassword); + expect(auth.provider).to.equal(provider); + expect(auth.imap).to.equal(imap); + expect(auth.smtp).to.equal(smtp); expect(auth.username).to.equal(username); expect(auth.realname).to.equal(realname); - expect(auth.password).to.equal(password); - expect(auth.smtp).to.equal(smtp); - expect(auth.imap).to.equal(imap); - expect(auth.credentialsDirty).to.be.true; - }); - }); + expect(auth.passwordNeedsDecryption).to.be.true; - describe('#storeCredentials', function() { - it('should persist ALL the things!', function(done) { - auth.credentialsDirty = true; - auth.emailAddress = emailAddress; - auth.username = username; - auth.realname = realname; - auth.password = password; - auth.smtp = smtp; - auth.imap = imap; - auth.provider = provider; + expect(storageStub.listItems.callCount).to.equal(7); - storageStub.storeList.withArgs([encryptedPassword], PASSWD_DB_KEY).yieldsAsync(); - storageStub.storeList.withArgs([emailAddress], EMAIL_ADDR_DB_KEY).yieldsAsync(); - storageStub.storeList.withArgs([provider], PROVIDER_DB_KEY).yieldsAsync(); - storageStub.storeList.withArgs([username], USERNAME_DB_KEY).yieldsAsync(); - storageStub.storeList.withArgs([realname], REALNAME_DB_KEY).yieldsAsync(); - storageStub.storeList.withArgs([imap], IMAP_DB_KEY).yieldsAsync(); - storageStub.storeList.withArgs([smtp], SMTP_DB_KEY).yieldsAsync(); - pgpStub.encrypt.withArgs(password).yields(null, encryptedPassword); - - auth.storeCredentials(function(err) { - expect(err).to.not.exist; - - expect(storageStub.storeList.callCount).to.equal(7); - expect(pgpStub.encrypt.calledOnce).to.be.true; - - done(); - }); + done(); }); }); - describe('#getOAuthToken', function() { - it('should refresh token with known email address', function(done) { - auth.emailAddress = emailAddress; - auth.oauthToken = 'oldToken'; + it('should fail', function(done) { + storageStub.listItems.yieldsAsync(new Error()); - oauthStub.refreshToken.withArgs({ - emailAddress: emailAddress, - oldToken: 'oldToken' - }).yieldsAsync(null, oauthToken); + auth._loadCredentials(function(err) { + expect(err).to.exist; + expect(auth.emailAddress).to.not.exist; + expect(auth.password).to.not.exist; + expect(auth.provider).to.not.exist; + expect(auth.imap).to.not.exist; + expect(auth.smtp).to.not.exist; + expect(auth.username).to.not.exist; + expect(auth.realname).to.not.exist; - auth.getOAuthToken(function(err) { - expect(err).to.not.exist; - expect(auth.emailAddress).to.equal(emailAddress); - expect(auth.oauthToken).to.equal(oauthToken); + expect(storageStub.listItems.calledOnce).to.be.true; - expect(oauthStub.refreshToken.calledOnce).to.be.true; - - done(); - }); - }); - - it('should fetch token with known email address', function(done) { - auth.emailAddress = emailAddress; - oauthStub.getOAuthToken.withArgs(emailAddress).yieldsAsync(null, oauthToken); - - auth.getOAuthToken(function(err) { - expect(err).to.not.exist; - expect(auth.emailAddress).to.equal(emailAddress); - expect(auth.oauthToken).to.equal(oauthToken); - - expect(oauthStub.getOAuthToken.calledOnce).to.be.true; - - done(); - }); - }); - - it('should fetch token with unknown email address', function(done) { - oauthStub.getOAuthToken.withArgs(undefined).yieldsAsync(null, oauthToken); - oauthStub.queryEmailAddress.withArgs(oauthToken).yieldsAsync(null, emailAddress); - - auth.getOAuthToken(function(err) { - expect(err).to.not.exist; - expect(auth.emailAddress).to.equal(emailAddress); - expect(auth.oauthToken).to.equal(oauthToken); - - expect(oauthStub.getOAuthToken.calledOnce).to.be.true; - expect(oauthStub.queryEmailAddress.calledOnce).to.be.true; - - done(); - }); - }); - - it('should fail when email address fetch fails', function(done) { - oauthStub.getOAuthToken.yieldsAsync(null, oauthToken); - oauthStub.queryEmailAddress.yieldsAsync(new Error()); - - auth.getOAuthToken(function(err) { - expect(err).to.exist; - expect(auth.emailAddress).to.not.exist; - expect(auth.oauthToken).to.not.exist; - - expect(oauthStub.getOAuthToken.calledOnce).to.be.true; - expect(oauthStub.queryEmailAddress.calledOnce).to.be.true; - - done(); - }); - }); - - it('should fail when oauth fetch fails', function(done) { - oauthStub.getOAuthToken.yieldsAsync(new Error()); - - auth.getOAuthToken(function(err) { - expect(err).to.exist; - expect(auth.emailAddress).to.not.exist; - expect(auth.oauthToken).to.not.exist; - - expect(oauthStub.getOAuthToken.calledOnce).to.be.true; - expect(oauthStub.queryEmailAddress.called).to.be.false; - - done(); - }); + done(); }); }); + }); - describe('#_loadCredentials', function() { - it('should work', function(done) { - storageStub.listItems.withArgs(EMAIL_ADDR_DB_KEY, 0, null).yieldsAsync(null, [emailAddress]); - storageStub.listItems.withArgs(PASSWD_DB_KEY, 0, null).yieldsAsync(null, [encryptedPassword]); - storageStub.listItems.withArgs(PROVIDER_DB_KEY, 0, null).yieldsAsync(null, [provider]); - storageStub.listItems.withArgs(USERNAME_DB_KEY, 0, null).yieldsAsync(null, [username]); - storageStub.listItems.withArgs(REALNAME_DB_KEY, 0, null).yieldsAsync(null, [realname]); - storageStub.listItems.withArgs(IMAP_DB_KEY, 0, null).yieldsAsync(null, [imap]); - storageStub.listItems.withArgs(SMTP_DB_KEY, 0, null).yieldsAsync(null, [smtp]); + describe('#handleCertificateUpdate', function() { + var storeCredentialsStub; + var dummyCert = 'cert'; - auth._loadCredentials(function(err) { - expect(err).to.not.exist; - expect(auth.emailAddress).to.equal(emailAddress); - expect(auth.password).to.equal(encryptedPassword); - expect(auth.provider).to.equal(provider); - expect(auth.imap).to.equal(imap); - expect(auth.smtp).to.equal(smtp); - expect(auth.username).to.equal(username); - expect(auth.realname).to.equal(realname); + function onConnectDummy() {} - expect(auth.passwordNeedsDecryption).to.be.true; - - expect(storageStub.listItems.callCount).to.equal(7); - - done(); - }); - }); - - it('should fail', function(done) { - storageStub.listItems.yieldsAsync(new Error()); - - auth._loadCredentials(function(err) { - expect(err).to.exist; - expect(auth.emailAddress).to.not.exist; - expect(auth.password).to.not.exist; - expect(auth.provider).to.not.exist; - expect(auth.imap).to.not.exist; - expect(auth.smtp).to.not.exist; - expect(auth.username).to.not.exist; - expect(auth.realname).to.not.exist; - - expect(storageStub.listItems.calledOnce).to.be.true; - - done(); - }); - }); + beforeEach(function() { + storeCredentialsStub = sinon.stub(auth, 'storeCredentials'); }); - describe('#handleCertificateUpdate', function() { - var storeCredentialsStub; - var dummyCert = 'cert'; + it('should work for Trust on first use', function(done) { + auth.imap = {}; + storeCredentialsStub.yields(); - function onConnectDummy() {} + function callback(err) { + expect(err).to.not.exist; + expect(storeCredentialsStub.callCount).to.equal(1); + done(); + } + auth.handleCertificateUpdate('imap', onConnectDummy, callback, dummyCert); + }); - beforeEach(function() { - storeCredentialsStub = sinon.stub(auth, 'storeCredentials'); - }); + it('should work for stored cert', function() { + auth.imap = { + ca: dummyCert + }; + storeCredentialsStub.yields(); - it('should work for Trust on first use', function(done) { - auth.imap = {}; - storeCredentialsStub.yields(); + auth.handleCertificateUpdate('imap', onConnectDummy, onConnectDummy, dummyCert); + expect(storeCredentialsStub.callCount).to.equal(0); + }); - function callback(err) { - expect(err).to.not.exist; - expect(storeCredentialsStub.callCount).to.equal(1); - done(); - } - auth.handleCertificateUpdate('imap', onConnectDummy, callback, dummyCert); - }); + it('should work for pinned cert', function(done) { + auth.imap = { + ca: 'other', + pinned: true + }; + storeCredentialsStub.yields(); - it('should work for stored cert', function() { - auth.imap = { - ca: dummyCert - }; - storeCredentialsStub.yields(); - - auth.handleCertificateUpdate('imap', onConnectDummy, onConnectDummy, dummyCert); + function callback(err) { + expect(err).to.exist; + expect(err.message).to.exist; expect(storeCredentialsStub.callCount).to.equal(0); - }); + done(); + } + auth.handleCertificateUpdate('imap', onConnectDummy, callback, dummyCert); + }); - it('should work for pinned cert', function(done) { - auth.imap = { - ca: 'other', - pinned: true - }; - storeCredentialsStub.yields(); + it('should work for updated cert', function(done) { + auth.imap = { + ca: 'other' + }; + storeCredentialsStub.yields(); - function callback(err) { + function callback(err) { + if (err && err.callback) { expect(err).to.exist; expect(err.message).to.exist; expect(storeCredentialsStub.callCount).to.equal(0); + err.callback(true); + } else { + expect(storeCredentialsStub.callCount).to.equal(1); done(); } - auth.handleCertificateUpdate('imap', onConnectDummy, callback, dummyCert); - }); + } - it('should work for updated cert', function(done) { - auth.imap = { - ca: 'other' - }; - storeCredentialsStub.yields(); + function onConnect(callback) { + callback(); + } - function callback(err) { - if (err && err.callback) { - expect(err).to.exist; - expect(err.message).to.exist; - expect(storeCredentialsStub.callCount).to.equal(0); - err.callback(true); - } else { - expect(storeCredentialsStub.callCount).to.equal(1); - done(); - } - } + auth.handleCertificateUpdate('imap', onConnect, callback, dummyCert); + }); + }); - function onConnect(callback) { - callback(); - } + describe('#logout', function() { + it('should fail to to error in calling db clear', function(done) { + storageStub.clear.yields(new Error()); - auth.handleCertificateUpdate('imap', onConnect, callback, dummyCert); + auth.logout(function(err) { + expect(err).to.exist; + done(); }); }); - describe('#logout', function() { - it('should fail to to error in calling db clear', function(done) { - storageStub.clear.yields(new Error()); + it('should work', function(done) { + storageStub.clear.yields(); - auth.logout(function(err) { - expect(err).to.exist; - done(); - }); - }); - - it('should work', function(done) { - storageStub.clear.yields(); - - auth.logout(function(err) { - expect(err).to.not.exist; - expect(auth.password).to.be.undefined; - expect(auth.initialized).to.be.undefined; - expect(auth.credentialsDirty).to.be.undefined; - expect(auth.passwordNeedsDecryption).to.be.undefined; - done(); - }); + auth.logout(function(err) { + expect(err).to.not.exist; + expect(auth.password).to.be.undefined; + expect(auth.initialized).to.be.undefined; + expect(auth.credentialsDirty).to.be.undefined; + expect(auth.passwordNeedsDecryption).to.be.undefined; + done(); }); }); }); diff --git a/test/unit/backbutton-handler-test.js b/test/unit/backbutton-handler-test.js index 3e312b3..66fc05b 100644 --- a/test/unit/backbutton-handler-test.js +++ b/test/unit/backbutton-handler-test.js @@ -1,67 +1,64 @@ -define(function(require) { - 'use strict'; +'use strict'; - var btnHandler = require('js/util/backbutton-handler'), - expect = chai.expect; +var btnHandler = require('../../src/js/util/backbutton-handler'); - describe('Backbutton Handler', function() { - chai.Assertion.includeStack = true; +describe('Backbutton Handler', function() { + chai.Assertion.includeStack = true; - var scope, event; + var scope, event; - beforeEach(function() { - scope = { - state: {}, - $apply: function() {} - }; + beforeEach(function() { + scope = { + state: {}, + $apply: function() {} + }; - event = new CustomEvent('backbutton'); + event = new CustomEvent('backbutton'); - // this is a precondition for the test. throw an exception - // if this would produce side effects - expect(navigator.app).to.not.exist; - navigator.app = {}; + // this is a precondition for the test. throw an exception + // if this would produce side effects + expect(navigator.app).to.not.exist; + navigator.app = {}; - btnHandler.attachHandler(scope); - btnHandler.start(); - }); + btnHandler.attachHandler(scope); + btnHandler.start(); + }); - afterEach(function() { - btnHandler.stop(); - delete navigator.app; - }); + afterEach(function() { + btnHandler.stop(); + delete navigator.app; + }); - it('should close lightbox', function() { - scope.state.lightbox = 'asd'; - document.dispatchEvent(event); - expect(scope.state.lightbox).to.be.undefined; - }); + it('should close lightbox', function() { + scope.state.lightbox = 'asd'; + document.dispatchEvent(event); + expect(scope.state.lightbox).to.be.undefined; + }); - it('should close reader', function() { - scope.state.read = { - open: true, - toggle: function(state) { - scope.state.read.open = state; - } - }; - document.dispatchEvent(event); - expect(scope.state.read.open).to.be.false; - }); + it('should close reader', function() { + scope.state.read = { + open: true, + toggle: function(state) { + scope.state.read.open = state; + } + }; + document.dispatchEvent(event); + expect(scope.state.read.open).to.be.false; + }); - it('should close navigation', function() { - scope.state.nav = { - open: true, - toggle: function(state) { - scope.state.nav.open = state; - } - }; - document.dispatchEvent(event); - expect(scope.state.nav.open).to.be.false; - }); + it('should close navigation', function() { + scope.state.nav = { + open: true, + toggle: function(state) { + scope.state.nav.open = state; + } + }; + document.dispatchEvent(event); + expect(scope.state.nav.open).to.be.false; + }); - it('should close app', function(done) { - navigator.app.exitApp = done; - document.dispatchEvent(event); - }); + it('should close app', function(done) { + navigator.app.exitApp = done; + document.dispatchEvent(event); }); }); \ No newline at end of file diff --git a/test/unit/connection-doctor-test.js b/test/unit/connection-doctor-test.js index 1ffeb08..7a50499 100644 --- a/test/unit/connection-doctor-test.js +++ b/test/unit/connection-doctor-test.js @@ -1,413 +1,410 @@ -define(function(require) { - 'use strict'; +'use strict'; - var ConnectionDoctor = require('js/util/connection-doctor'), - TCPSocket = require('tcp-socket'), - ImapClient = require('imap-client'), - SmtpClient = require('smtpclient'), - cfg = require('js/app-config').config, - expect = chai.expect; +var TCPSocket = require('tcp-socket'), + ImapClient = require('imap-client'), + SmtpClient = require('wo-smtpclient'), + ConnectionDoctor = require('../../src/js/util/connection-doctor'), + cfg = require('../../src/js/app-config').config; - describe('Connection Doctor', function() { - var doctor; - var socketStub, imapStub, smtpStub, credentials; +describe('Connection Doctor', function() { + var doctor; + var socketStub, imapStub, smtpStub, credentials; - beforeEach(function() { - // - // Stubs - // + beforeEach(function() { + // + // Stubs + // - // there is no socket shim for for this use case, use dummy object - socketStub = { - close: function() { - this.onclose(); + // there is no socket shim for for this use case, use dummy object + socketStub = { + close: function() { + this.onclose(); + } + }; + + imapStub = sinon.createStubInstance(ImapClient); + smtpStub = sinon.createStubInstance(SmtpClient); + + // + // Fixture + // + credentials = { + imap: { + host: 'asd', + port: 1234, + secure: true, + ca: 'cert' + }, + smtp: { + host: 'qwe', + port: 5678, + secure: false, + ca: 'cert' + }, + username: 'username', + password: 'password' + }; + + sinon.stub(TCPSocket, 'open').returns(socketStub); // convenience constructors suck + + // + // Setup SUT + // + doctor = new ConnectionDoctor(); + doctor.configure(credentials); + doctor._imap = imapStub; + doctor._smtp = smtpStub; + }); + + afterEach(function() { + TCPSocket.open.restore(); + }); + + describe('#_checkOnline', function() { + it('should check if browser is online', function(done) { + doctor._checkOnline(function(error) { + if (navigator.onLine) { + expect(error).to.not.exist; + } else { + expect(error).to.exist; + expect(error.code).to.equal(ConnectionDoctor.OFFLINE); } - }; + done(); + }); + }); + }); - imapStub = sinon.createStubInstance(ImapClient); - smtpStub = sinon.createStubInstance(SmtpClient); + describe('#_checkReachable', function() { + it('should be able to reach the host w/o cert', function(done) { + credentials.imap.ca = undefined; - // - // Fixture - // - credentials = { - imap: { - host: 'asd', - port: 1234, - secure: true, - ca: 'cert' - }, - smtp: { - host: 'qwe', - port: 5678, - secure: false, - ca: 'cert' - }, - username: 'username', - password: 'password' - }; + doctor._checkReachable(credentials.imap, function(error) { + expect(error).to.not.exist; + expect(TCPSocket.open.calledOnce).to.be.true; + expect(TCPSocket.open.calledWith(credentials.imap.host, credentials.imap.port, { + binaryType: 'arraybuffer', + useSecureTransport: credentials.imap.secure, + ca: credentials.imap.ca + })).to.be.true; - sinon.stub(TCPSocket, 'open').returns(socketStub); // convenience constructors suck + done(); + }); - // - // Setup SUT - // - doctor = new ConnectionDoctor(); - doctor.configure(credentials); - doctor._imap = imapStub; - doctor._smtp = smtpStub; + socketStub.oncert(); + socketStub.onopen(); }); - afterEach(function() { - TCPSocket.open.restore(); + it('should catch Mozilla TCPSocket exception', function(done) { + // Mozilla forbids extensions to the TCPSocket object + Object.defineProperty(socketStub, 'oncert', { + set: function() { + throw 'Mozilla specific behavior'; + } + }); + + doctor._checkReachable(credentials.imap, function(error) { + expect(error).to.not.exist; + expect(TCPSocket.open.calledOnce).to.be.true; + expect(TCPSocket.open.calledWith(credentials.imap.host, credentials.imap.port, { + binaryType: 'arraybuffer', + useSecureTransport: credentials.imap.secure, + ca: credentials.imap.ca + })).to.be.true; + + done(); + }); + + socketStub.onopen(); }); - describe('#_checkOnline', function() { - it('should check if browser is online', function(done) { - doctor._checkOnline(function(error) { - if (navigator.onLine) { - expect(error).to.not.exist; - } else { - expect(error).to.exist; - expect(error.code).to.equal(ConnectionDoctor.OFFLINE); - } - done(); - }); + it('should fail w/ wrong cert', function(done) { + doctor._checkReachable(credentials.imap, function(error) { + expect(error).to.exist; + expect(error.code).to.equal(ConnectionDoctor.TLS_WRONG_CERT); + expect(TCPSocket.open.calledOnce).to.be.true; + expect(TCPSocket.open.calledWith(credentials.imap.host, credentials.imap.port, { + binaryType: 'arraybuffer', + useSecureTransport: credentials.imap.secure, + ca: credentials.imap.ca + })).to.be.true; + + done(); + }); + + socketStub.oncert(); + socketStub.onerror(); + socketStub.onclose(); + }); + + it('should fail w/ host unreachable', function(done) { + doctor._checkReachable(credentials.imap, function(error) { + expect(error).to.exist; + expect(error.code).to.equal(ConnectionDoctor.HOST_UNREACHABLE); + expect(TCPSocket.open.calledOnce).to.be.true; + + done(); + }); + + socketStub.onerror({ + data: new Error() + }); + socketStub.onclose(); + }); + + it('should fail w/ timeout', function(done) { + var origTimeout = cfg.connDocTimeout; // remember timeout from the config to reset it on done + cfg.connDocTimeout = 20; // set to 20ms for the test + + doctor._checkReachable(credentials.imap, function(error) { + expect(error).to.exist; + expect(error.code).to.equal(ConnectionDoctor.HOST_TIMEOUT); + expect(TCPSocket.open.calledOnce).to.be.true; + cfg.connDocTimeout = origTimeout; + + done(); + }); + }); + }); + + describe('#_checkImap', function() { + it('should perform IMAP login, list folders, logout', function(done) { + imapStub.login.yieldsAsync(); + imapStub.listWellKnownFolders.yieldsAsync(null, { + Inbox: [{}] + }); + imapStub.logout.yieldsAsync(); + + doctor._checkImap(function(error) { + expect(error).to.not.exist; + expect(imapStub.login.calledOnce).to.be.true; + expect(imapStub.listWellKnownFolders.calledOnce).to.be.true; + expect(imapStub.logout.calledOnce).to.be.true; + + done(); }); }); - describe('#_checkReachable', function() { - it('should be able to reach the host w/o cert', function(done) { - credentials.imap.ca = undefined; - - doctor._checkReachable(credentials.imap, function(error) { - expect(error).to.not.exist; - expect(TCPSocket.open.calledOnce).to.be.true; - expect(TCPSocket.open.calledWith(credentials.imap.host, credentials.imap.port, { - binaryType: 'arraybuffer', - useSecureTransport: credentials.imap.secure, - ca: credentials.imap.ca - })).to.be.true; - - done(); - }); - - socketStub.oncert(); - socketStub.onopen(); + it('should fail w/ generic error on logout', function(done) { + imapStub.login.yieldsAsync(); + imapStub.listWellKnownFolders.yieldsAsync(null, { + Inbox: [{}] }); - it('should catch Mozilla TCPSocket exception', function(done) { - // Mozilla forbids extensions to the TCPSocket object - Object.defineProperty(socketStub, 'oncert', { - set: function() { - throw 'Mozilla specific behavior'; - } - }); + doctor._checkImap(function(error) { + expect(error).to.exist; + expect(error.code).to.equal(ConnectionDoctor.GENERIC_ERROR); + expect(error.underlyingError).to.exist; + expect(imapStub.login.calledOnce).to.be.true; + expect(imapStub.listWellKnownFolders.calledOnce).to.be.true; + expect(imapStub.logout.calledOnce).to.be.true; - doctor._checkReachable(credentials.imap, function(error) { - expect(error).to.not.exist; - expect(TCPSocket.open.calledOnce).to.be.true; - expect(TCPSocket.open.calledWith(credentials.imap.host, credentials.imap.port, { - binaryType: 'arraybuffer', - useSecureTransport: credentials.imap.secure, - ca: credentials.imap.ca - })).to.be.true; - - done(); - }); - - socketStub.onopen(); + done(); }); - it('should fail w/ wrong cert', function(done) { - doctor._checkReachable(credentials.imap, function(error) { - expect(error).to.exist; - expect(error.code).to.equal(ConnectionDoctor.TLS_WRONG_CERT); - expect(TCPSocket.open.calledOnce).to.be.true; - expect(TCPSocket.open.calledWith(credentials.imap.host, credentials.imap.port, { - binaryType: 'arraybuffer', - useSecureTransport: credentials.imap.secure, - ca: credentials.imap.ca - })).to.be.true; + setTimeout(function() { + // this error is thrown while we're waiting for the logout + imapStub.onError(new Error()); + }, 50); + }); - done(); - }); - - socketStub.oncert(); - socketStub.onerror(); - socketStub.onclose(); + it('should fail w/ generic error on inbox missing', function(done) { + imapStub.login.yieldsAsync(); + imapStub.listWellKnownFolders.yieldsAsync(null, { + Inbox: [] }); - it('should fail w/ host unreachable', function(done) { - doctor._checkReachable(credentials.imap, function(error) { - expect(error).to.exist; - expect(error.code).to.equal(ConnectionDoctor.HOST_UNREACHABLE); - expect(TCPSocket.open.calledOnce).to.be.true; + doctor._checkImap(function(error) { + expect(error).to.exist; + expect(error.code).to.equal(ConnectionDoctor.NO_INBOX); + expect(imapStub.login.calledOnce).to.be.true; + expect(imapStub.listWellKnownFolders.calledOnce).to.be.true; + expect(imapStub.logout.called).to.be.false; - done(); - }); - - socketStub.onerror({ - data: new Error() - }); - socketStub.onclose(); - }); - - it('should fail w/ timeout', function(done) { - var origTimeout = cfg.connDocTimeout; // remember timeout from the config to reset it on done - cfg.connDocTimeout = 20; // set to 20ms for the test - - doctor._checkReachable(credentials.imap, function(error) { - expect(error).to.exist; - expect(error.code).to.equal(ConnectionDoctor.HOST_TIMEOUT); - expect(TCPSocket.open.calledOnce).to.be.true; - cfg.connDocTimeout = origTimeout; - - done(); - }); + done(); }); }); - describe('#_checkImap', function() { - it('should perform IMAP login, list folders, logout', function(done) { - imapStub.login.yieldsAsync(); - imapStub.listWellKnownFolders.yieldsAsync(null, { - Inbox: [{}] - }); - imapStub.logout.yieldsAsync(); + it('should fail w/ generic error on listing folders fails', function(done) { + imapStub.login.yieldsAsync(); + imapStub.listWellKnownFolders.yieldsAsync(new Error()); - doctor._checkImap(function(error) { - expect(error).to.not.exist; - expect(imapStub.login.calledOnce).to.be.true; - expect(imapStub.listWellKnownFolders.calledOnce).to.be.true; - expect(imapStub.logout.calledOnce).to.be.true; + doctor._checkImap(function(error) { + expect(error).to.exist; + expect(error.code).to.equal(ConnectionDoctor.GENERIC_ERROR); + expect(error.underlyingError).to.exist; + expect(imapStub.login.calledOnce).to.be.true; + expect(imapStub.listWellKnownFolders.calledOnce).to.be.true; + expect(imapStub.logout.called).to.be.false; - done(); - }); - }); - - it('should fail w/ generic error on logout', function(done) { - imapStub.login.yieldsAsync(); - imapStub.listWellKnownFolders.yieldsAsync(null, { - Inbox: [{}] - }); - - doctor._checkImap(function(error) { - expect(error).to.exist; - expect(error.code).to.equal(ConnectionDoctor.GENERIC_ERROR); - expect(error.underlyingError).to.exist; - expect(imapStub.login.calledOnce).to.be.true; - expect(imapStub.listWellKnownFolders.calledOnce).to.be.true; - expect(imapStub.logout.calledOnce).to.be.true; - - done(); - }); - - setTimeout(function() { - // this error is thrown while we're waiting for the logout - imapStub.onError(new Error()); - }, 50); - }); - - it('should fail w/ generic error on inbox missing', function(done) { - imapStub.login.yieldsAsync(); - imapStub.listWellKnownFolders.yieldsAsync(null, { - Inbox: [] - }); - - doctor._checkImap(function(error) { - expect(error).to.exist; - expect(error.code).to.equal(ConnectionDoctor.NO_INBOX); - expect(imapStub.login.calledOnce).to.be.true; - expect(imapStub.listWellKnownFolders.calledOnce).to.be.true; - expect(imapStub.logout.called).to.be.false; - - done(); - }); - }); - - it('should fail w/ generic error on listing folders fails', function(done) { - imapStub.login.yieldsAsync(); - imapStub.listWellKnownFolders.yieldsAsync(new Error()); - - doctor._checkImap(function(error) { - expect(error).to.exist; - expect(error.code).to.equal(ConnectionDoctor.GENERIC_ERROR); - expect(error.underlyingError).to.exist; - expect(imapStub.login.calledOnce).to.be.true; - expect(imapStub.listWellKnownFolders.calledOnce).to.be.true; - expect(imapStub.logout.called).to.be.false; - - done(); - }); - }); - - it('should fail w/ auth rejected', function(done) { - doctor._checkImap(function(error) { - expect(error).to.exist; - expect(error.code).to.equal(ConnectionDoctor.AUTH_REJECTED); - expect(error.underlyingError).to.exist; - expect(imapStub.login.calledOnce).to.be.true; - expect(imapStub.listWellKnownFolders.called).to.be.false; - expect(imapStub.logout.called).to.be.false; - - done(); - }); - - setTimeout(function() { - // this error is thrown while we're waiting for the login - imapStub.onError(new Error()); - }, 50); + done(); }); }); - describe('#_checkSmtp', function() { - it('should perform SMTP login, logout', function(done) { - doctor._checkSmtp(function(error) { - expect(error).to.not.exist; - expect(smtpStub.connect.calledOnce).to.be.true; - expect(smtpStub.quit.calledOnce).to.be.true; + it('should fail w/ auth rejected', function(done) { + doctor._checkImap(function(error) { + expect(error).to.exist; + expect(error.code).to.equal(ConnectionDoctor.AUTH_REJECTED); + expect(error.underlyingError).to.exist; + expect(imapStub.login.calledOnce).to.be.true; + expect(imapStub.listWellKnownFolders.called).to.be.false; + expect(imapStub.logout.called).to.be.false; - done(); - }); - - smtpStub.onidle(); - smtpStub.onclose(); + done(); }); - it('should fail w/ auth rejected', function(done) { - doctor._checkSmtp(function(error) { - expect(error).to.exist; - expect(error.code).to.equal(ConnectionDoctor.AUTH_REJECTED); - expect(error.underlyingError).to.exist; - expect(smtpStub.connect.calledOnce).to.be.true; - expect(smtpStub.quit.called).to.be.false; + setTimeout(function() { + // this error is thrown while we're waiting for the login + imapStub.onError(new Error()); + }, 50); + }); + }); - done(); - }); + describe('#_checkSmtp', function() { + it('should perform SMTP login, logout', function(done) { + doctor._checkSmtp(function(error) { + expect(error).to.not.exist; + expect(smtpStub.connect.calledOnce).to.be.true; + expect(smtpStub.quit.calledOnce).to.be.true; - smtpStub.onerror(new Error()); + done(); + }); + + smtpStub.onidle(); + smtpStub.onclose(); + }); + + it('should fail w/ auth rejected', function(done) { + doctor._checkSmtp(function(error) { + expect(error).to.exist; + expect(error.code).to.equal(ConnectionDoctor.AUTH_REJECTED); + expect(error.underlyingError).to.exist; + expect(smtpStub.connect.calledOnce).to.be.true; + expect(smtpStub.quit.called).to.be.false; + + done(); + }); + + smtpStub.onerror(new Error()); + }); + }); + + describe('#check', function() { + beforeEach(function() { + sinon.stub(doctor, '_checkOnline'); + sinon.stub(doctor, '_checkReachable'); + sinon.stub(doctor, '_checkImap'); + sinon.stub(doctor, '_checkSmtp'); + }); + + it('should perform all tests', function(done) { + doctor._checkOnline.yieldsAsync(); + doctor._checkReachable.withArgs(credentials.imap).yieldsAsync(); + doctor._checkReachable.withArgs(credentials.smtp).yieldsAsync(); + doctor._checkImap.yieldsAsync(); + doctor._checkSmtp.yieldsAsync(); + + doctor.check(function(err) { + expect(err).to.not.exist; + expect(doctor._checkOnline.calledOnce).to.be.true; + expect(doctor._checkReachable.calledTwice).to.be.true; + expect(doctor._checkImap.calledOnce).to.be.true; + expect(doctor._checkSmtp.calledOnce).to.be.true; + + done(); }); }); - describe('#check', function() { - beforeEach(function() { - sinon.stub(doctor, '_checkOnline'); - sinon.stub(doctor, '_checkReachable'); - sinon.stub(doctor, '_checkImap'); - sinon.stub(doctor, '_checkSmtp'); + it('should fail for smtp', function(done) { + doctor._checkOnline.yieldsAsync(); + doctor._checkReachable.withArgs(credentials.imap).yieldsAsync(); + doctor._checkReachable.withArgs(credentials.smtp).yieldsAsync(); + doctor._checkImap.yieldsAsync(); + doctor._checkSmtp.yieldsAsync(new Error()); + + doctor.check(function(err) { + expect(err).to.exist; + expect(doctor._checkOnline.calledOnce).to.be.true; + expect(doctor._checkReachable.calledTwice).to.be.true; + expect(doctor._checkImap.calledOnce).to.be.true; + expect(doctor._checkSmtp.calledOnce).to.be.true; + + done(); }); + }); - it('should perform all tests', function(done) { - doctor._checkOnline.yieldsAsync(); - doctor._checkReachable.withArgs(credentials.imap).yieldsAsync(); - doctor._checkReachable.withArgs(credentials.smtp).yieldsAsync(); - doctor._checkImap.yieldsAsync(); - doctor._checkSmtp.yieldsAsync(); + it('should fail for imap', function(done) { + doctor._checkOnline.yieldsAsync(); + doctor._checkReachable.withArgs(credentials.imap).yieldsAsync(); + doctor._checkReachable.withArgs(credentials.smtp).yieldsAsync(); + doctor._checkImap.yieldsAsync(new Error()); - doctor.check(function(err) { - expect(err).to.not.exist; - expect(doctor._checkOnline.calledOnce).to.be.true; - expect(doctor._checkReachable.calledTwice).to.be.true; - expect(doctor._checkImap.calledOnce).to.be.true; - expect(doctor._checkSmtp.calledOnce).to.be.true; + doctor.check(function(err) { + expect(err).to.exist; + expect(doctor._checkOnline.calledOnce).to.be.true; + expect(doctor._checkReachable.calledTwice).to.be.true; + expect(doctor._checkImap.calledOnce).to.be.true; + expect(doctor._checkSmtp.called).to.be.false; - done(); - }); + done(); }); + }); - it('should fail for smtp', function(done) { - doctor._checkOnline.yieldsAsync(); - doctor._checkReachable.withArgs(credentials.imap).yieldsAsync(); - doctor._checkReachable.withArgs(credentials.smtp).yieldsAsync(); - doctor._checkImap.yieldsAsync(); - doctor._checkSmtp.yieldsAsync(new Error()); + it('should fail for smtp reachability', function(done) { + doctor._checkOnline.yieldsAsync(); + doctor._checkReachable.withArgs(credentials.imap).yieldsAsync(); + doctor._checkReachable.withArgs(credentials.smtp).yieldsAsync(new Error()); - doctor.check(function(err) { - expect(err).to.exist; - expect(doctor._checkOnline.calledOnce).to.be.true; - expect(doctor._checkReachable.calledTwice).to.be.true; - expect(doctor._checkImap.calledOnce).to.be.true; - expect(doctor._checkSmtp.calledOnce).to.be.true; + doctor.check(function(err) { + expect(err).to.exist; + expect(doctor._checkOnline.calledOnce).to.be.true; + expect(doctor._checkReachable.calledTwice).to.be.true; + expect(doctor._checkImap.called).to.be.false; + expect(doctor._checkSmtp.called).to.be.false; - done(); - }); + done(); }); + }); - it('should fail for imap', function(done) { - doctor._checkOnline.yieldsAsync(); - doctor._checkReachable.withArgs(credentials.imap).yieldsAsync(); - doctor._checkReachable.withArgs(credentials.smtp).yieldsAsync(); - doctor._checkImap.yieldsAsync(new Error()); + it('should fail for imap reachability', function(done) { + doctor._checkOnline.yieldsAsync(); + doctor._checkReachable.withArgs(credentials.imap).yieldsAsync(new Error()); - doctor.check(function(err) { - expect(err).to.exist; - expect(doctor._checkOnline.calledOnce).to.be.true; - expect(doctor._checkReachable.calledTwice).to.be.true; - expect(doctor._checkImap.calledOnce).to.be.true; - expect(doctor._checkSmtp.called).to.be.false; + doctor.check(function(err) { + expect(err).to.exist; + expect(doctor._checkOnline.calledOnce).to.be.true; + expect(doctor._checkReachable.calledOnce).to.be.true; + expect(doctor._checkImap.called).to.be.false; + expect(doctor._checkSmtp.called).to.be.false; - done(); - }); + done(); }); + }); - it('should fail for smtp reachability', function(done) { - doctor._checkOnline.yieldsAsync(); - doctor._checkReachable.withArgs(credentials.imap).yieldsAsync(); - doctor._checkReachable.withArgs(credentials.smtp).yieldsAsync(new Error()); + it('should fail for offline', function(done) { + doctor._checkOnline.yieldsAsync(new Error()); - doctor.check(function(err) { - expect(err).to.exist; - expect(doctor._checkOnline.calledOnce).to.be.true; - expect(doctor._checkReachable.calledTwice).to.be.true; - expect(doctor._checkImap.called).to.be.false; - expect(doctor._checkSmtp.called).to.be.false; + doctor.check(function(err) { + expect(err).to.exist; + expect(doctor._checkOnline.calledOnce).to.be.true; + expect(doctor._checkReachable.called).to.be.false; + expect(doctor._checkImap.called).to.be.false; + expect(doctor._checkSmtp.called).to.be.false; - done(); - }); + done(); }); + }); - it('should fail for imap reachability', function(done) { - doctor._checkOnline.yieldsAsync(); - doctor._checkReachable.withArgs(credentials.imap).yieldsAsync(new Error()); + it('should fail w/o config', function(done) { + doctor.credentials = doctor._imap = doctor._smtp = undefined; - doctor.check(function(err) { - expect(err).to.exist; - expect(doctor._checkOnline.calledOnce).to.be.true; - expect(doctor._checkReachable.calledOnce).to.be.true; - expect(doctor._checkImap.called).to.be.false; - expect(doctor._checkSmtp.called).to.be.false; + doctor.check(function(err) { + expect(err).to.exist; + expect(doctor._checkOnline.called).to.be.false; + expect(doctor._checkReachable.called).to.be.false; + expect(doctor._checkImap.called).to.be.false; + expect(doctor._checkSmtp.called).to.be.false; - done(); - }); - }); - - it('should fail for offline', function(done) { - doctor._checkOnline.yieldsAsync(new Error()); - - doctor.check(function(err) { - expect(err).to.exist; - expect(doctor._checkOnline.calledOnce).to.be.true; - expect(doctor._checkReachable.called).to.be.false; - expect(doctor._checkImap.called).to.be.false; - expect(doctor._checkSmtp.called).to.be.false; - - done(); - }); - }); - - it('should fail w/o config', function(done) { - doctor.credentials = doctor._imap = doctor._smtp = undefined; - - doctor.check(function(err) { - expect(err).to.exist; - expect(doctor._checkOnline.called).to.be.false; - expect(doctor._checkReachable.called).to.be.false; - expect(doctor._checkImap.called).to.be.false; - expect(doctor._checkSmtp.called).to.be.false; - - done(); - }); + done(); }); }); }); diff --git a/test/unit/contacts-ctrl-test.js b/test/unit/contacts-ctrl-test.js index 15085ef..05212c4 100644 --- a/test/unit/contacts-ctrl-test.js +++ b/test/unit/contacts-ctrl-test.js @@ -1,190 +1,186 @@ -define(function(require) { - 'use strict'; +'use strict'; - var expect = chai.expect, - angular = require('angular'), - mocks = require('angularMocks'), - ContactsCtrl = require('js/controller/contacts'), - appController = require('js/app-controller'), - KeychainDAO = require('js/dao/keychain-dao'), - PGP = require('js/crypto/pgp'); +var mocks = angular.mocks, + ContactsCtrl = require('../../src/js/controller/contacts'), + appController = require('../../src/js/app-controller'), + KeychainDAO = require('../../src/js/dao/keychain-dao'), + PGP = require('../../src/js/crypto/pgp'); - describe('Contacts Controller unit test', function() { - var scope, contactsCtrl, - origKeychain, keychainMock, - origPgp, pgpMock; +describe('Contacts Controller unit test', function() { + var scope, contactsCtrl, + origKeychain, keychainMock, + origPgp, pgpMock; - beforeEach(function() { - origPgp = appController._pgp; - appController._pgp = pgpMock = sinon.createStubInstance(PGP); - origKeychain = appController._keychain; - appController._keychain = keychainMock = sinon.createStubInstance(KeychainDAO); + beforeEach(function() { + origPgp = appController._pgp; + appController._pgp = pgpMock = sinon.createStubInstance(PGP); + origKeychain = appController._keychain; + appController._keychain = keychainMock = sinon.createStubInstance(KeychainDAO); - angular.module('contactstest', []); - mocks.module('contactstest'); - mocks.inject(function($rootScope, $controller) { - scope = $rootScope.$new(); - scope.state = {}; - contactsCtrl = $controller(ContactsCtrl, { - $scope: scope - }); - }); - }); - - afterEach(function() { - // restore the module - appController._pgp = origPgp; - appController._keychain = origKeychain; - }); - - describe('scope variables', function() { - it('should be set correctly', function() { - expect(scope.fingerprint).to.equal('XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX'); - expect(scope.state.contacts.toggle).to.exist; - }); - }); - - describe('listKeys', function() { - it('should fail due to error in keychain.listLocalPublicKeys', function(done) { - keychainMock.listLocalPublicKeys.yields(42); - - scope.onError = function(err) { - expect(err).to.equal(42); - done(); - }; - - scope.listKeys(); - }); - - it('should work', function(done) { - keychainMock.listLocalPublicKeys.yields(null, [{ - _id: '12345' - }]); - pgpMock.getKeyParams.returns({ - fingerprint: 'asdf' - }); - - scope.$apply = function() { - expect(scope.keys.length).to.equal(1); - expect(scope.keys[0]._id).to.equal('12345'); - expect(scope.keys[0].fingerprint).to.equal('asdf'); - done(); - }; - - expect(scope.keys).to.not.exist; - scope.listKeys(); - }); - }); - - describe('getFingerprint', function() { - it('should work', function() { - var key = { - fingerprint: 'YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY' - }; - - scope.getFingerprint(key); - - expect(scope.fingerprint).to.equal('YYYY YYYY YYYY YYYY YYYY ... YYYY YYYY YYYY YYYY YYYY'); - }); - }); - - describe('importKey', function() { - it('should work', function(done) { - var keyArmored = '-----BEGIN PGP PUBLIC KEY BLOCK-----'; - - pgpMock.getKeyParams.returns({ - _id: '12345', - userId: 'max@example.com', - userIds: [] - }); - - keychainMock.saveLocalPublicKey.withArgs({ - _id: '12345', - userId: 'max@example.com', - userIds: [], - publicKey: '-----BEGIN PGP PUBLIC KEY BLOCK-----', - imported: true - }).yields(); - - scope.listKeys = function() { - done(); - }; - - scope.importKey(keyArmored); - }); - - it('should fail due to invalid armored key', function(done) { - var keyArmored = '-----BEGIN PGP PRIVATE KEY BLOCK-----'; - - scope.onError = function(err) { - expect(err).to.exist; - done(); - }; - - scope.importKey(keyArmored); - }); - - it('should fail due to error in pgp.getKeyParams', function(done) { - var keyArmored = '-----BEGIN PGP PUBLIC KEY BLOCK-----'; - - pgpMock.getKeyParams.throws(new Error('WAT')); - - scope.onError = function(err) { - expect(err).to.exist; - done(); - }; - - scope.importKey(keyArmored); - }); - - it('should fail due to error in keychain.saveLocalPublicKey', function(done) { - var keyArmored = '-----BEGIN PGP PUBLIC KEY BLOCK-----'; - - pgpMock.getKeyParams.returns({ - _id: '12345', - userId: 'max@example.com' - }); - - keychainMock.saveLocalPublicKey.yields(42); - - scope.onError = function(err) { - expect(err).to.equal(42); - done(); - }; - - scope.importKey(keyArmored); - }); - }); - - describe('removeKey', function() { - it('should work', function(done) { - var key = { - _id: '12345' - }; - - keychainMock.removeLocalPublicKey.withArgs('12345').yields(); - - scope.listKeys = function() { - done(); - }; - - scope.removeKey(key); - }); - - it('should fail due to error in keychain.removeLocalPublicKey', function(done) { - var key = { - _id: '12345' - }; - - keychainMock.removeLocalPublicKey.withArgs('12345').yields(42); - - scope.onError = function(err) { - expect(err).to.equal(42); - done(); - }; - - scope.removeKey(key); + angular.module('contactstest', []); + mocks.module('contactstest'); + mocks.inject(function($rootScope, $controller) { + scope = $rootScope.$new(); + scope.state = {}; + contactsCtrl = $controller(ContactsCtrl, { + $scope: scope }); }); }); + + afterEach(function() { + // restore the module + appController._pgp = origPgp; + appController._keychain = origKeychain; + }); + + describe('scope variables', function() { + it('should be set correctly', function() { + expect(scope.fingerprint).to.equal('XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX'); + expect(scope.state.contacts.toggle).to.exist; + }); + }); + + describe('listKeys', function() { + it('should fail due to error in keychain.listLocalPublicKeys', function(done) { + keychainMock.listLocalPublicKeys.yields(42); + + scope.onError = function(err) { + expect(err).to.equal(42); + done(); + }; + + scope.listKeys(); + }); + + it('should work', function(done) { + keychainMock.listLocalPublicKeys.yields(null, [{ + _id: '12345' + }]); + pgpMock.getKeyParams.returns({ + fingerprint: 'asdf' + }); + + scope.$apply = function() { + expect(scope.keys.length).to.equal(1); + expect(scope.keys[0]._id).to.equal('12345'); + expect(scope.keys[0].fingerprint).to.equal('asdf'); + done(); + }; + + expect(scope.keys).to.not.exist; + scope.listKeys(); + }); + }); + + describe('getFingerprint', function() { + it('should work', function() { + var key = { + fingerprint: 'YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY' + }; + + scope.getFingerprint(key); + + expect(scope.fingerprint).to.equal('YYYY YYYY YYYY YYYY YYYY ... YYYY YYYY YYYY YYYY YYYY'); + }); + }); + + describe('importKey', function() { + it('should work', function(done) { + var keyArmored = '-----BEGIN PGP PUBLIC KEY BLOCK-----'; + + pgpMock.getKeyParams.returns({ + _id: '12345', + userId: 'max@example.com', + userIds: [] + }); + + keychainMock.saveLocalPublicKey.withArgs({ + _id: '12345', + userId: 'max@example.com', + userIds: [], + publicKey: '-----BEGIN PGP PUBLIC KEY BLOCK-----', + imported: true + }).yields(); + + scope.listKeys = function() { + done(); + }; + + scope.importKey(keyArmored); + }); + + it('should fail due to invalid armored key', function(done) { + var keyArmored = '-----BEGIN PGP PRIVATE KEY BLOCK-----'; + + scope.onError = function(err) { + expect(err).to.exist; + done(); + }; + + scope.importKey(keyArmored); + }); + + it('should fail due to error in pgp.getKeyParams', function(done) { + var keyArmored = '-----BEGIN PGP PUBLIC KEY BLOCK-----'; + + pgpMock.getKeyParams.throws(new Error('WAT')); + + scope.onError = function(err) { + expect(err).to.exist; + done(); + }; + + scope.importKey(keyArmored); + }); + + it('should fail due to error in keychain.saveLocalPublicKey', function(done) { + var keyArmored = '-----BEGIN PGP PUBLIC KEY BLOCK-----'; + + pgpMock.getKeyParams.returns({ + _id: '12345', + userId: 'max@example.com' + }); + + keychainMock.saveLocalPublicKey.yields(42); + + scope.onError = function(err) { + expect(err).to.equal(42); + done(); + }; + + scope.importKey(keyArmored); + }); + }); + + describe('removeKey', function() { + it('should work', function(done) { + var key = { + _id: '12345' + }; + + keychainMock.removeLocalPublicKey.withArgs('12345').yields(); + + scope.listKeys = function() { + done(); + }; + + scope.removeKey(key); + }); + + it('should fail due to error in keychain.removeLocalPublicKey', function(done) { + var key = { + _id: '12345' + }; + + keychainMock.removeLocalPublicKey.withArgs('12345').yields(42); + + scope.onError = function(err) { + expect(err).to.equal(42); + done(); + }; + + scope.removeKey(key); + }); + }); }); \ No newline at end of file diff --git a/test/unit/crypto-test.js b/test/unit/crypto-test.js index 5cbe272..7647514 100644 --- a/test/unit/crypto-test.js +++ b/test/unit/crypto-test.js @@ -1,58 +1,55 @@ -define(function(require) { - 'use strict'; +'use strict'; - var Crypto = require('js/crypto/crypto'), - util = require('js/crypto/util'), - config = require('js/app-config').config, - expect = chai.expect; +var Crypto = require('../../src/js/crypto/crypto'), + config = require('../../src/js/app-config').config, + util = require('crypto-lib').util; - describe('Crypto unit tests', function() { - this.timeout(20000); +describe('Crypto unit tests', function() { + this.timeout(20000); - var crypto, - password = 'password', - keySize = config.symKeySize, - ivSize = config.symIvSize; + var crypto, + password = 'password', + keySize = config.symKeySize, + ivSize = config.symIvSize; - beforeEach(function() { - crypto = new Crypto(); - }); + beforeEach(function() { + crypto = new Crypto(); + }); - afterEach(function() {}); + afterEach(function() {}); - describe('AES encrypt/decrypt', function() { - it('should work', function(done) { - var plaintext = 'Hello, World!'; - var key = util.random(keySize); - var iv = util.random(ivSize); + describe('AES encrypt/decrypt', function() { + it('should work', function(done) { + var plaintext = 'Hello, World!'; + var key = util.random(keySize); + var iv = util.random(ivSize); - crypto.encrypt(plaintext, key, iv, function(err, ciphertext) { + crypto.encrypt(plaintext, key, iv, function(err, ciphertext) { + expect(err).to.not.exist; + expect(ciphertext).to.exist; + + crypto.decrypt(ciphertext, key, iv, function(err, decrypted) { expect(err).to.not.exist; - expect(ciphertext).to.exist; - - crypto.decrypt(ciphertext, key, iv, function(err, decrypted) { - expect(err).to.not.exist; - expect(decrypted).to.equal(plaintext); - - done(); - }); - }); - }); - }); - - describe("PBKDF2 (Async/Worker)", function() { - it('should work', function(done) { - var salt = util.random(keySize); - - crypto.deriveKey(password, salt, keySize, function(err, key) { - expect(err).to.not.exist; - expect(util.base642Str(key).length * 8).to.equal(keySize); + expect(decrypted).to.equal(plaintext); done(); }); }); + }); + }); + describe("PBKDF2 (Async/Worker)", function() { + it('should work', function(done) { + var salt = util.random(keySize); + + crypto.deriveKey(password, salt, keySize, function(err, key) { + expect(err).to.not.exist; + expect(util.base642Str(key).length * 8).to.equal(keySize); + + done(); + }); }); }); + }); \ No newline at end of file diff --git a/test/unit/devicestorage-dao-test.js b/test/unit/devicestorage-dao-test.js index f593e39..1cc49d5 100644 --- a/test/unit/devicestorage-dao-test.js +++ b/test/unit/devicestorage-dao-test.js @@ -1,105 +1,101 @@ -define(function(require) { - 'use strict'; +'use strict'; - var LawnchairDAO = require('js/dao/lawnchair-dao'), - DeviceStorageDAO = require('js/dao/devicestorage-dao'), - expect = chai.expect; +var LawnchairDAO = require('../../src/js/dao/lawnchair-dao'), + DeviceStorageDAO = require('../../src/js/dao/devicestorage-dao'); - var testUser = 'test@example.com'; +var testUser = 'test@example.com'; - describe('Device Storage DAO unit tests', function() { +describe('Device Storage DAO unit tests', function() { - var storageDao, lawnchairDaoStub; + var storageDao, lawnchairDaoStub; - beforeEach(function() { - lawnchairDaoStub = sinon.createStubInstance(LawnchairDAO); - storageDao = new DeviceStorageDAO(lawnchairDaoStub); + beforeEach(function() { + lawnchairDaoStub = sinon.createStubInstance(LawnchairDAO); + storageDao = new DeviceStorageDAO(lawnchairDaoStub); + }); + + afterEach(function() {}); + + describe('init', function() { + it('should work', function(done) { + lawnchairDaoStub.init.yields(); + + storageDao.init(testUser, function(err) { + expect(err).to.not.exist; + expect(lawnchairDaoStub.init.calledOnce).to.be.true; + done(); + }); }); + }); - afterEach(function() {}); + describe('store list', function() { + it('should fail', function(done) { + var list = [{}]; - describe('init', function() { - it('should work', function(done) { - lawnchairDaoStub.init.yields(); - - storageDao.init(testUser, function(err) { - expect(err).to.not.exist; - expect(lawnchairDaoStub.init.calledOnce).to.be.true; - done(); - }); + storageDao.storeList(list, '', function(err) { + expect(err).to.exist; + done(); }); }); - describe('store list', function() { - it('should fail', function(done) { - var list = [{}]; + it('should work with empty list', function(done) { + var list = []; - storageDao.storeList(list, '', function(err) { - expect(err).to.exist; - done(); - }); - }); - - it('should work with empty list', function(done) { - var list = []; - - storageDao.storeList(list, 'email', function(err) { - expect(err).to.not.exist; - done(); - }); - }); - - it('should work', function(done) { - lawnchairDaoStub.batch.yields(); - - var list = [{ - foo: 'bar' - }]; - - storageDao.storeList(list, 'email', function(err) { - expect(err).to.not.exist; - expect(lawnchairDaoStub.batch.calledOnce).to.be.true; - done(); - }); + storageDao.storeList(list, 'email', function(err) { + expect(err).to.not.exist; + done(); }); }); - describe('remove list', function() { - it('should work', function(done) { - lawnchairDaoStub.removeList.yields(); + it('should work', function(done) { + lawnchairDaoStub.batch.yields(); - storageDao.removeList('email', function(err) { - expect(err).to.not.exist; - expect(lawnchairDaoStub.removeList.calledOnce).to.be.true; - done(); - }); + var list = [{ + foo: 'bar' + }]; + + storageDao.storeList(list, 'email', function(err) { + expect(err).to.not.exist; + expect(lawnchairDaoStub.batch.calledOnce).to.be.true; + done(); }); }); + }); - describe('list items', function() { - it('should work', function(done) { - lawnchairDaoStub.list.yields(); + describe('remove list', function() { + it('should work', function(done) { + lawnchairDaoStub.removeList.yields(); - storageDao.listItems('email', 0, null, function(err) { - expect(err).to.not.exist; - expect(lawnchairDaoStub.list.calledOnce).to.be.true; - done(); - }); + storageDao.removeList('email', function(err) { + expect(err).to.not.exist; + expect(lawnchairDaoStub.removeList.calledOnce).to.be.true; + done(); }); }); + }); - describe('clear', function() { - it('should work', function(done) { - lawnchairDaoStub.clear.yields(); + describe('list items', function() { + it('should work', function(done) { + lawnchairDaoStub.list.yields(); - storageDao.clear(function(err) { - expect(err).to.not.exist; - expect(lawnchairDaoStub.clear.calledOnce).to.be.true; - done(); - }); + storageDao.listItems('email', 0, null, function(err) { + expect(err).to.not.exist; + expect(lawnchairDaoStub.list.calledOnce).to.be.true; + done(); }); }); + }); + describe('clear', function() { + it('should work', function(done) { + lawnchairDaoStub.clear.yields(); + + storageDao.clear(function(err) { + expect(err).to.not.exist; + expect(lawnchairDaoStub.clear.calledOnce).to.be.true; + done(); + }); + }); }); }); \ No newline at end of file diff --git a/test/unit/dialog-ctrl-test.js b/test/unit/dialog-ctrl-test.js index 7ff73cd..191cd4f 100644 --- a/test/unit/dialog-ctrl-test.js +++ b/test/unit/dialog-ctrl-test.js @@ -1,50 +1,46 @@ -define(function(require) { - 'use strict'; +'use strict'; - var expect = chai.expect, - angular = require('angular'), - mocks = require('angularMocks'), - DialogCtrl = require('js/controller/dialog'); +var mocks = angular.mocks, + DialogCtrl = require('../../src/js/controller/dialog'); - describe('Dialog Controller unit test', function() { - var scope, dialogCtrl; +describe('Dialog Controller unit test', function() { + var scope, dialogCtrl; - beforeEach(function() { - angular.module('dialogtest', []); - mocks.module('dialogtest'); - mocks.inject(function($rootScope, $controller) { - scope = $rootScope.$new(); - scope.state = { - dialog: {} - }; - dialogCtrl = $controller(DialogCtrl, { - $scope: scope - }); - }); - }); - - afterEach(function() {}); - - describe('confirm', function() { - it('should work', function(done) { - scope.state.dialog.callback = function(confirmed) { - expect(confirmed).to.be.true; - expect(scope.state.dialog.open).to.be.false; - done(); - }; - scope.confirm(true); - }); - }); - - describe('cancel', function() { - it('should work', function(done) { - scope.state.dialog.callback = function(confirmed) { - expect(confirmed).to.be.false; - expect(scope.state.dialog.open).to.be.false; - done(); - }; - scope.confirm(false); + beforeEach(function() { + angular.module('dialogtest', []); + mocks.module('dialogtest'); + mocks.inject(function($rootScope, $controller) { + scope = $rootScope.$new(); + scope.state = { + dialog: {} + }; + dialogCtrl = $controller(DialogCtrl, { + $scope: scope }); }); }); + + afterEach(function() {}); + + describe('confirm', function() { + it('should work', function(done) { + scope.state.dialog.callback = function(confirmed) { + expect(confirmed).to.be.true; + expect(scope.state.dialog.open).to.be.false; + done(); + }; + scope.confirm(true); + }); + }); + + describe('cancel', function() { + it('should work', function(done) { + scope.state.dialog.callback = function(confirmed) { + expect(confirmed).to.be.false; + expect(scope.state.dialog.open).to.be.false; + done(); + }; + scope.confirm(false); + }); + }); }); \ No newline at end of file diff --git a/test/unit/email-dao-test.js b/test/unit/email-dao-test.js index 305f8f1..0ba3553 100644 --- a/test/unit/email-dao-test.js +++ b/test/unit/email-dao-test.js @@ -1,2304 +1,2301 @@ -define(function(require) { - 'use strict'; +'use strict'; - var EmailDAO = require('js/dao/email-dao'), - KeychainDAO = require('js/dao/keychain-dao'), - ImapClient = require('imap-client'), - PgpMailer = require('pgpmailer'), - PgpBuilder = require('pgpbuilder'), - PGP = require('js/crypto/pgp'), - DeviceStorageDAO = require('js/dao/devicestorage-dao'), - mailreader = require('mailreader'), - cfg = require('js/app-config').config, - expect = chai.expect; +var mailreader = require('mailreader'), + ImapClient = require('imap-client'), + PgpMailer = require('pgpmailer'), + PgpBuilder = require('pgpbuilder'), + cfg = require('../../src/js/app-config').config, + EmailDAO = require('../../src/js/dao/email-dao'), + KeychainDAO = require('../../src/js/dao/keychain-dao'), + PGP = require('../../src/js/crypto/pgp'), + DeviceStorageDAO = require('../../src/js/dao/devicestorage-dao'); - describe('Email DAO unit tests', function() { - // show the stack trace when an error occurred - chai.Assertion.includeStack = true; +describe('Email DAO unit tests', function() { + // show the stack trace when an error occurred + chai.Assertion.includeStack = true; - // SUT - var dao; + // SUT + var dao; - // mocks - var keychainStub, imapClientStub, pgpMailerStub, pgpBuilderStub, pgpStub, devicestorageStub, parseStub; + // mocks + var keychainStub, imapClientStub, pgpMailerStub, pgpBuilderStub, pgpStub, devicestorageStub, parseStub; - // config - var emailAddress, passphrase, asymKeySize, account; + // config + var emailAddress, passphrase, asymKeySize, account; + // test data + var folders, inboxFolder, sentFolder, draftsFolder, outboxFolder, trashFolder, mockKeyPair; + + beforeEach(function() { + // // test data - var folders, inboxFolder, sentFolder, draftsFolder, outboxFolder, trashFolder, mockKeyPair; + // + emailAddress = 'asdf@asdf.com'; + passphrase = 'asdf'; + asymKeySize = 2048; - beforeEach(function() { - // - // test data - // - emailAddress = 'asdf@asdf.com'; - passphrase = 'asdf'; - asymKeySize = 2048; + inboxFolder = { + name: 'Inbox', + type: 'Inbox', + path: 'INBOX', + messages: [] + }; - inboxFolder = { - name: 'Inbox', - type: 'Inbox', - path: 'INBOX', - messages: [] - }; + sentFolder = { + name: 'Sent', + type: 'Sent', + path: 'SENT', + messages: [] + }; - sentFolder = { - name: 'Sent', - type: 'Sent', - path: 'SENT', - messages: [] - }; + draftsFolder = { + name: 'Drafts', + type: 'Drafts', + path: 'DRAFTS', + messages: [] + }; - draftsFolder = { - name: 'Drafts', - type: 'Drafts', - path: 'DRAFTS', - messages: [] - }; + outboxFolder = { + name: 'Outbox', + type: 'Outbox', + path: 'OUTBOX', + messages: [] + }; - outboxFolder = { - name: 'Outbox', - type: 'Outbox', - path: 'OUTBOX', - messages: [] - }; + trashFolder = { + name: 'Trash', + type: 'Trash', + path: 'TRASH', + messages: [] + }; - trashFolder = { - name: 'Trash', - type: 'Trash', - path: 'TRASH', - messages: [] - }; + folders = [inboxFolder, outboxFolder, trashFolder, sentFolder]; - folders = [inboxFolder, outboxFolder, trashFolder, sentFolder]; + account = { + emailAddress: emailAddress, + asymKeySize: asymKeySize, + folders: folders, + online: true + }; - account = { - emailAddress: emailAddress, - asymKeySize: asymKeySize, - folders: folders, - online: true - }; + mockKeyPair = { + publicKey: { + _id: 1234, + userId: emailAddress, + publicKey: 'publicpublicpublicpublic' + }, + privateKey: { + _id: 1234, + userId: emailAddress, + encryptedKey: 'privateprivateprivateprivate' + } + }; - mockKeyPair = { - publicKey: { - _id: 1234, - userId: emailAddress, - publicKey: 'publicpublicpublicpublic' - }, - privateKey: { - _id: 1234, - userId: emailAddress, - encryptedKey: 'privateprivateprivateprivate' - } - }; + // + // setup the mocks + // + keychainStub = sinon.createStubInstance(KeychainDAO); + imapClientStub = sinon.createStubInstance(ImapClient); + pgpMailerStub = sinon.createStubInstance(PgpMailer); + pgpBuilderStub = sinon.createStubInstance(PgpBuilder); + pgpStub = sinon.createStubInstance(PGP); + parseStub = sinon.stub(mailreader, 'parse'); + devicestorageStub = sinon.createStubInstance(DeviceStorageDAO); - // - // setup the mocks - // - keychainStub = sinon.createStubInstance(KeychainDAO); - imapClientStub = sinon.createStubInstance(ImapClient); - pgpMailerStub = sinon.createStubInstance(PgpMailer); - pgpBuilderStub = sinon.createStubInstance(PgpBuilder); - pgpStub = sinon.createStubInstance(PGP); - parseStub = sinon.stub(mailreader, 'parse'); - devicestorageStub = sinon.createStubInstance(DeviceStorageDAO); + // + // setup the SUT + // + dao = new EmailDAO(keychainStub, pgpStub, devicestorageStub, pgpBuilderStub, mailreader); + dao._account = account; + dao._pgpMailer = pgpMailerStub; + dao._imapClient = imapClientStub; - // - // setup the SUT - // - dao = new EmailDAO(keychainStub, pgpStub, devicestorageStub, pgpBuilderStub, mailreader); - dao._account = account; - dao._pgpMailer = pgpMailerStub; - dao._imapClient = imapClientStub; + // + // check configuration + // + expect(dao._keychain).to.equal(keychainStub); + expect(dao._pgp).to.equal(pgpStub); + expect(dao._devicestorage).to.equal(devicestorageStub); + expect(dao._mailreader).to.equal(mailreader); + expect(dao._pgpbuilder).to.equal(pgpBuilderStub); + }); - // - // check configuration - // - expect(dao._keychain).to.equal(keychainStub); - expect(dao._pgp).to.equal(pgpStub); - expect(dao._devicestorage).to.equal(devicestorageStub); - expect(dao._mailreader).to.equal(mailreader); - expect(dao._pgpbuilder).to.equal(pgpBuilderStub); - }); + afterEach(function() { + mailreader.parse.restore(); + }); - afterEach(function() { - mailreader.parse.restore(); - }); + describe('public API', function() { + describe('#init', function() { + var initFoldersStub; - describe('public API', function() { - describe('#init', function() { - var initFoldersStub; + beforeEach(function() { + delete dao._account; + initFoldersStub = sinon.stub(dao, '_initFoldersFromDisk'); + }); - beforeEach(function() { - delete dao._account; - initFoldersStub = sinon.stub(dao, '_initFoldersFromDisk'); - }); + it('should initialize folders and return keypair', function(done) { + keychainStub.getUserKeyPair.withArgs(emailAddress).yieldsAsync(null, mockKeyPair); + initFoldersStub.yieldsAsync(); - it('should initialize folders and return keypair', function(done) { - keychainStub.getUserKeyPair.withArgs(emailAddress).yieldsAsync(null, mockKeyPair); - initFoldersStub.yieldsAsync(); + dao.init({ + account: account + }, function(err, keypair) { + expect(err).to.not.exist; + expect(keypair).to.exist; + expect(keychainStub.getUserKeyPair.calledOnce).to.be.true; + expect(initFoldersStub.calledOnce).to.be.true; - dao.init({ - account: account - }, function(err, keypair) { - expect(err).to.not.exist; - expect(keypair).to.exist; - expect(keychainStub.getUserKeyPair.calledOnce).to.be.true; - expect(initFoldersStub.calledOnce).to.be.true; - - done(); - }); - }); - - it('should fail when keychain errors', function(done) { - keychainStub.getUserKeyPair.yieldsAsync({}); - - dao.init({ - account: account - }, function(err, keypair) { - expect(err).to.exist; - expect(keypair).to.not.exist; - expect(keychainStub.getUserKeyPair.calledOnce).to.be.true; - expect(initFoldersStub.called).to.be.false; - - done(); - }); + done(); }); }); - describe('#unlock', function() { - it('should unlock', function(done) { - pgpStub.getKeyParams.returns({ - _id: mockKeyPair.publicKey._id, - userId: emailAddress, - userIds: [{ - emailAddress: emailAddress - }] - }); + it('should fail when keychain errors', function(done) { + keychainStub.getUserKeyPair.yieldsAsync({}); - pgpStub.importKeys.withArgs({ - passphrase: passphrase, - privateKeyArmored: mockKeyPair.privateKey.encryptedKey, - publicKeyArmored: mockKeyPair.publicKey.publicKey - }).yieldsAsync(); - pgpStub._privateKey = { - foo: 'bar' - }; + dao.init({ + account: account + }, function(err, keypair) { + expect(err).to.exist; + expect(keypair).to.not.exist; + expect(keychainStub.getUserKeyPair.calledOnce).to.be.true; + expect(initFoldersStub.called).to.be.false; - dao.unlock({ - passphrase: passphrase, - keypair: mockKeyPair - }, function(err) { - expect(err).to.not.exist; + done(); + }); + }); + }); - expect(pgpStub.importKeys.calledOnce).to.be.true; - expect(dao._pgpbuilder._privateKey).to.equal(pgpStub._privateKey); - - done(); - }); + describe('#unlock', function() { + it('should unlock', function(done) { + pgpStub.getKeyParams.returns({ + _id: mockKeyPair.publicKey._id, + userId: emailAddress, + userIds: [{ + emailAddress: emailAddress + }] }); - it('should generate a keypair and unlock', function(done) { - var keypair = { - keyId: 123, - publicKeyArmored: mockKeyPair.publicKey.publicKey, - privateKeyArmored: mockKeyPair.privateKey.encryptedKey - }; + pgpStub.importKeys.withArgs({ + passphrase: passphrase, + privateKeyArmored: mockKeyPair.privateKey.encryptedKey, + publicKeyArmored: mockKeyPair.publicKey.publicKey + }).yieldsAsync(); + pgpStub._privateKey = { + foo: 'bar' + }; - pgpStub.generateKeys.withArgs({ - emailAddress: emailAddress, - keySize: asymKeySize, - passphrase: passphrase - }).yieldsAsync(null, keypair); + dao.unlock({ + passphrase: passphrase, + keypair: mockKeyPair + }, function(err) { + expect(err).to.not.exist; - pgpStub.importKeys.withArgs({ - passphrase: passphrase, - privateKeyArmored: mockKeyPair.privateKey.encryptedKey, - publicKeyArmored: mockKeyPair.publicKey.publicKey - }).yieldsAsync(); - keychainStub.putUserKeyPair.withArgs().yieldsAsync(); + expect(pgpStub.importKeys.calledOnce).to.be.true; + expect(dao._pgpbuilder._privateKey).to.equal(pgpStub._privateKey); - dao.unlock({ - passphrase: passphrase - }, function(err) { - expect(err).to.not.exist; - - expect(pgpStub.generateKeys.calledOnce).to.be.true; - expect(pgpStub.importKeys.calledOnce).to.be.true; - expect(keychainStub.putUserKeyPair.calledOnce).to.be.true; - - done(); - }); - }); - - it('should fail when persisting fails', function(done) { - var keypair = { - keyId: 123, - publicKeyArmored: 'qwerty', - privateKeyArmored: 'asdfgh' - }; - pgpStub.generateKeys.yieldsAsync(null, keypair); - pgpStub.importKeys.withArgs().yieldsAsync(); - keychainStub.putUserKeyPair.yieldsAsync({}); - - dao.unlock({ - passphrase: passphrase - }, function(err) { - expect(err).to.exist; - - expect(pgpStub.generateKeys.calledOnce).to.be.true; - expect(pgpStub.importKeys.calledOnce).to.be.true; - expect(keychainStub.putUserKeyPair.calledOnce).to.be.true; - - done(); - }); - }); - - it('should fail when import fails', function(done) { - var keypair = { - keyId: 123, - publicKeyArmored: 'qwerty', - privateKeyArmored: 'asdfgh' - }; - - pgpStub.generateKeys.withArgs().yieldsAsync(null, keypair); - pgpStub.importKeys.withArgs().yieldsAsync({}); - - dao.unlock({ - passphrase: passphrase - }, function(err) { - expect(err).to.exist; - - expect(pgpStub.generateKeys.calledOnce).to.be.true; - expect(pgpStub.importKeys.calledOnce).to.be.true; - - done(); - }); - }); - - it('should fail when generation fails', function(done) { - pgpStub.generateKeys.yieldsAsync({}); - - dao.unlock({ - passphrase: passphrase - }, function(err) { - expect(err).to.exist; - - expect(pgpStub.generateKeys.calledOnce).to.be.true; - - done(); - }); + done(); }); }); - describe('#openFolder', function() { - it('should open an imap mailbox', function(done) { - imapClientStub.selectMailbox.withArgs({ - path: inboxFolder.path - }).yieldsAsync(); + it('should generate a keypair and unlock', function(done) { + var keypair = { + keyId: 123, + publicKeyArmored: mockKeyPair.publicKey.publicKey, + privateKeyArmored: mockKeyPair.privateKey.encryptedKey + }; - dao.openFolder({ - folder: inboxFolder - }, function(err) { - expect(err).to.not.exist; - expect(imapClientStub.selectMailbox.calledOnce).to.be.true; - done(); - }); + pgpStub.generateKeys.withArgs({ + emailAddress: emailAddress, + keySize: asymKeySize, + passphrase: passphrase + }).yieldsAsync(null, keypair); + + pgpStub.importKeys.withArgs({ + passphrase: passphrase, + privateKeyArmored: mockKeyPair.privateKey.encryptedKey, + publicKeyArmored: mockKeyPair.publicKey.publicKey + }).yieldsAsync(); + keychainStub.putUserKeyPair.withArgs().yieldsAsync(); + + dao.unlock({ + passphrase: passphrase + }, function(err) { + expect(err).to.not.exist; + + expect(pgpStub.generateKeys.calledOnce).to.be.true; + expect(pgpStub.importKeys.calledOnce).to.be.true; + expect(keychainStub.putUserKeyPair.calledOnce).to.be.true; + + done(); + }); + }); + + it('should fail when persisting fails', function(done) { + var keypair = { + keyId: 123, + publicKeyArmored: 'qwerty', + privateKeyArmored: 'asdfgh' + }; + pgpStub.generateKeys.yieldsAsync(null, keypair); + pgpStub.importKeys.withArgs().yieldsAsync(); + keychainStub.putUserKeyPair.yieldsAsync({}); + + dao.unlock({ + passphrase: passphrase + }, function(err) { + expect(err).to.exist; + + expect(pgpStub.generateKeys.calledOnce).to.be.true; + expect(pgpStub.importKeys.calledOnce).to.be.true; + expect(keychainStub.putUserKeyPair.calledOnce).to.be.true; + + done(); + }); + }); + + it('should fail when import fails', function(done) { + var keypair = { + keyId: 123, + publicKeyArmored: 'qwerty', + privateKeyArmored: 'asdfgh' + }; + + pgpStub.generateKeys.withArgs().yieldsAsync(null, keypair); + pgpStub.importKeys.withArgs().yieldsAsync({}); + + dao.unlock({ + passphrase: passphrase + }, function(err) { + expect(err).to.exist; + + expect(pgpStub.generateKeys.calledOnce).to.be.true; + expect(pgpStub.importKeys.calledOnce).to.be.true; + + done(); + }); + }); + + it('should fail when generation fails', function(done) { + pgpStub.generateKeys.yieldsAsync({}); + + dao.unlock({ + passphrase: passphrase + }, function(err) { + expect(err).to.exist; + + expect(pgpStub.generateKeys.calledOnce).to.be.true; + + done(); + }); + }); + }); + + describe('#openFolder', function() { + it('should open an imap mailbox', function(done) { + imapClientStub.selectMailbox.withArgs({ + path: inboxFolder.path + }).yieldsAsync(); + + dao.openFolder({ + folder: inboxFolder + }, function(err) { + expect(err).to.not.exist; + expect(imapClientStub.selectMailbox.calledOnce).to.be.true; + done(); + }); + }); + + it('should not open the virtual outbox folder in imap', function() { + dao.openFolder({ + folder: outboxFolder }); - it('should not open the virtual outbox folder in imap', function() { - dao.openFolder({ - folder: outboxFolder - }); + expect(imapClientStub.selectMailbox.called).to.be.false; + }); + it('should not do anything in offline mode', function(done) { + account.online = false; + + dao.openFolder({ + folder: inboxFolder + }, function(err) { + expect(err).to.exist; expect(imapClientStub.selectMailbox.called).to.be.false; + done(); }); - it('should not do anything in offline mode', function(done) { - account.online = false; + }); + }); - dao.openFolder({ - folder: inboxFolder - }, function(err) { - expect(err).to.exist; - expect(imapClientStub.selectMailbox.called).to.be.false; - done(); - }); + describe('#refreshFolder', function() { + var localListStub, mail; + beforeEach(function() { + localListStub = sinon.stub(dao, '_localListMessages'); + mail = { + uid: 123, + unread: true + }; + }); + + it('should add messages from disk', function(done) { + localListStub.withArgs({ + folder: inboxFolder + }).yieldsAsync(null, [mail]); + + dao.refreshFolder({ + folder: inboxFolder + }, function(err) { + expect(err).to.not.exist; + expect(inboxFolder.count).to.equal(1); + expect(inboxFolder.messages).to.contain(mail); + + done(); }); }); - describe('#refreshFolder', function() { - var localListStub, mail; + it('should not add messages from disk', function(done) { + inboxFolder.messages = [mail]; + localListStub.withArgs({ + folder: inboxFolder + }).yieldsAsync(null, [mail]); - beforeEach(function() { - localListStub = sinon.stub(dao, '_localListMessages'); - mail = { - uid: 123, - unread: true - }; - }); + dao.refreshFolder({ + folder: inboxFolder + }, function(err) { + expect(err).to.not.exist; + expect(inboxFolder.count).to.equal(1); + expect(inboxFolder.messages).to.contain(mail); - it('should add messages from disk', function(done) { - localListStub.withArgs({ - folder: inboxFolder - }).yieldsAsync(null, [mail]); - - dao.refreshFolder({ - folder: inboxFolder - }, function(err) { - expect(err).to.not.exist; - expect(inboxFolder.count).to.equal(1); - expect(inboxFolder.messages).to.contain(mail); - - done(); - }); - }); - - it('should not add messages from disk', function(done) { - inboxFolder.messages = [mail]; - localListStub.withArgs({ - folder: inboxFolder - }).yieldsAsync(null, [mail]); - - dao.refreshFolder({ - folder: inboxFolder - }, function(err) { - expect(err).to.not.exist; - expect(inboxFolder.count).to.equal(1); - expect(inboxFolder.messages).to.contain(mail); - - done(); - }); - }); - - it('should remove messages from memory', function(done) { - inboxFolder.messages = [mail]; - localListStub.withArgs({ - folder: inboxFolder - }).yieldsAsync(null, []); - - dao.refreshFolder({ - folder: inboxFolder - }, function(err) { - expect(err).to.not.exist; - expect(inboxFolder.count).to.equal(0); - expect(inboxFolder.messages).to.be.empty; - - done(); - }); + done(); }); }); - describe('#fetchMessages', function() { - var imapListStub, imapGetStub, imapDeleteStub, localStoreStub; - var opts, message, validUuid, corruptedUuid, verificationSubject; - var notified; + it('should remove messages from memory', function(done) { + inboxFolder.messages = [mail]; + localListStub.withArgs({ + folder: inboxFolder + }).yieldsAsync(null, []); - beforeEach(function() { - imapListStub = sinon.stub(dao, '_imapListMessages'); - imapDeleteStub = sinon.stub(dao, '_imapDeleteMessage'); - imapGetStub = sinon.stub(dao, '_getBodyParts'); - localStoreStub = sinon.stub(dao, '_localStoreMessages'); + dao.refreshFolder({ + folder: inboxFolder + }, function(err) { + expect(err).to.not.exist; + expect(inboxFolder.count).to.equal(0); + expect(inboxFolder.messages).to.be.empty; - opts = { - folder: inboxFolder, - firstUid: 123, - lastUid: 123 - }; - message = { - uid: 123, - subject: 'asdasd', - unread: true, - bodyParts: [] - }; - validUuid = '9A858952-17EE-4273-9E74-D309EAFDFAFB'; - corruptedUuid = 'OMFG_FUCKING_BASTARD_UUID_FROM_HELL!'; - verificationSubject = "[whiteout] New public key uploaded"; - - notified = false; - dao.onIncomingMessage = function(newMessages) { - expect(newMessages).to.contain(message); - notified = true; - }; + done(); }); + }); + }); - it('should fetch message downstream', function(done) { - imapListStub.withArgs(opts).yieldsAsync(null, [message]); + describe('#fetchMessages', function() { + var imapListStub, imapGetStub, imapDeleteStub, localStoreStub; + var opts, message, validUuid, corruptedUuid, verificationSubject; + var notified; - localStoreStub.withArgs({ - folder: inboxFolder, - emails: [message] - }).yieldsAsync(); + beforeEach(function() { + imapListStub = sinon.stub(dao, '_imapListMessages'); + imapDeleteStub = sinon.stub(dao, '_imapDeleteMessage'); + imapGetStub = sinon.stub(dao, '_getBodyParts'); + localStoreStub = sinon.stub(dao, '_localStoreMessages'); - dao.fetchMessages(opts, function(err) { - expect(err).to.not.exist; - expect(inboxFolder.messages).to.contain(message); - expect(notified).to.be.true; - expect(localStoreStub.calledOnce).to.be.true; - expect(imapListStub.calledOnce).to.be.true; + opts = { + folder: inboxFolder, + firstUid: 123, + lastUid: 123 + }; + message = { + uid: 123, + subject: 'asdasd', + unread: true, + bodyParts: [] + }; + validUuid = '9A858952-17EE-4273-9E74-D309EAFDFAFB'; + corruptedUuid = 'OMFG_FUCKING_BASTARD_UUID_FROM_HELL!'; + verificationSubject = "[whiteout] New public key uploaded"; - done(); - }); - }); + notified = false; + dao.onIncomingMessage = function(newMessages) { + expect(newMessages).to.contain(message); + notified = true; + }; + }); - it('should not notify for other folders', function(done) { - opts.folder = sentFolder; + it('should fetch message downstream', function(done) { + imapListStub.withArgs(opts).yieldsAsync(null, [message]); - imapListStub.withArgs(opts).yieldsAsync(null, [message]); + localStoreStub.withArgs({ + folder: inboxFolder, + emails: [message] + }).yieldsAsync(); - localStoreStub.withArgs({ - folder: sentFolder, - emails: [message] - }).yieldsAsync(); + dao.fetchMessages(opts, function(err) { + expect(err).to.not.exist; + expect(inboxFolder.messages).to.contain(message); + expect(notified).to.be.true; + expect(localStoreStub.calledOnce).to.be.true; + expect(imapListStub.calledOnce).to.be.true; - dao.fetchMessages(opts, function(err) { - expect(err).to.not.exist; - expect(sentFolder.messages).to.contain(message); - expect(notified).to.be.false; - expect(localStoreStub.calledOnce).to.be.true; - expect(imapListStub.calledOnce).to.be.true; - - done(); - }); - }); - - it('should verify verification mails', function(done) { - message.subject = verificationSubject; - - imapListStub.withArgs(opts).yieldsAsync(null, [message]); - - imapGetStub.withArgs({ - folder: inboxFolder, - uid: message.uid, - bodyParts: message.bodyParts - }).yieldsAsync(null, [{ - type: 'text', - content: '' + cfg.cloudUrl + cfg.verificationUrl + validUuid - }]); - - keychainStub.verifyPublicKey.withArgs(validUuid).yieldsAsync(); - - imapDeleteStub.withArgs({ - folder: inboxFolder, - uid: message.uid - }).yieldsAsync(); - - dao.fetchMessages(opts, function(err) { - expect(err).to.not.exist; - expect(inboxFolder.messages).to.not.contain(message); - expect(notified).to.be.false; - expect(imapListStub.calledOnce).to.be.true; - expect(imapGetStub.calledOnce).to.be.true; - expect(keychainStub.verifyPublicKey.calledOnce).to.be.true; - expect(imapDeleteStub.calledOnce).to.be.true; - expect(localStoreStub.called).to.be.false; - done(); - }); - }); - - it('should not verify invalid verification mails', function(done) { - message.subject = verificationSubject; - - imapListStub.withArgs(opts).yieldsAsync(null, [message]); - - imapGetStub.withArgs({ - folder: inboxFolder, - uid: message.uid, - bodyParts: message.bodyParts - }).yieldsAsync(null, [{ - type: 'text', - content: '' + cfg.cloudUrl + cfg.verificationUrl + corruptedUuid - }]); - - localStoreStub.withArgs({ - folder: inboxFolder, - emails: [message] - }).yieldsAsync(); - - dao.fetchMessages(opts, function(err) { - expect(err).to.not.exist; - expect(inboxFolder.messages).to.contain(message); - expect(notified).to.be.true; - expect(imapListStub.calledOnce).to.be.true; - expect(imapGetStub.calledOnce).to.be.true; - expect(keychainStub.verifyPublicKey.called).to.be.false; - expect(imapDeleteStub.called).to.be.false; - expect(localStoreStub.calledOnce).to.be.true; - done(); - }); - }); - - it('should display verification mail when verification failed', function(done) { - message.subject = verificationSubject; - - imapListStub.withArgs(opts).yieldsAsync(null, [message]); - - imapGetStub.withArgs({ - folder: inboxFolder, - uid: message.uid, - bodyParts: message.bodyParts - }).yieldsAsync(null, [{ - type: 'text', - content: '' + cfg.cloudUrl + cfg.verificationUrl + validUuid - }]); - - keychainStub.verifyPublicKey.withArgs(validUuid).yieldsAsync({}); - - localStoreStub.withArgs({ - folder: inboxFolder, - emails: [message] - }).yieldsAsync(); - - - dao.fetchMessages(opts, function(err) { - expect(err).to.not.exist; - expect(inboxFolder.messages).to.contain(message); - expect(notified).to.be.true; - expect(imapListStub.calledOnce).to.be.true; - expect(imapGetStub.calledOnce).to.be.true; - expect(keychainStub.verifyPublicKey.calledOnce).to.be.true; - expect(imapDeleteStub.called).to.be.false; - expect(localStoreStub.calledOnce).to.be.true; - done(); - }); + done(); }); }); - describe('#deleteMessage', function() { - var imapDeleteStub, localDeleteStub, message; + it('should not notify for other folders', function(done) { + opts.folder = sentFolder; - beforeEach(function() { - message = { - uid: 1234 - }; - imapDeleteStub = sinon.stub(dao, '_imapDeleteMessage'); - localDeleteStub = sinon.stub(dao, '_localDeleteMessage'); - inboxFolder.messages = [message]; - outboxFolder.messages = [message]; - }); + imapListStub.withArgs(opts).yieldsAsync(null, [message]); - it('should delete from imap, local, memory', function(done) { - var deleteOpts = { - folder: inboxFolder, - uid: message.uid - }; + localStoreStub.withArgs({ + folder: sentFolder, + emails: [message] + }).yieldsAsync(); - imapDeleteStub.withArgs(deleteOpts).yieldsAsync(); - localDeleteStub.withArgs(deleteOpts).yieldsAsync(); + dao.fetchMessages(opts, function(err) { + expect(err).to.not.exist; + expect(sentFolder.messages).to.contain(message); + expect(notified).to.be.false; + expect(localStoreStub.calledOnce).to.be.true; + expect(imapListStub.calledOnce).to.be.true; - dao.deleteMessage({ - folder: inboxFolder, - message: message - }, function(err) { - expect(err).to.not.exist; - expect(imapDeleteStub.calledOnce).to.be.true; - expect(localDeleteStub.calledOnce).to.be.true; - expect(inboxFolder.messages).to.not.contain(message); - - done(); - }); - }); - - it('should delete from local, memory', function(done) { - var deleteOpts = { - folder: inboxFolder, - uid: message.uid - }; - - localDeleteStub.withArgs(deleteOpts).yieldsAsync(); - - dao.deleteMessage({ - folder: inboxFolder, - message: message, - localOnly: true - }, function(err) { - expect(err).to.not.exist; - expect(imapDeleteStub.called).to.be.false; - expect(localDeleteStub.calledOnce).to.be.true; - expect(inboxFolder.messages).to.not.contain(message); - - done(); - }); - }); - - it('should delete from outbox from local, memory', function(done) { - var deleteOpts = { - folder: outboxFolder, - uid: message.uid - }; - - localDeleteStub.withArgs(deleteOpts).yieldsAsync(); - - dao.deleteMessage({ - folder: outboxFolder, - message: message - }, function(err) { - expect(err).to.not.exist; - expect(imapDeleteStub.called).to.be.false; - expect(localDeleteStub.calledOnce).to.be.true; - expect(outboxFolder.messages).to.not.contain(message); - - done(); - }); - }); - - it('should fail at delete from local', function(done) { - var deleteOpts = { - folder: inboxFolder, - uid: message.uid - }; - - imapDeleteStub.withArgs(deleteOpts).yieldsAsync(); - localDeleteStub.withArgs(deleteOpts).yieldsAsync({}); - - dao.deleteMessage({ - folder: inboxFolder, - message: message - }, function(err) { - expect(err).to.exist; - expect(imapDeleteStub.calledOnce).to.be.true; - expect(localDeleteStub.calledOnce).to.be.true; - expect(inboxFolder.messages).to.contain(message); - - done(); - }); - }); - - it('should fail at delete from imap', function(done) { - var deleteOpts = { - folder: inboxFolder, - uid: message.uid - }; - - imapDeleteStub.withArgs(deleteOpts).yieldsAsync({}); - - dao.deleteMessage({ - folder: inboxFolder, - message: message - }, function(err) { - expect(err).to.exist; - expect(imapDeleteStub.calledOnce).to.be.true; - expect(localDeleteStub.called).to.be.false; - expect(inboxFolder.messages).to.contain(message); - - done(); - }); - }); - - it('should fail at delete from imap in offline', function(done) { - account.online = false; - dao.deleteMessage({ - folder: inboxFolder, - message: message - }, function(err) { - expect(err).to.exist; - expect(imapDeleteStub.called).to.be.false; - expect(localDeleteStub.called).to.be.false; - expect(inboxFolder.messages).to.contain(message); - - done(); - }); + done(); }); }); - describe('#setFlags', function() { - var imapMark, localListStub, localStoreStub, message; + it('should verify verification mails', function(done) { + message.subject = verificationSubject; - beforeEach(function() { - message = { - uid: 1234 - }; - imapMark = sinon.stub(dao, '_imapMark'); - localListStub = sinon.stub(dao, '_localListMessages'); - localStoreStub = sinon.stub(dao, '_localStoreMessages'); - inboxFolder.messages = [message]; - outboxFolder.messages = [message]; - }); + imapListStub.withArgs(opts).yieldsAsync(null, [message]); - it('should set flags for imap, disk, memory', function(done) { - imapMark.withArgs({ - folder: inboxFolder, - uid: message.uid, - unread: message.unread, - answered: message.answered - }).yieldsAsync(); + imapGetStub.withArgs({ + folder: inboxFolder, + uid: message.uid, + bodyParts: message.bodyParts + }).yieldsAsync(null, [{ + type: 'text', + content: '' + cfg.cloudUrl + cfg.verificationUrl + validUuid + }]); - localListStub.withArgs({ - folder: inboxFolder, - uid: message.uid - }).yieldsAsync(null, [message]); + keychainStub.verifyPublicKey.withArgs(validUuid).yieldsAsync(); - localStoreStub.withArgs({ - folder: inboxFolder, - emails: [message] - }).yieldsAsync(); + imapDeleteStub.withArgs({ + folder: inboxFolder, + uid: message.uid + }).yieldsAsync(); - dao.setFlags({ - folder: inboxFolder, - message: message - }, function(err) { - expect(err).to.not.exist; - expect(imapMark.calledOnce).to.be.true; - expect(localListStub.calledOnce).to.be.true; - expect(localStoreStub.calledOnce).to.be.true; - - done(); - }); - }); - - it('should set flags for outbox for disk, memory', function(done) { - localListStub.withArgs({ - folder: outboxFolder, - uid: message.uid - }).yieldsAsync(null, [message]); - - localStoreStub.withArgs({ - folder: outboxFolder, - emails: [message] - }).yieldsAsync(); - - dao.setFlags({ - folder: outboxFolder, - message: message - }, function(err) { - expect(err).to.not.exist; - expect(imapMark.called).to.be.false; - expect(localListStub.calledOnce).to.be.true; - expect(localStoreStub.calledOnce).to.be.true; - - done(); - }); - }); - - it('should set flags for disk, memory', function(done) { - localListStub.withArgs({ - folder: inboxFolder, - uid: message.uid - }).yieldsAsync(null, [message]); - - localStoreStub.withArgs({ - folder: inboxFolder, - emails: [message] - }).yieldsAsync(); - - dao.setFlags({ - folder: inboxFolder, - message: message, - localOnly: true - }, function(err) { - expect(err).to.not.exist; - expect(imapMark.called).to.be.false; - expect(localListStub.calledOnce).to.be.true; - expect(localStoreStub.calledOnce).to.be.true; - - done(); - }); - }); - - it('should fail to set flags for imap', function(done) { - imapMark.yieldsAsync({}); - localListStub.yieldsAsync(null, [message]); - localStoreStub.yieldsAsync(); - - dao.setFlags({ - folder: inboxFolder, - message: message - }, function(err) { - expect(err).to.exist; - expect(imapMark.calledOnce).to.be.true; - expect(localListStub.called).to.be.false; - expect(localStoreStub.called).to.be.false; - - done(); - }); - }); - it('should fail to set flags for imap in offline mode', function(done) { - account.online = false; - localListStub.yieldsAsync(null, [message]); - localStoreStub.yieldsAsync(); - - dao.setFlags({ - folder: inboxFolder, - message: message - }, function(err) { - expect(err).to.exist; - expect(imapMark.called).to.be.false; - expect(localListStub.called).to.be.false; - expect(localStoreStub.called).to.be.false; - - done(); - }); + dao.fetchMessages(opts, function(err) { + expect(err).to.not.exist; + expect(inboxFolder.messages).to.not.contain(message); + expect(notified).to.be.false; + expect(imapListStub.calledOnce).to.be.true; + expect(imapGetStub.calledOnce).to.be.true; + expect(keychainStub.verifyPublicKey.calledOnce).to.be.true; + expect(imapDeleteStub.calledOnce).to.be.true; + expect(localStoreStub.called).to.be.false; + done(); }); }); - describe('#getBody', function() { - var localListStub, localStoreStub, imapGetStub, uid; + it('should not verify invalid verification mails', function(done) { + message.subject = verificationSubject; - beforeEach(function() { - uid = 12345, - localListStub = sinon.stub(dao, '_localListMessages'); - localStoreStub = sinon.stub(dao, '_localStoreMessages'); - imapGetStub = sinon.stub(dao, '_getBodyParts'); + imapListStub.withArgs(opts).yieldsAsync(null, [message]); + + imapGetStub.withArgs({ + folder: inboxFolder, + uid: message.uid, + bodyParts: message.bodyParts + }).yieldsAsync(null, [{ + type: 'text', + content: '' + cfg.cloudUrl + cfg.verificationUrl + corruptedUuid + }]); + + localStoreStub.withArgs({ + folder: inboxFolder, + emails: [message] + }).yieldsAsync(); + + dao.fetchMessages(opts, function(err) { + expect(err).to.not.exist; + expect(inboxFolder.messages).to.contain(message); + expect(notified).to.be.true; + expect(imapListStub.calledOnce).to.be.true; + expect(imapGetStub.calledOnce).to.be.true; + expect(keychainStub.verifyPublicKey.called).to.be.false; + expect(imapDeleteStub.called).to.be.false; + expect(localStoreStub.calledOnce).to.be.true; + done(); + }); + }); + + it('should display verification mail when verification failed', function(done) { + message.subject = verificationSubject; + + imapListStub.withArgs(opts).yieldsAsync(null, [message]); + + imapGetStub.withArgs({ + folder: inboxFolder, + uid: message.uid, + bodyParts: message.bodyParts + }).yieldsAsync(null, [{ + type: 'text', + content: '' + cfg.cloudUrl + cfg.verificationUrl + validUuid + }]); + + keychainStub.verifyPublicKey.withArgs(validUuid).yieldsAsync({}); + + localStoreStub.withArgs({ + folder: inboxFolder, + emails: [message] + }).yieldsAsync(); + + + dao.fetchMessages(opts, function(err) { + expect(err).to.not.exist; + expect(inboxFolder.messages).to.contain(message); + expect(notified).to.be.true; + expect(imapListStub.calledOnce).to.be.true; + expect(imapGetStub.calledOnce).to.be.true; + expect(keychainStub.verifyPublicKey.calledOnce).to.be.true; + expect(imapDeleteStub.called).to.be.false; + expect(localStoreStub.calledOnce).to.be.true; + done(); + }); + }); + }); + + describe('#deleteMessage', function() { + var imapDeleteStub, localDeleteStub, message; + + beforeEach(function() { + message = { + uid: 1234 + }; + imapDeleteStub = sinon.stub(dao, '_imapDeleteMessage'); + localDeleteStub = sinon.stub(dao, '_localDeleteMessage'); + inboxFolder.messages = [message]; + outboxFolder.messages = [message]; + }); + + it('should delete from imap, local, memory', function(done) { + var deleteOpts = { + folder: inboxFolder, + uid: message.uid + }; + + imapDeleteStub.withArgs(deleteOpts).yieldsAsync(); + localDeleteStub.withArgs(deleteOpts).yieldsAsync(); + + dao.deleteMessage({ + folder: inboxFolder, + message: message + }, function(err) { + expect(err).to.not.exist; + expect(imapDeleteStub.calledOnce).to.be.true; + expect(localDeleteStub.calledOnce).to.be.true; + expect(inboxFolder.messages).to.not.contain(message); + + done(); + }); + }); + + it('should delete from local, memory', function(done) { + var deleteOpts = { + folder: inboxFolder, + uid: message.uid + }; + + localDeleteStub.withArgs(deleteOpts).yieldsAsync(); + + dao.deleteMessage({ + folder: inboxFolder, + message: message, + localOnly: true + }, function(err) { + expect(err).to.not.exist; + expect(imapDeleteStub.called).to.be.false; + expect(localDeleteStub.calledOnce).to.be.true; + expect(inboxFolder.messages).to.not.contain(message); + + done(); + }); + }); + + it('should delete from outbox from local, memory', function(done) { + var deleteOpts = { + folder: outboxFolder, + uid: message.uid + }; + + localDeleteStub.withArgs(deleteOpts).yieldsAsync(); + + dao.deleteMessage({ + folder: outboxFolder, + message: message + }, function(err) { + expect(err).to.not.exist; + expect(imapDeleteStub.called).to.be.false; + expect(localDeleteStub.calledOnce).to.be.true; + expect(outboxFolder.messages).to.not.contain(message); + + done(); + }); + }); + + it('should fail at delete from local', function(done) { + var deleteOpts = { + folder: inboxFolder, + uid: message.uid + }; + + imapDeleteStub.withArgs(deleteOpts).yieldsAsync(); + localDeleteStub.withArgs(deleteOpts).yieldsAsync({}); + + dao.deleteMessage({ + folder: inboxFolder, + message: message + }, function(err) { + expect(err).to.exist; + expect(imapDeleteStub.calledOnce).to.be.true; + expect(localDeleteStub.calledOnce).to.be.true; + expect(inboxFolder.messages).to.contain(message); + + done(); + }); + }); + + it('should fail at delete from imap', function(done) { + var deleteOpts = { + folder: inboxFolder, + uid: message.uid + }; + + imapDeleteStub.withArgs(deleteOpts).yieldsAsync({}); + + dao.deleteMessage({ + folder: inboxFolder, + message: message + }, function(err) { + expect(err).to.exist; + expect(imapDeleteStub.calledOnce).to.be.true; + expect(localDeleteStub.called).to.be.false; + expect(inboxFolder.messages).to.contain(message); + + done(); + }); + }); + + it('should fail at delete from imap in offline', function(done) { + account.online = false; + dao.deleteMessage({ + folder: inboxFolder, + message: message + }, function(err) { + expect(err).to.exist; + expect(imapDeleteStub.called).to.be.false; + expect(localDeleteStub.called).to.be.false; + expect(inboxFolder.messages).to.contain(message); + + done(); + }); + }); + }); + + describe('#setFlags', function() { + var imapMark, localListStub, localStoreStub, message; + + beforeEach(function() { + message = { + uid: 1234 + }; + imapMark = sinon.stub(dao, '_imapMark'); + localListStub = sinon.stub(dao, '_localListMessages'); + localStoreStub = sinon.stub(dao, '_localStoreMessages'); + inboxFolder.messages = [message]; + outboxFolder.messages = [message]; + }); + + it('should set flags for imap, disk, memory', function(done) { + imapMark.withArgs({ + folder: inboxFolder, + uid: message.uid, + unread: message.unread, + answered: message.answered + }).yieldsAsync(); + + localListStub.withArgs({ + folder: inboxFolder, + uid: message.uid + }).yieldsAsync(null, [message]); + + localStoreStub.withArgs({ + folder: inboxFolder, + emails: [message] + }).yieldsAsync(); + + dao.setFlags({ + folder: inboxFolder, + message: message + }, function(err) { + expect(err).to.not.exist; + expect(imapMark.calledOnce).to.be.true; + expect(localListStub.calledOnce).to.be.true; + expect(localStoreStub.calledOnce).to.be.true; + + done(); + }); + }); + + it('should set flags for outbox for disk, memory', function(done) { + localListStub.withArgs({ + folder: outboxFolder, + uid: message.uid + }).yieldsAsync(null, [message]); + + localStoreStub.withArgs({ + folder: outboxFolder, + emails: [message] + }).yieldsAsync(); + + dao.setFlags({ + folder: outboxFolder, + message: message + }, function(err) { + expect(err).to.not.exist; + expect(imapMark.called).to.be.false; + expect(localListStub.calledOnce).to.be.true; + expect(localStoreStub.calledOnce).to.be.true; + + done(); + }); + }); + + it('should set flags for disk, memory', function(done) { + localListStub.withArgs({ + folder: inboxFolder, + uid: message.uid + }).yieldsAsync(null, [message]); + + localStoreStub.withArgs({ + folder: inboxFolder, + emails: [message] + }).yieldsAsync(); + + dao.setFlags({ + folder: inboxFolder, + message: message, + localOnly: true + }, function(err) { + expect(err).to.not.exist; + expect(imapMark.called).to.be.false; + expect(localListStub.calledOnce).to.be.true; + expect(localStoreStub.calledOnce).to.be.true; + + done(); + }); + }); + + it('should fail to set flags for imap', function(done) { + imapMark.yieldsAsync({}); + localListStub.yieldsAsync(null, [message]); + localStoreStub.yieldsAsync(); + + dao.setFlags({ + folder: inboxFolder, + message: message + }, function(err) { + expect(err).to.exist; + expect(imapMark.calledOnce).to.be.true; + expect(localListStub.called).to.be.false; + expect(localStoreStub.called).to.be.false; + + done(); + }); + }); + it('should fail to set flags for imap in offline mode', function(done) { + account.online = false; + localListStub.yieldsAsync(null, [message]); + localStoreStub.yieldsAsync(); + + dao.setFlags({ + folder: inboxFolder, + message: message + }, function(err) { + expect(err).to.exist; + expect(imapMark.called).to.be.false; + expect(localListStub.called).to.be.false; + expect(localStoreStub.called).to.be.false; + + done(); + }); + }); + }); + + describe('#getBody', function() { + var localListStub, localStoreStub, imapGetStub, uid; + + beforeEach(function() { + uid = 12345, + localListStub = sinon.stub(dao, '_localListMessages'); + localStoreStub = sinon.stub(dao, '_localStoreMessages'); + imapGetStub = sinon.stub(dao, '_getBodyParts'); + }); + + it('should not do anything if the message already has content', function() { + var message = { + body: 'bender is great!' + }; + + dao.getBody({ + message: message }); - it('should not do anything if the message already has content', function() { - var message = { - body: 'bender is great!' - }; + // should do nothing + }); - dao.getBody({ - message: message - }); + it('should read an unencrypted body from the device', function(done) { + var message, body; - // should do nothing - }); + body = 'bender is great! bender is great!'; + message = { + uid: uid + }; - it('should read an unencrypted body from the device', function(done) { - var message, body; - - body = 'bender is great! bender is great!'; - message = { - uid: uid - }; - - localListStub.withArgs({ - folder: inboxFolder, - uid: uid - }).yieldsAsync(null, [{ - bodyParts: [{ - type: 'text', - content: body - }] - }]); - - dao.getBody({ - message: message, - folder: inboxFolder - }, function(err, msg) { - expect(err).to.not.exist; - - expect(msg).to.equal(message); - expect(msg.body).to.equal(body); - expect(msg.loadingBody).to.be.false; - - expect(localListStub.calledOnce).to.be.true; - - done(); - }); - expect(message.loadingBody).to.be.true; - }); - - it('should read a pgp/mime from the device', function(done) { - var message, ct, pt; - - pt = 'bender is great!'; - ct = '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----'; - message = { - uid: uid, - encrypted: true - }; - - localListStub.withArgs({ - folder: inboxFolder, - uid: uid - }).yieldsAsync(null, [{ - bodyParts: [{ - type: 'text', - content: pt - }, { - type: 'encrypted', - content: ct - }] - }]); - - dao.getBody({ - message: message, - folder: inboxFolder - }, function(err, msg) { - expect(err).to.not.exist; - - expect(msg).to.equal(message); - expect(msg.body).to.equal(ct); - expect(msg.encrypted).to.be.true; - expect(message.loadingBody).to.be.false; - - expect(localListStub.calledOnce).to.be.true; - - done(); - }); - expect(message.loadingBody).to.be.true; - }); - - it('should read a signed pgp/mime from the device', function(done) { - var message, signed, pt, signedMimeTree, signature; - - pt = 'bender is great!'; - signed = 'omg signed text'; - signedMimeTree = 'trallalalalala'; - signature = 'ugauga'; - message = { - uid: uid, - signed: true, - from: [{ - address: 'asdasdasd' - }] - }; - - localListStub.withArgs({ - folder: inboxFolder, - uid: uid - }).yieldsAsync(null, [{ - bodyParts: [{ - type: 'text', - content: pt - }, { - type: 'signed', - content: [{ - type: 'text', - content: signed - }], - signedMessage: signedMimeTree, - signature: signature - }] - }]); - keychainStub.getReceiverPublicKey.withArgs(message.from[0].address).yieldsAsync(null, mockKeyPair.publicKey); - pgpStub.verifySignedMessage.withArgs(signedMimeTree, signature, mockKeyPair.publicKey.publicKey).yieldsAsync(null, true); - - dao.getBody({ - message: message, - folder: inboxFolder - }, function(err, msg) { - expect(err).to.not.exist; - - expect(msg).to.equal(message); - expect(msg.body).to.equal(signed); - expect(message.signed).to.be.true; - expect(message.signaturesValid).to.be.true; - expect(message.loadingBody).to.be.false; - - expect(localListStub.calledOnce).to.be.true; - expect(pgpStub.verifySignedMessage.calledOnce).to.be.true; - expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true; - - done(); - }); - expect(message.loadingBody).to.be.true; - }); - - it('should read a pgp/inline from the device', function(done) { - var message, ct, pt; - - ct = '-----BEGIN PGP MESSAGE-----\nasdasdasd\n-----END PGP MESSAGE-----'; - pt = 'bla bla yadda yadda'; - message = { - uid: uid - }; - - localListStub.yieldsAsync(null, [{ - bodyParts: [{ - type: 'text', - content: pt - }, { - type: 'text', - content: ct - }, { - type: 'text', - content: pt - }] - }]); - - dao.getBody({ - message: message, - folder: inboxFolder - }, function(err, msg) { - expect(err).to.not.exist; - - expect(msg).to.equal(message); - expect(msg.body).to.equal(ct); - expect(msg.bodyParts[0].type).to.equal('encrypted'); - expect(msg.bodyParts[0].content).to.equal(ct); - expect(msg.encrypted).to.be.true; - expect(message.loadingBody).to.be.false; - - expect(localListStub.calledOnce).to.be.true; - - done(); - }); - expect(message.loadingBody).to.be.true; - }); - - it('should read a signed pgp/inline from the device', function(done) { - var message, pt, expected; - - expected = 'Lorem ipsum Aliquip tempor veniam proident.\n\nafguab;igab;igubalw\n\nLorem ipsum Dolor sed irure sint in non.\n\n\n'; - pt = '-----BEGIN PGP SIGNED MESSAGE-----\nHash: WTFHASH\n\n' + expected + '\n-----BEGIN PGP SIGNATURE----------END PGP SIGNATURE-----'; - message = { - uid: uid, - from: [{ - address: 'asdasdasd' - }] - }; - - localListStub.yieldsAsync(null, [{ - bodyParts: [{ - type: 'text', - content: pt - }] - }]); - keychainStub.getReceiverPublicKey.withArgs(message.from[0].address).yieldsAsync(null, mockKeyPair.publicKey); - pgpStub.verifyClearSignedMessage.withArgs(pt, mockKeyPair.publicKey.publicKey).yieldsAsync(null, true); - - dao.getBody({ - message: message, - folder: inboxFolder - }, function(err, msg) { - expect(err).to.not.exist; - - expect(msg).to.equal(message); - expect(msg.body).to.equal(expected); - expect(message.signed).to.be.true; - expect(message.signaturesValid).to.be.true; - expect(message.loadingBody).to.be.false; - - expect(localListStub.calledOnce).to.be.true; - expect(pgpStub.verifyClearSignedMessage.calledOnce).to.be.true; - expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true; - - done(); - }); - expect(message.loadingBody).to.be.true; - }); - - it('should stream from imap and set plain text body', function(done) { - var message, body, uid; - - body = 'bender is great! bender is great!'; - uid = 1234; - message = { - uid: uid, - bodyParts: [{ - type: 'text' - }] - }; - - localListStub.withArgs({ - folder: inboxFolder, - uid: uid - }).yieldsAsync(null, [message]); - - localStoreStub.withArgs({ - folder: inboxFolder, - emails: [message] - }).yieldsAsync(); - - imapGetStub.withArgs({ - folder: inboxFolder, - uid: message.uid, - bodyParts: message.bodyParts - }).yieldsAsync(null, [{ + localListStub.withArgs({ + folder: inboxFolder, + uid: uid + }).yieldsAsync(null, [{ + bodyParts: [{ type: 'text', content: body - }]); + }] + }]); - dao.getBody({ - message: message, - folder: inboxFolder - }, function(err, msg) { - expect(err).to.not.exist; + dao.getBody({ + message: message, + folder: inboxFolder + }, function(err, msg) { + expect(err).to.not.exist; - expect(msg).to.equal(message); - expect(msg.body).to.equal(body); - expect(msg.loadingBody).to.be.false; + expect(msg).to.equal(message); + expect(msg.body).to.equal(body); + expect(msg.loadingBody).to.be.false; - expect(localListStub.calledOnce).to.be.true; - expect(imapGetStub.calledOnce).to.be.true; - expect(localStoreStub.calledOnce).to.be.true; + expect(localListStub.calledOnce).to.be.true; - done(); - }); - expect(message.loadingBody).to.be.true; + done(); }); + expect(message.loadingBody).to.be.true; + }); - it('should stream from imap and set encrypted body', function(done) { - var message, ct, pt; + it('should read a pgp/mime from the device', function(done) { + var message, ct, pt; - pt = 'bender is great'; - ct = '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----'; - message = { - uid: uid, - encrypted: true, - bodyParts: [{ - type: 'text' - }, { - type: 'encrypted' - }] - }; + pt = 'bender is great!'; + ct = '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----'; + message = { + uid: uid, + encrypted: true + }; - localListStub.withArgs({ - folder: inboxFolder, - uid: uid - }).yieldsAsync(null, [message]); - - localStoreStub.withArgs({ - folder: inboxFolder, - emails: [message] - }).yieldsAsync(); - - imapGetStub.withArgs({ - folder: inboxFolder, - uid: message.uid, - bodyParts: message.bodyParts - }).yieldsAsync(null, [{ + localListStub.withArgs({ + folder: inboxFolder, + uid: uid + }).yieldsAsync(null, [{ + bodyParts: [{ type: 'text', content: pt }, { type: 'encrypted', content: ct - }]); + }] + }]); + dao.getBody({ + message: message, + folder: inboxFolder + }, function(err, msg) { + expect(err).to.not.exist; - dao.getBody({ - message: message, - folder: inboxFolder - }, function(err, msg) { - expect(err).to.not.exist; + expect(msg).to.equal(message); + expect(msg.body).to.equal(ct); + expect(msg.encrypted).to.be.true; + expect(message.loadingBody).to.be.false; - expect(msg).to.equal(message); - expect(msg.body).to.equal(ct); - expect(msg.encrypted).to.be.true; - expect(msg.loadingBody).to.be.false; + expect(localListStub.calledOnce).to.be.true; - expect(localListStub.calledOnce).to.be.true; - expect(imapGetStub.calledOnce).to.be.true; - expect(localStoreStub.calledOnce).to.be.true; - - done(); - }); - expect(message.loadingBody).to.be.true; + done(); }); + expect(message.loadingBody).to.be.true; + }); - it('fail to stream from imap due to error when persisting', function(done) { - var message = { - uid: uid, - bodyParts: [{ - type: 'text' - }] - }; + it('should read a signed pgp/mime from the device', function(done) { + var message, signed, pt, signedMimeTree, signature; - localListStub.yieldsAsync(null, [message]); - localStoreStub.yieldsAsync({}); - imapGetStub.yieldsAsync(null, [{ + pt = 'bender is great!'; + signed = 'omg signed text'; + signedMimeTree = 'trallalalalala'; + signature = 'ugauga'; + message = { + uid: uid, + signed: true, + from: [{ + address: 'asdasdasd' + }] + }; + + localListStub.withArgs({ + folder: inboxFolder, + uid: uid + }).yieldsAsync(null, [{ + bodyParts: [{ type: 'text', - content: 'bender is great! bender is great!' - }]); - - dao.getBody({ - message: message, - folder: inboxFolder - }, function(err, msg) { - expect(err).to.exist; - expect(msg).to.not.exist; - expect(localListStub.calledOnce).to.be.true; - expect(imapGetStub.calledOnce).to.be.true; - expect(localStoreStub.calledOnce).to.be.true; - - expect(message.loadingBody).to.be.false; - - done(); - }); - }); - - it('fail to stream from imap due to stream error', function(done) { - var message = { - uid: uid, - bodyParts: [{ - type: 'text' - }] - }; - - localListStub.yieldsAsync(null, [message]); - imapGetStub.yieldsAsync({}); - - dao.getBody({ - message: message, - folder: inboxFolder - }, function(err, msg) { - expect(err).to.exist; - expect(msg).to.not.exist; - expect(localListStub.calledOnce).to.be.true; - expect(imapGetStub.calledOnce).to.be.true; - expect(localStoreStub.called).to.be.false; - - expect(message.loadingBody).to.be.false; - - done(); - }); - }); - }); - - describe('#getAttachment', function() { - var imapGetStub, uid; - - beforeEach(function() { - uid = 123456; - imapGetStub = sinon.stub(dao, '_getBodyParts'); - }); - - it('should fetch an attachment from imap', function(done) { - var attmt = {}; - - imapGetStub.withArgs({ - folder: inboxFolder, - uid: uid, - bodyParts: [attmt] - }).yieldsAsync(null, [{ - content: 'CONTENT!!!' - }]); - - dao.getAttachment({ - folder: inboxFolder, - uid: uid, - attachment: attmt - }, function(err, fetchedAttmt) { - expect(err).to.not.exist; - expect(fetchedAttmt).to.equal(attmt); - expect(attmt.content).to.not.be.empty; - expect(imapGetStub.calledOnce).to.be.true; - - done(); - }); - }); - - it('should error during fetch', function(done) { - var attmt = {}; - - imapGetStub.yieldsAsync({}); - - dao.getAttachment({ - folder: inboxFolder, - uid: uid, - attachment: attmt - }, function(err, fetchedAttmt) { - expect(err).to.exist; - expect(fetchedAttmt).to.not.exist; - expect(imapGetStub.calledOnce).to.be.true; - - done(); - }); - }); - }); - - describe('#decryptBody', function() { - it('should do nothing when the message is not encrypted', function(done) { - var message = { - encrypted: false, - decrypted: true, - body: 'asd' - }; - - dao.decryptBody({ - message: message - }, done); - }); - - it('should do nothing when the message is already decrypted', function(done) { - var message = { - encrypted: true, - decrypted: true, - body: 'asd' - }; - - dao.decryptBody({ - message: message - }, done); - }); - - it('should do nothing when the message has no body', function(done) { - var message = { - encrypted: true, - decrypted: false, - body: '' - }; - - dao.decryptBody({ - message: message - }, done); - }); - - it('should do nothing when the message is decrypting', function(done) { - var message = { - encrypted: true, - decrypted: false, - body: 'asd', - decryptingBody: true - }; - - dao.decryptBody({ - message: message - }, done); - }); - - it('decrypt a pgp/mime message', function(done) { - var message, ct, pt, parsed; - - pt = 'bender is great'; - ct = '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----'; - parsed = 'bender! bender! bender!'; - message = { - from: [{ - address: 'asdasdasd' - }], - body: ct, - encrypted: true, - bodyParts: [{ - type: 'encrypted', - content: ct - }] - }; - - keychainStub.getReceiverPublicKey.withArgs(message.from[0].address).yieldsAsync(null, mockKeyPair.publicKey); - pgpStub.decrypt.withArgs(ct, mockKeyPair.publicKey.publicKey).yieldsAsync(null, pt, true); - parseStub.withArgs({ - bodyParts: [{ - type: 'encrypted', - content: ct, - raw: pt - }] - }).yieldsAsync(null, [{ - type: 'encrypted', + content: pt + }, { + type: 'signed', content: [{ type: 'text', - content: parsed - }] - }]); - - dao.decryptBody({ - message: message - }, function(error, msg) { - expect(error).to.not.exist; - expect(msg).to.equal(message); - expect(message.decrypted).to.be.true; - expect(message.signed).to.be.true; - expect(message.signaturesValid).to.be.true; - expect(message.body).to.equal(parsed); - expect(message.decryptingBody).to.be.false; - expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true; - expect(pgpStub.decrypt.calledOnce).to.be.true; - expect(parseStub.calledOnce).to.be.true; - - done(); - }); - - expect(message.decryptingBody).to.be.true; - }); - - it('decrypt a pgp/mime message with inner signature', function(done) { - var message, ct, pt, parsed, signed, signedMimeTree, signature; - - pt = 'bender is great'; - ct = '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----'; - signedMimeTree = 'trallalalalala'; - signature = 'ugauga'; - signed = 'omg signed text'; - parsed = 'bender! bender! bender!'; - message = { - from: [{ - address: 'asdasdasd' + content: signed }], - body: ct, - encrypted: true, - bodyParts: [{ - type: 'encrypted', - content: ct - }] - }; + signedMessage: signedMimeTree, + signature: signature + }] + }]); + keychainStub.getReceiverPublicKey.withArgs(message.from[0].address).yieldsAsync(null, mockKeyPair.publicKey); + pgpStub.verifySignedMessage.withArgs(signedMimeTree, signature, mockKeyPair.publicKey.publicKey).yieldsAsync(null, true); - keychainStub.getReceiverPublicKey.withArgs(message.from[0].address).yieldsAsync(null, mockKeyPair.publicKey); - pgpStub.decrypt.withArgs(ct, mockKeyPair.publicKey.publicKey).yieldsAsync(null, pt, undefined); - pgpStub.verifySignedMessage.withArgs(signedMimeTree, signature, mockKeyPair.publicKey.publicKey).yieldsAsync(null, true); + dao.getBody({ + message: message, + folder: inboxFolder + }, function(err, msg) { + expect(err).to.not.exist; - parseStub.withArgs({ - bodyParts: [{ - type: 'encrypted', - content: ct, - raw: pt - }] - }).yieldsAsync(null, [{ + expect(msg).to.equal(message); + expect(msg.body).to.equal(signed); + expect(message.signed).to.be.true; + expect(message.signaturesValid).to.be.true; + expect(message.loadingBody).to.be.false; + + expect(localListStub.calledOnce).to.be.true; + expect(pgpStub.verifySignedMessage.calledOnce).to.be.true; + expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true; + + done(); + }); + expect(message.loadingBody).to.be.true; + }); + + it('should read a pgp/inline from the device', function(done) { + var message, ct, pt; + + ct = '-----BEGIN PGP MESSAGE-----\nasdasdasd\n-----END PGP MESSAGE-----'; + pt = 'bla bla yadda yadda'; + message = { + uid: uid + }; + + localListStub.yieldsAsync(null, [{ + bodyParts: [{ + type: 'text', + content: pt + }, { + type: 'text', + content: ct + }, { + type: 'text', + content: pt + }] + }]); + + dao.getBody({ + message: message, + folder: inboxFolder + }, function(err, msg) { + expect(err).to.not.exist; + + expect(msg).to.equal(message); + expect(msg.body).to.equal(ct); + expect(msg.bodyParts[0].type).to.equal('encrypted'); + expect(msg.bodyParts[0].content).to.equal(ct); + expect(msg.encrypted).to.be.true; + expect(message.loadingBody).to.be.false; + + expect(localListStub.calledOnce).to.be.true; + + done(); + }); + expect(message.loadingBody).to.be.true; + }); + + it('should read a signed pgp/inline from the device', function(done) { + var message, pt, expected; + + expected = 'Lorem ipsum Aliquip tempor veniam proident.\n\nafguab;igab;igubalw\n\nLorem ipsum Dolor sed irure sint in non.\n\n\n'; + pt = '-----BEGIN PGP SIGNED MESSAGE-----\nHash: WTFHASH\n\n' + expected + '\n-----BEGIN PGP SIGNATURE----------END PGP SIGNATURE-----'; + message = { + uid: uid, + from: [{ + address: 'asdasdasd' + }] + }; + + localListStub.yieldsAsync(null, [{ + bodyParts: [{ + type: 'text', + content: pt + }] + }]); + keychainStub.getReceiverPublicKey.withArgs(message.from[0].address).yieldsAsync(null, mockKeyPair.publicKey); + pgpStub.verifyClearSignedMessage.withArgs(pt, mockKeyPair.publicKey.publicKey).yieldsAsync(null, true); + + dao.getBody({ + message: message, + folder: inboxFolder + }, function(err, msg) { + expect(err).to.not.exist; + + expect(msg).to.equal(message); + expect(msg.body).to.equal(expected); + expect(message.signed).to.be.true; + expect(message.signaturesValid).to.be.true; + expect(message.loadingBody).to.be.false; + + expect(localListStub.calledOnce).to.be.true; + expect(pgpStub.verifyClearSignedMessage.calledOnce).to.be.true; + expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true; + + done(); + }); + expect(message.loadingBody).to.be.true; + }); + + it('should stream from imap and set plain text body', function(done) { + var message, body, uid; + + body = 'bender is great! bender is great!'; + uid = 1234; + message = { + uid: uid, + bodyParts: [{ + type: 'text' + }] + }; + + localListStub.withArgs({ + folder: inboxFolder, + uid: uid + }).yieldsAsync(null, [message]); + + localStoreStub.withArgs({ + folder: inboxFolder, + emails: [message] + }).yieldsAsync(); + + imapGetStub.withArgs({ + folder: inboxFolder, + uid: message.uid, + bodyParts: message.bodyParts + }).yieldsAsync(null, [{ + type: 'text', + content: body + }]); + + dao.getBody({ + message: message, + folder: inboxFolder + }, function(err, msg) { + expect(err).to.not.exist; + + expect(msg).to.equal(message); + expect(msg.body).to.equal(body); + expect(msg.loadingBody).to.be.false; + + expect(localListStub.calledOnce).to.be.true; + expect(imapGetStub.calledOnce).to.be.true; + expect(localStoreStub.calledOnce).to.be.true; + + done(); + }); + expect(message.loadingBody).to.be.true; + }); + + it('should stream from imap and set encrypted body', function(done) { + var message, ct, pt; + + pt = 'bender is great'; + ct = '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----'; + message = { + uid: uid, + encrypted: true, + bodyParts: [{ + type: 'text' + }, { + type: 'encrypted' + }] + }; + + localListStub.withArgs({ + folder: inboxFolder, + uid: uid + }).yieldsAsync(null, [message]); + + localStoreStub.withArgs({ + folder: inboxFolder, + emails: [message] + }).yieldsAsync(); + + imapGetStub.withArgs({ + folder: inboxFolder, + uid: message.uid, + bodyParts: message.bodyParts + }).yieldsAsync(null, [{ + type: 'text', + content: pt + }, { + type: 'encrypted', + content: ct + }]); + + + dao.getBody({ + message: message, + folder: inboxFolder + }, function(err, msg) { + expect(err).to.not.exist; + + expect(msg).to.equal(message); + expect(msg.body).to.equal(ct); + expect(msg.encrypted).to.be.true; + expect(msg.loadingBody).to.be.false; + + expect(localListStub.calledOnce).to.be.true; + expect(imapGetStub.calledOnce).to.be.true; + expect(localStoreStub.calledOnce).to.be.true; + + done(); + }); + expect(message.loadingBody).to.be.true; + }); + + it('fail to stream from imap due to error when persisting', function(done) { + var message = { + uid: uid, + bodyParts: [{ + type: 'text' + }] + }; + + localListStub.yieldsAsync(null, [message]); + localStoreStub.yieldsAsync({}); + imapGetStub.yieldsAsync(null, [{ + type: 'text', + content: 'bender is great! bender is great!' + }]); + + dao.getBody({ + message: message, + folder: inboxFolder + }, function(err, msg) { + expect(err).to.exist; + expect(msg).to.not.exist; + expect(localListStub.calledOnce).to.be.true; + expect(imapGetStub.calledOnce).to.be.true; + expect(localStoreStub.calledOnce).to.be.true; + + expect(message.loadingBody).to.be.false; + + done(); + }); + }); + + it('fail to stream from imap due to stream error', function(done) { + var message = { + uid: uid, + bodyParts: [{ + type: 'text' + }] + }; + + localListStub.yieldsAsync(null, [message]); + imapGetStub.yieldsAsync({}); + + dao.getBody({ + message: message, + folder: inboxFolder + }, function(err, msg) { + expect(err).to.exist; + expect(msg).to.not.exist; + expect(localListStub.calledOnce).to.be.true; + expect(imapGetStub.calledOnce).to.be.true; + expect(localStoreStub.called).to.be.false; + + expect(message.loadingBody).to.be.false; + + done(); + }); + }); + }); + + describe('#getAttachment', function() { + var imapGetStub, uid; + + beforeEach(function() { + uid = 123456; + imapGetStub = sinon.stub(dao, '_getBodyParts'); + }); + + it('should fetch an attachment from imap', function(done) { + var attmt = {}; + + imapGetStub.withArgs({ + folder: inboxFolder, + uid: uid, + bodyParts: [attmt] + }).yieldsAsync(null, [{ + content: 'CONTENT!!!' + }]); + + dao.getAttachment({ + folder: inboxFolder, + uid: uid, + attachment: attmt + }, function(err, fetchedAttmt) { + expect(err).to.not.exist; + expect(fetchedAttmt).to.equal(attmt); + expect(attmt.content).to.not.be.empty; + expect(imapGetStub.calledOnce).to.be.true; + + done(); + }); + }); + + it('should error during fetch', function(done) { + var attmt = {}; + + imapGetStub.yieldsAsync({}); + + dao.getAttachment({ + folder: inboxFolder, + uid: uid, + attachment: attmt + }, function(err, fetchedAttmt) { + expect(err).to.exist; + expect(fetchedAttmt).to.not.exist; + expect(imapGetStub.calledOnce).to.be.true; + + done(); + }); + }); + }); + + describe('#decryptBody', function() { + it('should do nothing when the message is not encrypted', function(done) { + var message = { + encrypted: false, + decrypted: true, + body: 'asd' + }; + + dao.decryptBody({ + message: message + }, done); + }); + + it('should do nothing when the message is already decrypted', function(done) { + var message = { + encrypted: true, + decrypted: true, + body: 'asd' + }; + + dao.decryptBody({ + message: message + }, done); + }); + + it('should do nothing when the message has no body', function(done) { + var message = { + encrypted: true, + decrypted: false, + body: '' + }; + + dao.decryptBody({ + message: message + }, done); + }); + + it('should do nothing when the message is decrypting', function(done) { + var message = { + encrypted: true, + decrypted: false, + body: 'asd', + decryptingBody: true + }; + + dao.decryptBody({ + message: message + }, done); + }); + + it('decrypt a pgp/mime message', function(done) { + var message, ct, pt, parsed; + + pt = 'bender is great'; + ct = '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----'; + parsed = 'bender! bender! bender!'; + message = { + from: [{ + address: 'asdasdasd' + }], + body: ct, + encrypted: true, + bodyParts: [{ type: 'encrypted', + content: ct + }] + }; + + keychainStub.getReceiverPublicKey.withArgs(message.from[0].address).yieldsAsync(null, mockKeyPair.publicKey); + pgpStub.decrypt.withArgs(ct, mockKeyPair.publicKey.publicKey).yieldsAsync(null, pt, true); + parseStub.withArgs({ + bodyParts: [{ + type: 'encrypted', + content: ct, + raw: pt + }] + }).yieldsAsync(null, [{ + type: 'encrypted', + content: [{ + type: 'text', + content: parsed + }] + }]); + + dao.decryptBody({ + message: message + }, function(error, msg) { + expect(error).to.not.exist; + expect(msg).to.equal(message); + expect(message.decrypted).to.be.true; + expect(message.signed).to.be.true; + expect(message.signaturesValid).to.be.true; + expect(message.body).to.equal(parsed); + expect(message.decryptingBody).to.be.false; + expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true; + expect(pgpStub.decrypt.calledOnce).to.be.true; + expect(parseStub.calledOnce).to.be.true; + + done(); + }); + + expect(message.decryptingBody).to.be.true; + }); + + it('decrypt a pgp/mime message with inner signature', function(done) { + var message, ct, pt, parsed, signed, signedMimeTree, signature; + + pt = 'bender is great'; + ct = '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----'; + signedMimeTree = 'trallalalalala'; + signature = 'ugauga'; + signed = 'omg signed text'; + parsed = 'bender! bender! bender!'; + message = { + from: [{ + address: 'asdasdasd' + }], + body: ct, + encrypted: true, + bodyParts: [{ + type: 'encrypted', + content: ct + }] + }; + + keychainStub.getReceiverPublicKey.withArgs(message.from[0].address).yieldsAsync(null, mockKeyPair.publicKey); + pgpStub.decrypt.withArgs(ct, mockKeyPair.publicKey.publicKey).yieldsAsync(null, pt, undefined); + pgpStub.verifySignedMessage.withArgs(signedMimeTree, signature, mockKeyPair.publicKey.publicKey).yieldsAsync(null, true); + + parseStub.withArgs({ + bodyParts: [{ + type: 'encrypted', + content: ct, + raw: pt + }] + }).yieldsAsync(null, [{ + type: 'encrypted', + content: [{ + type: 'signed', content: [{ - type: 'signed', - content: [{ - type: 'text', - content: signed - }], - signedMessage: signedMimeTree, - signature: signature - }] - }]); + type: 'text', + content: signed + }], + signedMessage: signedMimeTree, + signature: signature + }] + }]); - dao.decryptBody({ - message: message - }, function(error, msg) { - expect(error).to.not.exist; - expect(msg).to.equal(message); - expect(message.decrypted).to.be.true; - expect(message.body).to.equal(signed); - expect(message.signed).to.be.true; - expect(message.signaturesValid).to.be.true; - expect(message.decryptingBody).to.be.false; - expect(keychainStub.getReceiverPublicKey.calledTwice).to.be.true; - expect(pgpStub.decrypt.calledOnce).to.be.true; - expect(pgpStub.verifySignedMessage.calledOnce).to.be.true; - expect(parseStub.calledOnce).to.be.true; + dao.decryptBody({ + message: message + }, function(error, msg) { + expect(error).to.not.exist; + expect(msg).to.equal(message); + expect(message.decrypted).to.be.true; + expect(message.body).to.equal(signed); + expect(message.signed).to.be.true; + expect(message.signaturesValid).to.be.true; + expect(message.decryptingBody).to.be.false; + expect(keychainStub.getReceiverPublicKey.calledTwice).to.be.true; + expect(pgpStub.decrypt.calledOnce).to.be.true; + expect(pgpStub.verifySignedMessage.calledOnce).to.be.true; + expect(parseStub.calledOnce).to.be.true; - done(); - }); - - expect(message.decryptingBody).to.be.true; + done(); }); - it('decrypt a pgp/inline message', function(done) { - var message, ct, pt; + expect(message.decryptingBody).to.be.true; + }); - pt = 'bender is great'; - ct = '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----'; - message = { - from: [{ - address: 'asdasdasd' - }], - body: ct, - encrypted: true, - bodyParts: [{ - type: 'encrypted', - content: ct, - _isPgpInline: true - }] - }; + it('decrypt a pgp/inline message', function(done) { + var message, ct, pt; - keychainStub.getReceiverPublicKey.withArgs(message.from[0].address).yieldsAsync(null, mockKeyPair.publicKey); - pgpStub.decrypt.withArgs(ct, mockKeyPair.publicKey.publicKey).yieldsAsync(null, pt, true); + pt = 'bender is great'; + ct = '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----'; + message = { + from: [{ + address: 'asdasdasd' + }], + body: ct, + encrypted: true, + bodyParts: [{ + type: 'encrypted', + content: ct, + _isPgpInline: true + }] + }; - dao.decryptBody({ - message: message - }, function(error, msg) { - expect(error).to.not.exist; + keychainStub.getReceiverPublicKey.withArgs(message.from[0].address).yieldsAsync(null, mockKeyPair.publicKey); + pgpStub.decrypt.withArgs(ct, mockKeyPair.publicKey.publicKey).yieldsAsync(null, pt, true); - expect(msg).to.equal(message); - expect(message.decrypted).to.be.true; - expect(message.body).to.equal(pt); - expect(message.decryptingBody).to.be.false; - expect(message.signed).to.be.true; - expect(message.signaturesValid).to.be.true; - expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true; - expect(pgpStub.decrypt.calledOnce).to.be.true; - expect(parseStub.called).to.be.false; + dao.decryptBody({ + message: message + }, function(error, msg) { + expect(error).to.not.exist; - done(); - }); + expect(msg).to.equal(message); + expect(message.decrypted).to.be.true; + expect(message.body).to.equal(pt); + expect(message.decryptingBody).to.be.false; + expect(message.signed).to.be.true; + expect(message.signaturesValid).to.be.true; + expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true; + expect(pgpStub.decrypt.calledOnce).to.be.true; + expect(parseStub.called).to.be.false; - expect(message.decryptingBody).to.be.true; + done(); }); - it('should fail during decryption message', function(done) { - var message = { - from: [{ - address: 'asdasdasd' - }], - body: 'asdjafuad', - encrypted: true, - bodyParts: [{ - type: 'encrypted', - content: '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----' - }] - }; + expect(message.decryptingBody).to.be.true; + }); - keychainStub.getReceiverPublicKey.yieldsAsync(null, mockKeyPair.publicKey); - pgpStub.decrypt.yieldsAsync(new Error('fail.')); + it('should fail during decryption message', function(done) { + var message = { + from: [{ + address: 'asdasdasd' + }], + body: 'asdjafuad', + encrypted: true, + bodyParts: [{ + type: 'encrypted', + content: '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----' + }] + }; - dao.decryptBody({ - message: message - }, function(error, msg) { - expect(error).to.not.exist; - expect(msg.body).to.equal('fail.'); - expect(msg).to.exist; - expect(message.decryptingBody).to.be.false; - expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true; - expect(pgpStub.decrypt.calledOnce).to.be.true; - expect(parseStub.called).to.be.false; + keychainStub.getReceiverPublicKey.yieldsAsync(null, mockKeyPair.publicKey); + pgpStub.decrypt.yieldsAsync(new Error('fail.')); - done(); - }); - }); + dao.decryptBody({ + message: message + }, function(error, msg) { + expect(error).to.not.exist; + expect(msg.body).to.equal('fail.'); + expect(msg).to.exist; + expect(message.decryptingBody).to.be.false; + expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true; + expect(pgpStub.decrypt.calledOnce).to.be.true; + expect(parseStub.called).to.be.false; - it('should fail during key export', function(done) { - var message = { - from: [{ - address: 'asdasdasd' - }], - encrypted: true, - body: 'asdjafuad', - bodyParts: [{ - type: 'encrypted', - content: '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----' - }] - }; - - keychainStub.getReceiverPublicKey.yieldsAsync({}); - - dao.decryptBody({ - message: message - }, function(error, msg) { - expect(error).to.exist; - expect(msg).to.not.exist; - expect(message.decryptingBody).to.be.false; - expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true; - expect(pgpStub.decrypt.called).to.be.false; - expect(parseStub.called).to.be.false; - - done(); - }); + done(); }); }); - describe('#sendEncrypted', function() { - var publicKeys = ["PUBLIC KEY"], - dummyMail = { - publicKeysArmored: publicKeys - }; + it('should fail during key export', function(done) { + var message = { + from: [{ + address: 'asdasdasd' + }], + encrypted: true, + body: 'asdjafuad', + bodyParts: [{ + type: 'encrypted', + content: '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----' + }] + }; - it('should send encrypted and upload to sent', function(done) { - var msg = 'wow. such message. much rfc2822.'; + keychainStub.getReceiverPublicKey.yieldsAsync({}); - imapClientStub.uploadMessage.withArgs({ - path: sentFolder.path, - message: msg - }).yields(); + dao.decryptBody({ + message: message + }, function(error, msg) { + expect(error).to.exist; + expect(msg).to.not.exist; + expect(message.decryptingBody).to.be.false; + expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true; + expect(pgpStub.decrypt.called).to.be.false; + expect(parseStub.called).to.be.false; - pgpMailerStub.send.withArgs({ - encrypt: true, - mail: dummyMail, - smtpclient: undefined, - publicKeysArmored: publicKeys - }).yieldsAsync(null, msg); - - dao.sendEncrypted({ - email: dummyMail - }, function(err) { - expect(err).to.not.exist; - - expect(pgpMailerStub.send.calledOnce).to.be.true; - expect(imapClientStub.uploadMessage.calledOnce).to.be.true; - - done(); - }); - }); - - it('should send encrypted and not upload to sent', function(done) { - var msg = 'wow. such message. much rfc2822.'; - - dao.ignoreUploadOnSent = true; - - pgpMailerStub.send.withArgs({ - encrypt: true, - mail: dummyMail, - smtpclient: undefined, - publicKeysArmored: publicKeys - }).yieldsAsync(null, msg); - - dao.sendEncrypted({ - email: dummyMail - }, function(err) { - expect(err).to.not.exist; - - expect(pgpMailerStub.send.calledOnce).to.be.true; - expect(imapClientStub.uploadMessage.called).to.be.false; - - done(); - }); - }); - - it('should not send when pgpmailer fails', function(done) { - pgpMailerStub.send.yieldsAsync({}); - - dao.sendEncrypted({ - email: dummyMail - }, function(err) { - expect(err).to.exist; - - expect(pgpMailerStub.send.calledOnce).to.be.true; - expect(imapClientStub.uploadMessage.called).to.be.false; - - done(); - }); - }); - - it('should not send in offline mode', function(done) { - account.online = false; - - dao.sendEncrypted({}, function(err) { - expect(err).to.exist; - expect(pgpMailerStub.send.called).to.be.false; - expect(imapClientStub.uploadMessage.called).to.be.false; - done(); - }); - }); - - }); - - describe('#sendPlaintext', function() { - var dummyMail = {}; - - it('should send in the plain and upload to sent', function(done) { - var msg = 'wow. such message. much rfc2822.'; - - pgpMailerStub.send.withArgs({ - smtpclient: undefined, - mail: dummyMail - }).yieldsAsync(null, msg); - - imapClientStub.uploadMessage.withArgs({ - path: sentFolder.path, - message: msg - }).yields(); - - dao.sendPlaintext({ - email: dummyMail - }, function(err) { - expect(err).to.not.exist; - expect(pgpMailerStub.send.calledOnce).to.be.true; - expect(imapClientStub.uploadMessage.calledOnce).to.be.true; - done(); - }); - }); - - it('should send in the plain and not upload to sent', function(done) { - var msg = 'wow. such message. much rfc2822.'; - - dao.ignoreUploadOnSent = true; - - pgpMailerStub.send.withArgs({ - smtpclient: undefined, - mail: dummyMail - }).yieldsAsync(null, msg); - - dao.sendPlaintext({ - email: dummyMail - }, function(err) { - expect(err).to.not.exist; - expect(pgpMailerStub.send.calledOnce).to.be.true; - expect(imapClientStub.uploadMessage.called).to.be.false; - done(); - }); - }); - - it('should not send due to error', function(done) { - pgpMailerStub.send.yieldsAsync({}); - - dao.sendPlaintext({ - email: dummyMail - }, function(err) { - expect(err).to.exist; - expect(pgpMailerStub.send.calledOnce).to.be.true; - expect(imapClientStub.uploadMessage.called).to.be.false; - done(); - }); - }); - - it('should not send in offline mode', function(done) { - account.online = false; - - dao.sendPlaintext({}, function(err) { - expect(err).to.exist; - expect(pgpMailerStub.send.called).to.be.false; - expect(imapClientStub.uploadMessage.called).to.be.false; - done(); - }); - }); - }); - - describe('#encrypt', function() { - it('should encrypt', function(done) { - pgpBuilderStub.encrypt.yieldsAsync(); - - dao.encrypt({}, function() { - expect(pgpBuilderStub.encrypt.calledOnce).to.be.true; - done(); - }); + done(); }); }); }); - describe('event handlers', function() { + describe('#sendEncrypted', function() { + var publicKeys = ["PUBLIC KEY"], + dummyMail = { + publicKeysArmored: publicKeys + }; - describe('#onConnect', function() { - var initFoldersStub; + it('should send encrypted and upload to sent', function(done) { + var msg = 'wow. such message. much rfc2822.'; - beforeEach(function() { - initFoldersStub = sinon.stub(dao, '_initFoldersFromImap'); - delete dao._imapClient; - delete dao._pgpMailer; - }); + imapClientStub.uploadMessage.withArgs({ + path: sentFolder.path, + message: msg + }).yields(); - it('should connect', function(done) { - inboxFolder.messages = [{ - uid: 123, - modseq: 123 - }]; - imapClientStub.login.yieldsAsync(); - imapClientStub.listenForChanges.yieldsAsync(); - initFoldersStub.yieldsAsync(); + pgpMailerStub.send.withArgs({ + encrypt: true, + mail: dummyMail, + smtpclient: undefined, + publicKeysArmored: publicKeys + }).yieldsAsync(null, msg); - dao.onConnect({ - imapClient: imapClientStub, - pgpMailer: pgpMailerStub - }, function(err) { + dao.sendEncrypted({ + email: dummyMail + }, function(err) { + expect(err).to.not.exist; - expect(err).to.not.exist; - expect(imapClientStub.login.calledOnce).to.be.true; - expect(initFoldersStub.calledOnce).to.be.true; - expect(imapClientStub.mailboxCache).to.deep.equal({ - 'INBOX': { - exists: 123, - uidNext: 124, - uidlist: [123], - highestModseq: 123 - } - }); + expect(pgpMailerStub.send.calledOnce).to.be.true; + expect(imapClientStub.uploadMessage.calledOnce).to.be.true; - done(); - }); + done(); }); }); - describe('#onDisconnect', function() { - it('should discard imapClient and pgpMailer', function(done) { - imapClientStub.logout.yieldsAsync(); + it('should send encrypted and not upload to sent', function(done) { + var msg = 'wow. such message. much rfc2822.'; - dao.onDisconnect(done); + dao.ignoreUploadOnSent = true; - expect(dao._account.online).to.be.false; - expect(dao._imapClient).to.not.exist; - expect(dao._pgpMailer).to.not.exist; + pgpMailerStub.send.withArgs({ + encrypt: true, + mail: dummyMail, + smtpclient: undefined, + publicKeysArmored: publicKeys + }).yieldsAsync(null, msg); + + dao.sendEncrypted({ + email: dummyMail + }, function(err) { + expect(err).to.not.exist; + + expect(pgpMailerStub.send.calledOnce).to.be.true; + expect(imapClientStub.uploadMessage.called).to.be.false; + + done(); }); }); - describe('#_onSyncUpdate', function() { - var fetchMessagesStub, deleteMessagesStub, setFlagsStub, msgs; + it('should not send when pgpmailer fails', function(done) { + pgpMailerStub.send.yieldsAsync({}); - beforeEach(function() { - msgs = [{ - uid: 5, - flags: ['\\Answered', '\\Seen'] - }]; - inboxFolder.messages = msgs; - fetchMessagesStub = sinon.stub(dao, 'fetchMessages'); - deleteMessagesStub = sinon.stub(dao, 'deleteMessage'); - setFlagsStub = sinon.stub(dao, 'setFlags'); + dao.sendEncrypted({ + email: dummyMail + }, function(err) { + expect(err).to.exist; + + expect(pgpMailerStub.send.calledOnce).to.be.true; + expect(imapClientStub.uploadMessage.called).to.be.false; + + done(); }); + }); - it('should get new message', function(done) { - fetchMessagesStub.withArgs({ - folder: inboxFolder, - firstUid: 1, - lastUid: 3 - }).yieldsAsync(); + it('should not send in offline mode', function(done) { + account.online = false; - dao.onError = function(err) { - expect(err).to.not.exist; - expect(fetchMessagesStub.calledOnce).to.be.true; - done(); - }; - - dao._onSyncUpdate({ - type: 'new', - path: inboxFolder.path, - list: [1, 3] - }); + dao.sendEncrypted({}, function(err) { + expect(err).to.exist; + expect(pgpMailerStub.send.called).to.be.false; + expect(imapClientStub.uploadMessage.called).to.be.false; + done(); }); + }); - it('should delete message', function(done) { - deleteMessagesStub.withArgs({ - folder: inboxFolder, - message: msgs[0], - localOnly: true - }).yieldsAsync(); + }); - dao.onError = function(err) { - expect(err).to.not.exist; - expect(deleteMessagesStub.calledOnce).to.be.true; - done(); - }; + describe('#sendPlaintext', function() { + var dummyMail = {}; - dao._onSyncUpdate({ - type: 'deleted', - path: inboxFolder.path, - list: [5] - }); + it('should send in the plain and upload to sent', function(done) { + var msg = 'wow. such message. much rfc2822.'; + + pgpMailerStub.send.withArgs({ + smtpclient: undefined, + mail: dummyMail + }).yieldsAsync(null, msg); + + imapClientStub.uploadMessage.withArgs({ + path: sentFolder.path, + message: msg + }).yields(); + + dao.sendPlaintext({ + email: dummyMail + }, function(err) { + expect(err).to.not.exist; + expect(pgpMailerStub.send.calledOnce).to.be.true; + expect(imapClientStub.uploadMessage.calledOnce).to.be.true; + done(); }); + }); - it('should fetch flags', function(done) { - setFlagsStub.withArgs({ - folder: inboxFolder, - message: msgs[0], - localOnly: true - }).yieldsAsync(); + it('should send in the plain and not upload to sent', function(done) { + var msg = 'wow. such message. much rfc2822.'; - dao.onError = function(err) { - expect(err).to.not.exist; - expect(setFlagsStub.calledOnce).to.be.true; - done(); - }; + dao.ignoreUploadOnSent = true; - dao._onSyncUpdate({ - type: 'messages', - path: inboxFolder.path, - list: msgs - }); + pgpMailerStub.send.withArgs({ + smtpclient: undefined, + mail: dummyMail + }).yieldsAsync(null, msg); + + dao.sendPlaintext({ + email: dummyMail + }, function(err) { + expect(err).to.not.exist; + expect(pgpMailerStub.send.calledOnce).to.be.true; + expect(imapClientStub.uploadMessage.called).to.be.false; + done(); + }); + }); + + it('should not send due to error', function(done) { + pgpMailerStub.send.yieldsAsync({}); + + dao.sendPlaintext({ + email: dummyMail + }, function(err) { + expect(err).to.exist; + expect(pgpMailerStub.send.calledOnce).to.be.true; + expect(imapClientStub.uploadMessage.called).to.be.false; + done(); + }); + }); + + it('should not send in offline mode', function(done) { + account.online = false; + + dao.sendPlaintext({}, function(err) { + expect(err).to.exist; + expect(pgpMailerStub.send.called).to.be.false; + expect(imapClientStub.uploadMessage.called).to.be.false; + done(); }); }); }); + describe('#encrypt', function() { + it('should encrypt', function(done) { + pgpBuilderStub.encrypt.yieldsAsync(); - describe('internal API', function() { - describe('#_checkSignatures', function() { - it('should check signatures in clearsigned message', function(done) { - var message = { - from: [{ - address: 'asdasdasd' - }], - clearSignedMessage: 'trallalalalala' - }; - - keychainStub.getReceiverPublicKey.withArgs(message.from[0].address).yieldsAsync(null, mockKeyPair.publicKey); - pgpStub.verifyClearSignedMessage.withArgs(message.clearSignedMessage, mockKeyPair.publicKey.publicKey).yieldsAsync(null, true); - - dao._checkSignatures(message, function(error, signaturesValid) { - expect(error).to.not.exist; - expect(signaturesValid).to.be.true; - expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true; - expect(pgpStub.verifyClearSignedMessage.calledOnce).to.be.true; - done(); - }); + dao.encrypt({}, function() { + expect(pgpBuilderStub.encrypt.calledOnce).to.be.true; + done(); }); - - it('should check signatures in pgp/mime signed message', function(done) { - var message = { - from: [{ - address: 'asdasdasd' - }], - signedMessage: 'trallalalalala', - signature: 'ugauga' - }; - - keychainStub.getReceiverPublicKey.withArgs(message.from[0].address).yieldsAsync(null, mockKeyPair.publicKey); - pgpStub.verifySignedMessage.withArgs(message.signedMessage, message.signature, mockKeyPair.publicKey.publicKey).yieldsAsync(null, true); - - dao._checkSignatures(message, function(error, signaturesValid) { - expect(error).to.not.exist; - expect(signaturesValid).to.be.true; - expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true; - expect(pgpStub.verifySignedMessage.calledOnce).to.be.true; - done(); - }); - }); - - it('should error while checking signatures', function(done) { - var message = { - from: [{ - address: 'asdasdasd' - }], - signedMessage: 'trallalalalala', - signature: 'ugauga' - }; - - keychainStub.getReceiverPublicKey.withArgs(message.from[0].address).yieldsAsync(null, mockKeyPair.publicKey); - pgpStub.verifySignedMessage.yieldsAsync(new Error()); - - dao._checkSignatures(message, function(error, signaturesValid) { - expect(error).to.exist; - expect(signaturesValid).to.not.exist; - expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true; - expect(pgpStub.verifySignedMessage.calledOnce).to.be.true; - done(); - }); - }); - - it('should error while fetching public key', function(done) { - var message = { - from: [{ - address: 'asdasdasd' - }], - signedMessage: 'trallalalalala', - signature: 'ugauga' - }; - - keychainStub.getReceiverPublicKey.yieldsAsync(new Error()); - - dao._checkSignatures(message, function(error, signaturesValid) { - expect(error).to.exist; - expect(signaturesValid).to.not.exist; - expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true; - expect(pgpStub.verifySignedMessage.called).to.be.false; - done(); - }); - }); - }); - - describe('#_initFoldersFromDisk', function() { - beforeEach(function() { - sinon.stub(dao, 'refreshFolder'); - }); - - it('should initialize from disk if offline and not refresh folder', function(done) { - devicestorageStub.listItems.withArgs('folders').yieldsAsync(null, [ - [inboxFolder] - ]); - dao.refreshFolder.withArgs({ - folder: inboxFolder - }).yieldsAsync(); - - dao._initFoldersFromDisk(function(err) { - expect(err).to.not.exist; - expect(devicestorageStub.listItems.calledOnce).to.be.true; - expect(dao.refreshFolder.called).to.be.false; - done(); - }); - }); - - it('should initialize from disk if offline and refresh folder', function(done) { - delete inboxFolder.messages; - devicestorageStub.listItems.withArgs('folders').yieldsAsync(null, [ - [inboxFolder] - ]); - dao.refreshFolder.withArgs({ - folder: inboxFolder - }).yieldsAsync(); - - dao._initFoldersFromDisk(function(err) { - expect(err).to.not.exist; - expect(devicestorageStub.listItems.calledOnce).to.be.true; - expect(dao.refreshFolder.calledOnce).to.be.true; - done(); - }); - }); - }); - - describe('#_initFoldersFromImap', function() { - beforeEach(function() { - sinon.stub(dao, 'refreshFolder'); - }); - - it('should initialize from imap if online', function(done) { - account.folders = []; - imapClientStub.listWellKnownFolders.yieldsAsync(null, { - Inbox: [inboxFolder], - Sent: [sentFolder], - Drafts: [draftsFolder], - Trash: [trashFolder] - }); - devicestorageStub.storeList.withArgs(sinon.match(function(arg) { - expect(arg[0][0].name).to.deep.equal(inboxFolder.name); - expect(arg[0][0].path).to.deep.equal(inboxFolder.path); - expect(arg[0][0].type).to.deep.equal(inboxFolder.type); - expect(arg[0][1].name).to.deep.equal(sentFolder.name); - expect(arg[0][1].path).to.deep.equal(sentFolder.path); - expect(arg[0][1].type).to.deep.equal(sentFolder.type); - expect(arg[0][2].name).to.deep.equal(outboxFolder.name); - expect(arg[0][2].path).to.deep.equal(outboxFolder.path); - expect(arg[0][2].type).to.deep.equal(outboxFolder.type); - expect(arg[0][3].name).to.deep.equal(draftsFolder.name); - expect(arg[0][3].path).to.deep.equal(draftsFolder.path); - expect(arg[0][3].type).to.deep.equal(draftsFolder.type); - expect(arg[0][4].name).to.deep.equal(trashFolder.name); - expect(arg[0][4].path).to.deep.equal(trashFolder.path); - expect(arg[0][4].type).to.deep.equal(trashFolder.type); - return true; - }), 'folders').yieldsAsync(); - - dao.refreshFolder.yieldsAsync(); - - dao._initFoldersFromImap(function(err) { - expect(err).to.not.exist; - expect(imapClientStub.listWellKnownFolders.calledOnce).to.be.true; - expect(devicestorageStub.storeList.calledOnce).to.be.true; - done(); - }); - }); - - it('should update folders from imap', function(done) { - account.folders = [inboxFolder, outboxFolder, trashFolder, { - name: 'foo', - type: 'Sent', - path: 'bar', - }]; - - imapClientStub.listWellKnownFolders.yieldsAsync(null, { - Inbox: [inboxFolder], - Sent: [sentFolder], - Drafts: [draftsFolder], - Trash: [trashFolder] - }); - devicestorageStub.storeList.withArgs(sinon.match(function(arg) { - expect(arg[0]).to.deep.equal([{ - name: inboxFolder.name, - path: inboxFolder.path, - type: inboxFolder.type - }, { - name: outboxFolder.name, - path: outboxFolder.path, - type: outboxFolder.type - }, { - name: trashFolder.name, - path: trashFolder.path, - type: trashFolder.type - }, { - name: sentFolder.name, - path: sentFolder.path, - type: sentFolder.type - }, { - name: draftsFolder.name, - path: draftsFolder.path, - type: draftsFolder.type - }]); - - return true; - }), 'folders').yieldsAsync(); - - dao.refreshFolder.yieldsAsync(); - - dao._initFoldersFromImap(function(err) { - expect(err).to.not.exist; - expect(imapClientStub.listWellKnownFolders.calledOnce).to.be.true; - expect(devicestorageStub.storeList.calledOnce).to.be.true; - done(); - }); - }); - }); - - describe('#_imapMark', function() { - it('should flag a mail', function(done) { - imapClientStub.updateFlags.withArgs({ - path: inboxFolder.path, - folder: inboxFolder, - uid: 1, - unread: false, - answered: false - }).yieldsAsync(); - - dao._imapMark({ - folder: inboxFolder, - uid: 1, - unread: false, - answered: false - }, function(err) { - expect(err).to.not.exist; - expect(imapClientStub.updateFlags.calledOnce).to.be.true; - done(); - }); - }); - }); - - describe('#_imapDeleteMessage', function() { - var uid = 1337; - - it('should fail when disconnected', function(done) { - dao._account.online = false; - - dao._imapDeleteMessage({}, function(err) { - expect(err.code).to.equal(42); - done(); - }); - }); - - it('should move to trash', function(done) { - imapClientStub.moveMessage.withArgs({ - path: inboxFolder.path, - uid: uid, - destination: trashFolder.path - }).yieldsAsync(); - - dao._imapDeleteMessage({ - folder: inboxFolder, - uid: uid - }, done); - }); - - it('should purge message', function(done) { - imapClientStub.deleteMessage.withArgs({ - path: trashFolder.path, - uid: uid - }).yieldsAsync(); - - dao._imapDeleteMessage({ - folder: trashFolder, - uid: uid - }, done); - }); - }); - - describe('#_imapListMessages', function() { - var firstUid = 1337, - lastUid = 1339; - - it('should list messages', function(done) { - imapClientStub.listMessages.withArgs({ - folder: inboxFolder, - path: inboxFolder.path, - firstUid: firstUid, - lastUid: lastUid - }).yieldsAsync(null, []); - - dao._imapListMessages({ - folder: inboxFolder, - firstUid: firstUid, - lastUid: lastUid - }, function(err, msgs) { - expect(err).to.not.exist; - expect(msgs).to.exist; - - expect(imapClientStub.listMessages.calledOnce).to.be.true; - - done(); - }); - }); - - it('should fail when listMessages fails', function(done) { - imapClientStub.listMessages.yieldsAsync({}); - - dao._imapListMessages({ - folder: inboxFolder, - firstUid: firstUid, - lastUid: lastUid - }, function(err, msgs) { - expect(err).to.exist; - expect(msgs).to.not.exist; - expect(imapClientStub.listMessages.calledOnce).to.be.true; - - done(); - }); - }); - - it('should fail when disconnected', function(done) { - dao._account.online = false; - - dao._imapListMessages({}, function(err) { - expect(err.code).to.equal(42); - done(); - }); - }); - }); - - describe('#_getBodyParts', function() { - it('should get bodyParts', function(done) { - imapClientStub.getBodyParts.withArgs({ - folder: inboxFolder, - path: inboxFolder.path, - uid: 123, - bodyParts: [] - }).yieldsAsync(null, {}); - parseStub.yieldsAsync(null, []); - - dao._getBodyParts({ - folder: inboxFolder, - uid: 123, - bodyParts: [] - }, function(err, parts) { - expect(err).to.not.exist; - expect(parts).to.exist; - - expect(imapClientStub.getBodyParts.calledOnce).to.be.true; - expect(parseStub.calledOnce).to.be.true; - - done(); - }); - }); - - it('should fail when getBody fails', function(done) { - imapClientStub.getBodyParts.yieldsAsync({}); - - dao._getBodyParts({ - folder: inboxFolder, - uid: 123, - bodyParts: [] - }, function(err, msg) { - expect(err).to.exist; - expect(msg).to.not.exist; - - expect(imapClientStub.getBodyParts.calledOnce).to.be.true; - expect(parseStub.called).to.be.false; - - done(); - }); - }); - - it('should fail when disconnected', function(done) { - dao._account.online = false; - - dao._getBodyParts({}, function(err) { - expect(err.code).to.equal(42); - done(); - }); - }); - }); - - describe('#_localListMessages', function() { - var uid = 123; - - it('should list without uid', function(done) { - devicestorageStub.listItems.withArgs('email_' + inboxFolder.path, 0, null).yieldsAsync(); - - dao._localListMessages({ - folder: inboxFolder - }, done); - }); - - it('should list with uid', function(done) { - devicestorageStub.listItems.withArgs('email_' + inboxFolder.path + '_' + uid, 0, null).yieldsAsync(); - - dao._localListMessages({ - folder: inboxFolder, - uid: uid - }, done); - }); - - }); - - describe('#_localStoreMessages', function() { - it('should store messages', function(done) { - devicestorageStub.storeList.withArgs([{}], 'email_' + inboxFolder.path).yieldsAsync(); - - dao._localStoreMessages({ - folder: inboxFolder, - emails: [{}] - }, done); - }); - }); - - describe('#_localDeleteMessage', function() { - var uid = 1337; - - it('should delete message', function(done) { - devicestorageStub.removeList.withArgs('email_' + inboxFolder.path + '_' + uid).yieldsAsync(); - - dao._localDeleteMessage({ - folder: inboxFolder, - uid: uid - }, done); - }); - - it('should fail when uid is missing', function(done) { - dao._localDeleteMessage({ - folder: inboxFolder - }, function(err) { - expect(err).to.exist; - done(); - }); - }); - }); }); }); + + describe('event handlers', function() { + + describe('#onConnect', function() { + var initFoldersStub; + + beforeEach(function() { + initFoldersStub = sinon.stub(dao, '_initFoldersFromImap'); + delete dao._imapClient; + delete dao._pgpMailer; + }); + + it('should connect', function(done) { + inboxFolder.messages = [{ + uid: 123, + modseq: 123 + }]; + imapClientStub.login.yieldsAsync(); + imapClientStub.listenForChanges.yieldsAsync(); + initFoldersStub.yieldsAsync(); + + dao.onConnect({ + imapClient: imapClientStub, + pgpMailer: pgpMailerStub + }, function(err) { + + expect(err).to.not.exist; + expect(imapClientStub.login.calledOnce).to.be.true; + expect(initFoldersStub.calledOnce).to.be.true; + expect(imapClientStub.mailboxCache).to.deep.equal({ + 'INBOX': { + exists: 123, + uidNext: 124, + uidlist: [123], + highestModseq: 123 + } + }); + + done(); + }); + }); + }); + + describe('#onDisconnect', function() { + it('should discard imapClient and pgpMailer', function(done) { + imapClientStub.logout.yieldsAsync(); + + dao.onDisconnect(done); + + expect(dao._account.online).to.be.false; + expect(dao._imapClient).to.not.exist; + expect(dao._pgpMailer).to.not.exist; + }); + }); + + describe('#_onSyncUpdate', function() { + var fetchMessagesStub, deleteMessagesStub, setFlagsStub, msgs; + + beforeEach(function() { + msgs = [{ + uid: 5, + flags: ['\\Answered', '\\Seen'] + }]; + inboxFolder.messages = msgs; + fetchMessagesStub = sinon.stub(dao, 'fetchMessages'); + deleteMessagesStub = sinon.stub(dao, 'deleteMessage'); + setFlagsStub = sinon.stub(dao, 'setFlags'); + }); + + it('should get new message', function(done) { + fetchMessagesStub.withArgs({ + folder: inboxFolder, + firstUid: 1, + lastUid: 3 + }).yieldsAsync(); + + dao.onError = function(err) { + expect(err).to.not.exist; + expect(fetchMessagesStub.calledOnce).to.be.true; + done(); + }; + + dao._onSyncUpdate({ + type: 'new', + path: inboxFolder.path, + list: [1, 3] + }); + }); + + it('should delete message', function(done) { + deleteMessagesStub.withArgs({ + folder: inboxFolder, + message: msgs[0], + localOnly: true + }).yieldsAsync(); + + dao.onError = function(err) { + expect(err).to.not.exist; + expect(deleteMessagesStub.calledOnce).to.be.true; + done(); + }; + + dao._onSyncUpdate({ + type: 'deleted', + path: inboxFolder.path, + list: [5] + }); + }); + + it('should fetch flags', function(done) { + setFlagsStub.withArgs({ + folder: inboxFolder, + message: msgs[0], + localOnly: true + }).yieldsAsync(); + + dao.onError = function(err) { + expect(err).to.not.exist; + expect(setFlagsStub.calledOnce).to.be.true; + done(); + }; + + dao._onSyncUpdate({ + type: 'messages', + path: inboxFolder.path, + list: msgs + }); + }); + }); + }); + + + describe('internal API', function() { + describe('#_checkSignatures', function() { + it('should check signatures in clearsigned message', function(done) { + var message = { + from: [{ + address: 'asdasdasd' + }], + clearSignedMessage: 'trallalalalala' + }; + + keychainStub.getReceiverPublicKey.withArgs(message.from[0].address).yieldsAsync(null, mockKeyPair.publicKey); + pgpStub.verifyClearSignedMessage.withArgs(message.clearSignedMessage, mockKeyPair.publicKey.publicKey).yieldsAsync(null, true); + + dao._checkSignatures(message, function(error, signaturesValid) { + expect(error).to.not.exist; + expect(signaturesValid).to.be.true; + expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true; + expect(pgpStub.verifyClearSignedMessage.calledOnce).to.be.true; + done(); + }); + }); + + it('should check signatures in pgp/mime signed message', function(done) { + var message = { + from: [{ + address: 'asdasdasd' + }], + signedMessage: 'trallalalalala', + signature: 'ugauga' + }; + + keychainStub.getReceiverPublicKey.withArgs(message.from[0].address).yieldsAsync(null, mockKeyPair.publicKey); + pgpStub.verifySignedMessage.withArgs(message.signedMessage, message.signature, mockKeyPair.publicKey.publicKey).yieldsAsync(null, true); + + dao._checkSignatures(message, function(error, signaturesValid) { + expect(error).to.not.exist; + expect(signaturesValid).to.be.true; + expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true; + expect(pgpStub.verifySignedMessage.calledOnce).to.be.true; + done(); + }); + }); + + it('should error while checking signatures', function(done) { + var message = { + from: [{ + address: 'asdasdasd' + }], + signedMessage: 'trallalalalala', + signature: 'ugauga' + }; + + keychainStub.getReceiverPublicKey.withArgs(message.from[0].address).yieldsAsync(null, mockKeyPair.publicKey); + pgpStub.verifySignedMessage.yieldsAsync(new Error()); + + dao._checkSignatures(message, function(error, signaturesValid) { + expect(error).to.exist; + expect(signaturesValid).to.not.exist; + expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true; + expect(pgpStub.verifySignedMessage.calledOnce).to.be.true; + done(); + }); + }); + + it('should error while fetching public key', function(done) { + var message = { + from: [{ + address: 'asdasdasd' + }], + signedMessage: 'trallalalalala', + signature: 'ugauga' + }; + + keychainStub.getReceiverPublicKey.yieldsAsync(new Error()); + + dao._checkSignatures(message, function(error, signaturesValid) { + expect(error).to.exist; + expect(signaturesValid).to.not.exist; + expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true; + expect(pgpStub.verifySignedMessage.called).to.be.false; + done(); + }); + }); + }); + + describe('#_initFoldersFromDisk', function() { + beforeEach(function() { + sinon.stub(dao, 'refreshFolder'); + }); + + it('should initialize from disk if offline and not refresh folder', function(done) { + devicestorageStub.listItems.withArgs('folders').yieldsAsync(null, [ + [inboxFolder] + ]); + dao.refreshFolder.withArgs({ + folder: inboxFolder + }).yieldsAsync(); + + dao._initFoldersFromDisk(function(err) { + expect(err).to.not.exist; + expect(devicestorageStub.listItems.calledOnce).to.be.true; + expect(dao.refreshFolder.called).to.be.false; + done(); + }); + }); + + it('should initialize from disk if offline and refresh folder', function(done) { + delete inboxFolder.messages; + devicestorageStub.listItems.withArgs('folders').yieldsAsync(null, [ + [inboxFolder] + ]); + dao.refreshFolder.withArgs({ + folder: inboxFolder + }).yieldsAsync(); + + dao._initFoldersFromDisk(function(err) { + expect(err).to.not.exist; + expect(devicestorageStub.listItems.calledOnce).to.be.true; + expect(dao.refreshFolder.calledOnce).to.be.true; + done(); + }); + }); + }); + + describe('#_initFoldersFromImap', function() { + beforeEach(function() { + sinon.stub(dao, 'refreshFolder'); + }); + + it('should initialize from imap if online', function(done) { + account.folders = []; + imapClientStub.listWellKnownFolders.yieldsAsync(null, { + Inbox: [inboxFolder], + Sent: [sentFolder], + Drafts: [draftsFolder], + Trash: [trashFolder] + }); + devicestorageStub.storeList.withArgs(sinon.match(function(arg) { + expect(arg[0][0].name).to.deep.equal(inboxFolder.name); + expect(arg[0][0].path).to.deep.equal(inboxFolder.path); + expect(arg[0][0].type).to.deep.equal(inboxFolder.type); + expect(arg[0][1].name).to.deep.equal(sentFolder.name); + expect(arg[0][1].path).to.deep.equal(sentFolder.path); + expect(arg[0][1].type).to.deep.equal(sentFolder.type); + expect(arg[0][2].name).to.deep.equal(outboxFolder.name); + expect(arg[0][2].path).to.deep.equal(outboxFolder.path); + expect(arg[0][2].type).to.deep.equal(outboxFolder.type); + expect(arg[0][3].name).to.deep.equal(draftsFolder.name); + expect(arg[0][3].path).to.deep.equal(draftsFolder.path); + expect(arg[0][3].type).to.deep.equal(draftsFolder.type); + expect(arg[0][4].name).to.deep.equal(trashFolder.name); + expect(arg[0][4].path).to.deep.equal(trashFolder.path); + expect(arg[0][4].type).to.deep.equal(trashFolder.type); + return true; + }), 'folders').yieldsAsync(); + + dao.refreshFolder.yieldsAsync(); + + dao._initFoldersFromImap(function(err) { + expect(err).to.not.exist; + expect(imapClientStub.listWellKnownFolders.calledOnce).to.be.true; + expect(devicestorageStub.storeList.calledOnce).to.be.true; + done(); + }); + }); + + it('should update folders from imap', function(done) { + account.folders = [inboxFolder, outboxFolder, trashFolder, { + name: 'foo', + type: 'Sent', + path: 'bar', + }]; + + imapClientStub.listWellKnownFolders.yieldsAsync(null, { + Inbox: [inboxFolder], + Sent: [sentFolder], + Drafts: [draftsFolder], + Trash: [trashFolder] + }); + devicestorageStub.storeList.withArgs(sinon.match(function(arg) { + expect(arg[0]).to.deep.equal([{ + name: inboxFolder.name, + path: inboxFolder.path, + type: inboxFolder.type + }, { + name: outboxFolder.name, + path: outboxFolder.path, + type: outboxFolder.type + }, { + name: trashFolder.name, + path: trashFolder.path, + type: trashFolder.type + }, { + name: sentFolder.name, + path: sentFolder.path, + type: sentFolder.type + }, { + name: draftsFolder.name, + path: draftsFolder.path, + type: draftsFolder.type + }]); + + return true; + }), 'folders').yieldsAsync(); + + dao.refreshFolder.yieldsAsync(); + + dao._initFoldersFromImap(function(err) { + expect(err).to.not.exist; + expect(imapClientStub.listWellKnownFolders.calledOnce).to.be.true; + expect(devicestorageStub.storeList.calledOnce).to.be.true; + done(); + }); + }); + }); + + describe('#_imapMark', function() { + it('should flag a mail', function(done) { + imapClientStub.updateFlags.withArgs({ + path: inboxFolder.path, + folder: inboxFolder, + uid: 1, + unread: false, + answered: false + }).yieldsAsync(); + + dao._imapMark({ + folder: inboxFolder, + uid: 1, + unread: false, + answered: false + }, function(err) { + expect(err).to.not.exist; + expect(imapClientStub.updateFlags.calledOnce).to.be.true; + done(); + }); + }); + }); + + describe('#_imapDeleteMessage', function() { + var uid = 1337; + + it('should fail when disconnected', function(done) { + dao._account.online = false; + + dao._imapDeleteMessage({}, function(err) { + expect(err.code).to.equal(42); + done(); + }); + }); + + it('should move to trash', function(done) { + imapClientStub.moveMessage.withArgs({ + path: inboxFolder.path, + uid: uid, + destination: trashFolder.path + }).yieldsAsync(); + + dao._imapDeleteMessage({ + folder: inboxFolder, + uid: uid + }, done); + }); + + it('should purge message', function(done) { + imapClientStub.deleteMessage.withArgs({ + path: trashFolder.path, + uid: uid + }).yieldsAsync(); + + dao._imapDeleteMessage({ + folder: trashFolder, + uid: uid + }, done); + }); + }); + + describe('#_imapListMessages', function() { + var firstUid = 1337, + lastUid = 1339; + + it('should list messages', function(done) { + imapClientStub.listMessages.withArgs({ + folder: inboxFolder, + path: inboxFolder.path, + firstUid: firstUid, + lastUid: lastUid + }).yieldsAsync(null, []); + + dao._imapListMessages({ + folder: inboxFolder, + firstUid: firstUid, + lastUid: lastUid + }, function(err, msgs) { + expect(err).to.not.exist; + expect(msgs).to.exist; + + expect(imapClientStub.listMessages.calledOnce).to.be.true; + + done(); + }); + }); + + it('should fail when listMessages fails', function(done) { + imapClientStub.listMessages.yieldsAsync({}); + + dao._imapListMessages({ + folder: inboxFolder, + firstUid: firstUid, + lastUid: lastUid + }, function(err, msgs) { + expect(err).to.exist; + expect(msgs).to.not.exist; + expect(imapClientStub.listMessages.calledOnce).to.be.true; + + done(); + }); + }); + + it('should fail when disconnected', function(done) { + dao._account.online = false; + + dao._imapListMessages({}, function(err) { + expect(err.code).to.equal(42); + done(); + }); + }); + }); + + describe('#_getBodyParts', function() { + it('should get bodyParts', function(done) { + imapClientStub.getBodyParts.withArgs({ + folder: inboxFolder, + path: inboxFolder.path, + uid: 123, + bodyParts: [] + }).yieldsAsync(null, {}); + parseStub.yieldsAsync(null, []); + + dao._getBodyParts({ + folder: inboxFolder, + uid: 123, + bodyParts: [] + }, function(err, parts) { + expect(err).to.not.exist; + expect(parts).to.exist; + + expect(imapClientStub.getBodyParts.calledOnce).to.be.true; + expect(parseStub.calledOnce).to.be.true; + + done(); + }); + }); + + it('should fail when getBody fails', function(done) { + imapClientStub.getBodyParts.yieldsAsync({}); + + dao._getBodyParts({ + folder: inboxFolder, + uid: 123, + bodyParts: [] + }, function(err, msg) { + expect(err).to.exist; + expect(msg).to.not.exist; + + expect(imapClientStub.getBodyParts.calledOnce).to.be.true; + expect(parseStub.called).to.be.false; + + done(); + }); + }); + + it('should fail when disconnected', function(done) { + dao._account.online = false; + + dao._getBodyParts({}, function(err) { + expect(err.code).to.equal(42); + done(); + }); + }); + }); + + describe('#_localListMessages', function() { + var uid = 123; + + it('should list without uid', function(done) { + devicestorageStub.listItems.withArgs('email_' + inboxFolder.path, 0, null).yieldsAsync(); + + dao._localListMessages({ + folder: inboxFolder + }, done); + }); + + it('should list with uid', function(done) { + devicestorageStub.listItems.withArgs('email_' + inboxFolder.path + '_' + uid, 0, null).yieldsAsync(); + + dao._localListMessages({ + folder: inboxFolder, + uid: uid + }, done); + }); + + }); + + describe('#_localStoreMessages', function() { + it('should store messages', function(done) { + devicestorageStub.storeList.withArgs([{}], 'email_' + inboxFolder.path).yieldsAsync(); + + dao._localStoreMessages({ + folder: inboxFolder, + emails: [{}] + }, done); + }); + }); + + describe('#_localDeleteMessage', function() { + var uid = 1337; + + it('should delete message', function(done) { + devicestorageStub.removeList.withArgs('email_' + inboxFolder.path + '_' + uid).yieldsAsync(); + + dao._localDeleteMessage({ + folder: inboxFolder, + uid: uid + }, done); + }); + + it('should fail when uid is missing', function(done) { + dao._localDeleteMessage({ + folder: inboxFolder + }, function(err) { + expect(err).to.exist; + done(); + }); + }); + + }); + }); }); \ No newline at end of file diff --git a/test/unit/index.html b/test/unit/index.html index 8a0094a..c6366e0 100644 --- a/test/unit/index.html +++ b/test/unit/index.html @@ -8,12 +8,71 @@
+ + - + - + + + + diff --git a/test/unit/invitation-dao-test.js b/test/unit/invitation-dao-test.js index 9982119..136c088 100644 --- a/test/unit/invitation-dao-test.js +++ b/test/unit/invitation-dao-test.js @@ -1,108 +1,105 @@ -define(function(require) { - 'use strict'; +'use strict'; - var RestDAO = require('js/dao/rest-dao'), - InvitationDAO = require('js/dao/invitation-dao'), - expect = chai.expect; +var RestDAO = require('../../src/js/dao/rest-dao'), + InvitationDAO = require('../../src/js/dao/invitation-dao'); - describe('Invitation DAO unit tests', function() { - var restDaoStub, invitationDao, - alice = 'zuhause@aol.com', - bob = 'manfred.mustermann@musterdomain.com', - expectedUri = '/invitation/recipient/' + alice + '/sender/' + bob; +describe('Invitation DAO unit tests', function() { + var restDaoStub, invitationDao, + alice = 'zuhause@aol.com', + bob = 'manfred.mustermann@musterdomain.com', + expectedUri = '/invitation/recipient/' + alice + '/sender/' + bob; - beforeEach(function() { - restDaoStub = sinon.createStubInstance(RestDAO); - invitationDao = new InvitationDAO(restDaoStub); + beforeEach(function() { + restDaoStub = sinon.createStubInstance(RestDAO); + invitationDao = new InvitationDAO(restDaoStub); + }); + + describe('initialization', function() { + it('should wire up correctly', function() { + expect(invitationDao._restDao).to.equal(restDaoStub); + expect(invitationDao.invite).to.exist; + expect(InvitationDAO.INVITE_MISSING).to.equal(1); + expect(InvitationDAO.INVITE_PENDING).to.equal(2); + expect(InvitationDAO.INVITE_SUCCESS).to.equal(4); }); + }); - describe('initialization', function() { - it('should wire up correctly', function() { - expect(invitationDao._restDao).to.equal(restDaoStub); - expect(invitationDao.invite).to.exist; - expect(InvitationDAO.INVITE_MISSING).to.equal(1); - expect(InvitationDAO.INVITE_PENDING).to.equal(2); - expect(InvitationDAO.INVITE_SUCCESS).to.equal(4); + describe('invite', function() { + it('should invite the recipient', function(done) { + restDaoStub.put.yieldsAsync(null, undefined, 201); + + invitationDao.invite({ + recipient: alice, + sender: bob + }, function(err, status) { + expect(err).to.not.exist; + expect(status).to.equal(InvitationDAO.INVITE_SUCCESS); + expect(restDaoStub.put.calledWith({}, expectedUri)).to.be.true; + done(); }); }); - describe('invite', function() { - it('should invite the recipient', function(done) { - restDaoStub.put.yieldsAsync(null, undefined, 201); + it('should point out already invited recipient', function(done) { + restDaoStub.put.yieldsAsync(null, undefined, 304); - invitationDao.invite({ - recipient: alice, - sender: bob - }, function(err, status) { - expect(err).to.not.exist; - expect(status).to.equal(InvitationDAO.INVITE_SUCCESS); - expect(restDaoStub.put.calledWith({}, expectedUri)).to.be.true; - done(); - }); + invitationDao.invite({ + recipient: alice, + sender: bob + }, function(err, status) { + expect(err).to.not.exist; + expect(status).to.equal(InvitationDAO.INVITE_PENDING); + done(); + }); + }); + + it('should not work for http error', function(done) { + restDaoStub.put.yieldsAsync({ + errMsg: 'jawollja.' }); - it('should point out already invited recipient', function(done) { - restDaoStub.put.yieldsAsync(null, undefined, 304); - - invitationDao.invite({ - recipient: alice, - sender: bob - }, function(err, status) { - expect(err).to.not.exist; - expect(status).to.equal(InvitationDAO.INVITE_PENDING); - done(); - }); + invitationDao.invite({ + recipient: alice, + sender: bob + }, function(err, status) { + expect(err).to.exist; + expect(status).to.not.exist; + done(); }); + }); - it('should not work for http error', function(done) { - restDaoStub.put.yieldsAsync({ - errMsg: 'jawollja.' - }); + it('should not work for unexpected response', function(done) { + restDaoStub.put.yieldsAsync(null, undefined, 1337); - invitationDao.invite({ - recipient: alice, - sender: bob - }, function(err, status) { - expect(err).to.exist; - expect(status).to.not.exist; - done(); - }); + invitationDao.invite({ + recipient: alice, + sender: bob + }, function(err, status) { + expect(err).to.exist; + expect(status).to.not.exist; + done(); }); + }); - it('should not work for unexpected response', function(done) { - restDaoStub.put.yieldsAsync(null, undefined, 1337); + it('should report erroneous usage', function() { + invitationDao.invite({ + sender: bob + }, expectError); - invitationDao.invite({ - recipient: alice, - sender: bob - }, function(err, status) { - expect(err).to.exist; - expect(status).to.not.exist; - done(); - }); - }); + invitationDao.invite({ + recipient: alice, + }, expectError); - it('should report erroneous usage', function() { - invitationDao.invite({ - sender: bob - }, expectError); + invitationDao.invite({ + recipient: 123, + sender: 123 + }, expectError); - invitationDao.invite({ - recipient: alice, - }, expectError); + invitationDao.invite('asd', expectError); - invitationDao.invite({ - recipient: 123, - sender: 123 - }, expectError); - - invitationDao.invite('asd', expectError); - - function expectError(err, status) { - expect(err).to.exist; - expect(status).to.not.exist; - } - }); + function expectError(err, status) { + expect(err).to.exist; + expect(status).to.not.exist; + } }); }); }); \ No newline at end of file diff --git a/test/unit/keychain-dao-test.js b/test/unit/keychain-dao-test.js index c05cef1..34f5b5f 100644 --- a/test/unit/keychain-dao-test.js +++ b/test/unit/keychain-dao-test.js @@ -1,1386 +1,1382 @@ -define(function(require) { - 'use strict'; +'use strict'; - var LawnchairDAO = require('js/dao/lawnchair-dao'), - PublicKeyDAO = require('js/dao/publickey-dao'), - KeychainDAO = require('js/dao/keychain-dao'), - PrivateKeyDAO = require('js/dao/privatekey-dao'), - Crypto = require('js/crypto/crypto'), - PGP = require('js/crypto/pgp'), - expect = chai.expect; +var LawnchairDAO = require('../../src/js/dao/lawnchair-dao'), + PublicKeyDAO = require('../../src/js/dao/publickey-dao'), + KeychainDAO = require('../../src/js/dao/keychain-dao'), + PrivateKeyDAO = require('../../src/js/dao/privatekey-dao'), + Crypto = require('../../src/js/crypto/crypto'), + PGP = require('../../src/js/crypto/pgp'); - var testUser = 'test@example.com'; +var testUser = 'test@example.com'; - describe('Keychain DAO unit tests', function() { +describe('Keychain DAO unit tests', function() { - var keychainDao, lawnchairDaoStub, pubkeyDaoStub, privkeyDaoStub, cryptoStub, pgpStub; + var keychainDao, lawnchairDaoStub, pubkeyDaoStub, privkeyDaoStub, cryptoStub, pgpStub; + + beforeEach(function() { + lawnchairDaoStub = sinon.createStubInstance(LawnchairDAO); + pubkeyDaoStub = sinon.createStubInstance(PublicKeyDAO); + privkeyDaoStub = sinon.createStubInstance(PrivateKeyDAO); + cryptoStub = sinon.createStubInstance(Crypto); + pgpStub = sinon.createStubInstance(PGP); + keychainDao = new KeychainDAO(lawnchairDaoStub, pubkeyDaoStub, privkeyDaoStub, cryptoStub, pgpStub); + }); + + afterEach(function() {}); + + describe('verify public key', function() { + it('should verify public key', function(done) { + var uuid = 'asdfasdfasdfasdf'; + pubkeyDaoStub.verify.yields(); + + keychainDao.verifyPublicKey(uuid, function() { + expect(pubkeyDaoStub.verify.calledWith(uuid)).to.be.true; + done(); + }); + }); + }); + + describe('listLocalPublicKeys', function() { + it('should work', function(done) { + lawnchairDaoStub.list.withArgs('publickey', 0, null).yields(); + + keychainDao.listLocalPublicKeys(function() { + expect(lawnchairDaoStub.list.callCount).to.equal(1); + done(); + }); + }); + }); + + describe('removeLocalPublicKey', function() { + it('should work', function(done) { + var id = 'asdf'; + + lawnchairDaoStub.remove.withArgs('publickey_' + id).yields(); + + keychainDao.removeLocalPublicKey(id, function() { + expect(lawnchairDaoStub.remove.callCount).to.equal(1); + done(); + }); + }); + }); + + describe('refreshKeyForUserId', function() { + var getPubKeyStub, + oldKey = { + _id: 123 + }, + newKey = { + _id: 456 + }, + importedKey = { + _id: 789, + imported: true + }; beforeEach(function() { - lawnchairDaoStub = sinon.createStubInstance(LawnchairDAO); - pubkeyDaoStub = sinon.createStubInstance(PublicKeyDAO); - privkeyDaoStub = sinon.createStubInstance(PrivateKeyDAO); - cryptoStub = sinon.createStubInstance(Crypto); - pgpStub = sinon.createStubInstance(PGP); - keychainDao = new KeychainDAO(lawnchairDaoStub, pubkeyDaoStub, privkeyDaoStub, cryptoStub, pgpStub); + getPubKeyStub = sinon.stub(keychainDao, 'getReceiverPublicKey'); }); - afterEach(function() {}); + afterEach(function() { + keychainDao.getReceiverPublicKey.restore(); + delete keychainDao.requestPermissionForKeyUpdate; + }); - describe('verify public key', function() { - it('should verify public key', function(done) { - var uuid = 'asdfasdfasdfasdf'; - pubkeyDaoStub.verify.yields(); + it('should not find a key', function(done) { + getPubKeyStub.yields(); - keychainDao.verifyPublicKey(uuid, function() { - expect(pubkeyDaoStub.verify.calledWith(uuid)).to.be.true; - done(); - }); + keychainDao.refreshKeyForUserId(testUser, function(err, key) { + expect(err).to.not.exist; + expect(key).to.not.exist; + + done(); }); }); - describe('listLocalPublicKeys', function() { - it('should work', function(done) { - lawnchairDaoStub.list.withArgs('publickey', 0, null).yields(); + it('should not update the key when up to date', function(done) { + getPubKeyStub.yields(null, oldKey); + pubkeyDaoStub.get.withArgs(oldKey._id).yields(null, oldKey); - keychainDao.listLocalPublicKeys(function() { - expect(lawnchairDaoStub.list.callCount).to.equal(1); - done(); - }); + keychainDao.refreshKeyForUserId(testUser, function(err, key) { + expect(err).to.not.exist; + expect(key).to.to.equal(oldKey); + + expect(getPubKeyStub.calledOnce).to.be.true; + expect(pubkeyDaoStub.get.calledOnce).to.be.true; + + + done(); }); }); - describe('removeLocalPublicKey', function() { - it('should work', function(done) { - var id = 'asdf'; + it('should update key', function(done) { + getPubKeyStub.yields(null, oldKey); + pubkeyDaoStub.get.withArgs(oldKey._id).yields(); + pubkeyDaoStub.getByUserId.withArgs(testUser).yields(null, newKey); + keychainDao.requestPermissionForKeyUpdate = function(opts, cb) { + expect(opts.userId).to.equal(testUser); + expect(opts.newKey).to.equal(newKey); + cb(true); + }; + lawnchairDaoStub.remove.withArgs('publickey_' + oldKey._id).yields(); + lawnchairDaoStub.persist.withArgs('publickey_' + newKey._id, newKey).yields(); - lawnchairDaoStub.remove.withArgs('publickey_' + id).yields(); + keychainDao.refreshKeyForUserId(testUser, function(err, key) { + expect(err).to.not.exist; + expect(key).to.equal(newKey); - keychainDao.removeLocalPublicKey(id, function() { - expect(lawnchairDaoStub.remove.callCount).to.equal(1); - done(); - }); + expect(getPubKeyStub.calledOnce).to.be.true; + expect(pubkeyDaoStub.get.calledOnce).to.be.true; + expect(pubkeyDaoStub.getByUserId.calledOnce).to.be.true; + expect(lawnchairDaoStub.remove.calledOnce).to.be.true; + expect(lawnchairDaoStub.persist.calledOnce).to.be.true; + + done(); }); }); - describe('refreshKeyForUserId', function() { - var getPubKeyStub, - oldKey = { - _id: 123 - }, - newKey = { - _id: 456 - }, - importedKey = { - _id: 789, - imported: true - }; + it('should remove key', function(done) { + getPubKeyStub.yields(null, oldKey); + pubkeyDaoStub.get.withArgs(oldKey._id).yields(); + pubkeyDaoStub.getByUserId.withArgs(testUser).yields(); + keychainDao.requestPermissionForKeyUpdate = function(opts, cb) { + expect(opts.userId).to.equal(testUser); + expect(opts.newKey).to.not.exist; + cb(true); + }; + lawnchairDaoStub.remove.withArgs('publickey_' + oldKey._id).yields(); - beforeEach(function() { - getPubKeyStub = sinon.stub(keychainDao, 'getReceiverPublicKey'); - }); + keychainDao.refreshKeyForUserId(testUser, function(err, key) { + expect(err).to.not.exist; + expect(key).to.not.exist; - afterEach(function() { - keychainDao.getReceiverPublicKey.restore(); - delete keychainDao.requestPermissionForKeyUpdate; - }); + expect(getPubKeyStub.calledOnce).to.be.true; + expect(pubkeyDaoStub.get.calledOnce).to.be.true; + expect(pubkeyDaoStub.getByUserId.calledOnce).to.be.true; + expect(lawnchairDaoStub.remove.calledOnce).to.be.true; + expect(lawnchairDaoStub.persist.called).to.be.false; - it('should not find a key', function(done) { - getPubKeyStub.yields(); - - keychainDao.refreshKeyForUserId(testUser, function(err, key) { - expect(err).to.not.exist; - expect(key).to.not.exist; - - done(); - }); - }); - - it('should not update the key when up to date', function(done) { - getPubKeyStub.yields(null, oldKey); - pubkeyDaoStub.get.withArgs(oldKey._id).yields(null, oldKey); - - keychainDao.refreshKeyForUserId(testUser, function(err, key) { - expect(err).to.not.exist; - expect(key).to.to.equal(oldKey); - - expect(getPubKeyStub.calledOnce).to.be.true; - expect(pubkeyDaoStub.get.calledOnce).to.be.true; - - - done(); - }); - }); - - it('should update key', function(done) { - getPubKeyStub.yields(null, oldKey); - pubkeyDaoStub.get.withArgs(oldKey._id).yields(); - pubkeyDaoStub.getByUserId.withArgs(testUser).yields(null, newKey); - keychainDao.requestPermissionForKeyUpdate = function(opts, cb) { - expect(opts.userId).to.equal(testUser); - expect(opts.newKey).to.equal(newKey); - cb(true); - }; - lawnchairDaoStub.remove.withArgs('publickey_' + oldKey._id).yields(); - lawnchairDaoStub.persist.withArgs('publickey_' + newKey._id, newKey).yields(); - - keychainDao.refreshKeyForUserId(testUser, function(err, key) { - expect(err).to.not.exist; - expect(key).to.equal(newKey); - - expect(getPubKeyStub.calledOnce).to.be.true; - expect(pubkeyDaoStub.get.calledOnce).to.be.true; - expect(pubkeyDaoStub.getByUserId.calledOnce).to.be.true; - expect(lawnchairDaoStub.remove.calledOnce).to.be.true; - expect(lawnchairDaoStub.persist.calledOnce).to.be.true; - - done(); - }); - }); - - it('should remove key', function(done) { - getPubKeyStub.yields(null, oldKey); - pubkeyDaoStub.get.withArgs(oldKey._id).yields(); - pubkeyDaoStub.getByUserId.withArgs(testUser).yields(); - keychainDao.requestPermissionForKeyUpdate = function(opts, cb) { - expect(opts.userId).to.equal(testUser); - expect(opts.newKey).to.not.exist; - cb(true); - }; - lawnchairDaoStub.remove.withArgs('publickey_' + oldKey._id).yields(); - - keychainDao.refreshKeyForUserId(testUser, function(err, key) { - expect(err).to.not.exist; - expect(key).to.not.exist; - - expect(getPubKeyStub.calledOnce).to.be.true; - expect(pubkeyDaoStub.get.calledOnce).to.be.true; - expect(pubkeyDaoStub.getByUserId.calledOnce).to.be.true; - expect(lawnchairDaoStub.remove.calledOnce).to.be.true; - expect(lawnchairDaoStub.persist.called).to.be.false; - - done(); - }); - }); - - it('should go offline while fetching new key', function(done) { - getPubKeyStub.yields(null, oldKey); - pubkeyDaoStub.get.withArgs(oldKey._id).yields(); - pubkeyDaoStub.getByUserId.withArgs(testUser).yields({ - code: 42 - }); - - keychainDao.refreshKeyForUserId(testUser, function(err, key) { - expect(err).to.not.exist; - expect(key).to.to.equal(oldKey); - - expect(getPubKeyStub.calledOnce).to.be.true; - expect(pubkeyDaoStub.get.calledOnce).to.be.true; - expect(pubkeyDaoStub.getByUserId.calledOnce).to.be.true; - expect(lawnchairDaoStub.remove.called).to.be.false; - expect(lawnchairDaoStub.persist.called).to.be.false; - - done(); - }); - }); - - it('should not remove old key on user rejection', function(done) { - getPubKeyStub.yields(null, oldKey); - pubkeyDaoStub.get.withArgs(oldKey._id).yields(); - pubkeyDaoStub.getByUserId.withArgs(testUser).yields(null, newKey); - keychainDao.requestPermissionForKeyUpdate = function(opts, cb) { - expect(opts.userId).to.equal(testUser); - expect(opts.newKey).to.exist; - cb(false); - }; - - keychainDao.refreshKeyForUserId(testUser, function(err, key) { - expect(err).to.not.exist; - expect(key).to.equal(oldKey); - - expect(getPubKeyStub.calledOnce).to.be.true; - expect(pubkeyDaoStub.get.calledOnce).to.be.true; - expect(pubkeyDaoStub.getByUserId.calledOnce).to.be.true; - expect(lawnchairDaoStub.remove.called).to.be.false; - expect(lawnchairDaoStub.persist.called).to.be.false; - - done(); - }); - }); - - it('should not remove manually imported key', function(done) { - getPubKeyStub.yields(null, importedKey); - - keychainDao.refreshKeyForUserId(testUser, function(err, key) { - expect(err).to.not.exist; - expect(key).to.equal(importedKey); - - expect(getPubKeyStub.calledOnce).to.be.true; - expect(pubkeyDaoStub.get.calledOnce).to.be.false; - - done(); - }); - }); - - it('should update not the key when offline', function(done) { - getPubKeyStub.yields(null, oldKey); - pubkeyDaoStub.get.withArgs(oldKey._id).yields({ - code: 42 - }); - - keychainDao.refreshKeyForUserId(testUser, function(err, key) { - expect(err).to.not.exist; - expect(key).to.to.equal(oldKey); - - expect(getPubKeyStub.calledOnce).to.be.true; - expect(pubkeyDaoStub.get.calledOnce).to.be.true; - expect(pubkeyDaoStub.getByUserId.called).to.be.false; - expect(lawnchairDaoStub.remove.called).to.be.false; - expect(lawnchairDaoStub.persist.called).to.be.false; - - done(); - }); - }); - - it('should error while persisting new key', function(done) { - getPubKeyStub.yields(null, oldKey); - pubkeyDaoStub.get.withArgs(oldKey._id).yields(); - pubkeyDaoStub.getByUserId.withArgs(testUser).yields(null, newKey); - keychainDao.requestPermissionForKeyUpdate = function(opts, cb) { - expect(opts.userId).to.equal(testUser); - expect(opts.newKey).to.equal(newKey); - cb(true); - }; - lawnchairDaoStub.remove.withArgs('publickey_' + oldKey._id).yields(); - lawnchairDaoStub.persist.yields({}); - - keychainDao.refreshKeyForUserId(testUser, function(err, key) { - expect(err).to.exist; - expect(key).to.not.exist; - - expect(getPubKeyStub.calledOnce).to.be.true; - expect(pubkeyDaoStub.get.calledOnce).to.be.true; - expect(pubkeyDaoStub.getByUserId.calledOnce).to.be.true; - expect(lawnchairDaoStub.remove.calledOnce).to.be.true; - expect(lawnchairDaoStub.persist.calledOnce).to.be.true; - - done(); - }); - }); - - it('should error while deleting old key', function(done) { - getPubKeyStub.yields(null, oldKey); - pubkeyDaoStub.get.withArgs(oldKey._id).yields(); - pubkeyDaoStub.getByUserId.withArgs(testUser).yields(); - keychainDao.requestPermissionForKeyUpdate = function(opts, cb) { - expect(opts.userId).to.equal(testUser); - cb(true); - }; - lawnchairDaoStub.remove.yields({}); - - keychainDao.refreshKeyForUserId(testUser, function(err, key) { - expect(err).to.exist; - expect(key).to.not.exist; - - expect(getPubKeyStub.calledOnce).to.be.true; - expect(pubkeyDaoStub.get.calledOnce).to.be.true; - expect(lawnchairDaoStub.remove.calledOnce).to.be.true; - expect(pubkeyDaoStub.getByUserId.calledOnce).to.be.true; - expect(lawnchairDaoStub.persist.called).to.be.false; - - done(); - }); - }); - - it('should error while persisting new key', function(done) { - getPubKeyStub.yields(null, oldKey); - pubkeyDaoStub.get.withArgs(oldKey._id).yields(); - pubkeyDaoStub.getByUserId.withArgs(testUser).yields(null, newKey); - keychainDao.requestPermissionForKeyUpdate = function(opts, cb) { - expect(opts.userId).to.equal(testUser); - expect(opts.newKey).to.equal(newKey); - cb(true); - }; - lawnchairDaoStub.remove.withArgs('publickey_' + oldKey._id).yields(); - lawnchairDaoStub.persist.yields({}); - - keychainDao.refreshKeyForUserId(testUser, function(err, key) { - expect(err).to.exist; - expect(key).to.not.exist; - - expect(getPubKeyStub.calledOnce).to.be.true; - expect(pubkeyDaoStub.get.calledOnce).to.be.true; - expect(pubkeyDaoStub.getByUserId.calledOnce).to.be.true; - expect(lawnchairDaoStub.remove.calledOnce).to.be.true; - expect(lawnchairDaoStub.persist.calledOnce).to.be.true; - - done(); - }); - }); - - it('should error when get failed', function(done) { - getPubKeyStub.yields(null, oldKey); - pubkeyDaoStub.get.withArgs(oldKey._id).yields({}); - - keychainDao.refreshKeyForUserId(testUser, function(err, key) { - expect(err).to.exist; - expect(key).to.not.exist; - - done(); - }); + done(); }); }); - describe('lookup public key', function() { - it('should fail', function(done) { - keychainDao.lookupPublicKey(undefined, function(err, key) { - expect(err).to.exist; - expect(key).to.not.exist; - done(); - }); + it('should go offline while fetching new key', function(done) { + getPubKeyStub.yields(null, oldKey); + pubkeyDaoStub.get.withArgs(oldKey._id).yields(); + pubkeyDaoStub.getByUserId.withArgs(testUser).yields({ + code: 42 }); - it('should fail', function(done) { - lawnchairDaoStub.read.yields(42); + keychainDao.refreshKeyForUserId(testUser, function(err, key) { + expect(err).to.not.exist; + expect(key).to.to.equal(oldKey); - keychainDao.lookupPublicKey('12345', function(err, key) { - expect(err).to.exist; - expect(key).to.not.exist; - expect(lawnchairDaoStub.read.calledOnce).to.be.true; - done(); - }); - }); + expect(getPubKeyStub.calledOnce).to.be.true; + expect(pubkeyDaoStub.get.calledOnce).to.be.true; + expect(pubkeyDaoStub.getByUserId.calledOnce).to.be.true; + expect(lawnchairDaoStub.remove.called).to.be.false; + expect(lawnchairDaoStub.persist.called).to.be.false; - it('should work from local storage', function(done) { - lawnchairDaoStub.read.yields(null, { - _id: '12345', - publicKey: 'asdf' - }); - - keychainDao.lookupPublicKey('12345', function(err, key) { - expect(err).to.not.exist; - expect(key).to.exist; - expect(lawnchairDaoStub.read.calledOnce).to.be.true; - done(); - }); - }); - - it('should work from cloud', function(done) { - lawnchairDaoStub.read.yields(); - pubkeyDaoStub.get.yields(null, { - _id: '12345', - publicKey: 'asdf' - }); - lawnchairDaoStub.persist.yields(); - - keychainDao.lookupPublicKey('12345', function(err, key) { - expect(err).to.not.exist; - expect(key).to.exist; - expect(key._id).to.equal('12345'); - expect(lawnchairDaoStub.read.calledOnce).to.be.true; - expect(pubkeyDaoStub.get.calledOnce).to.be.true; - expect(lawnchairDaoStub.persist.calledOnce).to.be.true; - done(); - }); + done(); }); }); - describe('get public keys by id', function() { - it('should fail', function(done) { - keychainDao.getPublicKeys([], function(err, keys) { - expect(err).to.not.exist; - expect(keys.length).to.equal(0); - done(); - }); - }); + it('should not remove old key on user rejection', function(done) { + getPubKeyStub.yields(null, oldKey); + pubkeyDaoStub.get.withArgs(oldKey._id).yields(); + pubkeyDaoStub.getByUserId.withArgs(testUser).yields(null, newKey); + keychainDao.requestPermissionForKeyUpdate = function(opts, cb) { + expect(opts.userId).to.equal(testUser); + expect(opts.newKey).to.exist; + cb(false); + }; - it('should fail', function(done) { - lawnchairDaoStub.read.yields(42); + keychainDao.refreshKeyForUserId(testUser, function(err, key) { + expect(err).to.not.exist; + expect(key).to.equal(oldKey); - var ids = [{ - _id: '12345' - }]; - keychainDao.getPublicKeys(ids, function(err, keys) { - expect(err).to.exist; - expect(keys).to.not.exist; - expect(lawnchairDaoStub.read.calledOnce).to.be.true; - done(); - }); - }); + expect(getPubKeyStub.calledOnce).to.be.true; + expect(pubkeyDaoStub.get.calledOnce).to.be.true; + expect(pubkeyDaoStub.getByUserId.calledOnce).to.be.true; + expect(lawnchairDaoStub.remove.called).to.be.false; + expect(lawnchairDaoStub.persist.called).to.be.false; - it('should work from local storage', function(done) { - lawnchairDaoStub.read.yields(null, { - _id: '12345', - publicKey: 'asdf' - }); - - var ids = [{ - _id: '12345' - }]; - keychainDao.getPublicKeys(ids, function(err, keys) { - expect(err).to.not.exist; - expect(keys.length).to.equal(1); - expect(keys[0]._id).to.equal('12345'); - expect(lawnchairDaoStub.read.calledOnce).to.be.true; - done(); - }); + done(); }); }); - describe('get receiver public key', function() { - it('should fail due to error in lawnchair list', function(done) { - lawnchairDaoStub.list.yields(42); + it('should not remove manually imported key', function(done) { + getPubKeyStub.yields(null, importedKey); - keychainDao.getReceiverPublicKey(testUser, function(err, key) { - expect(err).to.exist; - expect(key).to.not.exist; - expect(lawnchairDaoStub.list.calledOnce).to.be.true; - done(); - }); - }); + keychainDao.refreshKeyForUserId(testUser, function(err, key) { + expect(err).to.not.exist; + expect(key).to.equal(importedKey); - it('should work from lawnchair list', function(done) { - lawnchairDaoStub.list.yields(null, [{ - _id: '12345', - userId: testUser, - publicKey: 'asdf' - }]); + expect(getPubKeyStub.calledOnce).to.be.true; + expect(pubkeyDaoStub.get.calledOnce).to.be.false; - keychainDao.getReceiverPublicKey(testUser, function(err, key) { - expect(err).to.not.exist; - expect(key).to.exist; - expect(key._id).to.equal('12345'); - expect(lawnchairDaoStub.list.calledOnce).to.be.true; - done(); - }); - }); - - it('should work for keys with secondary userIds', function(done) { - lawnchairDaoStub.list.yields(null, [{ - _id: '12345', - userId: 'not testUser', - userIds: [{ - emailAddress: testUser - }], - publicKey: 'asdf' - }]); - - keychainDao.getReceiverPublicKey(testUser, function(err, key) { - expect(err).to.not.exist; - expect(key).to.exist; - expect(key._id).to.equal('12345'); - expect(lawnchairDaoStub.list.calledOnce).to.be.true; - done(); - }); - }); - - it('should fail due to error in pubkey dao', function(done) { - lawnchairDaoStub.list.yields(null, []); - pubkeyDaoStub.getByUserId.yields({}); - - keychainDao.getReceiverPublicKey(testUser, function(err, key) { - expect(err).to.exist; - expect(key).to.not.exist; - expect(lawnchairDaoStub.list.calledOnce).to.be.true; - expect(pubkeyDaoStub.getByUserId.calledOnce).to.be.true; - done(); - }); - }); - - it('should work from pubkey dao with empty result', function(done) { - lawnchairDaoStub.list.yields(null, []); - pubkeyDaoStub.getByUserId.yields(); - - keychainDao.getReceiverPublicKey(testUser, function(err, key) { - expect(err).to.not.exist; - expect(key).to.not.exist; - expect(lawnchairDaoStub.list.calledOnce).to.be.true; - expect(pubkeyDaoStub.getByUserId.calledOnce).to.be.true; - done(); - }); - }); - - it('should work from pubkey dao', function(done) { - lawnchairDaoStub.list.yields(null, []); - pubkeyDaoStub.getByUserId.yields(null, { - _id: '12345', - publicKey: 'asdf' - }); - lawnchairDaoStub.persist.yields(); - - keychainDao.getReceiverPublicKey(testUser, function(err, key) { - expect(err).to.not.exist; - expect(key).to.exist; - expect(key._id).to.equal('12345'); - expect(lawnchairDaoStub.list.calledOnce).to.be.true; - expect(pubkeyDaoStub.getByUserId.calledOnce).to.be.true; - expect(lawnchairDaoStub.persist.calledOnce).to.be.true; - done(); - }); + done(); }); }); - describe('get user key pair', function() { - it('should work if local keys are already present', function(done) { - lawnchairDaoStub.list.yields(null, [{ - _id: '12345', - userId: testUser, - publicKey: 'asdf' - }]); - lawnchairDaoStub.read.yields(null, { - _id: '12345', - publicKey: 'asdf', - encryptedKey: 'qwer' - }); - - keychainDao.getUserKeyPair(testUser, function(err, keys) { - expect(err).to.not.exist; - expect(keys).to.exist; - expect(keys.publicKey).to.exist; - expect(keys.privateKey).to.exist; - expect(lawnchairDaoStub.list.calledOnce).to.be.true; - expect(lawnchairDaoStub.read.calledTwice).to.be.true; - done(); - }); + it('should update not the key when offline', function(done) { + getPubKeyStub.yields(null, oldKey); + pubkeyDaoStub.get.withArgs(oldKey._id).yields({ + code: 42 }); - it('should work if local keys are not already present', function(done) { - lawnchairDaoStub.list.yields(); - pubkeyDaoStub.getByUserId.yields(); + keychainDao.refreshKeyForUserId(testUser, function(err, key) { + expect(err).to.not.exist; + expect(key).to.to.equal(oldKey); - keychainDao.getUserKeyPair(testUser, function(err, keys) { - expect(err).to.not.exist; - expect(keys).to.not.exist; - expect(lawnchairDaoStub.list.calledOnce).to.be.true; - expect(pubkeyDaoStub.getByUserId.calledOnce).to.be.true; - done(); - }); - }); + expect(getPubKeyStub.calledOnce).to.be.true; + expect(pubkeyDaoStub.get.calledOnce).to.be.true; + expect(pubkeyDaoStub.getByUserId.called).to.be.false; + expect(lawnchairDaoStub.remove.called).to.be.false; + expect(lawnchairDaoStub.persist.called).to.be.false; - it('should work if local keys are not already present', function(done) { - lawnchairDaoStub.list.yields(); - pubkeyDaoStub.getByUserId.yields(null, { - _id: '12345', - publicKey: 'asdf' - }); - lawnchairDaoStub.read.yields(null, { - _id: '12345', - publicKey: 'asdf', - encryptedKey: 'qwer' - }); - - keychainDao.getUserKeyPair(testUser, function(err, keys) { - expect(err).to.not.exist; - expect(keys).to.exist; - expect(keys.publicKey).to.exist; - expect(keys.privateKey).to.exist; - expect(lawnchairDaoStub.list.calledOnce).to.be.true; - expect(lawnchairDaoStub.read.calledTwice).to.be.true; - done(); - }); + done(); }); }); - describe('setDeviceName', function() { - it('should work', function(done) { - lawnchairDaoStub.persist.yields(); + it('should error while persisting new key', function(done) { + getPubKeyStub.yields(null, oldKey); + pubkeyDaoStub.get.withArgs(oldKey._id).yields(); + pubkeyDaoStub.getByUserId.withArgs(testUser).yields(null, newKey); + keychainDao.requestPermissionForKeyUpdate = function(opts, cb) { + expect(opts.userId).to.equal(testUser); + expect(opts.newKey).to.equal(newKey); + cb(true); + }; + lawnchairDaoStub.remove.withArgs('publickey_' + oldKey._id).yields(); + lawnchairDaoStub.persist.yields({}); - keychainDao.setDeviceName('iPhone', done); + keychainDao.refreshKeyForUserId(testUser, function(err, key) { + expect(err).to.exist; + expect(key).to.not.exist; + + expect(getPubKeyStub.calledOnce).to.be.true; + expect(pubkeyDaoStub.get.calledOnce).to.be.true; + expect(pubkeyDaoStub.getByUserId.calledOnce).to.be.true; + expect(lawnchairDaoStub.remove.calledOnce).to.be.true; + expect(lawnchairDaoStub.persist.calledOnce).to.be.true; + + done(); }); }); - describe('getDeviceName', function() { - it('should fail when device name is not set', function(done) { - lawnchairDaoStub.read.withArgs('devicename').yields(); + it('should error while deleting old key', function(done) { + getPubKeyStub.yields(null, oldKey); + pubkeyDaoStub.get.withArgs(oldKey._id).yields(); + pubkeyDaoStub.getByUserId.withArgs(testUser).yields(); + keychainDao.requestPermissionForKeyUpdate = function(opts, cb) { + expect(opts.userId).to.equal(testUser); + cb(true); + }; + lawnchairDaoStub.remove.yields({}); - keychainDao.getDeviceName(function(err, deviceName) { - expect(err.message).to.equal('Device name not set!'); - expect(deviceName).to.not.exist; - done(); - }); - }); + keychainDao.refreshKeyForUserId(testUser, function(err, key) { + expect(err).to.exist; + expect(key).to.not.exist; - it('should fail due to error when reading device name', function(done) { - lawnchairDaoStub.read.withArgs('devicename').yields(42); + expect(getPubKeyStub.calledOnce).to.be.true; + expect(pubkeyDaoStub.get.calledOnce).to.be.true; + expect(lawnchairDaoStub.remove.calledOnce).to.be.true; + expect(pubkeyDaoStub.getByUserId.calledOnce).to.be.true; + expect(lawnchairDaoStub.persist.called).to.be.false; - keychainDao.getDeviceName(function(err, deviceName) { - expect(err).to.equal(42); - expect(deviceName).to.not.exist; - done(); - }); - }); - - it('should work', function(done) { - lawnchairDaoStub.read.withArgs('devicename').yields(null, 'iPhone'); - - keychainDao.getDeviceName(function(err, deviceName) { - expect(err).to.not.exist; - expect(deviceName).to.equal('iPhone'); - done(); - }); + done(); }); }); - describe('getDeviceSecret', function() { - it('should fail due to error when reading device secret', function(done) { - lawnchairDaoStub.read.withArgs('devicename').yields(null, 'iPhone'); - lawnchairDaoStub.read.withArgs('devicesecret').yields(42); + it('should error while persisting new key', function(done) { + getPubKeyStub.yields(null, oldKey); + pubkeyDaoStub.get.withArgs(oldKey._id).yields(); + pubkeyDaoStub.getByUserId.withArgs(testUser).yields(null, newKey); + keychainDao.requestPermissionForKeyUpdate = function(opts, cb) { + expect(opts.userId).to.equal(testUser); + expect(opts.newKey).to.equal(newKey); + cb(true); + }; + lawnchairDaoStub.remove.withArgs('publickey_' + oldKey._id).yields(); + lawnchairDaoStub.persist.yields({}); - keychainDao.getDeviceSecret(function(err, deviceSecret) { - expect(err).to.equal(42); - expect(deviceSecret).to.not.exist; - done(); - }); - }); + keychainDao.refreshKeyForUserId(testUser, function(err, key) { + expect(err).to.exist; + expect(key).to.not.exist; - it('should fail due to error when storing device secret', function(done) { - lawnchairDaoStub.read.withArgs('devicename').yields(null, 'iPhone'); - lawnchairDaoStub.read.withArgs('devicesecret').yields(); - lawnchairDaoStub.persist.withArgs('devicesecret').yields(42); + expect(getPubKeyStub.calledOnce).to.be.true; + expect(pubkeyDaoStub.get.calledOnce).to.be.true; + expect(pubkeyDaoStub.getByUserId.calledOnce).to.be.true; + expect(lawnchairDaoStub.remove.calledOnce).to.be.true; + expect(lawnchairDaoStub.persist.calledOnce).to.be.true; - keychainDao.getDeviceSecret(function(err, deviceSecret) { - expect(err).to.equal(42); - expect(deviceSecret).to.not.exist; - done(); - }); - }); - - it('should work when device secret is not set', function(done) { - lawnchairDaoStub.read.withArgs('devicename').yields(null, 'iPhone'); - lawnchairDaoStub.read.withArgs('devicesecret').yields(); - lawnchairDaoStub.persist.withArgs('devicesecret').yields(); - - keychainDao.getDeviceSecret(function(err, deviceSecret) { - expect(err).to.not.exist; - expect(deviceSecret).to.exist; - done(); - }); - }); - - it('should work when device secret is set', function(done) { - lawnchairDaoStub.read.withArgs('devicename').yields(null, 'iPhone'); - lawnchairDaoStub.read.withArgs('devicesecret').yields(null, 'secret'); - - keychainDao.getDeviceSecret(function(err, deviceSecret) { - expect(err).to.not.exist; - expect(deviceSecret).to.equal('secret'); - done(); - }); + done(); }); }); - describe('registerDevice', function() { - var getDeviceNameStub, lookupPublicKeyStub, getDeviceSecretStub; + it('should error when get failed', function(done) { + getPubKeyStub.yields(null, oldKey); + pubkeyDaoStub.get.withArgs(oldKey._id).yields({}); - beforeEach(function() { - getDeviceNameStub = sinon.stub(keychainDao, 'getDeviceName'); - lookupPublicKeyStub = sinon.stub(keychainDao, 'lookupPublicKey'); - getDeviceSecretStub = sinon.stub(keychainDao, 'getDeviceSecret'); - }); - afterEach(function() { - getDeviceNameStub.restore(); - lookupPublicKeyStub.restore(); - getDeviceSecretStub.restore(); + keychainDao.refreshKeyForUserId(testUser, function(err, key) { + expect(err).to.exist; + expect(key).to.not.exist; + + done(); }); + }); + }); - it('should fail when reading devicename', function(done) { - getDeviceNameStub.yields(42); - - keychainDao.registerDevice({}, function(err) { - expect(err).to.equal(42); - done(); - }); - }); - - it('should fail in requestDeviceRegistration', function(done) { - getDeviceNameStub.yields(null, 'iPhone'); - - privkeyDaoStub.requestDeviceRegistration.withArgs({ - userId: testUser, - deviceName: 'iPhone' - }).yields(42); - - keychainDao.registerDevice({ - userId: testUser - }, function(err) { - expect(err).to.equal(42); - done(); - }); - }); - - it('should fail due to invalid requestDeviceRegistration return value', function(done) { - getDeviceNameStub.yields(null, 'iPhone'); - - privkeyDaoStub.requestDeviceRegistration.withArgs({ - userId: testUser, - deviceName: 'iPhone' - }).yields(null, {}); - - keychainDao.registerDevice({ - userId: testUser - }, function(err) { - expect(err.message).to.equal('Invalid format for session key!'); - done(); - }); - }); - - it('should fail in lookupPublicKey', function(done) { - getDeviceNameStub.yields(null, 'iPhone'); - - privkeyDaoStub.requestDeviceRegistration.withArgs({ - userId: testUser, - deviceName: 'iPhone' - }).yields(null, { - encryptedRegSessionKey: 'asdf' - }); - - lookupPublicKeyStub.yields(42); - - keychainDao.registerDevice({ - userId: testUser - }, function(err) { - expect(err).to.equal(42); - done(); - }); - }); - - it('should fail when server public key not found', function(done) { - getDeviceNameStub.yields(null, 'iPhone'); - - privkeyDaoStub.requestDeviceRegistration.withArgs({ - userId: testUser, - deviceName: 'iPhone' - }).yields(null, { - encryptedRegSessionKey: 'asdf' - }); - - lookupPublicKeyStub.yields(); - - keychainDao.registerDevice({ - userId: testUser - }, function(err) { - expect(err).to.exist; - done(); - }); - }); - - it('should fail in decrypt', function(done) { - getDeviceNameStub.yields(null, 'iPhone'); - - privkeyDaoStub.requestDeviceRegistration.withArgs({ - userId: testUser, - deviceName: 'iPhone' - }).yields(null, { - encryptedRegSessionKey: 'asdf' - }); - - lookupPublicKeyStub.yields(null, { - publicKey: 'pubkey' - }); - pgpStub.decrypt.withArgs('asdf', 'pubkey').yields(42); - - keychainDao.registerDevice({ - userId: testUser - }, function(err) { - expect(err).to.equal(42); - done(); - }); - }); - - it('should fail in getDeviceSecret', function(done) { - getDeviceNameStub.yields(null, 'iPhone'); - - privkeyDaoStub.requestDeviceRegistration.withArgs({ - userId: testUser, - deviceName: 'iPhone' - }).yields(null, { - encryptedRegSessionKey: 'asdf' - }); - - lookupPublicKeyStub.yields(null, { - publicKey: 'pubkey' - }); - pgpStub.decrypt.withArgs('asdf', 'pubkey').yields(null, 'decrypted', true, true); - getDeviceSecretStub.yields(42); - - keychainDao.registerDevice({ - userId: testUser - }, function(err) { - expect(err).to.equal(42); - done(); - }); - }); - - it('should fail in encrypt', function(done) { - getDeviceNameStub.yields(null, 'iPhone'); - - privkeyDaoStub.requestDeviceRegistration.withArgs({ - userId: testUser, - deviceName: 'iPhone' - }).yields(null, { - encryptedRegSessionKey: 'asdf' - }); - - lookupPublicKeyStub.yields(null, { - publicKey: 'pubkey' - }); - pgpStub.decrypt.withArgs('asdf', 'pubkey').yields(null, 'decrypted', true, true); - getDeviceSecretStub.yields(null, 'secret'); - cryptoStub.encrypt.withArgs('secret', 'decrypted').yields(42); - - keychainDao.registerDevice({ - userId: testUser - }, function(err) { - expect(err).to.equal(42); - done(); - }); - }); - - it('should work', function(done) { - getDeviceNameStub.yields(null, 'iPhone'); - - privkeyDaoStub.requestDeviceRegistration.withArgs({ - userId: testUser, - deviceName: 'iPhone' - }).yields(null, { - encryptedRegSessionKey: 'asdf' - }); - - lookupPublicKeyStub.yields(null, { - publicKey: 'pubkey' - }); - pgpStub.decrypt.withArgs('asdf', 'pubkey').yields(null, 'decrypted', true, true); - getDeviceSecretStub.yields(null, 'secret'); - cryptoStub.encrypt.withArgs('secret', 'decrypted').yields(null, 'encryptedDeviceSecret'); - privkeyDaoStub.uploadDeviceSecret.yields(); - - keychainDao.registerDevice({ - userId: testUser - }, function(err) { - expect(err).not.exist; - expect(privkeyDaoStub.uploadDeviceSecret.calledOnce).to.be.true; - done(); - }); + describe('lookup public key', function() { + it('should fail', function(done) { + keychainDao.lookupPublicKey(undefined, function(err, key) { + expect(err).to.exist; + expect(key).to.not.exist; + done(); }); }); - describe('_authenticateToPrivateKeyServer', function() { - var lookupPublicKeyStub, getDeviceSecretStub; + it('should fail', function(done) { + lawnchairDaoStub.read.yields(42); - beforeEach(function() { - lookupPublicKeyStub = sinon.stub(keychainDao, 'lookupPublicKey'); - getDeviceSecretStub = sinon.stub(keychainDao, 'getDeviceSecret'); + keychainDao.lookupPublicKey('12345', function(err, key) { + expect(err).to.exist; + expect(key).to.not.exist; + expect(lawnchairDaoStub.read.calledOnce).to.be.true; + done(); }); - afterEach(function() { - lookupPublicKeyStub.restore(); - getDeviceSecretStub.restore(); + }); + + it('should work from local storage', function(done) { + lawnchairDaoStub.read.yields(null, { + _id: '12345', + publicKey: 'asdf' }); - it('should fail due to privkeyDao.requestAuthSessionKey', function(done) { - privkeyDaoStub.requestAuthSessionKey.withArgs({ - userId: testUser - }).yields(42); + keychainDao.lookupPublicKey('12345', function(err, key) { + expect(err).to.not.exist; + expect(key).to.exist; + expect(lawnchairDaoStub.read.calledOnce).to.be.true; + done(); + }); + }); - keychainDao._authenticateToPrivateKeyServer(testUser, function(err, authSessionKey) { - expect(err).to.equal(42); - expect(authSessionKey).to.not.exist; - done(); - }); + it('should work from cloud', function(done) { + lawnchairDaoStub.read.yields(); + pubkeyDaoStub.get.yields(null, { + _id: '12345', + publicKey: 'asdf' + }); + lawnchairDaoStub.persist.yields(); + + keychainDao.lookupPublicKey('12345', function(err, key) { + expect(err).to.not.exist; + expect(key).to.exist; + expect(key._id).to.equal('12345'); + expect(lawnchairDaoStub.read.calledOnce).to.be.true; + expect(pubkeyDaoStub.get.calledOnce).to.be.true; + expect(lawnchairDaoStub.persist.calledOnce).to.be.true; + done(); + }); + }); + }); + + describe('get public keys by id', function() { + it('should fail', function(done) { + keychainDao.getPublicKeys([], function(err, keys) { + expect(err).to.not.exist; + expect(keys.length).to.equal(0); + done(); + }); + }); + + it('should fail', function(done) { + lawnchairDaoStub.read.yields(42); + + var ids = [{ + _id: '12345' + }]; + keychainDao.getPublicKeys(ids, function(err, keys) { + expect(err).to.exist; + expect(keys).to.not.exist; + expect(lawnchairDaoStub.read.calledOnce).to.be.true; + done(); + }); + }); + + it('should work from local storage', function(done) { + lawnchairDaoStub.read.yields(null, { + _id: '12345', + publicKey: 'asdf' }); - it('should fail due to privkeyDao.requestAuthSessionKey response', function(done) { - privkeyDaoStub.requestAuthSessionKey.yields(null, {}); + var ids = [{ + _id: '12345' + }]; + keychainDao.getPublicKeys(ids, function(err, keys) { + expect(err).to.not.exist; + expect(keys.length).to.equal(1); + expect(keys[0]._id).to.equal('12345'); + expect(lawnchairDaoStub.read.calledOnce).to.be.true; + done(); + }); + }); + }); - keychainDao._authenticateToPrivateKeyServer(testUser, function(err, authSessionKey) { - expect(err).to.exist; - expect(authSessionKey).to.not.exist; - done(); - }); + describe('get receiver public key', function() { + it('should fail due to error in lawnchair list', function(done) { + lawnchairDaoStub.list.yields(42); + + keychainDao.getReceiverPublicKey(testUser, function(err, key) { + expect(err).to.exist; + expect(key).to.not.exist; + expect(lawnchairDaoStub.list.calledOnce).to.be.true; + done(); + }); + }); + + it('should work from lawnchair list', function(done) { + lawnchairDaoStub.list.yields(null, [{ + _id: '12345', + userId: testUser, + publicKey: 'asdf' + }]); + + keychainDao.getReceiverPublicKey(testUser, function(err, key) { + expect(err).to.not.exist; + expect(key).to.exist; + expect(key._id).to.equal('12345'); + expect(lawnchairDaoStub.list.calledOnce).to.be.true; + done(); + }); + }); + + it('should work for keys with secondary userIds', function(done) { + lawnchairDaoStub.list.yields(null, [{ + _id: '12345', + userId: 'not testUser', + userIds: [{ + emailAddress: testUser + }], + publicKey: 'asdf' + }]); + + keychainDao.getReceiverPublicKey(testUser, function(err, key) { + expect(err).to.not.exist; + expect(key).to.exist; + expect(key._id).to.equal('12345'); + expect(lawnchairDaoStub.list.calledOnce).to.be.true; + done(); + }); + }); + + it('should fail due to error in pubkey dao', function(done) { + lawnchairDaoStub.list.yields(null, []); + pubkeyDaoStub.getByUserId.yields({}); + + keychainDao.getReceiverPublicKey(testUser, function(err, key) { + expect(err).to.exist; + expect(key).to.not.exist; + expect(lawnchairDaoStub.list.calledOnce).to.be.true; + expect(pubkeyDaoStub.getByUserId.calledOnce).to.be.true; + done(); + }); + }); + + it('should work from pubkey dao with empty result', function(done) { + lawnchairDaoStub.list.yields(null, []); + pubkeyDaoStub.getByUserId.yields(); + + keychainDao.getReceiverPublicKey(testUser, function(err, key) { + expect(err).to.not.exist; + expect(key).to.not.exist; + expect(lawnchairDaoStub.list.calledOnce).to.be.true; + expect(pubkeyDaoStub.getByUserId.calledOnce).to.be.true; + done(); + }); + }); + + it('should work from pubkey dao', function(done) { + lawnchairDaoStub.list.yields(null, []); + pubkeyDaoStub.getByUserId.yields(null, { + _id: '12345', + publicKey: 'asdf' + }); + lawnchairDaoStub.persist.yields(); + + keychainDao.getReceiverPublicKey(testUser, function(err, key) { + expect(err).to.not.exist; + expect(key).to.exist; + expect(key._id).to.equal('12345'); + expect(lawnchairDaoStub.list.calledOnce).to.be.true; + expect(pubkeyDaoStub.getByUserId.calledOnce).to.be.true; + expect(lawnchairDaoStub.persist.calledOnce).to.be.true; + done(); + }); + }); + }); + + describe('get user key pair', function() { + it('should work if local keys are already present', function(done) { + lawnchairDaoStub.list.yields(null, [{ + _id: '12345', + userId: testUser, + publicKey: 'asdf' + }]); + lawnchairDaoStub.read.yields(null, { + _id: '12345', + publicKey: 'asdf', + encryptedKey: 'qwer' }); - it('should fail due to lookupPublicKey', function(done) { - privkeyDaoStub.requestAuthSessionKey.yields(null, { - encryptedAuthSessionKey: 'encryptedAuthSessionKey', - encryptedChallenge: 'encryptedChallenge', + keychainDao.getUserKeyPair(testUser, function(err, keys) { + expect(err).to.not.exist; + expect(keys).to.exist; + expect(keys.publicKey).to.exist; + expect(keys.privateKey).to.exist; + expect(lawnchairDaoStub.list.calledOnce).to.be.true; + expect(lawnchairDaoStub.read.calledTwice).to.be.true; + done(); + }); + }); + + it('should work if local keys are not already present', function(done) { + lawnchairDaoStub.list.yields(); + pubkeyDaoStub.getByUserId.yields(); + + keychainDao.getUserKeyPair(testUser, function(err, keys) { + expect(err).to.not.exist; + expect(keys).to.not.exist; + expect(lawnchairDaoStub.list.calledOnce).to.be.true; + expect(pubkeyDaoStub.getByUserId.calledOnce).to.be.true; + done(); + }); + }); + + it('should work if local keys are not already present', function(done) { + lawnchairDaoStub.list.yields(); + pubkeyDaoStub.getByUserId.yields(null, { + _id: '12345', + publicKey: 'asdf' + }); + lawnchairDaoStub.read.yields(null, { + _id: '12345', + publicKey: 'asdf', + encryptedKey: 'qwer' + }); + + keychainDao.getUserKeyPair(testUser, function(err, keys) { + expect(err).to.not.exist; + expect(keys).to.exist; + expect(keys.publicKey).to.exist; + expect(keys.privateKey).to.exist; + expect(lawnchairDaoStub.list.calledOnce).to.be.true; + expect(lawnchairDaoStub.read.calledTwice).to.be.true; + done(); + }); + }); + }); + + describe('setDeviceName', function() { + it('should work', function(done) { + lawnchairDaoStub.persist.yields(); + + keychainDao.setDeviceName('iPhone', done); + }); + }); + + describe('getDeviceName', function() { + it('should fail when device name is not set', function(done) { + lawnchairDaoStub.read.withArgs('devicename').yields(); + + keychainDao.getDeviceName(function(err, deviceName) { + expect(err.message).to.equal('Device name not set!'); + expect(deviceName).to.not.exist; + done(); + }); + }); + + it('should fail due to error when reading device name', function(done) { + lawnchairDaoStub.read.withArgs('devicename').yields(42); + + keychainDao.getDeviceName(function(err, deviceName) { + expect(err).to.equal(42); + expect(deviceName).to.not.exist; + done(); + }); + }); + + it('should work', function(done) { + lawnchairDaoStub.read.withArgs('devicename').yields(null, 'iPhone'); + + keychainDao.getDeviceName(function(err, deviceName) { + expect(err).to.not.exist; + expect(deviceName).to.equal('iPhone'); + done(); + }); + }); + }); + + describe('getDeviceSecret', function() { + it('should fail due to error when reading device secret', function(done) { + lawnchairDaoStub.read.withArgs('devicename').yields(null, 'iPhone'); + lawnchairDaoStub.read.withArgs('devicesecret').yields(42); + + keychainDao.getDeviceSecret(function(err, deviceSecret) { + expect(err).to.equal(42); + expect(deviceSecret).to.not.exist; + done(); + }); + }); + + it('should fail due to error when storing device secret', function(done) { + lawnchairDaoStub.read.withArgs('devicename').yields(null, 'iPhone'); + lawnchairDaoStub.read.withArgs('devicesecret').yields(); + lawnchairDaoStub.persist.withArgs('devicesecret').yields(42); + + keychainDao.getDeviceSecret(function(err, deviceSecret) { + expect(err).to.equal(42); + expect(deviceSecret).to.not.exist; + done(); + }); + }); + + it('should work when device secret is not set', function(done) { + lawnchairDaoStub.read.withArgs('devicename').yields(null, 'iPhone'); + lawnchairDaoStub.read.withArgs('devicesecret').yields(); + lawnchairDaoStub.persist.withArgs('devicesecret').yields(); + + keychainDao.getDeviceSecret(function(err, deviceSecret) { + expect(err).to.not.exist; + expect(deviceSecret).to.exist; + done(); + }); + }); + + it('should work when device secret is set', function(done) { + lawnchairDaoStub.read.withArgs('devicename').yields(null, 'iPhone'); + lawnchairDaoStub.read.withArgs('devicesecret').yields(null, 'secret'); + + keychainDao.getDeviceSecret(function(err, deviceSecret) { + expect(err).to.not.exist; + expect(deviceSecret).to.equal('secret'); + done(); + }); + }); + }); + + describe('registerDevice', function() { + var getDeviceNameStub, lookupPublicKeyStub, getDeviceSecretStub; + + beforeEach(function() { + getDeviceNameStub = sinon.stub(keychainDao, 'getDeviceName'); + lookupPublicKeyStub = sinon.stub(keychainDao, 'lookupPublicKey'); + getDeviceSecretStub = sinon.stub(keychainDao, 'getDeviceSecret'); + }); + afterEach(function() { + getDeviceNameStub.restore(); + lookupPublicKeyStub.restore(); + getDeviceSecretStub.restore(); + }); + + it('should fail when reading devicename', function(done) { + getDeviceNameStub.yields(42); + + keychainDao.registerDevice({}, function(err) { + expect(err).to.equal(42); + done(); + }); + }); + + it('should fail in requestDeviceRegistration', function(done) { + getDeviceNameStub.yields(null, 'iPhone'); + + privkeyDaoStub.requestDeviceRegistration.withArgs({ + userId: testUser, + deviceName: 'iPhone' + }).yields(42); + + keychainDao.registerDevice({ + userId: testUser + }, function(err) { + expect(err).to.equal(42); + done(); + }); + }); + + it('should fail due to invalid requestDeviceRegistration return value', function(done) { + getDeviceNameStub.yields(null, 'iPhone'); + + privkeyDaoStub.requestDeviceRegistration.withArgs({ + userId: testUser, + deviceName: 'iPhone' + }).yields(null, {}); + + keychainDao.registerDevice({ + userId: testUser + }, function(err) { + expect(err.message).to.equal('Invalid format for session key!'); + done(); + }); + }); + + it('should fail in lookupPublicKey', function(done) { + getDeviceNameStub.yields(null, 'iPhone'); + + privkeyDaoStub.requestDeviceRegistration.withArgs({ + userId: testUser, + deviceName: 'iPhone' + }).yields(null, { + encryptedRegSessionKey: 'asdf' + }); + + lookupPublicKeyStub.yields(42); + + keychainDao.registerDevice({ + userId: testUser + }, function(err) { + expect(err).to.equal(42); + done(); + }); + }); + + it('should fail when server public key not found', function(done) { + getDeviceNameStub.yields(null, 'iPhone'); + + privkeyDaoStub.requestDeviceRegistration.withArgs({ + userId: testUser, + deviceName: 'iPhone' + }).yields(null, { + encryptedRegSessionKey: 'asdf' + }); + + lookupPublicKeyStub.yields(); + + keychainDao.registerDevice({ + userId: testUser + }, function(err) { + expect(err).to.exist; + done(); + }); + }); + + it('should fail in decrypt', function(done) { + getDeviceNameStub.yields(null, 'iPhone'); + + privkeyDaoStub.requestDeviceRegistration.withArgs({ + userId: testUser, + deviceName: 'iPhone' + }).yields(null, { + encryptedRegSessionKey: 'asdf' + }); + + lookupPublicKeyStub.yields(null, { + publicKey: 'pubkey' + }); + pgpStub.decrypt.withArgs('asdf', 'pubkey').yields(42); + + keychainDao.registerDevice({ + userId: testUser + }, function(err) { + expect(err).to.equal(42); + done(); + }); + }); + + it('should fail in getDeviceSecret', function(done) { + getDeviceNameStub.yields(null, 'iPhone'); + + privkeyDaoStub.requestDeviceRegistration.withArgs({ + userId: testUser, + deviceName: 'iPhone' + }).yields(null, { + encryptedRegSessionKey: 'asdf' + }); + + lookupPublicKeyStub.yields(null, { + publicKey: 'pubkey' + }); + pgpStub.decrypt.withArgs('asdf', 'pubkey').yields(null, 'decrypted', true, true); + getDeviceSecretStub.yields(42); + + keychainDao.registerDevice({ + userId: testUser + }, function(err) { + expect(err).to.equal(42); + done(); + }); + }); + + it('should fail in encrypt', function(done) { + getDeviceNameStub.yields(null, 'iPhone'); + + privkeyDaoStub.requestDeviceRegistration.withArgs({ + userId: testUser, + deviceName: 'iPhone' + }).yields(null, { + encryptedRegSessionKey: 'asdf' + }); + + lookupPublicKeyStub.yields(null, { + publicKey: 'pubkey' + }); + pgpStub.decrypt.withArgs('asdf', 'pubkey').yields(null, 'decrypted', true, true); + getDeviceSecretStub.yields(null, 'secret'); + cryptoStub.encrypt.withArgs('secret', 'decrypted').yields(42); + + keychainDao.registerDevice({ + userId: testUser + }, function(err) { + expect(err).to.equal(42); + done(); + }); + }); + + it('should work', function(done) { + getDeviceNameStub.yields(null, 'iPhone'); + + privkeyDaoStub.requestDeviceRegistration.withArgs({ + userId: testUser, + deviceName: 'iPhone' + }).yields(null, { + encryptedRegSessionKey: 'asdf' + }); + + lookupPublicKeyStub.yields(null, { + publicKey: 'pubkey' + }); + pgpStub.decrypt.withArgs('asdf', 'pubkey').yields(null, 'decrypted', true, true); + getDeviceSecretStub.yields(null, 'secret'); + cryptoStub.encrypt.withArgs('secret', 'decrypted').yields(null, 'encryptedDeviceSecret'); + privkeyDaoStub.uploadDeviceSecret.yields(); + + keychainDao.registerDevice({ + userId: testUser + }, function(err) { + expect(err).not.exist; + expect(privkeyDaoStub.uploadDeviceSecret.calledOnce).to.be.true; + done(); + }); + }); + }); + + describe('_authenticateToPrivateKeyServer', function() { + var lookupPublicKeyStub, getDeviceSecretStub; + + beforeEach(function() { + lookupPublicKeyStub = sinon.stub(keychainDao, 'lookupPublicKey'); + getDeviceSecretStub = sinon.stub(keychainDao, 'getDeviceSecret'); + }); + afterEach(function() { + lookupPublicKeyStub.restore(); + getDeviceSecretStub.restore(); + }); + + it('should fail due to privkeyDao.requestAuthSessionKey', function(done) { + privkeyDaoStub.requestAuthSessionKey.withArgs({ + userId: testUser + }).yields(42); + + keychainDao._authenticateToPrivateKeyServer(testUser, function(err, authSessionKey) { + expect(err).to.equal(42); + expect(authSessionKey).to.not.exist; + done(); + }); + }); + + it('should fail due to privkeyDao.requestAuthSessionKey response', function(done) { + privkeyDaoStub.requestAuthSessionKey.yields(null, {}); + + keychainDao._authenticateToPrivateKeyServer(testUser, function(err, authSessionKey) { + expect(err).to.exist; + expect(authSessionKey).to.not.exist; + done(); + }); + }); + + it('should fail due to lookupPublicKey', function(done) { + privkeyDaoStub.requestAuthSessionKey.yields(null, { + encryptedAuthSessionKey: 'encryptedAuthSessionKey', + encryptedChallenge: 'encryptedChallenge', + sessionId: 'sessionId' + }); + + lookupPublicKeyStub.yields(42); + + keychainDao._authenticateToPrivateKeyServer(testUser, function(err, authSessionKey) { + expect(err).to.exist; + expect(authSessionKey).to.not.exist; + done(); + }); + }); + + it('should fail due to pgp.decrypt', function(done) { + privkeyDaoStub.requestAuthSessionKey.yields(null, { + encryptedAuthSessionKey: 'encryptedAuthSessionKey', + encryptedChallenge: 'encryptedChallenge', + sessionId: 'sessionId' + }); + + lookupPublicKeyStub.yields(null, { + publickKey: 'publicKey' + }); + + pgpStub.decrypt.yields(42); + + keychainDao._authenticateToPrivateKeyServer(testUser, function(err, authSessionKey) { + expect(err).to.exist; + expect(authSessionKey).to.not.exist; + done(); + }); + }); + + it('should fail due to getDeviceSecret', function(done) { + privkeyDaoStub.requestAuthSessionKey.yields(null, { + encryptedAuthSessionKey: 'encryptedAuthSessionKey', + encryptedChallenge: 'encryptedChallenge', + sessionId: 'sessionId' + }); + + lookupPublicKeyStub.yields(null, { + publickKey: 'publicKey' + }); + + pgpStub.decrypt.yields(null, 'decryptedStuff'); + getDeviceSecretStub.yields(42); + + keychainDao._authenticateToPrivateKeyServer(testUser, function(err, authSessionKey) { + expect(err).to.exist; + expect(authSessionKey).to.not.exist; + done(); + }); + }); + + it('should fail due to crypto.encrypt', function(done) { + privkeyDaoStub.requestAuthSessionKey.yields(null, { + encryptedAuthSessionKey: 'encryptedAuthSessionKey', + encryptedChallenge: 'encryptedChallenge', + sessionId: 'sessionId' + }); + + lookupPublicKeyStub.yields(null, { + publickKey: 'publicKey' + }); + + pgpStub.decrypt.yields(null, 'decryptedStuff'); + getDeviceSecretStub.yields(null, 'deviceSecret'); + cryptoStub.encrypt.yields(42); + + keychainDao._authenticateToPrivateKeyServer(testUser, function(err, authSessionKey) { + expect(err).to.exist; + expect(authSessionKey).to.not.exist; + done(); + }); + }); + + it('should fail due to privkeyDao.verifyAuthentication', function(done) { + privkeyDaoStub.requestAuthSessionKey.yields(null, { + encryptedAuthSessionKey: 'encryptedAuthSessionKey', + encryptedChallenge: 'encryptedChallenge', + sessionId: 'sessionId' + }); + + lookupPublicKeyStub.yields(null, { + publickKey: 'publicKey' + }); + + pgpStub.decrypt.yields(null, 'decryptedStuff', true, true); + getDeviceSecretStub.yields(null, 'deviceSecret'); + cryptoStub.encrypt.yields(null, 'encryptedStuff'); + privkeyDaoStub.verifyAuthentication.yields(42); + + keychainDao._authenticateToPrivateKeyServer(testUser, function(err, authSessionKey) { + expect(err).to.exist; + expect(authSessionKey).to.not.exist; + done(); + }); + }); + + it('should fail due to server public key nto found', function(done) { + privkeyDaoStub.requestAuthSessionKey.yields(null, { + encryptedAuthSessionKey: 'encryptedAuthSessionKey', + encryptedChallenge: 'encryptedChallenge', + sessionId: 'sessionId' + }); + + lookupPublicKeyStub.yields(); + + pgpStub.decrypt.yields(null, 'decryptedStuff', true, true); + getDeviceSecretStub.yields(null, 'deviceSecret'); + cryptoStub.encrypt.yields(null, 'encryptedStuff'); + privkeyDaoStub.verifyAuthentication.yields(); + + keychainDao._authenticateToPrivateKeyServer(testUser, function(err, authSessionKey) { + expect(err).to.exist; + expect(authSessionKey).to.not.exist; + done(); + }); + }); + + it('should work', function(done) { + privkeyDaoStub.requestAuthSessionKey.yields(null, { + encryptedAuthSessionKey: 'encryptedAuthSessionKey', + encryptedChallenge: 'encryptedChallenge', + sessionId: 'sessionId' + }); + + lookupPublicKeyStub.yields(null, { + publicKey: 'publicKey' + }); + + pgpStub.decrypt.yields(null, 'decryptedStuff', true, true); + getDeviceSecretStub.yields(null, 'deviceSecret'); + cryptoStub.encrypt.yields(null, 'encryptedStuff'); + privkeyDaoStub.verifyAuthentication.yields(); + + keychainDao._authenticateToPrivateKeyServer(testUser, function(err, authSessionKey) { + expect(err).to.not.exist; + expect(authSessionKey).to.deep.equal({ + sessionKey: 'decryptedStuff', sessionId: 'sessionId' }); - - lookupPublicKeyStub.yields(42); - - keychainDao._authenticateToPrivateKeyServer(testUser, function(err, authSessionKey) { - expect(err).to.exist; - expect(authSessionKey).to.not.exist; - done(); - }); + done(); }); + }); + }); - it('should fail due to pgp.decrypt', function(done) { - privkeyDaoStub.requestAuthSessionKey.yields(null, { - encryptedAuthSessionKey: 'encryptedAuthSessionKey', - encryptedChallenge: 'encryptedChallenge', - sessionId: 'sessionId' - }); + describe('uploadPrivateKey', function() { + var getUserKeyPairStub, _authenticateToPrivateKeyServerStub; - lookupPublicKeyStub.yields(null, { - publickKey: 'publicKey' - }); + beforeEach(function() { + getUserKeyPairStub = sinon.stub(keychainDao, 'getUserKeyPair'); + _authenticateToPrivateKeyServerStub = sinon.stub(keychainDao, '_authenticateToPrivateKeyServer'); + }); + afterEach(function() { + getUserKeyPairStub.restore(); + _authenticateToPrivateKeyServerStub.restore(); + }); - pgpStub.decrypt.yields(42); - - keychainDao._authenticateToPrivateKeyServer(testUser, function(err, authSessionKey) { - expect(err).to.exist; - expect(authSessionKey).to.not.exist; - done(); - }); - }); - - it('should fail due to getDeviceSecret', function(done) { - privkeyDaoStub.requestAuthSessionKey.yields(null, { - encryptedAuthSessionKey: 'encryptedAuthSessionKey', - encryptedChallenge: 'encryptedChallenge', - sessionId: 'sessionId' - }); - - lookupPublicKeyStub.yields(null, { - publickKey: 'publicKey' - }); - - pgpStub.decrypt.yields(null, 'decryptedStuff'); - getDeviceSecretStub.yields(42); - - keychainDao._authenticateToPrivateKeyServer(testUser, function(err, authSessionKey) { - expect(err).to.exist; - expect(authSessionKey).to.not.exist; - done(); - }); - }); - - it('should fail due to crypto.encrypt', function(done) { - privkeyDaoStub.requestAuthSessionKey.yields(null, { - encryptedAuthSessionKey: 'encryptedAuthSessionKey', - encryptedChallenge: 'encryptedChallenge', - sessionId: 'sessionId' - }); - - lookupPublicKeyStub.yields(null, { - publickKey: 'publicKey' - }); - - pgpStub.decrypt.yields(null, 'decryptedStuff'); - getDeviceSecretStub.yields(null, 'deviceSecret'); - cryptoStub.encrypt.yields(42); - - keychainDao._authenticateToPrivateKeyServer(testUser, function(err, authSessionKey) { - expect(err).to.exist; - expect(authSessionKey).to.not.exist; - done(); - }); - }); - - it('should fail due to privkeyDao.verifyAuthentication', function(done) { - privkeyDaoStub.requestAuthSessionKey.yields(null, { - encryptedAuthSessionKey: 'encryptedAuthSessionKey', - encryptedChallenge: 'encryptedChallenge', - sessionId: 'sessionId' - }); - - lookupPublicKeyStub.yields(null, { - publickKey: 'publicKey' - }); - - pgpStub.decrypt.yields(null, 'decryptedStuff', true, true); - getDeviceSecretStub.yields(null, 'deviceSecret'); - cryptoStub.encrypt.yields(null, 'encryptedStuff'); - privkeyDaoStub.verifyAuthentication.yields(42); - - keychainDao._authenticateToPrivateKeyServer(testUser, function(err, authSessionKey) { - expect(err).to.exist; - expect(authSessionKey).to.not.exist; - done(); - }); - }); - - it('should fail due to server public key nto found', function(done) { - privkeyDaoStub.requestAuthSessionKey.yields(null, { - encryptedAuthSessionKey: 'encryptedAuthSessionKey', - encryptedChallenge: 'encryptedChallenge', - sessionId: 'sessionId' - }); - - lookupPublicKeyStub.yields(); - - pgpStub.decrypt.yields(null, 'decryptedStuff', true, true); - getDeviceSecretStub.yields(null, 'deviceSecret'); - cryptoStub.encrypt.yields(null, 'encryptedStuff'); - privkeyDaoStub.verifyAuthentication.yields(); - - keychainDao._authenticateToPrivateKeyServer(testUser, function(err, authSessionKey) { - expect(err).to.exist; - expect(authSessionKey).to.not.exist; - done(); - }); - }); - - it('should work', function(done) { - privkeyDaoStub.requestAuthSessionKey.yields(null, { - encryptedAuthSessionKey: 'encryptedAuthSessionKey', - encryptedChallenge: 'encryptedChallenge', - sessionId: 'sessionId' - }); - - lookupPublicKeyStub.yields(null, { - publicKey: 'publicKey' - }); - - pgpStub.decrypt.yields(null, 'decryptedStuff', true, true); - getDeviceSecretStub.yields(null, 'deviceSecret'); - cryptoStub.encrypt.yields(null, 'encryptedStuff'); - privkeyDaoStub.verifyAuthentication.yields(); - - keychainDao._authenticateToPrivateKeyServer(testUser, function(err, authSessionKey) { - expect(err).to.not.exist; - expect(authSessionKey).to.deep.equal({ - sessionKey: 'decryptedStuff', - sessionId: 'sessionId' - }); - done(); - }); + it('should fail due to missing args', function(done) { + keychainDao.uploadPrivateKey({}, function(err) { + expect(err).to.exist; + done(); }); }); - describe('uploadPrivateKey', function() { - var getUserKeyPairStub, _authenticateToPrivateKeyServerStub; + it('should fail due to error in derive key', function(done) { + cryptoStub.deriveKey.yields(42); - beforeEach(function() { - getUserKeyPairStub = sinon.stub(keychainDao, 'getUserKeyPair'); - _authenticateToPrivateKeyServerStub = sinon.stub(keychainDao, '_authenticateToPrivateKeyServer'); - }); - afterEach(function() { - getUserKeyPairStub.restore(); - _authenticateToPrivateKeyServerStub.restore(); - }); - - it('should fail due to missing args', function(done) { - keychainDao.uploadPrivateKey({}, function(err) { - expect(err).to.exist; - done(); - }); - }); - - it('should fail due to error in derive key', function(done) { - cryptoStub.deriveKey.yields(42); - - keychainDao.uploadPrivateKey({ - code: 'code', - userId: testUser - }, function(err) { - expect(err).to.exist; - expect(cryptoStub.deriveKey.calledOnce).to.be.true; - done(); - }); - }); - - it('should fail due to error in getUserKeyPair', function(done) { - cryptoStub.deriveKey.yields(null, 'derivedKey'); - getUserKeyPairStub.yields(42); - - keychainDao.uploadPrivateKey({ - code: 'code', - userId: testUser - }, function(err) { - expect(err).to.exist; - expect(cryptoStub.deriveKey.calledOnce).to.be.true; - expect(getUserKeyPairStub.calledOnce).to.be.true; - done(); - }); - }); - - it('should fail due to error in crypto.encrypt', function(done) { - cryptoStub.deriveKey.yields(null, 'derivedKey'); - getUserKeyPairStub.yields(null, { - privateKey: { - _id: 'pgpKeyId', - encryptedKey: 'pgpKey' - } - }); - cryptoStub.encrypt.yields(42); - - keychainDao.uploadPrivateKey({ - code: 'code', - userId: testUser - }, function(err) { - expect(err).to.exist; - expect(cryptoStub.deriveKey.calledOnce).to.be.true; - expect(getUserKeyPairStub.calledOnce).to.be.true; - expect(cryptoStub.encrypt.calledOnce).to.be.true; - done(); - }); - }); - - it('should fail due to error in _authenticateToPrivateKeyServer', function(done) { - cryptoStub.deriveKey.yields(null, 'derivedKey'); - getUserKeyPairStub.yields(null, { - privateKey: { - _id: 'pgpKeyId', - encryptedKey: 'pgpKey' - } - }); - cryptoStub.encrypt.yields(null, 'encryptedPgpKey'); - _authenticateToPrivateKeyServerStub.yields(42); - - keychainDao.uploadPrivateKey({ - code: 'code', - userId: testUser - }, function(err) { - expect(err).to.exist; - expect(cryptoStub.deriveKey.calledOnce).to.be.true; - expect(getUserKeyPairStub.calledOnce).to.be.true; - expect(cryptoStub.encrypt.calledOnce).to.be.true; - expect(_authenticateToPrivateKeyServerStub.calledOnce).to.be.true; - done(); - }); - }); - - it('should fail due to error in cryptoStub.encrypt', function(done) { - cryptoStub.deriveKey.yields(null, 'derivedKey'); - getUserKeyPairStub.yields(null, { - privateKey: { - _id: 'pgpKeyId', - encryptedKey: 'pgpKey' - } - }); - cryptoStub.encrypt.withArgs('pgpKey').yields(null, 'encryptedPgpKey'); - _authenticateToPrivateKeyServerStub.yields(null, { - sessionId: 'sessionId', - sessionKey: 'sessionKey' - }); - cryptoStub.encrypt.withArgs('encryptedPgpKey').yields(42); - - keychainDao.uploadPrivateKey({ - code: 'code', - userId: testUser - }, function(err) { - expect(err).to.exist; - expect(cryptoStub.deriveKey.calledOnce).to.be.true; - expect(getUserKeyPairStub.calledOnce).to.be.true; - expect(cryptoStub.encrypt.calledTwice).to.be.true; - expect(_authenticateToPrivateKeyServerStub.calledOnce).to.be.true; - done(); - }); - }); - - it('should work', function(done) { - cryptoStub.deriveKey.yields(null, 'derivedKey'); - getUserKeyPairStub.yields(null, { - privateKey: { - _id: 'pgpKeyId', - encryptedKey: 'pgpKey' - } - }); - cryptoStub.encrypt.withArgs('pgpKey').yields(null, 'encryptedPgpKey'); - _authenticateToPrivateKeyServerStub.yields(null, { - sessionId: 'sessionId', - sessionKey: 'sessionKey' - }); - cryptoStub.encrypt.withArgs('encryptedPgpKey').yields(null, 'doubleEncryptedPgpKey'); - privkeyDaoStub.upload.yields(); - - keychainDao.uploadPrivateKey({ - code: 'code', - userId: testUser - }, function(err) { - expect(err).to.not.exist; - expect(cryptoStub.deriveKey.calledOnce).to.be.true; - expect(getUserKeyPairStub.calledOnce).to.be.true; - expect(cryptoStub.encrypt.calledTwice).to.be.true; - expect(_authenticateToPrivateKeyServerStub.calledOnce).to.be.true; - expect(privkeyDaoStub.upload.calledOnce).to.be.true; - done(); - }); + keychainDao.uploadPrivateKey({ + code: 'code', + userId: testUser + }, function(err) { + expect(err).to.exist; + expect(cryptoStub.deriveKey.calledOnce).to.be.true; + done(); }); }); - describe('requestPrivateKeyDownload', function() { - it('should work', function(done) { - var options = { - userId: testUser, - keyId: 'someId' - }; + it('should fail due to error in getUserKeyPair', function(done) { + cryptoStub.deriveKey.yields(null, 'derivedKey'); + getUserKeyPairStub.yields(42); - privkeyDaoStub.requestDownload.withArgs(options).yields(); - keychainDao.requestPrivateKeyDownload(options, done); + keychainDao.uploadPrivateKey({ + code: 'code', + userId: testUser + }, function(err) { + expect(err).to.exist; + expect(cryptoStub.deriveKey.calledOnce).to.be.true; + expect(getUserKeyPairStub.calledOnce).to.be.true; + done(); }); }); - describe('hasPrivateKey', function() { - it('should work', function(done) { - var options = { - userId: testUser, - keyId: 'someId' - }; + it('should fail due to error in crypto.encrypt', function(done) { + cryptoStub.deriveKey.yields(null, 'derivedKey'); + getUserKeyPairStub.yields(null, { + privateKey: { + _id: 'pgpKeyId', + encryptedKey: 'pgpKey' + } + }); + cryptoStub.encrypt.yields(42); - privkeyDaoStub.hasPrivateKey.withArgs(options).yields(); - keychainDao.hasPrivateKey(options, done); + keychainDao.uploadPrivateKey({ + code: 'code', + userId: testUser + }, function(err) { + expect(err).to.exist; + expect(cryptoStub.deriveKey.calledOnce).to.be.true; + expect(getUserKeyPairStub.calledOnce).to.be.true; + expect(cryptoStub.encrypt.calledOnce).to.be.true; + done(); }); }); - describe('downloadPrivateKey', function() { - it('should work', function(done) { - var options = { - recoveryToken: 'token' - }; + it('should fail due to error in _authenticateToPrivateKeyServer', function(done) { + cryptoStub.deriveKey.yields(null, 'derivedKey'); + getUserKeyPairStub.yields(null, { + privateKey: { + _id: 'pgpKeyId', + encryptedKey: 'pgpKey' + } + }); + cryptoStub.encrypt.yields(null, 'encryptedPgpKey'); + _authenticateToPrivateKeyServerStub.yields(42); - privkeyDaoStub.download.withArgs(options).yields(); - keychainDao.downloadPrivateKey(options, done); + keychainDao.uploadPrivateKey({ + code: 'code', + userId: testUser + }, function(err) { + expect(err).to.exist; + expect(cryptoStub.deriveKey.calledOnce).to.be.true; + expect(getUserKeyPairStub.calledOnce).to.be.true; + expect(cryptoStub.encrypt.calledOnce).to.be.true; + expect(_authenticateToPrivateKeyServerStub.calledOnce).to.be.true; + done(); }); }); - describe('decryptAndStorePrivateKeyLocally', function() { - var saveLocalPrivateKeyStub, testData; + it('should fail due to error in cryptoStub.encrypt', function(done) { + cryptoStub.deriveKey.yields(null, 'derivedKey'); + getUserKeyPairStub.yields(null, { + privateKey: { + _id: 'pgpKeyId', + encryptedKey: 'pgpKey' + } + }); + cryptoStub.encrypt.withArgs('pgpKey').yields(null, 'encryptedPgpKey'); + _authenticateToPrivateKeyServerStub.yields(null, { + sessionId: 'sessionId', + sessionKey: 'sessionKey' + }); + cryptoStub.encrypt.withArgs('encryptedPgpKey').yields(42); - beforeEach(function() { - testData = { + keychainDao.uploadPrivateKey({ + code: 'code', + userId: testUser + }, function(err) { + expect(err).to.exist; + expect(cryptoStub.deriveKey.calledOnce).to.be.true; + expect(getUserKeyPairStub.calledOnce).to.be.true; + expect(cryptoStub.encrypt.calledTwice).to.be.true; + expect(_authenticateToPrivateKeyServerStub.calledOnce).to.be.true; + done(); + }); + }); + + it('should work', function(done) { + cryptoStub.deriveKey.yields(null, 'derivedKey'); + getUserKeyPairStub.yields(null, { + privateKey: { + _id: 'pgpKeyId', + encryptedKey: 'pgpKey' + } + }); + cryptoStub.encrypt.withArgs('pgpKey').yields(null, 'encryptedPgpKey'); + _authenticateToPrivateKeyServerStub.yields(null, { + sessionId: 'sessionId', + sessionKey: 'sessionKey' + }); + cryptoStub.encrypt.withArgs('encryptedPgpKey').yields(null, 'doubleEncryptedPgpKey'); + privkeyDaoStub.upload.yields(); + + keychainDao.uploadPrivateKey({ + code: 'code', + userId: testUser + }, function(err) { + expect(err).to.not.exist; + expect(cryptoStub.deriveKey.calledOnce).to.be.true; + expect(getUserKeyPairStub.calledOnce).to.be.true; + expect(cryptoStub.encrypt.calledTwice).to.be.true; + expect(_authenticateToPrivateKeyServerStub.calledOnce).to.be.true; + expect(privkeyDaoStub.upload.calledOnce).to.be.true; + done(); + }); + }); + }); + + describe('requestPrivateKeyDownload', function() { + it('should work', function(done) { + var options = { + userId: testUser, + keyId: 'someId' + }; + + privkeyDaoStub.requestDownload.withArgs(options).yields(); + keychainDao.requestPrivateKeyDownload(options, done); + }); + }); + + describe('hasPrivateKey', function() { + it('should work', function(done) { + var options = { + userId: testUser, + keyId: 'someId' + }; + + privkeyDaoStub.hasPrivateKey.withArgs(options).yields(); + keychainDao.hasPrivateKey(options, done); + }); + }); + + describe('downloadPrivateKey', function() { + it('should work', function(done) { + var options = { + recoveryToken: 'token' + }; + + privkeyDaoStub.download.withArgs(options).yields(); + keychainDao.downloadPrivateKey(options, done); + }); + }); + + describe('decryptAndStorePrivateKeyLocally', function() { + var saveLocalPrivateKeyStub, testData; + + beforeEach(function() { + testData = { + _id: 'keyId', + userId: testUser, + encryptedPrivateKey: 'encryptedPrivateKey', + code: 'code', + salt: 'salt', + iv: 'iv' + }; + + saveLocalPrivateKeyStub = sinon.stub(keychainDao, 'saveLocalPrivateKey'); + }); + afterEach(function() { + saveLocalPrivateKeyStub.restore(); + }); + + it('should fail due to invlaid args', function(done) { + keychainDao.decryptAndStorePrivateKeyLocally({}, function(err) { + expect(err).to.exist; + done(); + }); + }); + + it('should fail due to crypto.deriveKey', function(done) { + cryptoStub.deriveKey.yields(42); + + keychainDao.decryptAndStorePrivateKeyLocally(testData, function(err) { + expect(err).to.exist; + expect(cryptoStub.deriveKey.calledOnce).to.be.true; + done(); + }); + }); + + it('should fail due to crypto.decrypt', function(done) { + cryptoStub.deriveKey.yields(null, 'derivedKey'); + cryptoStub.decrypt.yields(42); + + keychainDao.decryptAndStorePrivateKeyLocally(testData, function(err) { + expect(err).to.exist; + expect(cryptoStub.deriveKey.calledOnce).to.be.true; + expect(cryptoStub.decrypt.calledOnce).to.be.true; + done(); + }); + }); + + it('should fail due to pgp.getKeyParams', function(done) { + cryptoStub.deriveKey.yields(null, 'derivedKey'); + cryptoStub.decrypt.yields(null, 'privateKeyArmored'); + pgpStub.getKeyParams.throws(new Error()); + + keychainDao.decryptAndStorePrivateKeyLocally(testData, function(err) { + expect(err).to.exist; + expect(cryptoStub.deriveKey.calledOnce).to.be.true; + expect(cryptoStub.decrypt.calledOnce).to.be.true; + expect(pgpStub.getKeyParams.calledOnce).to.be.true; + done(); + }); + }); + + it('should fail due to saveLocalPrivateKey', function(done) { + cryptoStub.deriveKey.yields(null, 'derivedKey'); + cryptoStub.decrypt.yields(null, 'privateKeyArmored'); + pgpStub.getKeyParams.returns(testData); + saveLocalPrivateKeyStub.yields(42); + + keychainDao.decryptAndStorePrivateKeyLocally(testData, function(err) { + expect(err).to.exist; + expect(cryptoStub.deriveKey.calledOnce).to.be.true; + expect(cryptoStub.decrypt.calledOnce).to.be.true; + expect(pgpStub.getKeyParams.calledOnce).to.be.true; + expect(saveLocalPrivateKeyStub.calledOnce).to.be.true; + done(); + }); + }); + + it('should work', function(done) { + cryptoStub.deriveKey.yields(null, 'derivedKey'); + cryptoStub.decrypt.yields(null, 'privateKeyArmored'); + pgpStub.getKeyParams.returns(testData); + saveLocalPrivateKeyStub.yields(); + + keychainDao.decryptAndStorePrivateKeyLocally(testData, function(err, keyObject) { + expect(err).to.not.exist; + expect(keyObject).to.deep.equal({ _id: 'keyId', userId: testUser, - encryptedPrivateKey: 'encryptedPrivateKey', - code: 'code', - salt: 'salt', - iv: 'iv' - }; - - saveLocalPrivateKeyStub = sinon.stub(keychainDao, 'saveLocalPrivateKey'); - }); - afterEach(function() { - saveLocalPrivateKeyStub.restore(); - }); - - it('should fail due to invlaid args', function(done) { - keychainDao.decryptAndStorePrivateKeyLocally({}, function(err) { - expect(err).to.exist; - done(); + encryptedKey: 'privateKeyArmored' }); + expect(cryptoStub.deriveKey.calledOnce).to.be.true; + expect(cryptoStub.decrypt.calledOnce).to.be.true; + expect(pgpStub.getKeyParams.calledOnce).to.be.true; + expect(saveLocalPrivateKeyStub.calledOnce).to.be.true; + + done(); }); + }); + }); - it('should fail due to crypto.deriveKey', function(done) { - cryptoStub.deriveKey.yields(42); + describe('put user keypair', function() { + it('should fail', function(done) { + var keypair = { + publicKey: { + _id: '12345', + userId: testUser, + publicKey: 'asdf' + }, + privateKey: { + _id: '12345', + encryptedKey: 'qwer' + } + }; - keychainDao.decryptAndStorePrivateKeyLocally(testData, function(err) { - expect(err).to.exist; - expect(cryptoStub.deriveKey.calledOnce).to.be.true; - done(); - }); - }); - - it('should fail due to crypto.decrypt', function(done) { - cryptoStub.deriveKey.yields(null, 'derivedKey'); - cryptoStub.decrypt.yields(42); - - keychainDao.decryptAndStorePrivateKeyLocally(testData, function(err) { - expect(err).to.exist; - expect(cryptoStub.deriveKey.calledOnce).to.be.true; - expect(cryptoStub.decrypt.calledOnce).to.be.true; - done(); - }); - }); - - it('should fail due to pgp.getKeyParams', function(done) { - cryptoStub.deriveKey.yields(null, 'derivedKey'); - cryptoStub.decrypt.yields(null, 'privateKeyArmored'); - pgpStub.getKeyParams.throws(new Error()); - - keychainDao.decryptAndStorePrivateKeyLocally(testData, function(err) { - expect(err).to.exist; - expect(cryptoStub.deriveKey.calledOnce).to.be.true; - expect(cryptoStub.decrypt.calledOnce).to.be.true; - expect(pgpStub.getKeyParams.calledOnce).to.be.true; - done(); - }); - }); - - it('should fail due to saveLocalPrivateKey', function(done) { - cryptoStub.deriveKey.yields(null, 'derivedKey'); - cryptoStub.decrypt.yields(null, 'privateKeyArmored'); - pgpStub.getKeyParams.returns(testData); - saveLocalPrivateKeyStub.yields(42); - - keychainDao.decryptAndStorePrivateKeyLocally(testData, function(err) { - expect(err).to.exist; - expect(cryptoStub.deriveKey.calledOnce).to.be.true; - expect(cryptoStub.decrypt.calledOnce).to.be.true; - expect(pgpStub.getKeyParams.calledOnce).to.be.true; - expect(saveLocalPrivateKeyStub.calledOnce).to.be.true; - done(); - }); - }); - - it('should work', function(done) { - cryptoStub.deriveKey.yields(null, 'derivedKey'); - cryptoStub.decrypt.yields(null, 'privateKeyArmored'); - pgpStub.getKeyParams.returns(testData); - saveLocalPrivateKeyStub.yields(); - - keychainDao.decryptAndStorePrivateKeyLocally(testData, function(err, keyObject) { - expect(err).to.not.exist; - expect(keyObject).to.deep.equal({ - _id: 'keyId', - userId: testUser, - encryptedKey: 'privateKeyArmored' - }); - expect(cryptoStub.deriveKey.calledOnce).to.be.true; - expect(cryptoStub.decrypt.calledOnce).to.be.true; - expect(pgpStub.getKeyParams.calledOnce).to.be.true; - expect(saveLocalPrivateKeyStub.calledOnce).to.be.true; - - done(); - }); + keychainDao.putUserKeyPair(keypair, function(err) { + expect(err).to.exist; + done(); }); }); - describe('put user keypair', function() { - it('should fail', function(done) { - var keypair = { - publicKey: { - _id: '12345', - userId: testUser, - publicKey: 'asdf' - }, - privateKey: { - _id: '12345', - encryptedKey: 'qwer' - } - }; + it('should work', function(done) { + var keypair = { + publicKey: { + _id: '12345', + userId: testUser, + publicKey: 'asdf' + }, + privateKey: { + _id: '12345', + userId: testUser, + encryptedKey: 'qwer' + } + }; - keychainDao.putUserKeyPair(keypair, function(err) { - expect(err).to.exist; - done(); - }); - }); + lawnchairDaoStub.persist.yields(); + pubkeyDaoStub.put.yields(); - it('should work', function(done) { - var keypair = { - publicKey: { - _id: '12345', - userId: testUser, - publicKey: 'asdf' - }, - privateKey: { - _id: '12345', - userId: testUser, - encryptedKey: 'qwer' - } - }; - - lawnchairDaoStub.persist.yields(); - pubkeyDaoStub.put.yields(); - - keychainDao.putUserKeyPair(keypair, function(err) { - expect(err).to.not.exist; - expect(lawnchairDaoStub.persist.calledTwice).to.be.true; - expect(pubkeyDaoStub.put.calledOnce).to.be.true; - done(); - }); + keychainDao.putUserKeyPair(keypair, function(err) { + expect(err).to.not.exist; + expect(lawnchairDaoStub.persist.calledTwice).to.be.true; + expect(pubkeyDaoStub.put.calledOnce).to.be.true; + done(); }); }); - }); }); \ No newline at end of file diff --git a/test/unit/lawnchair-dao-test.js b/test/unit/lawnchair-dao-test.js index fd59c46..5eb3d5f 100644 --- a/test/unit/lawnchair-dao-test.js +++ b/test/unit/lawnchair-dao-test.js @@ -1,152 +1,148 @@ -define(function(require) { - 'use strict'; +'use strict'; - var LawnchairDAO = require('js/dao/lawnchair-dao'), - expect = chai.expect; +var LawnchairDAO = require('../../src/js/dao/lawnchair-dao'); - var dbName = 'lawnchair@test.com'; +var dbName = 'lawnchair@test.com'; - var key = 'type_1'; - var data = { - name: 'testName1', - type: 'testType1' - }; +var key = 'type_1'; +var data = { + name: 'testName1', + type: 'testType1' +}; - var key2 = 'type_2'; - var data2 = { - name: 'testName2', - type: 'testType2' - }; +var key2 = 'type_2'; +var data2 = { + name: 'testName2', + type: 'testType2' +}; - describe('Lawnchair DAO unit tests', function() { - var lawnchairDao; +describe('Lawnchair DAO unit tests', function() { + var lawnchairDao; - beforeEach(function(done) { - lawnchairDao = new LawnchairDAO(); - lawnchairDao.init(dbName, function(err) { - expect(err).to.not.exist; - expect(lawnchairDao._db).to.exist; + beforeEach(function(done) { + lawnchairDao = new LawnchairDAO(); + lawnchairDao.init(dbName, function(err) { + expect(err).to.not.exist; + expect(lawnchairDao._db).to.exist; + done(); + }); + }); + + afterEach(function(done) { + lawnchairDao.clear(function(err) { + expect(err).to.not.exist; + done(); + }); + }); + + describe('read', function() { + it('should fail', function(done) { + lawnchairDao.read(undefined, function(err) { + expect(err).to.exist; + done(); + }); + }); + }); + + describe('list', function() { + it('should fail', function(done) { + lawnchairDao.list(undefined, 0, null, function(err) { + expect(err).to.exist; + done(); + }); + }); + }); + + describe('remove list', function() { + it('should fail', function(done) { + lawnchairDao.removeList(undefined, function(err) { + expect(err).to.exist; + done(); + }); + }); + }); + + describe('persist/read/remove', function() { + it('should fail', function(done) { + lawnchairDao.persist(undefined, data, function(err) { + expect(err).to.exist; + done(); + }); + }); + it('should fail', function(done) { + lawnchairDao.persist('1234', undefined, function(err) { + expect(err).to.exist; done(); }); }); - afterEach(function(done) { - lawnchairDao.clear(function(err) { + it('should work', function(done) { + lawnchairDao.persist(key, data, function(err) { expect(err).to.not.exist; + lawnchairDao.read(key, onRead); + }); + + function onRead(err, fetched) { + expect(err).to.not.exist; + expect(fetched).to.deep.equal(data); + lawnchairDao.remove(key, onRemove); + } + + function onRemove(err) { + expect(err).to.not.exist; + lawnchairDao.read(key, onReadAgain); + } + + function onReadAgain(err, fetched) { + expect(err).to.not.exist; + expect(fetched).to.not.exist; + done(); + } + }); + }); + + describe('batch/list/removeList', function() { + it('should fails', function(done) { + lawnchairDao.batch({}, function(err) { + expect(err).to.exist; done(); }); }); - describe('read', function() { - it('should fail', function(done) { - lawnchairDao.read(undefined, function(err) { - expect(err).to.exist; - done(); - }); + it('should work', function(done) { + var list = [{ + key: key, + object: data + }, { + key: key2, + object: data2 + }]; + + lawnchairDao.batch(list, function(err) { + expect(err).to.not.exist; + lawnchairDao.list('type', 0, null, onList); }); + + function onList(err, fetched) { + expect(err).to.not.exist; + expect(fetched.length).to.equal(2); + expect(fetched[0]).to.deep.equal(list[0].object); + lawnchairDao.removeList('type', onRemoveList); + } + + function onRemoveList(err) { + expect(err).to.not.exist; + lawnchairDao.list('type', 0, null, onListAgain); + } + + function onListAgain(err, fetched) { + expect(err).to.not.exist; + expect(fetched).to.exist; + expect(fetched.length).to.equal(0); + done(); + } }); - - describe('list', function() { - it('should fail', function(done) { - lawnchairDao.list(undefined, 0, null, function(err) { - expect(err).to.exist; - done(); - }); - }); - }); - - describe('remove list', function() { - it('should fail', function(done) { - lawnchairDao.removeList(undefined, function(err) { - expect(err).to.exist; - done(); - }); - }); - }); - - describe('persist/read/remove', function() { - it('should fail', function(done) { - lawnchairDao.persist(undefined, data, function(err) { - expect(err).to.exist; - done(); - }); - }); - it('should fail', function(done) { - lawnchairDao.persist('1234', undefined, function(err) { - expect(err).to.exist; - done(); - }); - }); - - it('should work', function(done) { - lawnchairDao.persist(key, data, function(err) { - expect(err).to.not.exist; - lawnchairDao.read(key, onRead); - }); - - function onRead(err, fetched) { - expect(err).to.not.exist; - expect(fetched).to.deep.equal(data); - lawnchairDao.remove(key, onRemove); - } - - function onRemove(err) { - expect(err).to.not.exist; - lawnchairDao.read(key, onReadAgain); - } - - function onReadAgain(err, fetched) { - expect(err).to.not.exist; - expect(fetched).to.not.exist; - done(); - } - }); - }); - - describe('batch/list/removeList', function() { - it('should fails', function(done) { - lawnchairDao.batch({}, function(err) { - expect(err).to.exist; - done(); - }); - }); - - it('should work', function(done) { - var list = [{ - key: key, - object: data - }, { - key: key2, - object: data2 - }]; - - lawnchairDao.batch(list, function(err) { - expect(err).to.not.exist; - lawnchairDao.list('type', 0, null, onList); - }); - - function onList(err, fetched) { - expect(err).to.not.exist; - expect(fetched.length).to.equal(2); - expect(fetched[0]).to.deep.equal(list[0].object); - lawnchairDao.removeList('type', onRemoveList); - } - - function onRemoveList(err) { - expect(err).to.not.exist; - lawnchairDao.list('type', 0, null, onListAgain); - } - - function onListAgain(err, fetched) { - expect(err).to.not.exist; - expect(fetched).to.exist; - expect(fetched.length).to.equal(0); - done(); - } - }); - }); - }); }); \ No newline at end of file diff --git a/test/unit/login-ctrl-test.js b/test/unit/login-ctrl-test.js index 1a790de..65ad8aa 100644 --- a/test/unit/login-ctrl-test.js +++ b/test/unit/login-ctrl-test.js @@ -1,236 +1,232 @@ -define(function(require) { - 'use strict'; +'use strict'; - var expect = chai.expect, - angular = require('angular'), - mocks = require('angularMocks'), - LoginCtrl = require('js/controller/login'), - EmailDAO = require('js/dao/email-dao'), - Auth = require('js/bo/auth'), - appController = require('js/app-controller'), - KeychainDAO = require('js/dao/keychain-dao'); +var mocks = angular.mocks, + LoginCtrl = require('../../src/js/controller/login'), + EmailDAO = require('../../src/js/dao/email-dao'), + Auth = require('../../src/js/bo/auth'), + appController = require('../../src/js/app-controller'), + KeychainDAO = require('../../src/js/dao/keychain-dao'); - describe('Login Controller unit test', function() { - var scope, location, ctrl, - origEmailDao, emailDaoMock, - origKeychain, keychainMock, - origAuth, authStub, - emailAddress = 'fred@foo.com', - startAppStub, - checkForUpdateStub, - initStub; +describe('Login Controller unit test', function() { + var scope, location, ctrl, + origEmailDao, emailDaoMock, + origKeychain, keychainMock, + origAuth, authStub, + emailAddress = 'fred@foo.com', + startAppStub, + checkForUpdateStub, + initStub; - describe('initialization', function() { - var hasChrome, hasIdentity; + describe('initialization', function() { + var hasChrome, hasIdentity; - beforeEach(function() { - hasChrome = !!window.chrome; - hasIdentity = !!window.chrome.identity; - window.chrome = window.chrome || {}; - window.chrome.identity = window.chrome.identity || {}; + beforeEach(function() { + hasChrome = !!window.chrome; + hasIdentity = !!window.chrome.identity; + window.chrome = window.chrome || {}; + window.chrome.identity = window.chrome.identity || {}; - // remember original module to restore later, then replace it - origEmailDao = appController._emailDao; - origKeychain = appController._keychain; - origAuth = appController._auth; - appController._emailDao = emailDaoMock = sinon.createStubInstance(EmailDAO); - appController._keychain = keychainMock = sinon.createStubInstance(KeychainDAO); - appController._auth = authStub = sinon.createStubInstance(Auth); + // remember original module to restore later, then replace it + origEmailDao = appController._emailDao; + origKeychain = appController._keychain; + origAuth = appController._auth; + appController._emailDao = emailDaoMock = sinon.createStubInstance(EmailDAO); + appController._keychain = keychainMock = sinon.createStubInstance(KeychainDAO); + appController._auth = authStub = sinon.createStubInstance(Auth); - startAppStub = sinon.stub(appController, 'start'); - checkForUpdateStub = sinon.stub(appController, 'checkForUpdate'); - initStub = sinon.stub(appController, 'init'); + startAppStub = sinon.stub(appController, 'start'); + checkForUpdateStub = sinon.stub(appController, 'checkForUpdate'); + initStub = sinon.stub(appController, 'init'); + }); + + afterEach(function() { + // restore the browser + if (!hasIdentity) { + delete window.chrome.identity; + } + + if (!hasChrome) { + delete window.chrome; + } + + // restore the app controller module + appController._emailDao = origEmailDao; + appController._keychain = origKeychain; + appController._auth = origAuth; + appController.start.restore && appController.start.restore(); + appController.checkForUpdate.restore && appController.checkForUpdate.restore(); + appController.init.restore && appController.init.restore(); + location.path.restore && location.path.restore(); + + startAppStub.restore(); + checkForUpdateStub.restore(); + initStub.restore(); + }); + + it('should forward directly to desktop for empty passphrase', function(done) { + var testKeys = { + privateKey: 'a', + publicKey: 'b' + }; + + startAppStub.yields(); + authStub.getEmailAddress.yields(null, { + emailAddress: emailAddress, + realname: 'asd' }); + initStub.yields(null, testKeys); - afterEach(function() { - // restore the browser - if (!hasIdentity) { - delete window.chrome.identity; - } + emailDaoMock.unlock.withArgs({ + keypair: testKeys, + passphrase: undefined + }).yields(); - if (!hasChrome) { - delete window.chrome; - } - - // restore the app controller module - appController._emailDao = origEmailDao; - appController._keychain = origKeychain; - appController._auth = origAuth; - appController.start.restore && appController.start.restore(); - appController.checkForUpdate.restore && appController.checkForUpdate.restore(); - appController.init.restore && appController.init.restore(); - location.path.restore && location.path.restore(); - - startAppStub.restore(); - checkForUpdateStub.restore(); - initStub.restore(); - }); - - it('should forward directly to desktop for empty passphrase', function(done) { - var testKeys = { - privateKey: 'a', - publicKey: 'b' - }; - - startAppStub.yields(); - authStub.getEmailAddress.yields(null, { - emailAddress: emailAddress, - realname: 'asd' + angular.module('logintest', []); + mocks.module('logintest'); + mocks.inject(function($controller, $rootScope, $location) { + location = $location; + sinon.stub(location, 'path', function(path) { + expect(path).to.equal('/desktop'); + expect(startAppStub.calledOnce).to.be.true; + expect(checkForUpdateStub.calledOnce).to.be.true; + expect(authStub.getEmailAddress.calledOnce).to.be.true; + done(); }); - initStub.yields(null, testKeys); - - emailDaoMock.unlock.withArgs({ - keypair: testKeys, - passphrase: undefined - }).yields(); - - angular.module('logintest', []); - mocks.module('logintest'); - mocks.inject(function($controller, $rootScope, $location) { - location = $location; - sinon.stub(location, 'path', function(path) { - expect(path).to.equal('/desktop'); - expect(startAppStub.calledOnce).to.be.true; - expect(checkForUpdateStub.calledOnce).to.be.true; - expect(authStub.getEmailAddress.calledOnce).to.be.true; - done(); - }); - scope = $rootScope.$new(); - scope.state = {}; - ctrl = $controller(LoginCtrl, { - $location: location, - $scope: scope - }); + scope = $rootScope.$new(); + scope.state = {}; + ctrl = $controller(LoginCtrl, { + $location: location, + $scope: scope }); }); + }); - it('should forward to existing user login', function(done) { - var testKeys = { - privateKey: 'a', - publicKey: 'b' - }; + it('should forward to existing user login', function(done) { + var testKeys = { + privateKey: 'a', + publicKey: 'b' + }; - startAppStub.yields(); - authStub.getEmailAddress.yields(null, { - emailAddress: emailAddress, - realname: 'asd' + startAppStub.yields(); + authStub.getEmailAddress.yields(null, { + emailAddress: emailAddress, + realname: 'asd' + }); + initStub.yields(null, testKeys); + + emailDaoMock.unlock.withArgs({ + keypair: testKeys, + passphrase: undefined + }).yields({}); + + angular.module('logintest', []); + mocks.module('logintest'); + mocks.inject(function($controller, $rootScope, $location) { + location = $location; + sinon.stub(location, 'path', function(path) { + expect(path).to.equal('/login-existing'); + expect(startAppStub.calledOnce).to.be.true; + expect(checkForUpdateStub.calledOnce).to.be.true; + expect(authStub.getEmailAddress.calledOnce).to.be.true; + done(); }); - initStub.yields(null, testKeys); - - emailDaoMock.unlock.withArgs({ - keypair: testKeys, - passphrase: undefined - }).yields({}); - - angular.module('logintest', []); - mocks.module('logintest'); - mocks.inject(function($controller, $rootScope, $location) { - location = $location; - sinon.stub(location, 'path', function(path) { - expect(path).to.equal('/login-existing'); - expect(startAppStub.calledOnce).to.be.true; - expect(checkForUpdateStub.calledOnce).to.be.true; - expect(authStub.getEmailAddress.calledOnce).to.be.true; - done(); - }); - scope = $rootScope.$new(); - scope.state = {}; - ctrl = $controller(LoginCtrl, { - $location: location, - $scope: scope - }); + scope = $rootScope.$new(); + scope.state = {}; + ctrl = $controller(LoginCtrl, { + $location: location, + $scope: scope }); }); + }); - it('should forward to privatekey download login', function(done) { - startAppStub.yields(); - authStub.getEmailAddress.yields(null, { - emailAddress: emailAddress, - realname: 'asd' - }); - initStub.yields(null, { - publicKey: 'b' - }); - keychainMock.requestPrivateKeyDownload.yields(null, {}); + it('should forward to privatekey download login', function(done) { + startAppStub.yields(); + authStub.getEmailAddress.yields(null, { + emailAddress: emailAddress, + realname: 'asd' + }); + initStub.yields(null, { + publicKey: 'b' + }); + keychainMock.requestPrivateKeyDownload.yields(null, {}); - angular.module('logintest', []); - mocks.module('logintest'); - mocks.inject(function($controller, $rootScope, $location) { - location = $location; - sinon.stub(location, 'path', function(path) { - expect(path).to.equal('/login-privatekey-download'); - expect(startAppStub.calledOnce).to.be.true; - expect(checkForUpdateStub.calledOnce).to.be.true; - expect(authStub.getEmailAddress.calledOnce).to.be.true; - expect(keychainMock.requestPrivateKeyDownload.calledOnce).to.be.true; - done(); - }); - scope = $rootScope.$new(); - scope.state = {}; - ctrl = $controller(LoginCtrl, { - $location: location, - $scope: scope - }); + angular.module('logintest', []); + mocks.module('logintest'); + mocks.inject(function($controller, $rootScope, $location) { + location = $location; + sinon.stub(location, 'path', function(path) { + expect(path).to.equal('/login-privatekey-download'); + expect(startAppStub.calledOnce).to.be.true; + expect(checkForUpdateStub.calledOnce).to.be.true; + expect(authStub.getEmailAddress.calledOnce).to.be.true; + expect(keychainMock.requestPrivateKeyDownload.calledOnce).to.be.true; + done(); + }); + scope = $rootScope.$new(); + scope.state = {}; + ctrl = $controller(LoginCtrl, { + $location: location, + $scope: scope }); }); + }); - it('should forward to new device login', function(done) { - startAppStub.yields(); - authStub.getEmailAddress.yields(null, { - emailAddress: emailAddress, - realname: 'asd' - }); - initStub.yields(null, { - publicKey: 'b' - }); - keychainMock.requestPrivateKeyDownload.yields(); + it('should forward to new device login', function(done) { + startAppStub.yields(); + authStub.getEmailAddress.yields(null, { + emailAddress: emailAddress, + realname: 'asd' + }); + initStub.yields(null, { + publicKey: 'b' + }); + keychainMock.requestPrivateKeyDownload.yields(); - angular.module('logintest', []); - mocks.module('logintest'); - mocks.inject(function($controller, $rootScope, $location) { - location = $location; - sinon.stub(location, 'path', function(path) { - expect(path).to.equal('/login-new-device'); - expect(startAppStub.calledOnce).to.be.true; - expect(checkForUpdateStub.calledOnce).to.be.true; - expect(authStub.getEmailAddress.calledOnce).to.be.true; - expect(keychainMock.requestPrivateKeyDownload.calledOnce).to.be.true; - done(); - }); - scope = $rootScope.$new(); - scope.state = {}; - ctrl = $controller(LoginCtrl, { - $location: location, - $scope: scope - }); + angular.module('logintest', []); + mocks.module('logintest'); + mocks.inject(function($controller, $rootScope, $location) { + location = $location; + sinon.stub(location, 'path', function(path) { + expect(path).to.equal('/login-new-device'); + expect(startAppStub.calledOnce).to.be.true; + expect(checkForUpdateStub.calledOnce).to.be.true; + expect(authStub.getEmailAddress.calledOnce).to.be.true; + expect(keychainMock.requestPrivateKeyDownload.calledOnce).to.be.true; + done(); + }); + scope = $rootScope.$new(); + scope.state = {}; + ctrl = $controller(LoginCtrl, { + $location: location, + $scope: scope }); }); + }); - it('should forward to initial login', function(done) { - startAppStub.yields(); - authStub.getEmailAddress.yields(null, { - emailAddress: emailAddress, - realname: 'asd' + it('should forward to initial login', function(done) { + startAppStub.yields(); + authStub.getEmailAddress.yields(null, { + emailAddress: emailAddress, + realname: 'asd' + }); + initStub.yields(); + + angular.module('logintest', []); + mocks.module('logintest'); + mocks.inject(function($controller, $rootScope, $location) { + location = $location; + sinon.stub(location, 'path', function(path) { + expect(path).to.equal('/login-initial'); + expect(startAppStub.calledOnce).to.be.true; + expect(checkForUpdateStub.calledOnce).to.be.true; + expect(authStub.getEmailAddress.calledOnce).to.be.true; + done(); }); - initStub.yields(); - - angular.module('logintest', []); - mocks.module('logintest'); - mocks.inject(function($controller, $rootScope, $location) { - location = $location; - sinon.stub(location, 'path', function(path) { - expect(path).to.equal('/login-initial'); - expect(startAppStub.calledOnce).to.be.true; - expect(checkForUpdateStub.calledOnce).to.be.true; - expect(authStub.getEmailAddress.calledOnce).to.be.true; - done(); - }); - scope = $rootScope.$new(); - scope.state = {}; - ctrl = $controller(LoginCtrl, { - $location: location, - $scope: scope - }); + scope = $rootScope.$new(); + scope.state = {}; + ctrl = $controller(LoginCtrl, { + $location: location, + $scope: scope }); }); }); diff --git a/test/unit/login-existing-ctrl-test.js b/test/unit/login-existing-ctrl-test.js index baa16e0..70843cb 100644 --- a/test/unit/login-existing-ctrl-test.js +++ b/test/unit/login-existing-ctrl-test.js @@ -1,116 +1,112 @@ -define(function(require) { - 'use strict'; +'use strict'; - var expect = chai.expect, - angular = require('angular'), - Auth = require('js/bo/auth'), - mocks = require('angularMocks'), - LoginExistingCtrl = require('js/controller/login-existing'), - EmailDAO = require('js/dao/email-dao'), - KeychainDAO = require('js/dao/keychain-dao'), - appController = require('js/app-controller'); +var Auth = require('../../src/js/bo/auth'), + mocks = angular.mocks, + LoginExistingCtrl = require('../../src/js/controller/login-existing'), + EmailDAO = require('../../src/js/dao/email-dao'), + KeychainDAO = require('../../src/js/dao/keychain-dao'), + appController = require('../../src/js/app-controller'); - describe('Login (existing user) Controller unit test', function() { - var scope, location, ctrl, origEmailDao, emailDaoMock, - origAuth, authMock, - emailAddress = 'fred@foo.com', - passphrase = 'asd', - keychainMock; +describe('Login (existing user) Controller unit test', function() { + var scope, location, ctrl, origEmailDao, emailDaoMock, + origAuth, authMock, + emailAddress = 'fred@foo.com', + passphrase = 'asd', + keychainMock; - beforeEach(function() { - // remember original module to restore later - origEmailDao = appController._emailDao; - origAuth = appController._auth; + beforeEach(function() { + // remember original module to restore later + origEmailDao = appController._emailDao; + origAuth = appController._auth; - appController._emailDao = emailDaoMock = sinon.createStubInstance(EmailDAO); - appController._auth = authMock = sinon.createStubInstance(Auth); + appController._emailDao = emailDaoMock = sinon.createStubInstance(EmailDAO); + appController._auth = authMock = sinon.createStubInstance(Auth); - keychainMock = sinon.createStubInstance(KeychainDAO); - emailDaoMock._keychain = keychainMock; + keychainMock = sinon.createStubInstance(KeychainDAO); + emailDaoMock._keychain = keychainMock; - emailDaoMock._account = { - emailAddress: emailAddress, - }; + emailDaoMock._account = { + emailAddress: emailAddress, + }; - angular.module('loginexistingtest', []); - mocks.module('loginexistingtest'); - mocks.inject(function($rootScope, $controller, $location) { - location = $location; - scope = $rootScope.$new(); - scope.state = {}; - ctrl = $controller(LoginExistingCtrl, { - $scope: scope, - $routeParams: {} - }); + angular.module('loginexistingtest', []); + mocks.module('loginexistingtest'); + mocks.inject(function($rootScope, $controller, $location) { + location = $location; + scope = $rootScope.$new(); + scope.state = {}; + ctrl = $controller(LoginExistingCtrl, { + $scope: scope, + $routeParams: {} }); }); + }); - afterEach(function() { - // restore the module - appController._emailDao = origEmailDao; - appController._auth = origAuth; + afterEach(function() { + // restore the module + appController._emailDao = origEmailDao; + appController._auth = origAuth; + }); + + describe('initial state', function() { + it('should be well defined', function() { + expect(scope.buttonEnabled).to.be.true; + expect(scope.incorrect).to.be.false; + expect(scope.change).to.exist; + expect(scope.confirmPassphrase).to.exist; }); + }); - describe('initial state', function() { - it('should be well defined', function() { - expect(scope.buttonEnabled).to.be.true; + describe('functionality', function() { + describe('change', function() { + it('should set incorrect to false', function() { + scope.incorrect = true; + + scope.change(); expect(scope.incorrect).to.be.false; - expect(scope.change).to.exist; - expect(scope.confirmPassphrase).to.exist; }); }); - describe('functionality', function() { - describe('change', function() { - it('should set incorrect to false', function() { - scope.incorrect = true; + describe('confirm passphrase', function() { + it('should unlock crypto and start', function() { + var keypair = {}, + pathSpy = sinon.spy(location, 'path'); + scope.passphrase = passphrase; + keychainMock.getUserKeyPair.withArgs(emailAddress).yields(null, keypair); + emailDaoMock.unlock.withArgs({ + keypair: keypair, + passphrase: passphrase + }).yields(); + authMock.storeCredentials.yields(); - scope.change(); - expect(scope.incorrect).to.be.false; - }); + + scope.confirmPassphrase(); + + expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; + expect(emailDaoMock.unlock.calledOnce).to.be.true; + expect(pathSpy.calledOnce).to.be.true; + expect(pathSpy.calledWith('/desktop')).to.be.true; }); - describe('confirm passphrase', function() { - it('should unlock crypto and start', function() { - var keypair = {}, - pathSpy = sinon.spy(location, 'path'); - scope.passphrase = passphrase; - keychainMock.getUserKeyPair.withArgs(emailAddress).yields(null, keypair); - emailDaoMock.unlock.withArgs({ - keypair: keypair, - passphrase: passphrase - }).yields(); - authMock.storeCredentials.yields(); + it('should not do anything without passphrase', function() { + var pathSpy = sinon.spy(location, 'path'); + scope.passphrase = ''; + scope.confirmPassphrase(); + expect(pathSpy.callCount).to.equal(0); + }); - scope.confirmPassphrase(); + it('should not work when keypair unavailable', function(done) { + scope.passphrase = passphrase; + keychainMock.getUserKeyPair.withArgs(emailAddress).yields(new Error('asd')); + scope.onError = function(err) { + expect(err.message).to.equal('asd'); expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; - expect(emailDaoMock.unlock.calledOnce).to.be.true; - expect(pathSpy.calledOnce).to.be.true; - expect(pathSpy.calledWith('/desktop')).to.be.true; - }); + done(); + }; - it('should not do anything without passphrase', function() { - var pathSpy = sinon.spy(location, 'path'); - scope.passphrase = ''; - - scope.confirmPassphrase(); - expect(pathSpy.callCount).to.equal(0); - }); - - it('should not work when keypair unavailable', function(done) { - scope.passphrase = passphrase; - keychainMock.getUserKeyPair.withArgs(emailAddress).yields(new Error('asd')); - - scope.onError = function(err) { - expect(err.message).to.equal('asd'); - expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; - done(); - }; - - scope.confirmPassphrase(); - }); + scope.confirmPassphrase(); }); }); }); diff --git a/test/unit/login-initial-ctrl-test.js b/test/unit/login-initial-ctrl-test.js index 533a1f4..17fdbfe 100644 --- a/test/unit/login-initial-ctrl-test.js +++ b/test/unit/login-initial-ctrl-test.js @@ -1,202 +1,197 @@ -define(function(require) { - 'use strict'; +'use strict'; - var expect = chai.expect, - angular = require('angular'), - Auth = require('js/bo/auth'), - mocks = require('angularMocks'), - LoginInitialCtrl = require('js/controller/login-initial'), - PGP = require('js/crypto/pgp'), - EmailDAO = require('js/dao/email-dao'), - appController = require('js/app-controller'); +var Auth = require('../../src/js/bo/auth'), + mocks = angular.mocks, + LoginInitialCtrl = require('../../src/js/controller/login-initial'), + PGP = require('../../src/js/crypto/pgp'), + EmailDAO = require('../../src/js/dao/email-dao'), + appController = require('../../src/js/app-controller'); - describe('Login (initial user) Controller unit test', function() { - var scope, ctrl, location, origEmailDao, emailDaoMock, - origAuth, authMock, - emailAddress = 'fred@foo.com', - keyId, expectedKeyId, - cryptoMock; +describe('Login (initial user) Controller unit test', function() { + var scope, ctrl, location, origEmailDao, emailDaoMock, + origAuth, authMock, + emailAddress = 'fred@foo.com', + keyId, expectedKeyId, + cryptoMock; + + beforeEach(function() { + // remember original module to restore later + origEmailDao = appController._emailDao; + origAuth = appController._auth; + + appController._emailDao = emailDaoMock = sinon.createStubInstance(EmailDAO); + appController._auth = authMock = sinon.createStubInstance(Auth); + + keyId = '9FEB47936E712926'; + expectedKeyId = '6E712926'; + cryptoMock = sinon.createStubInstance(PGP); + emailDaoMock._crypto = cryptoMock; + + emailDaoMock._account = { + emailAddress: emailAddress, + }; + + angular.module('logininitialtest', []); + mocks.module('logininitialtest'); + mocks.inject(function($rootScope, $controller, $location) { + scope = $rootScope.$new(); + location = $location; + scope.state = { + ui: {} + }; + ctrl = $controller(LoginInitialCtrl, { + $scope: scope, + $routeParams: {} + }); + }); + }); + + afterEach(function() { + // restore the module + appController._emailDao = origEmailDao; + appController._auth = origAuth; + }); + + describe('initial state', function() { + it('should be well defined', function() { + expect(scope.state.ui).to.equal(1); + }); + }); + + describe('signUpToNewsletter', function() { + var xhrMock, requests; beforeEach(function() { - // remember original module to restore later - origEmailDao = appController._emailDao; - origAuth = appController._auth; + xhrMock = sinon.useFakeXMLHttpRequest(); + requests = []; - appController._emailDao = emailDaoMock = sinon.createStubInstance(EmailDAO); - appController._auth = authMock = sinon.createStubInstance(Auth); - - keyId = '9FEB47936E712926'; - expectedKeyId = '6E712926'; - cryptoMock = sinon.createStubInstance(PGP); - emailDaoMock._crypto = cryptoMock; - - emailDaoMock._account = { - emailAddress: emailAddress, + xhrMock.onCreate = function(xhr) { + requests.push(xhr); }; - - angular.module('logininitialtest', []); - mocks.module('logininitialtest'); - mocks.inject(function($rootScope, $controller, $location) { - scope = $rootScope.$new(); - location = $location; - scope.state = { - ui: {} - }; - ctrl = $controller(LoginInitialCtrl, { - $scope: scope, - $routeParams: {} - }); - }); }); afterEach(function() { - // restore the module - appController._emailDao = origEmailDao; - appController._auth = origAuth; + xhrMock.restore(); }); - describe('initial state', function() { - it('should be well defined', function() { + it('should not signup', function() { + scope.state.newsletter = false; + + scope.signUpToNewsletter(); + expect(requests.length).to.equal(0); + }); + + it('should fail', function(done) { + scope.state.newsletter = true; + + scope.signUpToNewsletter(function(err, xhr) { + expect(err).to.exist; + expect(xhr).to.not.exist; + done(); + }); + + expect(requests.length).to.equal(1); + requests[0].onerror('err'); + }); + + it('should work without callback', function() { + scope.state.newsletter = true; + + scope.signUpToNewsletter(); + + expect(requests.length).to.equal(1); + requests[0].respond(200, { + "Content-Type": "text/plain" + }, 'foobar!'); + }); + }); + + describe('go to import key', function() { + var signUpToNewsletterStub; + beforeEach(function() { + signUpToNewsletterStub = sinon.stub(scope, 'signUpToNewsletter'); + }); + afterEach(function() { + signUpToNewsletterStub.restore(); + }); + + it('should not continue if terms are not accepted', function(done) { + scope.state.agree = undefined; + + scope.onError = function(err) { + expect(err.message).to.contain('Terms'); + expect(signUpToNewsletterStub.called).to.be.false; + done(); + }; + + scope.importKey(); + }); + + it('should work', function() { + scope.state.agree = true; + scope.importKey(); + expect(signUpToNewsletterStub.calledOnce).to.be.true; + expect(location.$$path).to.equal('/login-new-device'); + }); + }); + + describe('generate key', function() { + var signUpToNewsletterStub; + beforeEach(function() { + signUpToNewsletterStub = sinon.stub(scope, 'signUpToNewsletter'); + }); + afterEach(function() { + signUpToNewsletterStub.restore(); + }); + + it('should not continue if terms are not accepted', function(done) { + scope.state.agree = undefined; + + scope.onError = function(err) { + expect(err.message).to.contain('Terms'); expect(scope.state.ui).to.equal(1); - }); + expect(signUpToNewsletterStub.called).to.be.false; + done(); + }; + + scope.generateKey(); }); - describe('signUpToNewsletter', function() { - var xhrMock, requests; + it('should fail due to error in emailDao.unlock', function(done) { + scope.state.agree = true; - beforeEach(function() { - xhrMock = sinon.useFakeXMLHttpRequest(); - requests = []; + emailDaoMock.unlock.withArgs({ + passphrase: undefined + }).yields(new Error()); + authMock.storeCredentials.yields(); - xhrMock.onCreate = function(xhr) { - requests.push(xhr); - }; - }); + scope.onError = function(err) { + expect(err).to.exist; + expect(scope.state.ui).to.equal(1); + expect(signUpToNewsletterStub.called).to.be.true; + done(); + }; - afterEach(function() { - xhrMock.restore(); - }); - - it('should not signup', function() { - scope.state.newsletter = false; - - scope.signUpToNewsletter(); - expect(requests.length).to.equal(0); - }); - - it('should fail', function(done) { - scope.state.newsletter = true; - - scope.signUpToNewsletter(function(err, xhr) { - expect(err).to.exist; - expect(xhr).to.not.exist; - done(); - }); - - expect(requests.length).to.equal(1); - requests[0].onerror('err'); - }); - - it('should work without callback', function() { - scope.state.newsletter = true; - - scope.signUpToNewsletter(); - - expect(requests.length).to.equal(1); - requests[0].respond(200, { - "Content-Type": "text/plain" - }, 'foobar!'); - }); + scope.generateKey(); + expect(scope.state.ui).to.equal(2); }); - describe('go to import key', function() { - var signUpToNewsletterStub; - beforeEach(function() { - signUpToNewsletterStub = sinon.stub(scope, 'signUpToNewsletter'); - }); - afterEach(function() { - signUpToNewsletterStub.restore(); - }); + it('should unlock crypto', function(done) { + scope.state.agree = true; - it('should not continue if terms are not accepted', function(done) { - scope.state.agree = undefined; + emailDaoMock.unlock.withArgs({ + passphrase: undefined + }).yields(); + authMock.storeCredentials.yields(); - scope.onError = function(err) { - expect(err.message).to.contain('Terms'); - expect(signUpToNewsletterStub.called).to.be.false; - done(); - }; - - scope.importKey(); - }); - - it('should work', function() { - scope.state.agree = true; - scope.importKey(); - expect(signUpToNewsletterStub.calledOnce).to.be.true; - expect(location.$$path).to.equal('/login-new-device'); - }); - }); - - describe('generate key', function() { - var signUpToNewsletterStub; - beforeEach(function() { - signUpToNewsletterStub = sinon.stub(scope, 'signUpToNewsletter'); - }); - afterEach(function() { - signUpToNewsletterStub.restore(); - }); - - it('should not continue if terms are not accepted', function(done) { - scope.state.agree = undefined; - - scope.onError = function(err) { - expect(err.message).to.contain('Terms'); - expect(scope.state.ui).to.equal(1); - expect(signUpToNewsletterStub.called).to.be.false; - done(); - }; - - scope.generateKey(); - }); - - it('should fail due to error in emailDao.unlock', function(done) { - scope.state.agree = true; - - emailDaoMock.unlock.withArgs({ - passphrase: undefined - }).yields(new Error()); - authMock.storeCredentials.yields(); - - scope.onError = function(err) { - expect(err).to.exist; - expect(scope.state.ui).to.equal(1); - expect(signUpToNewsletterStub.called).to.be.true; - done(); - }; - - scope.generateKey(); + scope.$apply = function() { expect(scope.state.ui).to.equal(2); - }); + expect(location.$$path).to.equal('/desktop'); + expect(emailDaoMock.unlock.calledOnce).to.be.true; + done(); + }; - it('should unlock crypto', function(done) { - scope.state.agree = true; - - emailDaoMock.unlock.withArgs({ - passphrase: undefined - }).yields(); - authMock.storeCredentials.yields(); - - scope.$apply = function() { - expect(scope.state.ui).to.equal(2); - expect(location.$$path).to.equal('/desktop'); - expect(emailDaoMock.unlock.calledOnce).to.be.true; - done(); - }; - - scope.generateKey(); - }); + scope.generateKey(); }); - }); }); \ No newline at end of file diff --git a/test/unit/login-new-device-ctrl-test.js b/test/unit/login-new-device-ctrl-test.js index 977bedd..3577949 100644 --- a/test/unit/login-new-device-ctrl-test.js +++ b/test/unit/login-new-device-ctrl-test.js @@ -1,190 +1,186 @@ -define(function(require) { - 'use strict'; +'use strict'; - var expect = chai.expect, - angular = require('angular'), - mocks = require('angularMocks'), - PGP = require('js/crypto/pgp'), - LoginNewDeviceCtrl = require('js/controller/login-new-device'), - KeychainDAO = require('js/dao/keychain-dao'), - EmailDAO = require('js/dao/email-dao'), - appController = require('js/app-controller'); +var mocks = angular.mocks, + PGP = require('../../src/js/crypto/pgp'), + LoginNewDeviceCtrl = require('../../src/js/controller/login-new-device'), + KeychainDAO = require('../../src/js/dao/keychain-dao'), + EmailDAO = require('../../src/js/dao/email-dao'), + appController = require('../../src/js/app-controller'); - describe('Login (new device) Controller unit test', function() { - var scope, ctrl, origEmailDao, emailDaoMock, pgpMock, - emailAddress = 'fred@foo.com', - passphrase = 'asd', - keyId, - keychainMock; +describe('Login (new device) Controller unit test', function() { + var scope, ctrl, origEmailDao, emailDaoMock, pgpMock, + emailAddress = 'fred@foo.com', + passphrase = 'asd', + keyId, + keychainMock; - beforeEach(function() { - // remember original module to restore later - origEmailDao = appController._emailDao; + beforeEach(function() { + // remember original module to restore later + origEmailDao = appController._emailDao; - emailDaoMock = sinon.createStubInstance(EmailDAO); - appController._emailDao = emailDaoMock; + emailDaoMock = sinon.createStubInstance(EmailDAO); + appController._emailDao = emailDaoMock; - keyId = '9FEB47936E712926'; - emailDaoMock._keychain = keychainMock = sinon.createStubInstance(KeychainDAO); - appController._pgp = pgpMock = sinon.createStubInstance(PGP); - pgpMock.extractPublicKey.returns('publicKeyArmored'); + keyId = '9FEB47936E712926'; + emailDaoMock._keychain = keychainMock = sinon.createStubInstance(KeychainDAO); + appController._pgp = pgpMock = sinon.createStubInstance(PGP); + pgpMock.extractPublicKey.returns('publicKeyArmored'); - emailDaoMock._account = { - emailAddress: emailAddress, + emailDaoMock._account = { + emailAddress: emailAddress, + }; + + angular.module('loginnewdevicetest', []); + mocks.module('loginnewdevicetest'); + mocks.inject(function($rootScope, $controller) { + scope = $rootScope.$new(); + scope.state = { + ui: {} }; - - angular.module('loginnewdevicetest', []); - mocks.module('loginnewdevicetest'); - mocks.inject(function($rootScope, $controller) { - scope = $rootScope.$new(); - scope.state = { - ui: {} - }; - ctrl = $controller(LoginNewDeviceCtrl, { - $scope: scope, - $routeParams: {} - }); - }); - }); - - afterEach(function() { - // restore the module - appController._emailDao = origEmailDao; - }); - - describe('initial state', function() { - it('should be well defined', function() { - expect(scope.incorrect).to.be.false; - expect(scope.confirmPassphrase).to.exist; - }); - }); - - describe('confirm passphrase', function() { - it('should unlock crypto with a public key on the server', function() { - scope.passphrase = passphrase; - scope.key = { - privateKeyArmored: 'b' - }; - - pgpMock.getKeyParams.returns({ - _id: 'id', - userIds: [] - }); - - keychainMock.getUserKeyPair.withArgs(emailAddress).yields(null, { - _id: keyId, - publicKey: 'a' - }); - emailDaoMock.unlock.withArgs(sinon.match.any, passphrase).yields(); - keychainMock.putUserKeyPair.yields(); - - scope.confirmPassphrase(); - - expect(emailDaoMock.unlock.calledOnce).to.be.true; - expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; - }); - - it('should unlock crypto with no key on the server', function() { - scope.passphrase = passphrase; - scope.key = { - privateKeyArmored: 'b', - publicKeyArmored: 'a' - }; - - pgpMock.getKeyParams.returns({ - _id: 'id', - userIds: [] - }); - - keychainMock.getUserKeyPair.withArgs(emailAddress).yields(); - emailDaoMock.unlock.withArgs(sinon.match.any, passphrase).yields(); - keychainMock.putUserKeyPair.yields(); - - scope.confirmPassphrase(); - - expect(emailDaoMock.unlock.calledOnce).to.be.true; - expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; - }); - - it('should not work when keypair upload fails', function(done) { - scope.passphrase = passphrase; - scope.key = { - privateKeyArmored: 'b' - }; - - pgpMock.getKeyParams.returns({ - _id: 'id', - userIds: [] - }); - - keychainMock.getUserKeyPair.withArgs(emailAddress).yields(null, { - _id: keyId, - publicKey: 'a' - }); - emailDaoMock.unlock.yields(); - keychainMock.putUserKeyPair.yields({ - errMsg: 'yo mamma.' - }); - - scope.onError = function(err) { - expect(err.errMsg).to.equal('yo mamma.'); - done(); - }; - - scope.confirmPassphrase(); - - expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; - expect(emailDaoMock.unlock.calledOnce).to.be.true; - expect(keychainMock.putUserKeyPair.calledOnce).to.be.true; - }); - - it('should not work when unlock fails', function(done) { - scope.passphrase = passphrase; - scope.key = { - privateKeyArmored: 'b' - }; - - pgpMock.getKeyParams.returns({ - _id: 'id', - userIds: [] - }); - - keychainMock.getUserKeyPair.withArgs(emailAddress).yields(null, { - _id: keyId, - publicKey: 'a' - }); - emailDaoMock.unlock.yields({ - errMsg: 'yo mamma.' - }); - - scope.onError = function(err) { - expect(err.errMsg).to.equal('yo mamma.'); - done(); - }; - - scope.confirmPassphrase(); - - expect(scope.incorrect).to.be.true; - expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; - expect(emailDaoMock.unlock.calledOnce).to.be.true; - }); - - it('should not work when keypair retrieval', function(done) { - scope.passphrase = passphrase; - - keychainMock.getUserKeyPair.withArgs(emailAddress).yields({ - errMsg: 'yo mamma.' - }); - - scope.onError = function(err) { - expect(err.errMsg).to.equal('yo mamma.'); - done(); - }; - - scope.confirmPassphrase(); - - expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; + ctrl = $controller(LoginNewDeviceCtrl, { + $scope: scope, + $routeParams: {} }); }); }); + + afterEach(function() { + // restore the module + appController._emailDao = origEmailDao; + }); + + describe('initial state', function() { + it('should be well defined', function() { + expect(scope.incorrect).to.be.false; + expect(scope.confirmPassphrase).to.exist; + }); + }); + + describe('confirm passphrase', function() { + it('should unlock crypto with a public key on the server', function() { + scope.passphrase = passphrase; + scope.key = { + privateKeyArmored: 'b' + }; + + pgpMock.getKeyParams.returns({ + _id: 'id', + userIds: [] + }); + + keychainMock.getUserKeyPair.withArgs(emailAddress).yields(null, { + _id: keyId, + publicKey: 'a' + }); + emailDaoMock.unlock.withArgs(sinon.match.any, passphrase).yields(); + keychainMock.putUserKeyPair.yields(); + + scope.confirmPassphrase(); + + expect(emailDaoMock.unlock.calledOnce).to.be.true; + expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; + }); + + it('should unlock crypto with no key on the server', function() { + scope.passphrase = passphrase; + scope.key = { + privateKeyArmored: 'b', + publicKeyArmored: 'a' + }; + + pgpMock.getKeyParams.returns({ + _id: 'id', + userIds: [] + }); + + keychainMock.getUserKeyPair.withArgs(emailAddress).yields(); + emailDaoMock.unlock.withArgs(sinon.match.any, passphrase).yields(); + keychainMock.putUserKeyPair.yields(); + + scope.confirmPassphrase(); + + expect(emailDaoMock.unlock.calledOnce).to.be.true; + expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; + }); + + it('should not work when keypair upload fails', function(done) { + scope.passphrase = passphrase; + scope.key = { + privateKeyArmored: 'b' + }; + + pgpMock.getKeyParams.returns({ + _id: 'id', + userIds: [] + }); + + keychainMock.getUserKeyPair.withArgs(emailAddress).yields(null, { + _id: keyId, + publicKey: 'a' + }); + emailDaoMock.unlock.yields(); + keychainMock.putUserKeyPair.yields({ + errMsg: 'yo mamma.' + }); + + scope.onError = function(err) { + expect(err.errMsg).to.equal('yo mamma.'); + done(); + }; + + scope.confirmPassphrase(); + + expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; + expect(emailDaoMock.unlock.calledOnce).to.be.true; + expect(keychainMock.putUserKeyPair.calledOnce).to.be.true; + }); + + it('should not work when unlock fails', function(done) { + scope.passphrase = passphrase; + scope.key = { + privateKeyArmored: 'b' + }; + + pgpMock.getKeyParams.returns({ + _id: 'id', + userIds: [] + }); + + keychainMock.getUserKeyPair.withArgs(emailAddress).yields(null, { + _id: keyId, + publicKey: 'a' + }); + emailDaoMock.unlock.yields({ + errMsg: 'yo mamma.' + }); + + scope.onError = function(err) { + expect(err.errMsg).to.equal('yo mamma.'); + done(); + }; + + scope.confirmPassphrase(); + + expect(scope.incorrect).to.be.true; + expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; + expect(emailDaoMock.unlock.calledOnce).to.be.true; + }); + + it('should not work when keypair retrieval', function(done) { + scope.passphrase = passphrase; + + keychainMock.getUserKeyPair.withArgs(emailAddress).yields({ + errMsg: 'yo mamma.' + }); + + scope.onError = function(err) { + expect(err.errMsg).to.equal('yo mamma.'); + done(); + }; + + scope.confirmPassphrase(); + + expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; + }); + }); }); \ No newline at end of file diff --git a/test/unit/login-privatekey-download-ctrl-test.js b/test/unit/login-privatekey-download-ctrl-test.js index 532dbf1..2c6d62d 100644 --- a/test/unit/login-privatekey-download-ctrl-test.js +++ b/test/unit/login-privatekey-download-ctrl-test.js @@ -1,39 +1,250 @@ -define(function(require) { - 'use strict'; +'use strict'; - var expect = chai.expect, - angular = require('angular'), - mocks = require('angularMocks'), - Auth = require('js/bo/auth'), - LoginPrivateKeyDownloadCtrl = require('js/controller/login-privatekey-download'), - EmailDAO = require('js/dao/email-dao'), - appController = require('js/app-controller'), - KeychainDAO = require('js/dao/keychain-dao'); +var mocks = angular.mocks, + Auth = require('../../src/js/bo/auth'), + LoginPrivateKeyDownloadCtrl = require('../../src/js/controller/login-privatekey-download'), + EmailDAO = require('../../src/js/dao/email-dao'), + appController = require('../../src/js/app-controller'), + KeychainDAO = require('../../src/js/dao/keychain-dao'); - describe('Login Private Key Download Controller unit test', function() { - var scope, location, ctrl, - origEmailDao, emailDaoMock, - origAuth, authMock, - origKeychain, keychainMock, - emailAddress = 'fred@foo.com'; +describe('Login Private Key Download Controller unit test', function() { + var scope, location, ctrl, + origEmailDao, emailDaoMock, + origAuth, authMock, + origKeychain, keychainMock, + emailAddress = 'fred@foo.com'; - beforeEach(function(done) { - // remember original module to restore later, then replace it - origEmailDao = appController._emailDao; - origKeychain = appController._keychain; - origAuth = appController._auth; + beforeEach(function(done) { + // remember original module to restore later, then replace it + origEmailDao = appController._emailDao; + origKeychain = appController._keychain; + origAuth = appController._auth; - appController._emailDao = emailDaoMock = sinon.createStubInstance(EmailDAO); - appController._keychain = keychainMock = sinon.createStubInstance(KeychainDAO); - appController._auth = authMock = sinon.createStubInstance(Auth); + appController._emailDao = emailDaoMock = sinon.createStubInstance(EmailDAO); + appController._keychain = keychainMock = sinon.createStubInstance(KeychainDAO); + appController._auth = authMock = sinon.createStubInstance(Auth); - emailDaoMock._account = { - emailAddress: emailAddress + emailDaoMock._account = { + emailAddress: emailAddress + }; + + angular.module('login-privatekey-download-test', []); + mocks.module('login-privatekey-download-test'); + mocks.inject(function($controller, $rootScope) { + scope = $rootScope.$new(); + scope.state = {}; + ctrl = $controller(LoginPrivateKeyDownloadCtrl, { + $location: location, + $scope: scope, + $routeParams: {} + }); + done(); + }); + }); + + afterEach(function() { + // restore the app controller module + appController._emailDao = origEmailDao; + appController._keychain = origKeychain; + appController._auth = origAuth; + }); + + describe('initialization', function() { + it('should work', function() { + expect(scope.step).to.equal(1); + }); + }); + + describe('verifyRecoveryToken', function() { + var testKeypair = { + publicKey: { + _id: 'id' + } + }; + + it('should fail for empty recovery token', function(done) { + scope.onError = function(err) { + expect(err).to.exist; + done(); }; - angular.module('login-privatekey-download-test', []); - mocks.module('login-privatekey-download-test'); - mocks.inject(function($controller, $rootScope) { + scope.recoveryToken = undefined; + scope.verifyRecoveryToken(); + }); + + it('should fail in keychain.getUserKeyPair', function(done) { + keychainMock.getUserKeyPair.yields(42); + + scope.onError = function(err) { + expect(err).to.exist; + expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; + done(); + }; + + scope.recoveryToken = 'token'; + scope.verifyRecoveryToken(); + }); + + it('should fail in keychain.downloadPrivateKey', function(done) { + keychainMock.getUserKeyPair.yields(null, testKeypair); + keychainMock.downloadPrivateKey.yields(42); + + scope.onError = function(err) { + expect(err).to.exist; + expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; + expect(keychainMock.downloadPrivateKey.calledOnce).to.be.true; + done(); + }; + + scope.recoveryToken = 'token'; + scope.verifyRecoveryToken(); + }); + + it('should work', function(done) { + keychainMock.getUserKeyPair.yields(null, testKeypair); + keychainMock.downloadPrivateKey.yields(null, 'encryptedPrivateKey'); + + scope.recoveryToken = 'token'; + scope.verifyRecoveryToken(function() { + expect(scope.encryptedPrivateKey).to.equal('encryptedPrivateKey'); + done(); + }); + }); + }); + + describe('handlePaste', function() { + it('should work', function() { + scope.handlePaste({ + clipboardData: { + getData: function(val) { + expect(val).to.equal('text/plain'); + return '1qaz-2wsx-3edc-4rfv-5tgb-6yhn'; + } + } + }); + + expect(scope.code0).to.equal('1qaz'); + expect(scope.code1).to.equal('2wsx'); + expect(scope.code2).to.equal('3edc'); + expect(scope.code3).to.equal('4rfv'); + expect(scope.code4).to.equal('5tgb'); + expect(scope.code5).to.equal('6yhn'); + }); + }); + + describe('decryptAndStorePrivateKeyLocally', function() { + beforeEach(function() { + scope.code0 = '0'; + scope.code1 = '1'; + scope.code2 = '2'; + scope.code3 = '3'; + scope.code4 = '4'; + scope.code5 = '5'; + + scope.encryptedPrivateKey = { + encryptedPrivateKey: 'encryptedPrivateKey' + }; + scope.cachedKeypair = { + publicKey: { + _id: 'keyId' + } + }; + }); + + it('should fail on empty code', function(done) { + scope.code0 = ''; + scope.code1 = ''; + scope.code2 = ''; + scope.code3 = ''; + scope.code4 = ''; + scope.code5 = ''; + + scope.onError = function(err) { + expect(err).to.exist; + done(); + }; + + scope.decryptAndStorePrivateKeyLocally(); + }); + + it('should fail on decryptAndStorePrivateKeyLocally', function(done) { + keychainMock.decryptAndStorePrivateKeyLocally.yields(42); + + scope.onError = function(err) { + expect(err).to.exist; + expect(keychainMock.decryptAndStorePrivateKeyLocally.calledOnce).to.be.true; + done(); + }; + + scope.decryptAndStorePrivateKeyLocally(); + }); + + it('should goto /login-existing on emailDao.unlock fail', function(done) { + keychainMock.decryptAndStorePrivateKeyLocally.yields(null, { + encryptedKey: 'keyArmored' + }); + emailDaoMock.unlock.yields(42); + + scope.goTo = function(location) { + expect(location).to.equal('/login-existing'); + expect(keychainMock.decryptAndStorePrivateKeyLocally.calledOnce).to.be.true; + expect(emailDaoMock.unlock.calledOnce).to.be.true; + done(); + }; + + scope.decryptAndStorePrivateKeyLocally(); + }); + + it('should goto /desktop on emailDao.unlock success', function(done) { + keychainMock.decryptAndStorePrivateKeyLocally.yields(null, { + encryptedKey: 'keyArmored' + }); + emailDaoMock.unlock.yields(); + authMock.storeCredentials.yields(); + + scope.goTo = function(location) { + expect(location).to.equal('/desktop'); + expect(keychainMock.decryptAndStorePrivateKeyLocally.calledOnce).to.be.true; + expect(emailDaoMock.unlock.calledOnce).to.be.true; + done(); + }; + + scope.decryptAndStorePrivateKeyLocally(); + }); + }); + + describe('goForward', function() { + it('should work in step 1', function() { + var verifyRecoveryTokenStub = sinon.stub(scope, 'verifyRecoveryToken'); + verifyRecoveryTokenStub.yields(); + scope.step = 1; + + scope.goForward(); + + expect(verifyRecoveryTokenStub.calledOnce).to.be.true; + expect(scope.step).to.equal(2); + verifyRecoveryTokenStub.restore(); + }); + it('should work in step 2', function() { + var decryptAndStorePrivateKeyLocallyStub = sinon.stub(scope, 'decryptAndStorePrivateKeyLocally'); + decryptAndStorePrivateKeyLocallyStub.returns(); + scope.step = 2; + + scope.goForward(); + + expect(decryptAndStorePrivateKeyLocallyStub.calledOnce).to.be.true; + decryptAndStorePrivateKeyLocallyStub.restore(); + }); + }); + + describe('goTo', function() { + it('should work', function(done) { + mocks.inject(function($controller, $rootScope, $location) { + location = $location; + sinon.stub(location, 'path', function(path) { + expect(path).to.equal('/desktop'); + done(); + }); scope = $rootScope.$new(); scope.state = {}; ctrl = $controller(LoginPrivateKeyDownloadCtrl, { @@ -41,224 +252,9 @@ define(function(require) { $scope: scope, $routeParams: {} }); - done(); - }); - }); - - afterEach(function() { - // restore the app controller module - appController._emailDao = origEmailDao; - appController._keychain = origKeychain; - appController._auth = origAuth; - }); - - describe('initialization', function() { - it('should work', function() { - expect(scope.step).to.equal(1); - }); - }); - - describe('verifyRecoveryToken', function() { - var testKeypair = { - publicKey: { - _id: 'id' - } - }; - - it('should fail for empty recovery token', function(done) { - scope.onError = function(err) { - expect(err).to.exist; - done(); - }; - - scope.recoveryToken = undefined; - scope.verifyRecoveryToken(); }); - it('should fail in keychain.getUserKeyPair', function(done) { - keychainMock.getUserKeyPair.yields(42); - - scope.onError = function(err) { - expect(err).to.exist; - expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; - done(); - }; - - scope.recoveryToken = 'token'; - scope.verifyRecoveryToken(); - }); - - it('should fail in keychain.downloadPrivateKey', function(done) { - keychainMock.getUserKeyPair.yields(null, testKeypair); - keychainMock.downloadPrivateKey.yields(42); - - scope.onError = function(err) { - expect(err).to.exist; - expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; - expect(keychainMock.downloadPrivateKey.calledOnce).to.be.true; - done(); - }; - - scope.recoveryToken = 'token'; - scope.verifyRecoveryToken(); - }); - - it('should work', function(done) { - keychainMock.getUserKeyPair.yields(null, testKeypair); - keychainMock.downloadPrivateKey.yields(null, 'encryptedPrivateKey'); - - scope.recoveryToken = 'token'; - scope.verifyRecoveryToken(function() { - expect(scope.encryptedPrivateKey).to.equal('encryptedPrivateKey'); - done(); - }); - }); - }); - - describe('handlePaste', function() { - it('should work', function() { - scope.handlePaste({ - clipboardData: { - getData: function(val) { - expect(val).to.equal('text/plain'); - return '1qaz-2wsx-3edc-4rfv-5tgb-6yhn'; - } - } - }); - - expect(scope.code0).to.equal('1qaz'); - expect(scope.code1).to.equal('2wsx'); - expect(scope.code2).to.equal('3edc'); - expect(scope.code3).to.equal('4rfv'); - expect(scope.code4).to.equal('5tgb'); - expect(scope.code5).to.equal('6yhn'); - }); - }); - - describe('decryptAndStorePrivateKeyLocally', function() { - beforeEach(function() { - scope.code0 = '0'; - scope.code1 = '1'; - scope.code2 = '2'; - scope.code3 = '3'; - scope.code4 = '4'; - scope.code5 = '5'; - - scope.encryptedPrivateKey = { - encryptedPrivateKey: 'encryptedPrivateKey' - }; - scope.cachedKeypair = { - publicKey: { - _id: 'keyId' - } - }; - }); - - it('should fail on empty code', function(done) { - scope.code0 = ''; - scope.code1 = ''; - scope.code2 = ''; - scope.code3 = ''; - scope.code4 = ''; - scope.code5 = ''; - - scope.onError = function(err) { - expect(err).to.exist; - done(); - }; - - scope.decryptAndStorePrivateKeyLocally(); - }); - - it('should fail on decryptAndStorePrivateKeyLocally', function(done) { - keychainMock.decryptAndStorePrivateKeyLocally.yields(42); - - scope.onError = function(err) { - expect(err).to.exist; - expect(keychainMock.decryptAndStorePrivateKeyLocally.calledOnce).to.be.true; - done(); - }; - - scope.decryptAndStorePrivateKeyLocally(); - }); - - it('should goto /login-existing on emailDao.unlock fail', function(done) { - keychainMock.decryptAndStorePrivateKeyLocally.yields(null, { - encryptedKey: 'keyArmored' - }); - emailDaoMock.unlock.yields(42); - - scope.goTo = function(location) { - expect(location).to.equal('/login-existing'); - expect(keychainMock.decryptAndStorePrivateKeyLocally.calledOnce).to.be.true; - expect(emailDaoMock.unlock.calledOnce).to.be.true; - done(); - }; - - scope.decryptAndStorePrivateKeyLocally(); - }); - - it('should goto /desktop on emailDao.unlock success', function(done) { - keychainMock.decryptAndStorePrivateKeyLocally.yields(null, { - encryptedKey: 'keyArmored' - }); - emailDaoMock.unlock.yields(); - authMock.storeCredentials.yields(); - - scope.goTo = function(location) { - expect(location).to.equal('/desktop'); - expect(keychainMock.decryptAndStorePrivateKeyLocally.calledOnce).to.be.true; - expect(emailDaoMock.unlock.calledOnce).to.be.true; - done(); - }; - - scope.decryptAndStorePrivateKeyLocally(); - }); - }); - - describe('goForward', function() { - it('should work in step 1', function() { - var verifyRecoveryTokenStub = sinon.stub(scope, 'verifyRecoveryToken'); - verifyRecoveryTokenStub.yields(); - scope.step = 1; - - scope.goForward(); - - expect(verifyRecoveryTokenStub.calledOnce).to.be.true; - expect(scope.step).to.equal(2); - verifyRecoveryTokenStub.restore(); - }); - it('should work in step 2', function() { - var decryptAndStorePrivateKeyLocallyStub = sinon.stub(scope, 'decryptAndStorePrivateKeyLocally'); - decryptAndStorePrivateKeyLocallyStub.returns(); - scope.step = 2; - - scope.goForward(); - - expect(decryptAndStorePrivateKeyLocallyStub.calledOnce).to.be.true; - decryptAndStorePrivateKeyLocallyStub.restore(); - }); - }); - - describe('goTo', function() { - it('should work', function(done) { - mocks.inject(function($controller, $rootScope, $location) { - location = $location; - sinon.stub(location, 'path', function(path) { - expect(path).to.equal('/desktop'); - done(); - }); - scope = $rootScope.$new(); - scope.state = {}; - ctrl = $controller(LoginPrivateKeyDownloadCtrl, { - $location: location, - $scope: scope, - $routeParams: {} - }); - }); - - scope.goTo('/desktop'); - }); + scope.goTo('/desktop'); }); }); }); \ No newline at end of file diff --git a/test/unit/login-set-credentials-ctrl-test.js b/test/unit/login-set-credentials-ctrl-test.js index 41c1398..4b54066 100644 --- a/test/unit/login-set-credentials-ctrl-test.js +++ b/test/unit/login-set-credentials-ctrl-test.js @@ -1,101 +1,97 @@ -define(function(require) { - 'use strict'; +'use strict'; - var expect = chai.expect, - angular = require('angular'), - mocks = require('angularMocks'), - Auth = require('js/bo/auth'), - ConnectionDoctor = require('js/util/connection-doctor'), - SetCredentialsCtrl = require('js/controller/login-set-credentials'), - appController = require('js/app-controller'); +var mocks = angular.mocks, + Auth = require('../../src/js/bo/auth'), + ConnectionDoctor = require('../../src/js/util/connection-doctor'), + SetCredentialsCtrl = require('../../src/js/controller/login-set-credentials'), + appController = require('../../src/js/app-controller'); - describe('Login (Set Credentials) Controller unit test', function() { - // Angular parameters - var scope, location, provider; +describe('Login (Set Credentials) Controller unit test', function() { + // Angular parameters + var scope, location, provider; - // Stubs - var auth, origAuth, doctor, origDoctor; + // Stubs + var auth, origAuth, doctor, origDoctor; - // SUT - var setCredentialsCtrl; + // SUT + var setCredentialsCtrl; - beforeEach(function() { - // remeber pre-test state to restore later - origAuth = appController._auth; - origDoctor = appController._doctor; - auth = appController._auth = sinon.createStubInstance(Auth); - doctor = appController._doctor = sinon.createStubInstance(ConnectionDoctor); + beforeEach(function() { + // remeber pre-test state to restore later + origAuth = appController._auth; + origDoctor = appController._doctor; + auth = appController._auth = sinon.createStubInstance(Auth); + doctor = appController._doctor = sinon.createStubInstance(ConnectionDoctor); - // setup the controller - angular.module('setcredentialstest', []); - mocks.module('setcredentialstest'); - mocks.inject(function($rootScope, $controller, $location) { - scope = $rootScope.$new(); - location = $location; - location.search({ - provider: provider - }); - - scope.state = {}; - setCredentialsCtrl = $controller(SetCredentialsCtrl, { - $scope: scope, - $routeParams: {} - }); + // setup the controller + angular.module('setcredentialstest', []); + mocks.module('setcredentialstest'); + mocks.inject(function($rootScope, $controller, $location) { + scope = $rootScope.$new(); + location = $location; + location.search({ + provider: provider }); - }); - afterEach(function() { - // restore pre-test state - appController._auth = origAuth; - appController._doctor = origDoctor; - }); - - describe('set credentials', function() { - it('should work', function() { - scope.emailAddress = 'emailemailemailemail'; - scope.password = 'passwdpasswdpasswdpasswd'; - scope.smtpHost = 'hosthosthost'; - scope.smtpPort = 1337; - scope.smtpEncryption = '1'; // STARTTLS - scope.imapHost = 'hosthosthost'; - scope.imapPort = 1337; - scope.imapEncryption = '2'; // TLS - scope.realname = 'peter pan'; - - var expectedCredentials = { - provider: provider, - emailAddress: scope.emailAddress, - username: scope.username || scope.emailAddress, - realname: scope.realname, - password: scope.password, - xoauth2: undefined, - imap: { - host: scope.imapHost.toLowerCase(), - port: scope.imapPort, - secure: true, - ignoreTLS: false, - ca: undefined, - pinned: false - }, - smtp: { - host: scope.smtpHost.toLowerCase(), - port: scope.smtpPort, - secure: false, - ignoreTLS: false, - ca: undefined, - pinned: false - } - }; - - doctor.check.yields(); // synchronous yields! - - scope.test(); - - expect(doctor.check.calledOnce).to.be.true; - expect(doctor.configure.calledOnce).to.be.true; - expect(doctor.configure.calledWith(expectedCredentials)).to.be.true; - expect(auth.setCredentials.calledOnce).to.be.true; + scope.state = {}; + setCredentialsCtrl = $controller(SetCredentialsCtrl, { + $scope: scope, + $routeParams: {} }); }); }); + + afterEach(function() { + // restore pre-test state + appController._auth = origAuth; + appController._doctor = origDoctor; + }); + + describe('set credentials', function() { + it('should work', function() { + scope.emailAddress = 'emailemailemailemail'; + scope.password = 'passwdpasswdpasswdpasswd'; + scope.smtpHost = 'hosthosthost'; + scope.smtpPort = 1337; + scope.smtpEncryption = '1'; // STARTTLS + scope.imapHost = 'hosthosthost'; + scope.imapPort = 1337; + scope.imapEncryption = '2'; // TLS + scope.realname = 'peter pan'; + + var expectedCredentials = { + provider: provider, + emailAddress: scope.emailAddress, + username: scope.username || scope.emailAddress, + realname: scope.realname, + password: scope.password, + xoauth2: undefined, + imap: { + host: scope.imapHost.toLowerCase(), + port: scope.imapPort, + secure: true, + ignoreTLS: false, + ca: undefined, + pinned: false + }, + smtp: { + host: scope.smtpHost.toLowerCase(), + port: scope.smtpPort, + secure: false, + ignoreTLS: false, + ca: undefined, + pinned: false + } + }; + + doctor.check.yields(); // synchronous yields! + + scope.test(); + + expect(doctor.check.calledOnce).to.be.true; + expect(doctor.configure.calledOnce).to.be.true; + expect(doctor.configure.calledWith(expectedCredentials)).to.be.true; + expect(auth.setCredentials.calledOnce).to.be.true; + }); + }); }); \ No newline at end of file diff --git a/test/unit/mail-list-ctrl-test.js b/test/unit/mail-list-ctrl-test.js index 7dab559..7fe4e98 100644 --- a/test/unit/mail-list-ctrl-test.js +++ b/test/unit/mail-list-ctrl-test.js @@ -1,451 +1,447 @@ -define(function(require) { - 'use strict'; +'use strict'; - var expect = chai.expect, - angular = require('angular'), - mocks = require('angularMocks'), - MailListCtrl = require('js/controller/mail-list'), - EmailDAO = require('js/dao/email-dao'), - DeviceStorageDAO = require('js/dao/devicestorage-dao'), - KeychainDAO = require('js/dao/keychain-dao'), - appController = require('js/app-controller'), - notification = require('js/util/notification'); +var mocks = angular.mocks, + MailListCtrl = require('../../src/js/controller/mail-list'), + EmailDAO = require('../../src/js/dao/email-dao'), + DeviceStorageDAO = require('../../src/js/dao/devicestorage-dao'), + KeychainDAO = require('../../src/js/dao/keychain-dao'), + appController = require('../../src/js/app-controller'), + notification = require('../../src/js/util/notification'); - chai.Assertion.includeStack = true; +chai.Assertion.includeStack = true; - describe('Mail List controller unit test', function() { - var scope, ctrl, origEmailDao, emailDaoMock, keychainMock, deviceStorageMock, - emailAddress, emails, - hasChrome, hasSocket, hasRuntime, hasIdentity; +describe('Mail List controller unit test', function() { + var scope, ctrl, origEmailDao, emailDaoMock, keychainMock, deviceStorageMock, + emailAddress, emails, + hasChrome, hasSocket, hasRuntime, hasIdentity; - beforeEach(function() { - hasChrome = !!window.chrome; - hasSocket = !!window.chrome.socket; - hasIdentity = !!window.chrome.identity; - if (!hasChrome) { - window.chrome = {}; - } - if (!hasSocket) { - window.chrome.socket = {}; - } - if (!hasRuntime) { - window.chrome.runtime = { - getURL: function() {} - }; - } - if (!hasIdentity) { - window.chrome.identity = {}; - } + beforeEach(function() { + hasChrome = !!window.chrome; + hasSocket = !!window.chrome.socket; + hasIdentity = !!window.chrome.identity; + if (!hasChrome) { + window.chrome = {}; + } + if (!hasSocket) { + window.chrome.socket = {}; + } + if (!hasRuntime) { + window.chrome.runtime = { + getURL: function() {} + }; + } + if (!hasIdentity) { + window.chrome.identity = {}; + } - emails = [{ - unread: true - }, { - unread: true - }, { - unread: true - }]; - appController._outboxBo = { - pendingEmails: emails + emails = [{ + unread: true + }, { + unread: true + }, { + unread: true + }]; + appController._outboxBo = { + pendingEmails: emails + }; + + origEmailDao = appController._emailDao; + emailDaoMock = sinon.createStubInstance(EmailDAO); + appController._emailDao = emailDaoMock; + emailAddress = 'fred@foo.com'; + emailDaoMock._account = { + emailAddress: emailAddress, + }; + + + keychainMock = sinon.createStubInstance(KeychainDAO); + appController._keychain = keychainMock; + + deviceStorageMock = sinon.createStubInstance(DeviceStorageDAO); + emailDaoMock._devicestorage = deviceStorageMock; + + angular.module('maillisttest', []); + mocks.module('maillisttest'); + mocks.inject(function($rootScope, $controller) { + scope = $rootScope.$new(); + scope.state = { + read: { + toggle: function() {} + } }; - origEmailDao = appController._emailDao; - emailDaoMock = sinon.createStubInstance(EmailDAO); - appController._emailDao = emailDaoMock; - emailAddress = 'fred@foo.com'; - emailDaoMock._account = { - emailAddress: emailAddress, - }; - - - keychainMock = sinon.createStubInstance(KeychainDAO); - appController._keychain = keychainMock; - - deviceStorageMock = sinon.createStubInstance(DeviceStorageDAO); - emailDaoMock._devicestorage = deviceStorageMock; - - angular.module('maillisttest', []); - mocks.module('maillisttest'); - mocks.inject(function($rootScope, $controller) { - scope = $rootScope.$new(); - scope.state = { - read: { - toggle: function() {} - } - }; - - scope.loadVisibleBodies = function() {}; - ctrl = $controller(MailListCtrl, { - $scope: scope, - $routeParams: {} - }); - }); - }); - - afterEach(function() { - if (!hasSocket) { - delete window.chrome.socket; - } - if (!hasRuntime) { - delete window.chrome.runtime; - } - if (!hasChrome) { - delete window.chrome; - } - if (!hasIdentity) { - delete window.chrome.identity; - } - - // restore the module - appController._emailDao = origEmailDao; - }); - - describe('displayMore', function() { - beforeEach(function() { - scope.state.nav = { - currentFolder: { - messages: ['a', 'b'] - } - }; - }); - it('should not do anything when display length equals messages length', function() { - scope.displayMessages = ['a', 'b']; - - scope.displayMore(); - expect(scope.displayMessages.length).to.equal(scope.state.nav.currentFolder.messages.length); - }); - it('should append next message interval', function() { - scope.displayMessages = ['a']; - - scope.displayMore(); - expect(scope.displayMessages.length).to.equal(scope.state.nav.currentFolder.messages.length); - }); - }); - - describe('displaySearchResults', function() { - var clock; - - beforeEach(function() { - scope.state.nav = { - currentFolder: { - messages: ['a', 'b'] - } - }; - scope.watchMessages(); - scope.watchOnline(); - clock = sinon.useFakeTimers(); - }); - afterEach(function() { - clock.restore(); - }); - - it('should show initial message on empty', function() { - scope.displaySearchResults(); - expect(scope.searching).to.be.false; - expect(scope.lastUpdateLbl).to.equal('Online'); - expect(scope.displayMessages.length).to.equal(2); - }); - it('should show initial message on empty', function() { - var searchStub = sinon.stub(scope, 'search'); - searchStub.returns(['a']); - - - scope.displaySearchResults('query'); - expect(scope.searching).to.be.true; - expect(scope.lastUpdateLbl).to.equal('Searching ...'); - clock.tick(500); - - expect(scope.displayMessages).to.deep.equal(['a']); - expect(scope.searching).to.be.false; - expect(scope.lastUpdateLbl).to.equal('Matches in this folder'); - - }); - }); - - describe('search', function() { - var message1 = { - to: [{ - name: 'name1', - address: 'address1' - }], - subject: 'subject1', - body: 'body1', - html: 'html1' - }, - message2 = { - to: [{ - name: 'name2', - address: 'address2' - }], - subject: 'subject2', - body: 'body2', - html: 'html2' - }, - message3 = { - to: [{ - name: 'name3', - address: 'address3' - }], - subject: 'subject3', - body: 'body1', - html: 'html1', - encrypted: true - }, - message4 = { - to: [{ - name: 'name4', - address: 'address4' - }], - subject: 'subject4', - body: 'body1', - html: 'html1', - encrypted: true, - decrypted: true - }, - testMessages = [message1, message2, message3, message4]; - - it('return same messages array on empty query string', function() { - var result = scope.search(testMessages, ''); - expect(result).to.equal(testMessages); - }); - - it('return message1 on matching subject', function() { - var result = scope.search(testMessages, 'subject1'); - expect(result.length).to.equal(1); - expect(result[0]).to.equal(message1); - }); - - it('return message1 on matching name', function() { - var result = scope.search(testMessages, 'name1'); - expect(result.length).to.equal(1); - expect(result[0]).to.equal(message1); - }); - - it('return message1 on matching address', function() { - var result = scope.search(testMessages, 'address1'); - expect(result.length).to.equal(1); - expect(result[0]).to.equal(message1); - }); - - it('return plaintext and decrypted messages on matching body', function() { - var result = scope.search(testMessages, 'body1'); - expect(result.length).to.equal(2); - expect(result[0]).to.equal(message1); - expect(result[1]).to.equal(message4); - }); - - it('return plaintext and decrypted messages on matching html', function() { - var result = scope.search(testMessages, 'html1'); - expect(result.length).to.equal(2); - expect(result[0]).to.equal(message1); - expect(result[1]).to.equal(message4); - }); - }); - - describe('scope variables', function() { - it('should be set correctly', function() { - expect(scope.select).to.exist; - expect(scope.remove).to.exist; - expect(scope.state.mailList).to.exist; - }); - }); - - describe('push notification', function() { - beforeEach(function() { - scope._stopWatchTask(); - }); - - afterEach(function() { - notification.create.restore(); - }); - - it('should succeed for single mail', function(done) { - var mail = { - uid: 123, - from: [{ - address: 'asd' - }], - subject: 'this is the subject!', - unread: true - }; - - sinon.stub(notification, 'create', function(opts) { - expect(opts.title).to.equal(mail.from[0].address); - expect(opts.message).to.equal(mail.subject); - - opts.onClick(); - expect(scope.state.mailList.selected).to.equal(mail); - done(); - }); - - scope.state.nav = { - currentFolder: { - type: 'asd', - messages: [mail] - } - }; - - emailDaoMock.onIncomingMessage([mail]); - }); - - it('should succeed for multiple mails', function(done) { - var mails = [{ - uid: 1, - from: [{ - address: 'asd' - }], - subject: 'this is the subject!', - unread: true - }, { - uid: 2, - from: [{ - address: 'qwe' - }], - subject: 'this is the other subject!', - unread: true - }, { - uid: 3, - from: [{ - address: 'qwe' - }], - subject: 'this is the other subject!', - unread: false - }]; - - sinon.stub(notification, 'create', function(opts) { - expect(opts.title).to.equal('2 new messages'); - expect(opts.message).to.equal(mails[0].subject + '\n' + mails[1].subject); - - opts.onClick(); - expect(scope.state.mailList.selected).to.equal(mails[0]); - done(); - }); - - scope.state.nav = { - currentFolder: { - type: 'asd', - messages: mails - } - }; - - emailDaoMock.onIncomingMessage(mails); - }); - }); - - describe('getBody', function() { - it('should get the mail content', function() { - scope.state.nav = { - currentFolder: { - type: 'asd', - } - }; - - scope.getBody(); - expect(emailDaoMock.getBody.calledOnce).to.be.true; - }); - }); - - describe('select', function() { - it('should decrypt, focus mark an unread mail as read', function() { - scope.pendingNotifications = ['asd']; - sinon.stub(notification, 'close'); - - var mail = { - from: [{ - address: 'asd' - }], - unread: true, - }; - scope.state = { - nav: { - currentFolder: { - type: 'Inbox' - } - }, - mailList: {}, - read: { - toggle: function() {} - } - }; - - keychainMock.refreshKeyForUserId.withArgs(mail.from[0].address).yields(); - - scope.select(mail); - - expect(emailDaoMock.decryptBody.calledOnce).to.be.true; - expect(keychainMock.refreshKeyForUserId.calledOnce).to.be.true; - expect(scope.state.mailList.selected).to.equal(mail); - expect(notification.close.calledWith('asd')).to.be.true; - expect(notification.close.calledOnce).to.be.true; - - notification.close.restore(); - }); - - it('should decrypt and focus a read mail', function() { - var mail = { - from: [{ - address: 'asd' - }], - unread: false - }; - - scope.state = { - mailList: {}, - read: { - toggle: function() {} - }, - nav: { - currentFolder: { - type: 'asd' - } - } - }; - - keychainMock.refreshKeyForUserId.withArgs(mail.from[0].address).yields(); - - scope.select(mail); - - expect(emailDaoMock.decryptBody.calledOnce).to.be.true; - expect(keychainMock.refreshKeyForUserId.calledOnce).to.be.true; - expect(scope.state.mailList.selected).to.equal(mail); - }); - }); - - describe('remove', function() { - it('should not delete without a selected mail', function() { - scope.remove(); - }); - - it('should delete the selected mail', function() { - var uid, mail, currentFolder; - - scope._stopWatchTask(); - - scope.account = {}; - uid = 123; - mail = { - uid: uid, - from: [{ - address: 'asd' - }], - subject: '[whiteout] asdasd', - unread: true - }; - currentFolder = { - type: 'Inbox', - path: 'INBOX', - messages: [mail] - }; - scope.account.folders = [currentFolder]; - scope.state.nav = { - currentFolder: currentFolder - }; - emailDaoMock.deleteMessage.yields(); - - scope.remove(mail); - - expect(emailDaoMock.deleteMessage.calledOnce).to.be.true; - expect(scope.state.mailList.selected).to.exist; + scope.loadVisibleBodies = function() {}; + ctrl = $controller(MailListCtrl, { + $scope: scope, + $routeParams: {} }); }); }); + + afterEach(function() { + if (!hasSocket) { + delete window.chrome.socket; + } + if (!hasRuntime) { + delete window.chrome.runtime; + } + if (!hasChrome) { + delete window.chrome; + } + if (!hasIdentity) { + delete window.chrome.identity; + } + + // restore the module + appController._emailDao = origEmailDao; + }); + + describe('displayMore', function() { + beforeEach(function() { + scope.state.nav = { + currentFolder: { + messages: ['a', 'b'] + } + }; + }); + it('should not do anything when display length equals messages length', function() { + scope.displayMessages = ['a', 'b']; + + scope.displayMore(); + expect(scope.displayMessages.length).to.equal(scope.state.nav.currentFolder.messages.length); + }); + it('should append next message interval', function() { + scope.displayMessages = ['a']; + + scope.displayMore(); + expect(scope.displayMessages.length).to.equal(scope.state.nav.currentFolder.messages.length); + }); + }); + + describe('displaySearchResults', function() { + var clock; + + beforeEach(function() { + scope.state.nav = { + currentFolder: { + messages: ['a', 'b'] + } + }; + scope.watchMessages(); + scope.watchOnline(); + clock = sinon.useFakeTimers(); + }); + afterEach(function() { + clock.restore(); + }); + + it('should show initial message on empty', function() { + scope.displaySearchResults(); + expect(scope.searching).to.be.false; + expect(scope.lastUpdateLbl).to.equal('Online'); + expect(scope.displayMessages.length).to.equal(2); + }); + it('should show initial message on empty', function() { + var searchStub = sinon.stub(scope, 'search'); + searchStub.returns(['a']); + + + scope.displaySearchResults('query'); + expect(scope.searching).to.be.true; + expect(scope.lastUpdateLbl).to.equal('Searching ...'); + clock.tick(500); + + expect(scope.displayMessages).to.deep.equal(['a']); + expect(scope.searching).to.be.false; + expect(scope.lastUpdateLbl).to.equal('Matches in this folder'); + + }); + }); + + describe('search', function() { + var message1 = { + to: [{ + name: 'name1', + address: 'address1' + }], + subject: 'subject1', + body: 'body1', + html: 'html1' + }, + message2 = { + to: [{ + name: 'name2', + address: 'address2' + }], + subject: 'subject2', + body: 'body2', + html: 'html2' + }, + message3 = { + to: [{ + name: 'name3', + address: 'address3' + }], + subject: 'subject3', + body: 'body1', + html: 'html1', + encrypted: true + }, + message4 = { + to: [{ + name: 'name4', + address: 'address4' + }], + subject: 'subject4', + body: 'body1', + html: 'html1', + encrypted: true, + decrypted: true + }, + testMessages = [message1, message2, message3, message4]; + + it('return same messages array on empty query string', function() { + var result = scope.search(testMessages, ''); + expect(result).to.equal(testMessages); + }); + + it('return message1 on matching subject', function() { + var result = scope.search(testMessages, 'subject1'); + expect(result.length).to.equal(1); + expect(result[0]).to.equal(message1); + }); + + it('return message1 on matching name', function() { + var result = scope.search(testMessages, 'name1'); + expect(result.length).to.equal(1); + expect(result[0]).to.equal(message1); + }); + + it('return message1 on matching address', function() { + var result = scope.search(testMessages, 'address1'); + expect(result.length).to.equal(1); + expect(result[0]).to.equal(message1); + }); + + it('return plaintext and decrypted messages on matching body', function() { + var result = scope.search(testMessages, 'body1'); + expect(result.length).to.equal(2); + expect(result[0]).to.equal(message1); + expect(result[1]).to.equal(message4); + }); + + it('return plaintext and decrypted messages on matching html', function() { + var result = scope.search(testMessages, 'html1'); + expect(result.length).to.equal(2); + expect(result[0]).to.equal(message1); + expect(result[1]).to.equal(message4); + }); + }); + + describe('scope variables', function() { + it('should be set correctly', function() { + expect(scope.select).to.exist; + expect(scope.remove).to.exist; + expect(scope.state.mailList).to.exist; + }); + }); + + describe('push notification', function() { + beforeEach(function() { + scope._stopWatchTask(); + }); + + afterEach(function() { + notification.create.restore(); + }); + + it('should succeed for single mail', function(done) { + var mail = { + uid: 123, + from: [{ + address: 'asd' + }], + subject: 'this is the subject!', + unread: true + }; + + sinon.stub(notification, 'create', function(opts) { + expect(opts.title).to.equal(mail.from[0].address); + expect(opts.message).to.equal(mail.subject); + + opts.onClick(); + expect(scope.state.mailList.selected).to.equal(mail); + done(); + }); + + scope.state.nav = { + currentFolder: { + type: 'asd', + messages: [mail] + } + }; + + emailDaoMock.onIncomingMessage([mail]); + }); + + it('should succeed for multiple mails', function(done) { + var mails = [{ + uid: 1, + from: [{ + address: 'asd' + }], + subject: 'this is the subject!', + unread: true + }, { + uid: 2, + from: [{ + address: 'qwe' + }], + subject: 'this is the other subject!', + unread: true + }, { + uid: 3, + from: [{ + address: 'qwe' + }], + subject: 'this is the other subject!', + unread: false + }]; + + sinon.stub(notification, 'create', function(opts) { + expect(opts.title).to.equal('2 new messages'); + expect(opts.message).to.equal(mails[0].subject + '\n' + mails[1].subject); + + opts.onClick(); + expect(scope.state.mailList.selected).to.equal(mails[0]); + done(); + }); + + scope.state.nav = { + currentFolder: { + type: 'asd', + messages: mails + } + }; + + emailDaoMock.onIncomingMessage(mails); + }); + }); + + describe('getBody', function() { + it('should get the mail content', function() { + scope.state.nav = { + currentFolder: { + type: 'asd', + } + }; + + scope.getBody(); + expect(emailDaoMock.getBody.calledOnce).to.be.true; + }); + }); + + describe('select', function() { + it('should decrypt, focus mark an unread mail as read', function() { + scope.pendingNotifications = ['asd']; + sinon.stub(notification, 'close'); + + var mail = { + from: [{ + address: 'asd' + }], + unread: true, + }; + scope.state = { + nav: { + currentFolder: { + type: 'Inbox' + } + }, + mailList: {}, + read: { + toggle: function() {} + } + }; + + keychainMock.refreshKeyForUserId.withArgs(mail.from[0].address).yields(); + + scope.select(mail); + + expect(emailDaoMock.decryptBody.calledOnce).to.be.true; + expect(keychainMock.refreshKeyForUserId.calledOnce).to.be.true; + expect(scope.state.mailList.selected).to.equal(mail); + expect(notification.close.calledWith('asd')).to.be.true; + expect(notification.close.calledOnce).to.be.true; + + notification.close.restore(); + }); + + it('should decrypt and focus a read mail', function() { + var mail = { + from: [{ + address: 'asd' + }], + unread: false + }; + + scope.state = { + mailList: {}, + read: { + toggle: function() {} + }, + nav: { + currentFolder: { + type: 'asd' + } + } + }; + + keychainMock.refreshKeyForUserId.withArgs(mail.from[0].address).yields(); + + scope.select(mail); + + expect(emailDaoMock.decryptBody.calledOnce).to.be.true; + expect(keychainMock.refreshKeyForUserId.calledOnce).to.be.true; + expect(scope.state.mailList.selected).to.equal(mail); + }); + }); + + describe('remove', function() { + it('should not delete without a selected mail', function() { + scope.remove(); + }); + + it('should delete the selected mail', function() { + var uid, mail, currentFolder; + + scope._stopWatchTask(); + + scope.account = {}; + uid = 123; + mail = { + uid: uid, + from: [{ + address: 'asd' + }], + subject: '[whiteout] asdasd', + unread: true + }; + currentFolder = { + type: 'Inbox', + path: 'INBOX', + messages: [mail] + }; + scope.account.folders = [currentFolder]; + scope.state.nav = { + currentFolder: currentFolder + }; + emailDaoMock.deleteMessage.yields(); + + scope.remove(mail); + + expect(emailDaoMock.deleteMessage.calledOnce).to.be.true; + expect(scope.state.mailList.selected).to.exist; + }); + }); }); \ No newline at end of file diff --git a/test/unit/main.js b/test/unit/main.js deleted file mode 100644 index b889aba..0000000 --- a/test/unit/main.js +++ /dev/null @@ -1,113 +0,0 @@ -'use strict'; - -// Mozilla bind polyfill because phantomjs is stupid -if (!Function.prototype.bind) { - Function.prototype.bind = function(oThis) { - if (typeof this !== "function") { - // closest thing possible to the ECMAScript 5 internal IsCallable function - throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable"); - } - - var aArgs = Array.prototype.slice.call(arguments, 1), - fToBind = this, - FNOP = function() {}, - fBound = function() { - return fToBind.apply(this instanceof FNOP && oThis ? this : oThis, aArgs.concat(Array.prototype.slice.call(arguments))); - }; - - FNOP.prototype = this.prototype; - fBound.prototype = new FNOP(); - - return fBound; - }; -} - -// a warm round of applause for phantomjs for missing events -(function() { - function CustomEvent(event, params) { - params = params || { - bubbles: false, - cancelable: false, - detail: undefined - }; - var evt = document.createEvent('CustomEvent'); - evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); - return evt; - } - - CustomEvent.prototype = window.Event.prototype; - - window.CustomEvent = CustomEvent; -})(); - -require(['../../src/require-config'], function() { - require.config({ - baseUrl: '../../src/lib', - paths: { - angularMocks: '../../test/lib/angular-mocks' - }, - shim: { - angularMocks: { - exports: 'angular.mock', - deps: ['angular'] - } - } - }); - - - // Start the main app logic. - require(['js/app-config', 'axe'], function(app, axe) { - app.config.workerPath = '../../src/js'; - - // turn off logging in the test - axe.removeAppender(axe.defaultAppender); - - startTests(); - }); -}); - -function startTests() { - mocha.setup('bdd'); - - require( - [ - 'test/unit/oauth-test', - 'test/unit/auth-test', - 'test/unit/email-dao-test', - 'test/unit/app-controller-test', - 'test/unit/pgp-test', - 'test/unit/crypto-test', - 'test/unit/backbutton-handler-test', - 'test/unit/rest-dao-test', - 'test/unit/admin-dao-test', - 'test/unit/publickey-dao-test', - 'test/unit/privatekey-dao-test', - 'test/unit/lawnchair-dao-test', - 'test/unit/keychain-dao-test', - 'test/unit/devicestorage-dao-test', - 'test/unit/dialog-ctrl-test', - 'test/unit/add-account-ctrl-test', - 'test/unit/account-ctrl-test', - 'test/unit/set-passphrase-ctrl-test', - 'test/unit/contacts-ctrl-test', - 'test/unit/login-existing-ctrl-test', - 'test/unit/login-initial-ctrl-test', - 'test/unit/login-new-device-ctrl-test', - 'test/unit/login-privatekey-download-ctrl-test', - 'test/unit/login-set-credentials-ctrl-test', - 'test/unit/privatekey-upload-ctrl-test', - 'test/unit/login-ctrl-test', - 'test/unit/read-ctrl-test', - 'test/unit/navigation-ctrl-test', - 'test/unit/mail-list-ctrl-test', - 'test/unit/write-ctrl-test', - 'test/unit/outbox-bo-test', - 'test/unit/invitation-dao-test', - 'test/unit/update-handler-test', - 'test/unit/connection-doctor-test' - ], function() { - //Tests loaded, run tests - mocha.run(); - } - ); -} \ No newline at end of file diff --git a/test/unit/navigation-ctrl-test.js b/test/unit/navigation-ctrl-test.js index 9d977af..56b27d6 100644 --- a/test/unit/navigation-ctrl-test.js +++ b/test/unit/navigation-ctrl-test.js @@ -1,101 +1,97 @@ -define(function(require) { - 'use strict'; +'use strict'; - var expect = chai.expect, - angular = require('angular'), - mocks = require('angularMocks'), - NavigationCtrl = require('js/controller/navigation'), - EmailDAO = require('js/dao/email-dao'), - OutboxBO = require('js/bo/outbox'), - appController = require('js/app-controller'); +var mocks = angular.mocks, + NavigationCtrl = require('../../src/js/controller/navigation'), + EmailDAO = require('../../src/js/dao/email-dao'), + OutboxBO = require('../../src/js/bo/outbox'), + appController = require('../../src/js/app-controller'); - describe('Navigation Controller unit test', function() { - var scope, ctrl, origEmailDao, emailDaoMock, outboxBoMock, outboxFolder, onConnectStub; +describe('Navigation Controller unit test', function() { + var scope, ctrl, origEmailDao, emailDaoMock, outboxBoMock, outboxFolder, onConnectStub; - beforeEach(function(done) { - // remember original module to restore later - origEmailDao = appController._emailDao; - emailDaoMock = sinon.createStubInstance(EmailDAO); - emailDaoMock._account = { - folders: [{ - type: 'Inbox', - count: 2, - path: 'INBOX' - }, { - type: 'Outbox', - count: 0, - path: 'OUTBOX' - }] - }; - outboxFolder = emailDaoMock._account.folders[1]; - appController._emailDao = emailDaoMock; - outboxBoMock = sinon.createStubInstance(OutboxBO); - appController._outboxBo = outboxBoMock; - outboxBoMock.startChecking.returns(); - onConnectStub = sinon.stub(appController, 'onConnect'); - onConnectStub.yields(); + beforeEach(function(done) { + // remember original module to restore later + origEmailDao = appController._emailDao; + emailDaoMock = sinon.createStubInstance(EmailDAO); + emailDaoMock._account = { + folders: [{ + type: 'Inbox', + count: 2, + path: 'INBOX' + }, { + type: 'Outbox', + count: 0, + path: 'OUTBOX' + }] + }; + outboxFolder = emailDaoMock._account.folders[1]; + appController._emailDao = emailDaoMock; + outboxBoMock = sinon.createStubInstance(OutboxBO); + appController._outboxBo = outboxBoMock; + outboxBoMock.startChecking.returns(); + onConnectStub = sinon.stub(appController, 'onConnect'); + onConnectStub.yields(); - angular.module('navigationtest', []); - mocks.module('navigationtest'); - mocks.inject(function($rootScope, $controller) { - scope = $rootScope.$new(); - scope.state = {}; - ctrl = $controller(NavigationCtrl, { - $scope: scope, - $routeParams: {} - }); - done(); + angular.module('navigationtest', []); + mocks.module('navigationtest'); + mocks.inject(function($rootScope, $controller) { + scope = $rootScope.$new(); + scope.state = {}; + ctrl = $controller(NavigationCtrl, { + $scope: scope, + $routeParams: {} }); + done(); }); + }); - afterEach(function() { - // restore the module - appController._emailDao = origEmailDao; - onConnectStub.restore(); + afterEach(function() { + // restore the module + appController._emailDao = origEmailDao; + onConnectStub.restore(); + }); + + describe('initial state', function() { + it('should be well defined', function() { + expect(scope.state).to.exist; + expect(scope.state.lightbox).to.be.undefined; + expect(scope.account.folders).to.not.be.empty; + expect(scope.openFolder).to.exist; }); + }); - describe('initial state', function() { - it('should be well defined', function() { - expect(scope.state).to.exist; - expect(scope.state.lightbox).to.be.undefined; - expect(scope.account.folders).to.not.be.empty; - expect(scope.openFolder).to.exist; - }); + describe('open/close nav view', function() { + it('should open/close', function() { + expect(scope.state.nav.open).to.be.false; + scope.state.nav.toggle(true); + expect(scope.state.nav.open).to.be.true; + scope.state.nav.toggle(false); + expect(scope.state.nav.open).to.be.false; }); + }); - describe('open/close nav view', function() { - it('should open/close', function() { - expect(scope.state.nav.open).to.be.false; - scope.state.nav.toggle(true); - expect(scope.state.nav.open).to.be.true; - scope.state.nav.toggle(false); - expect(scope.state.nav.open).to.be.false; - }); + describe('open folder', function() { + it('should work', function() { + scope.state.nav.open = true; + + scope.openFolder('asd'); + expect(scope.state.nav.currentFolder).to.equal('asd'); + expect(scope.state.nav.open).to.be.false; }); + }); - describe('open folder', function() { - it('should work', function() { - scope.state.nav.open = true; + describe('empty outbox', function() { + it('should work', function() { + var callback; - scope.openFolder('asd'); - expect(scope.state.nav.currentFolder).to.equal('asd'); - expect(scope.state.nav.open).to.be.false; - }); - }); + expect(outboxBoMock.startChecking.callCount).to.equal(1); - describe('empty outbox', function() { - it('should work', function() { - var callback; + outboxBoMock.startChecking.calledWith(sinon.match(function(cb) { + callback = cb; + })); - expect(outboxBoMock.startChecking.callCount).to.equal(1); - - outboxBoMock.startChecking.calledWith(sinon.match(function(cb) { - callback = cb; - })); - - callback(null, 5); - expect(outboxFolder.count).to.equal(5); - }); + callback(null, 5); + expect(outboxFolder.count).to.equal(5); }); }); }); \ No newline at end of file diff --git a/test/unit/oauth-test.js b/test/unit/oauth-test.js index 39267d5..5178c0f 100644 --- a/test/unit/oauth-test.js +++ b/test/unit/oauth-test.js @@ -1,201 +1,198 @@ -define(function(require) { - 'use strict'; +'use strict'; - var OAuth = require('js/util/oauth'), - RestDAO = require('js/dao/rest-dao'), - expect = chai.expect; +var OAuth = require('../../src/js/util/oauth'), + RestDAO = require('../../src/js/dao/rest-dao'); - describe('OAuth unit tests', function() { - var oauth, googleApiStub, identityStub, getPlatformInfoStub, removeCachedStub, - testEmail = 'test@example.com'; +describe('OAuth unit tests', function() { + var oauth, googleApiStub, identityStub, getPlatformInfoStub, removeCachedStub, + testEmail = 'test@example.com'; + + beforeEach(function() { + googleApiStub = sinon.createStubInstance(RestDAO); + oauth = new OAuth(googleApiStub); + + window.chrome = window.chrome || {}; + + window.chrome.identity = window.chrome.identity || {}; + if (typeof window.chrome.identity.getAuthToken !== 'function') { + window.chrome.identity.getAuthToken = function() {}; + } + identityStub = sinon.stub(window.chrome.identity, 'getAuthToken'); + + if (typeof window.chrome.identity.removeCachedAuthToken !== 'function') { + window.chrome.identity.removeCachedAuthToken = function() {}; + } + removeCachedStub = sinon.stub(window.chrome.identity, 'removeCachedAuthToken'); + + window.chrome.runtime = window.chrome.runtime || {}; + if (typeof window.chrome.runtime.getPlatformInfo !== 'function') { + window.chrome.runtime.getPlatformInfo = function() {}; + } + getPlatformInfoStub = sinon.stub(window.chrome.runtime, 'getPlatformInfo'); + }); + + afterEach(function() { + identityStub.restore(); + getPlatformInfoStub.restore(); + removeCachedStub.restore(); + }); + + describe('isSupported', function() { + it('should work', function() { + expect(oauth.isSupported()).to.be.true; + }); + }); + + describe('refreshToken', function() { + var getOAuthTokenStub; beforeEach(function() { - googleApiStub = sinon.createStubInstance(RestDAO); - oauth = new OAuth(googleApiStub); - - window.chrome = window.chrome || {}; - - window.chrome.identity = window.chrome.identity || {}; - if (typeof window.chrome.identity.getAuthToken !== 'function') { - window.chrome.identity.getAuthToken = function() {}; - } - identityStub = sinon.stub(window.chrome.identity, 'getAuthToken'); - - if (typeof window.chrome.identity.removeCachedAuthToken !== 'function') { - window.chrome.identity.removeCachedAuthToken = function() {}; - } - removeCachedStub = sinon.stub(window.chrome.identity, 'removeCachedAuthToken'); - - window.chrome.runtime = window.chrome.runtime || {}; - if (typeof window.chrome.runtime.getPlatformInfo !== 'function') { - window.chrome.runtime.getPlatformInfo = function() {}; - } - getPlatformInfoStub = sinon.stub(window.chrome.runtime, 'getPlatformInfo'); + getOAuthTokenStub = sinon.stub(oauth, 'getOAuthToken'); }); - afterEach(function() { - identityStub.restore(); - getPlatformInfoStub.restore(); - removeCachedStub.restore(); + getOAuthTokenStub.restore(); }); - describe('isSupported', function() { - it('should work', function() { - expect(oauth.isSupported()).to.be.true; + it('should work', function() { + removeCachedStub.withArgs({ + token: 'oldToken' + }).yields(); + + getOAuthTokenStub.withArgs(testEmail).yields(); + + oauth.refreshToken({ + oldToken: 'oldToken', + emailAddress: testEmail + }, function(err) { + expect(err).to.not.exist; + expect(removeCachedStub.calledOnce).to.be.true; + expect(getOAuthTokenStub.calledOnce).to.be.true; }); }); - describe('refreshToken', function() { - var getOAuthTokenStub; + it('should work without email', function() { + removeCachedStub.withArgs({ + token: 'oldToken' + }).yields(); - beforeEach(function() { - getOAuthTokenStub = sinon.stub(oauth, 'getOAuthToken'); - }); - afterEach(function() { - getOAuthTokenStub.restore(); - }); + getOAuthTokenStub.withArgs(undefined).yields(); - it('should work', function() { - removeCachedStub.withArgs({ - token: 'oldToken' - }).yields(); - - getOAuthTokenStub.withArgs(testEmail).yields(); - - oauth.refreshToken({ - oldToken: 'oldToken', - emailAddress: testEmail - }, function(err) { - expect(err).to.not.exist; - expect(removeCachedStub.calledOnce).to.be.true; - expect(getOAuthTokenStub.calledOnce).to.be.true; - }); - }); - - it('should work without email', function() { - removeCachedStub.withArgs({ - token: 'oldToken' - }).yields(); - - getOAuthTokenStub.withArgs(undefined).yields(); - - oauth.refreshToken({ - oldToken: 'oldToken', - }, function(err) { - expect(err).to.not.exist; - expect(removeCachedStub.calledOnce).to.be.true; - expect(getOAuthTokenStub.calledOnce).to.be.true; - expect(getOAuthTokenStub.calledWith(undefined)).to.be.true; - }); - }); - - it('should fail without all options', function() { - oauth.refreshToken({ - emailAddress: testEmail - }, function(err) { - expect(err).to.exist; - expect(removeCachedStub.called).to.be.false; - expect(getOAuthTokenStub.called).to.be.false; - }); + oauth.refreshToken({ + oldToken: 'oldToken', + }, function(err) { + expect(err).to.not.exist; + expect(removeCachedStub.calledOnce).to.be.true; + expect(getOAuthTokenStub.calledOnce).to.be.true; + expect(getOAuthTokenStub.calledWith(undefined)).to.be.true; }); }); - describe('getOAuthToken', function() { - it('should work for empty emailAddress', function(done) { - getPlatformInfoStub.yields({ - os: 'android' - }); - identityStub.withArgs({ - interactive: true - }).yields('token'); - - oauth.getOAuthToken(undefined, function(err, token) { - expect(err).to.not.exist; - expect(token).to.equal('token'); - done(); - }); - }); - - it('should work on android app', function(done) { - getPlatformInfoStub.yields({ - os: 'android' - }); - identityStub.withArgs({ - interactive: true, - accountHint: testEmail - }).yields('token'); - - oauth.getOAuthToken(testEmail, function(err, token) { - expect(err).to.not.exist; - expect(token).to.equal('token'); - done(); - }); - }); - - it('should work on desktop chrome', function(done) { - getPlatformInfoStub.yields({ - os: 'mac' - }); - identityStub.withArgs({ - interactive: true - }).yields('token'); - - oauth.getOAuthToken(testEmail, function(err, token) { - expect(err).to.not.exist; - expect(token).to.equal('token'); - done(); - }); - }); - - it('should fail', function(done) { - getPlatformInfoStub.yields({ - os: 'android' - }); - identityStub.yields(); - - oauth.getOAuthToken(testEmail, function(err, token) { - expect(err).to.exist; - expect(token).to.not.exist; - done(); - }); + it('should fail without all options', function() { + oauth.refreshToken({ + emailAddress: testEmail + }, function(err) { + expect(err).to.exist; + expect(removeCachedStub.called).to.be.false; + expect(getOAuthTokenStub.called).to.be.false; }); }); - - describe('queryEmailAddress', function() { - it('should work', function(done) { - googleApiStub.get.withArgs({ - uri: '/oauth2/v3/userinfo?access_token=token' - }).yields(null, { - email: 'asdf@example.com' - }); - - oauth.queryEmailAddress('token', function(err, emailAddress) { - expect(err).to.not.exist; - expect(emailAddress).to.equal('asdf@example.com'); - done(); - }); - }); - - it('should fail due to invalid token', function(done) { - oauth.queryEmailAddress('', function(err, emailAddress) { - expect(err).to.exist; - expect(emailAddress).to.not.exist; - done(); - }); - }); - - it('should fail due to error in rest api', function(done) { - googleApiStub.get.withArgs({ - uri: '/oauth2/v3/userinfo?access_token=token' - }).yields(new Error()); - - oauth.queryEmailAddress('token', function(err, emailAddress) { - expect(err).to.exist; - expect(emailAddress).to.not.exist; - done(); - }); - }); - }); - }); + + describe('getOAuthToken', function() { + it('should work for empty emailAddress', function(done) { + getPlatformInfoStub.yields({ + os: 'android' + }); + identityStub.withArgs({ + interactive: true + }).yields('token'); + + oauth.getOAuthToken(undefined, function(err, token) { + expect(err).to.not.exist; + expect(token).to.equal('token'); + done(); + }); + }); + + it('should work on android app', function(done) { + getPlatformInfoStub.yields({ + os: 'android' + }); + identityStub.withArgs({ + interactive: true, + accountHint: testEmail + }).yields('token'); + + oauth.getOAuthToken(testEmail, function(err, token) { + expect(err).to.not.exist; + expect(token).to.equal('token'); + done(); + }); + }); + + it('should work on desktop chrome', function(done) { + getPlatformInfoStub.yields({ + os: 'mac' + }); + identityStub.withArgs({ + interactive: true + }).yields('token'); + + oauth.getOAuthToken(testEmail, function(err, token) { + expect(err).to.not.exist; + expect(token).to.equal('token'); + done(); + }); + }); + + it('should fail', function(done) { + getPlatformInfoStub.yields({ + os: 'android' + }); + identityStub.yields(); + + oauth.getOAuthToken(testEmail, function(err, token) { + expect(err).to.exist; + expect(token).to.not.exist; + done(); + }); + }); + }); + + describe('queryEmailAddress', function() { + it('should work', function(done) { + googleApiStub.get.withArgs({ + uri: '/oauth2/v3/userinfo?access_token=token' + }).yields(null, { + email: 'asdf@example.com' + }); + + oauth.queryEmailAddress('token', function(err, emailAddress) { + expect(err).to.not.exist; + expect(emailAddress).to.equal('asdf@example.com'); + done(); + }); + }); + + it('should fail due to invalid token', function(done) { + oauth.queryEmailAddress('', function(err, emailAddress) { + expect(err).to.exist; + expect(emailAddress).to.not.exist; + done(); + }); + }); + + it('should fail due to error in rest api', function(done) { + googleApiStub.get.withArgs({ + uri: '/oauth2/v3/userinfo?access_token=token' + }).yields(new Error()); + + oauth.queryEmailAddress('token', function(err, emailAddress) { + expect(err).to.exist; + expect(emailAddress).to.not.exist; + done(); + }); + }); + }); + }); \ No newline at end of file diff --git a/test/unit/outbox-bo-test.js b/test/unit/outbox-bo-test.js index 4d98198..55cc2a5 100644 --- a/test/unit/outbox-bo-test.js +++ b/test/unit/outbox-bo-test.js @@ -1,279 +1,276 @@ -define(function(require) { - 'use strict'; +'use strict'; - var expect = chai.expect, - OutboxBO = require('js/bo/outbox'), - KeychainDAO = require('js/dao/keychain-dao'), - EmailDAO = require('js/dao/email-dao'), - DeviceStorageDAO = require('js/dao/devicestorage-dao'); +var OutboxBO = require('../../src/js/bo/outbox'), + KeychainDAO = require('../../src/js/dao/keychain-dao'), + EmailDAO = require('../../src/js/dao/email-dao'), + DeviceStorageDAO = require('../../src/js/dao/devicestorage-dao'); - chai.Assertion.includeStack = true; +chai.Assertion.includeStack = true; - describe('Outbox Business Object unit test', function() { - var outbox, emailDaoStub, devicestorageStub, keychainStub, - dummyUser = 'spiderpig@springfield.com'; +describe('Outbox Business Object unit test', function() { + var outbox, emailDaoStub, devicestorageStub, keychainStub, + dummyUser = 'spiderpig@springfield.com'; - beforeEach(function() { - emailDaoStub = sinon.createStubInstance(EmailDAO); - emailDaoStub._account = { - emailAddress: dummyUser, - folders: [{ - type: 'Outbox' - }], - online: true + beforeEach(function() { + emailDaoStub = sinon.createStubInstance(EmailDAO); + emailDaoStub._account = { + emailAddress: dummyUser, + folders: [{ + type: 'Outbox' + }], + online: true + }; + devicestorageStub = sinon.createStubInstance(DeviceStorageDAO); + keychainStub = sinon.createStubInstance(KeychainDAO); + outbox = new OutboxBO(emailDaoStub, keychainStub, devicestorageStub); + }); + + afterEach(function() {}); + + describe('start/stop checking', function() { + it('should work', function() { + function onOutboxUpdate(err) { + expect(err).to.not.exist; + } + + outbox.startChecking(onOutboxUpdate); + expect(outbox._intervalId).to.exist; + + outbox.stopChecking(); + expect(outbox._intervalId).to.not.exist; + }); + }); + + describe('put', function() { + it('should not encrypt and store a mail', function(done) { + var mail, senderKey, receiverKey; + + senderKey = { + publicKey: 'SENDER PUBLIC KEY' }; - devicestorageStub = sinon.createStubInstance(DeviceStorageDAO); - keychainStub = sinon.createStubInstance(KeychainDAO); - outbox = new OutboxBO(emailDaoStub, keychainStub, devicestorageStub); - }); - - afterEach(function() {}); - - describe('start/stop checking', function() { - it('should work', function() { - function onOutboxUpdate(err) { - expect(err).to.not.exist; - } - - outbox.startChecking(onOutboxUpdate); - expect(outbox._intervalId).to.exist; - - outbox.stopChecking(); - expect(outbox._intervalId).to.not.exist; - }); - }); - - describe('put', function() { - it('should not encrypt and store a mail', function(done) { - var mail, senderKey, receiverKey; - - senderKey = { - publicKey: 'SENDER PUBLIC KEY' - }; - receiverKey = { - publicKey: 'RECEIVER PUBLIC KEY' - }; - mail = { - from: [{ - name: 'member', - address: 'member@whiteout.io' - }], - to: [{ - name: 'member', - address: 'member' - }, { - name: 'notamember', - address: 'notamember' - }], - cc: [], - bcc: [] - }; - - keychainStub.getReceiverPublicKey.withArgs(mail.from[0].address).yieldsAsync(null, senderKey); - keychainStub.getReceiverPublicKey.withArgs(mail.to[0].address).yieldsAsync(null, receiverKey); - keychainStub.getReceiverPublicKey.withArgs(mail.to[1].address).yieldsAsync(); - - devicestorageStub.storeList.withArgs([mail]).yieldsAsync(); - - outbox.put(mail, function(error) { - expect(error).to.not.exist; - - expect(mail.publicKeysArmored.length).to.equal(2); - expect(emailDaoStub.encrypt.called).to.be.false; - expect(devicestorageStub.storeList.calledOnce).to.be.true; - - done(); - }); - }); - - it('should not encrypt a mail with bcc and store a mail', function(done) { - var mail; - - mail = { - from: [{ - name: 'member', - address: 'member@whiteout.io' - }], - to: [{ - name: 'member', - address: 'member@whiteout.io' - }], - cc: [], - bcc: [{ - name: 'member', - address: 'member@whiteout.io' - }] - }; - - devicestorageStub.storeList.withArgs([mail]).yieldsAsync(); - - outbox.put(mail, function(error) { - expect(error).to.not.exist; - - expect(mail.publicKeysArmored.length).to.equal(0); - expect(keychainStub.getReceiverPublicKey.called).to.be.false; - expect(emailDaoStub.encrypt.called).to.be.false; - expect(devicestorageStub.storeList.calledOnce).to.be.true; - - done(); - }); - }); - - it('should encrypt and store a mail', function(done) { - var mail, senderKey, receiverKey; - - senderKey = { - publicKey: 'SENDER PUBLIC KEY' - }; - receiverKey = { - publicKey: 'RECEIVER PUBLIC KEY' - }; - mail = { - from: [{ - name: 'member', - address: 'member@whiteout.io' - }], - to: [{ - name: 'member', - address: 'member' - }, { - name: 'notamember', - address: 'notamember' - }], - cc: [], - bcc: [] - }; - - keychainStub.getReceiverPublicKey.withArgs(mail.from[0].address).yieldsAsync(null, senderKey); - keychainStub.getReceiverPublicKey.withArgs(mail.to[0].address).yieldsAsync(null, receiverKey); - keychainStub.getReceiverPublicKey.withArgs(mail.to[1].address).yieldsAsync(null, receiverKey); - - emailDaoStub.encrypt.withArgs({ - mail: mail, - publicKeysArmored: [senderKey.publicKey, receiverKey.publicKey, receiverKey.publicKey] - }).yieldsAsync(); - - devicestorageStub.storeList.withArgs([mail]).yieldsAsync(); - - outbox.put(mail, function(error) { - expect(error).to.not.exist; - - expect(mail.publicKeysArmored.length).to.equal(3); - expect(emailDaoStub.encrypt.calledOnce).to.be.true; - expect(devicestorageStub.storeList.calledOnce).to.be.true; - - done(); - }); - }); - }); - - describe('process outbox', function() { - it('should send to registered users and update pending mails', function(done) { - var from, member, invited, notinvited, newlyjoined, dummyMails, newlyjoinedKey; - - from = [{ + receiverKey = { + publicKey: 'RECEIVER PUBLIC KEY' + }; + mail = { + from: [{ name: 'member', address: 'member@whiteout.io' - }]; - member = { - id: '12', - from: from, - to: [{ - name: 'member', - address: 'member' - }], - encrypted: true, - publicKeysArmored: ['ARMORED KEY OF MEMBER'], - unregisteredUsers: [] - }; - invited = { - id: '34', - from: from, - to: [{ - name: 'invited', - address: 'invited' - }], - publicKeysArmored: [], - unregisteredUsers: [{ - name: 'invited', - address: 'invited' - }] - }; - notinvited = { - id: '56', - from: from, - to: [{ - name: 'notinvited', - address: 'notinvited' - }], - publicKeysArmored: [], - unregisteredUsers: [{ - name: 'notinvited', - address: 'notinvited' - }] - }; - newlyjoined = { - id: '78', - from: from, - to: [{ - name: 'newlyjoined', - address: 'newlyjoined' - }], - encrypted: true, - publicKeysArmored: [], - unregisteredUsers: [{ - name: 'newlyjoined', - address: 'newlyjoined' - }] - }; - newlyjoinedKey = { - publicKey: 'THIS IS THE NEWLY JOINED PUBLIC KEY!' - }; + }], + to: [{ + name: 'member', + address: 'member' + }, { + name: 'notamember', + address: 'notamember' + }], + cc: [], + bcc: [] + }; - dummyMails = [member, invited, notinvited, newlyjoined]; + keychainStub.getReceiverPublicKey.withArgs(mail.from[0].address).yieldsAsync(null, senderKey); + keychainStub.getReceiverPublicKey.withArgs(mail.to[0].address).yieldsAsync(null, receiverKey); + keychainStub.getReceiverPublicKey.withArgs(mail.to[1].address).yieldsAsync(); - devicestorageStub.listItems.yieldsAsync(null, dummyMails); + devicestorageStub.storeList.withArgs([mail]).yieldsAsync(); - emailDaoStub.sendPlaintext.yieldsAsync(); + outbox.put(mail, function(error) { + expect(error).to.not.exist; - emailDaoStub.sendEncrypted.withArgs({ - email: newlyjoined - }).yieldsAsync(); + expect(mail.publicKeysArmored.length).to.equal(2); + expect(emailDaoStub.encrypt.called).to.be.false; + expect(devicestorageStub.storeList.calledOnce).to.be.true; - emailDaoStub.sendEncrypted.withArgs({ - email: member - }).yieldsAsync(); - - devicestorageStub.removeList.yieldsAsync(); - - function onOutboxUpdate(err, count) { - expect(err).to.not.exist; - expect(count).to.equal(0); - - expect(outbox._outboxBusy).to.be.false; - expect(emailDaoStub.sendEncrypted.callCount).to.equal(2); - expect(emailDaoStub.sendPlaintext.callCount).to.equal(2); - expect(devicestorageStub.listItems.callCount).to.equal(1); - expect(devicestorageStub.removeList.callCount).to.equal(4); - expect(keychainStub.getReceiverPublicKey.callCount).to.equal(0); - - done(); - } - - outbox._processOutbox(onOutboxUpdate); + done(); }); + }); - it('should not process outbox in offline mode', function(done) { - emailDaoStub._account.online = false; - devicestorageStub.listItems.yieldsAsync(null, [{}]); + it('should not encrypt a mail with bcc and store a mail', function(done) { + var mail; - outbox._processOutbox(function(err, count) { - expect(err).to.not.exist; - expect(count).to.equal(1); - expect(devicestorageStub.listItems.callCount).to.equal(1); - expect(outbox._outboxBusy).to.be.false; - done(); - }); + mail = { + from: [{ + name: 'member', + address: 'member@whiteout.io' + }], + to: [{ + name: 'member', + address: 'member@whiteout.io' + }], + cc: [], + bcc: [{ + name: 'member', + address: 'member@whiteout.io' + }] + }; + + devicestorageStub.storeList.withArgs([mail]).yieldsAsync(); + + outbox.put(mail, function(error) { + expect(error).to.not.exist; + + expect(mail.publicKeysArmored.length).to.equal(0); + expect(keychainStub.getReceiverPublicKey.called).to.be.false; + expect(emailDaoStub.encrypt.called).to.be.false; + expect(devicestorageStub.storeList.calledOnce).to.be.true; + + done(); + }); + }); + + it('should encrypt and store a mail', function(done) { + var mail, senderKey, receiverKey; + + senderKey = { + publicKey: 'SENDER PUBLIC KEY' + }; + receiverKey = { + publicKey: 'RECEIVER PUBLIC KEY' + }; + mail = { + from: [{ + name: 'member', + address: 'member@whiteout.io' + }], + to: [{ + name: 'member', + address: 'member' + }, { + name: 'notamember', + address: 'notamember' + }], + cc: [], + bcc: [] + }; + + keychainStub.getReceiverPublicKey.withArgs(mail.from[0].address).yieldsAsync(null, senderKey); + keychainStub.getReceiverPublicKey.withArgs(mail.to[0].address).yieldsAsync(null, receiverKey); + keychainStub.getReceiverPublicKey.withArgs(mail.to[1].address).yieldsAsync(null, receiverKey); + + emailDaoStub.encrypt.withArgs({ + mail: mail, + publicKeysArmored: [senderKey.publicKey, receiverKey.publicKey, receiverKey.publicKey] + }).yieldsAsync(); + + devicestorageStub.storeList.withArgs([mail]).yieldsAsync(); + + outbox.put(mail, function(error) { + expect(error).to.not.exist; + + expect(mail.publicKeysArmored.length).to.equal(3); + expect(emailDaoStub.encrypt.calledOnce).to.be.true; + expect(devicestorageStub.storeList.calledOnce).to.be.true; + + done(); + }); + }); + }); + + describe('process outbox', function() { + it('should send to registered users and update pending mails', function(done) { + var from, member, invited, notinvited, newlyjoined, dummyMails, newlyjoinedKey; + + from = [{ + name: 'member', + address: 'member@whiteout.io' + }]; + member = { + id: '12', + from: from, + to: [{ + name: 'member', + address: 'member' + }], + encrypted: true, + publicKeysArmored: ['ARMORED KEY OF MEMBER'], + unregisteredUsers: [] + }; + invited = { + id: '34', + from: from, + to: [{ + name: 'invited', + address: 'invited' + }], + publicKeysArmored: [], + unregisteredUsers: [{ + name: 'invited', + address: 'invited' + }] + }; + notinvited = { + id: '56', + from: from, + to: [{ + name: 'notinvited', + address: 'notinvited' + }], + publicKeysArmored: [], + unregisteredUsers: [{ + name: 'notinvited', + address: 'notinvited' + }] + }; + newlyjoined = { + id: '78', + from: from, + to: [{ + name: 'newlyjoined', + address: 'newlyjoined' + }], + encrypted: true, + publicKeysArmored: [], + unregisteredUsers: [{ + name: 'newlyjoined', + address: 'newlyjoined' + }] + }; + newlyjoinedKey = { + publicKey: 'THIS IS THE NEWLY JOINED PUBLIC KEY!' + }; + + dummyMails = [member, invited, notinvited, newlyjoined]; + + devicestorageStub.listItems.yieldsAsync(null, dummyMails); + + emailDaoStub.sendPlaintext.yieldsAsync(); + + emailDaoStub.sendEncrypted.withArgs({ + email: newlyjoined + }).yieldsAsync(); + + emailDaoStub.sendEncrypted.withArgs({ + email: member + }).yieldsAsync(); + + devicestorageStub.removeList.yieldsAsync(); + + function onOutboxUpdate(err, count) { + expect(err).to.not.exist; + expect(count).to.equal(0); + + expect(outbox._outboxBusy).to.be.false; + expect(emailDaoStub.sendEncrypted.callCount).to.equal(2); + expect(emailDaoStub.sendPlaintext.callCount).to.equal(2); + expect(devicestorageStub.listItems.callCount).to.equal(1); + expect(devicestorageStub.removeList.callCount).to.equal(4); + expect(keychainStub.getReceiverPublicKey.callCount).to.equal(0); + + done(); + } + + outbox._processOutbox(onOutboxUpdate); + }); + + it('should not process outbox in offline mode', function(done) { + emailDaoStub._account.online = false; + devicestorageStub.listItems.yieldsAsync(null, [{}]); + + outbox._processOutbox(function(err, count) { + expect(err).to.not.exist; + expect(count).to.equal(1); + expect(devicestorageStub.listItems.callCount).to.equal(1); + expect(outbox._outboxBusy).to.be.false; + done(); }); }); }); diff --git a/test/unit/pgp-test.js b/test/unit/pgp-test.js index 982eda7..e0a01f1 100644 --- a/test/unit/pgp-test.js +++ b/test/unit/pgp-test.js @@ -1,464 +1,460 @@ -define(function(require) { - 'use strict'; +'use strict'; - var PGP = require('js/crypto/pgp'), - openpgp = require('openpgp'), - expect = chai.expect; +var PGP = require('../../src/js/crypto/pgp'); - describe('PGP Crypto Api unit tests', function() { - this.timeout(20000); +describe('PGP Crypto Api unit tests', function() { + this.timeout(20000); - var pgp, - user = 'whiteout.test@t-online.de', - passphrase = 'asdf', - keySize = 512, - keyId = 'F6F60E9B42CDFF4C', - pubkey = '-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n' + - 'Version: OpenPGP.js v0.7.2\r\n' + - 'Comment: Whiteout Mail - https://whiteout.io\r\n' + - '\r\n' + - 'xk0EUlhMvAEB/2MZtCUOAYvyLFjDp3OBMGn3Ev8FwjzyPbIF0JUw+L7y2XR5\r\n' + - 'RVGvbK88unV3cU/1tOYdNsXI6pSp/Ztjyv7vbBUAEQEAAc0pV2hpdGVvdXQg\r\n' + - 'VXNlciA8d2hpdGVvdXQudGVzdEB0LW9ubGluZS5kZT7CXAQQAQgAEAUCUlhM\r\n' + - 'vQkQ9vYOm0LN/0wAAAW4Af9C+kYW1AvNWmivdtr0M0iYCUjM9DNOQH1fcvXq\r\n' + - 'IiN602mWrkd8jcEzLsW5IUNzVPLhrFIuKyBDTpLnC07Loce1\r\n' + - '=6XMW\r\n' + - '-----END PGP PUBLIC KEY BLOCK-----\r\n\r\n', - privkey = '-----BEGIN PGP PRIVATE KEY BLOCK-----\r\n' + - 'Version: OpenPGP.js v0.7.2\r\n' + - 'Comment: Whiteout Mail - https://whiteout.io\r\n' + - '\r\n' + - 'xcBeBFJYTLwBAf9jGbQlDgGL8ixYw6dzgTBp9xL/BcI88j2yBdCVMPi+8tl0\r\n' + - 'eUVRr2yvPLp1d3FP9bTmHTbFyOqUqf2bY8r+72wVABEBAAH+AwMIhNB4ivtv\r\n' + - 'Y2xg6VeMcjjHxZayESHACV+nQx5Tx6ev6xzIF1Qh72fNPDppLhFSFOuTTMsU\r\n' + - 'kTN4c+BVYt29spH+cA1jcDAxQ2ULrNAXo+hheOqhpedTs8aCbcLFkJAS16hk\r\n' + - 'YSk4OnJgp/z24rVju1SHRSFbgundPzmNgXeX9e8IkviGhhQ11Wc5YwVkx03t\r\n' + - 'Z3MdDMF0jyhopbPIoBdyJB0dhvBh98w3JmwpYh9wjUA9MBHD1tvHpRmSZ3BM\r\n' + - 'UCmATn2ZLWBRWiYqFbgDnL1GM80pV2hpdGVvdXQgVXNlciA8d2hpdGVvdXQu\r\n' + - 'dGVzdEB0LW9ubGluZS5kZT7CXAQQAQgAEAUCUlhMvQkQ9vYOm0LN/0wAAAW4\r\n' + - 'Af9C+kYW1AvNWmivdtr0M0iYCUjM9DNOQH1fcvXqIiN602mWrkd8jcEzLsW5\r\n' + - 'IUNzVPLhrFIuKyBDTpLnC07Loce1\r\n' + - '=ULta\r\n' + - '-----END PGP PRIVATE KEY BLOCK-----\r\n'; + var pgp, + user = 'whiteout.test@t-online.de', + passphrase = 'asdf', + keySize = 512, + keyId = 'F6F60E9B42CDFF4C', + pubkey = '-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n' + + 'Version: OpenPGP.js v0.7.2\r\n' + + 'Comment: Whiteout Mail - https://whiteout.io\r\n' + + '\r\n' + + 'xk0EUlhMvAEB/2MZtCUOAYvyLFjDp3OBMGn3Ev8FwjzyPbIF0JUw+L7y2XR5\r\n' + + 'RVGvbK88unV3cU/1tOYdNsXI6pSp/Ztjyv7vbBUAEQEAAc0pV2hpdGVvdXQg\r\n' + + 'VXNlciA8d2hpdGVvdXQudGVzdEB0LW9ubGluZS5kZT7CXAQQAQgAEAUCUlhM\r\n' + + 'vQkQ9vYOm0LN/0wAAAW4Af9C+kYW1AvNWmivdtr0M0iYCUjM9DNOQH1fcvXq\r\n' + + 'IiN602mWrkd8jcEzLsW5IUNzVPLhrFIuKyBDTpLnC07Loce1\r\n' + + '=6XMW\r\n' + + '-----END PGP PUBLIC KEY BLOCK-----\r\n\r\n', + privkey = '-----BEGIN PGP PRIVATE KEY BLOCK-----\r\n' + + 'Version: OpenPGP.js v0.7.2\r\n' + + 'Comment: Whiteout Mail - https://whiteout.io\r\n' + + '\r\n' + + 'xcBeBFJYTLwBAf9jGbQlDgGL8ixYw6dzgTBp9xL/BcI88j2yBdCVMPi+8tl0\r\n' + + 'eUVRr2yvPLp1d3FP9bTmHTbFyOqUqf2bY8r+72wVABEBAAH+AwMIhNB4ivtv\r\n' + + 'Y2xg6VeMcjjHxZayESHACV+nQx5Tx6ev6xzIF1Qh72fNPDppLhFSFOuTTMsU\r\n' + + 'kTN4c+BVYt29spH+cA1jcDAxQ2ULrNAXo+hheOqhpedTs8aCbcLFkJAS16hk\r\n' + + 'YSk4OnJgp/z24rVju1SHRSFbgundPzmNgXeX9e8IkviGhhQ11Wc5YwVkx03t\r\n' + + 'Z3MdDMF0jyhopbPIoBdyJB0dhvBh98w3JmwpYh9wjUA9MBHD1tvHpRmSZ3BM\r\n' + + 'UCmATn2ZLWBRWiYqFbgDnL1GM80pV2hpdGVvdXQgVXNlciA8d2hpdGVvdXQu\r\n' + + 'dGVzdEB0LW9ubGluZS5kZT7CXAQQAQgAEAUCUlhMvQkQ9vYOm0LN/0wAAAW4\r\n' + + 'Af9C+kYW1AvNWmivdtr0M0iYCUjM9DNOQH1fcvXqIiN602mWrkd8jcEzLsW5\r\n' + + 'IUNzVPLhrFIuKyBDTpLnC07Loce1\r\n' + + '=ULta\r\n' + + '-----END PGP PRIVATE KEY BLOCK-----\r\n'; - beforeEach(function() { - pgp = new PGP(); + beforeEach(function() { + pgp = new PGP(); + }); + + afterEach(function() {}); + + describe('Generate key pair', function() { + it('should fail', function(done) { + pgp.generateKeys({ + emailAddress: 'whiteout.test@t-onlinede', + keySize: keySize, + passphrase: passphrase + }, function(err, keys) { + expect(err).to.exist; + expect(keys).to.not.exist; + done(); + }); }); - - afterEach(function() {}); - - describe('Generate key pair', function() { - it('should fail', function(done) { - pgp.generateKeys({ - emailAddress: 'whiteout.test@t-onlinede', - keySize: keySize, - passphrase: passphrase - }, function(err, keys) { - expect(err).to.exist; - expect(keys).to.not.exist; - done(); - }); + it('should fail', function(done) { + pgp.generateKeys({ + emailAddress: 'whiteout.testt-online.de', + keySize: keySize, + passphrase: passphrase + }, function(err, keys) { + expect(err).to.exist; + expect(keys).to.not.exist; + done(); }); - it('should fail', function(done) { - pgp.generateKeys({ - emailAddress: 'whiteout.testt-online.de', - keySize: keySize, - passphrase: passphrase - }, function(err, keys) { - expect(err).to.exist; - expect(keys).to.not.exist; - done(); - }); - }); - it('should work with passphrase', function(done) { - pgp.generateKeys({ - emailAddress: user, - keySize: keySize, - passphrase: passphrase - }, function(err, keys) { + }); + it('should work with passphrase', function(done) { + pgp.generateKeys({ + emailAddress: user, + keySize: keySize, + passphrase: passphrase + }, function(err, keys) { + expect(err).to.not.exist; + expect(keys.keyId).to.exist; + expect(keys.privateKeyArmored).to.exist; + expect(keys.publicKeyArmored).to.exist; + + // test encrypt/decrypt + pgp.importKeys({ + passphrase: passphrase, + privateKeyArmored: keys.privateKeyArmored, + publicKeyArmored: keys.publicKeyArmored + }, function(err) { expect(err).to.not.exist; - expect(keys.keyId).to.exist; - expect(keys.privateKeyArmored).to.exist; - expect(keys.publicKeyArmored).to.exist; - // test encrypt/decrypt - pgp.importKeys({ - passphrase: passphrase, - privateKeyArmored: keys.privateKeyArmored, - publicKeyArmored: keys.publicKeyArmored - }, function(err) { + pgp.encrypt('secret', [keys.publicKeyArmored], function(err, ct) { expect(err).to.not.exist; + expect(ct).to.exist; - pgp.encrypt('secret', [keys.publicKeyArmored], function(err, ct) { + pgp.decrypt(ct, keys.publicKeyArmored, function(err, pt, signValid) { expect(err).to.not.exist; - expect(ct).to.exist; - - pgp.decrypt(ct, keys.publicKeyArmored, function(err, pt, signValid) { - expect(err).to.not.exist; - expect(pt).to.equal('secret'); - expect(signValid).to.be.true; - done(); - }); - }); - }); - }); - }); - it('should work without passphrase', function(done) { - pgp.generateKeys({ - emailAddress: user, - keySize: keySize, - passphrase: '' - }, function(err, keys) { - expect(err).to.not.exist; - expect(keys.keyId).to.exist; - expect(keys.privateKeyArmored).to.exist; - expect(keys.publicKeyArmored).to.exist; - - // test encrypt/decrypt - pgp.importKeys({ - passphrase: undefined, - privateKeyArmored: keys.privateKeyArmored, - publicKeyArmored: keys.publicKeyArmored - }, function(err) { - expect(err).to.not.exist; - - pgp.encrypt('secret', [keys.publicKeyArmored], function(err, ct) { - expect(err).to.not.exist; - expect(ct).to.exist; - - pgp.decrypt(ct, keys.publicKeyArmored, function(err, pt, signValid) { - expect(err).to.not.exist; - expect(pt).to.equal('secret'); - expect(signValid).to.be.true; - done(); - }); + expect(pt).to.equal('secret'); + expect(signValid).to.be.true; + done(); }); }); }); }); }); + it('should work without passphrase', function(done) { + pgp.generateKeys({ + emailAddress: user, + keySize: keySize, + passphrase: '' + }, function(err, keys) { + expect(err).to.not.exist; + expect(keys.keyId).to.exist; + expect(keys.privateKeyArmored).to.exist; + expect(keys.publicKeyArmored).to.exist; - describe('Import/Export key pair', function() { - it('should fail', function(done) { + // test encrypt/decrypt pgp.importKeys({ - passphrase: 'asd', - privateKeyArmored: privkey, - publicKeyArmored: pubkey - }, function(err) { - expect(err).to.exist; - expect(err.message).to.equal('Incorrect passphrase!'); - - pgp.exportKeys(function(err, keys) { - expect(err).to.exist; - expect(keys).to.not.exist; - done(); - }); - }); - }); - it('should work', function(done) { - pgp.importKeys({ - passphrase: passphrase, - privateKeyArmored: privkey, - publicKeyArmored: pubkey + passphrase: undefined, + privateKeyArmored: keys.privateKeyArmored, + publicKeyArmored: keys.publicKeyArmored }, function(err) { expect(err).to.not.exist; - pgp.exportKeys(function(err, keys) { - expect(err).to.not.exist; - expect(keys.keyId).to.equal(keyId); - expect(keys.privateKeyArmored.replace(/\r/g, '')).to.equal(privkey.replace(/\r/g, '')); - expect(keys.publicKeyArmored.replace(/\r/g, '')).to.equal(pubkey.replace(/\r/g, '')); - done(); - }); - }); - }); - }); - - describe('Change passphrase of private key', function() { - it('should work with new passphrase', function(done) { - pgp.changePassphrase({ - privateKeyArmored: privkey, - oldPassphrase: passphrase, - newPassphrase: 'yxcv' - }, function(err, reEncryptedKey) { - expect(err).to.not.exist; - expect(reEncryptedKey).to.exist; - - pgp.importKeys({ - passphrase: 'yxcv', - privateKeyArmored: reEncryptedKey, - publicKeyArmored: pubkey - }, function(err) { - expect(err).to.not.exist; - done(); - }); - }); - }); - it('should work with empty passphrase', function(done) { - pgp.changePassphrase({ - privateKeyArmored: privkey, - oldPassphrase: passphrase, - newPassphrase: undefined - }, function(err, reEncryptedKey) { - expect(err).to.not.exist; - expect(reEncryptedKey).to.exist; - - pgp.importKeys({ - passphrase: undefined, - privateKeyArmored: reEncryptedKey, - publicKeyArmored: pubkey - }, function(err) { - expect(err).to.not.exist; - done(); - }); - }); - }); - it('should fail when passphrases are equal', function(done) { - pgp.changePassphrase({ - privateKeyArmored: privkey, - oldPassphrase: passphrase, - newPassphrase: passphrase - }, function(err, reEncryptedKey) { - expect(err).to.exist; - expect(reEncryptedKey).to.not.exist; - done(); - }); - }); - it('should fail when old passphrase is incorrect', function(done) { - pgp.changePassphrase({ - privateKeyArmored: privkey, - oldPassphrase: 'asd', - newPassphrase: 'yxcv' - }, function(err, reEncryptedKey) { - expect(err).to.exist; - expect(reEncryptedKey).to.not.exist; - done(); - }); - }); - }); - - describe('Encrypt/Sign/Decrypt/Verify', function() { - var message = 'asdfs\n\nThursday, Nov 21, 2013 7:38 PM asdf@example.com wrote:\n' + - '> asdf\n' + - '> \n' + - '> Thursday, Nov 21, 2013 7:32 PM asdf@example.com wrote:\n' + - '> > secret 3'; - var wrongPubkey = '-----BEGIN PGP PUBLIC KEY BLOCK-----\r\nVersion: OpenPGP.js v.1.20131116\r\nComment: Whiteout Mail - http://whiteout.io\r\n\r\nxsBNBFKODs4BB/9iOF4THsjQMY+WEpT7ShgKxj4bHzRRaQkqczS4nZvP0U3g\r\nqeqCnbpagyeKXA+bhWFQW4GmXtgAoeD5PXs6AZYrw3tWNxLKu2Oe6Tp9K/XI\r\nxTMQ2wl4qZKDXHvuPsJ7cmgaWqpPyXtxA4zHHS3WrkI/6VzHAcI/y6x4szSB\r\nKgSuhI3hjh3s7TybUC1U6AfoQGx/S7e3WwlCOrK8GTClirN/2mCPRC5wuIft\r\nnkoMfA6jK8d2OPrJ63shy5cgwHOjQg/xuk46dNS7tkvGmbaa+X0PgqSKB+Hf\r\nYPPNS/ylg911DH9qa8BqYU2QpNh9jUKXSF+HbaOM+plWkCSAL7czV+R3ABEB\r\nAAHNLVdoaXRlb3V0IFVzZXIgPHNhZmV3aXRobWUudGVzdHVzZXJAZ21haWwu\r\nY29tPsLAXAQQAQgAEAUCUo4O2gkQ1/uT/N+/wjwAAN2cB/9gFRmAfvEQ2qz+\r\nWubmT2EsSSnjPMxzG4uyykFoa+TaZCWo2Xa2tQghmU103kEkQb1OEjRjpgwJ\r\nYX9Kghnl8DByM686L5AXnRyHP78qRJCLXSXl0AGicboUDp5sovaa4rswQceH\r\nvcdWgZ/mgHTRoiQeJddy9k+H6MPFiyFaVcFwegVsmpc+dCcC8yT+qh8ZIbyG\r\nRJU60PmKKN7LUusP+8DbSv39zCGJCBlVVKyA4MzdF5uM+sqTdXbKzOrT5DGd\r\nCZaox4s+w16Sq1rHzZKFWfQPfKLDB9pyA0ufCVRA3AF6BUi7G3ZqhZiHNhMP\r\nNvE45V/hS1PbZcfPVoUjE2qc1Ix1\r\n=7Wpe\r\n-----END PGP PUBLIC KEY BLOCK-----'; - - beforeEach(function(done) { - pgp.importKeys({ - passphrase: passphrase, - privateKeyArmored: privkey, - publicKeyArmored: pubkey - }, function(err) { - expect(err).to.not.exist; - done(); - }); - }); - - describe('Get KeyId', function() { - it('should work without param', function() { - var keyId = pgp.getKeyId(); - expect(keyId).to.equal('F6F60E9B42CDFF4C'); - }); - it('should work with param', function() { - var keyId = pgp.getKeyId(pubkey); - expect(keyId).to.equal('F6F60E9B42CDFF4C'); - }); - }); - - describe('Get Fingerprint', function() { - it('should work without param', function() { - var fingerprint = pgp.getFingerprint(); - expect(fingerprint).to.equal('5856CEF789C3A307E8A1B976F6F60E9B42CDFF4C'); - }); - it('should work with param', function() { - var fingerprint = pgp.getFingerprint(pubkey); - expect(fingerprint).to.equal('5856CEF789C3A307E8A1B976F6F60E9B42CDFF4C'); - }); - }); - - describe('getKeyParams', function() { - it('should work with param', function() { - var params = pgp.getKeyParams(pubkey); - expect(params.fingerprint).to.equal('5856CEF789C3A307E8A1B976F6F60E9B42CDFF4C'); - expect(params._id).to.equal("F6F60E9B42CDFF4C"); - expect(params.bitSize).to.equal(keySize); - expect(params.userId).to.equal("whiteout.test@t-online.de"); - expect(params.userIds[0].name).to.equal("Whiteout User"); - expect(params.userIds[0].emailAddress).to.equal("whiteout.test@t-online.de"); - expect(params.algorithm).to.equal("rsa_encrypt_sign"); - }); - - it('should work without param', function() { - var params = pgp.getKeyParams(); - expect(params.fingerprint).to.equal('5856CEF789C3A307E8A1B976F6F60E9B42CDFF4C'); - expect(params._id).to.equal("F6F60E9B42CDFF4C"); - expect(params.bitSize).to.equal(keySize); - expect(params.userId).to.equal("whiteout.test@t-online.de"); - expect(params.userIds[0].name).to.equal("Whiteout User"); - expect(params.userIds[0].emailAddress).to.equal("whiteout.test@t-online.de"); - expect(params.algorithm).to.equal("rsa_encrypt_sign"); - }); - }); - - describe('extractPublicKey', function() { - it('should work', function() { - var pk = pgp.extractPublicKey(privkey); - expect(pk).to.exist; - expect(pk).to.contain('-----BEGIN PGP PUBLIC KEY BLOCK-----'); - }); - }); - - describe('Encrypt and sign', function() { - it('should fail', function(done) { - var input = null; - - pgp.encrypt(input, [pubkey], function(err, ct) { - expect(err).to.exist; - expect(ct).to.not.exist; - done(); - }); - }); - it('should work', function(done) { - pgp.encrypt(message, [pubkey], function(err, ct) { + pgp.encrypt('secret', [keys.publicKeyArmored], function(err, ct) { expect(err).to.not.exist; expect(ct).to.exist; - done(); - }); - }); - it('should encrypt to myself if public keys are empty', function(done) { - pgp.encrypt(message, undefined, function(err, ct) { - expect(err).to.not.exist; - expect(ct).to.exist; - done(); - }); - }); - }); - describe('Decrypt and verify', function() { - var ciphertext; - - beforeEach(function(done) { - pgp.encrypt(message, [pubkey], function(err, ct) { - expect(err).to.not.exist; - expect(ct).to.exist; - ciphertext = ct; - done(); - }); - }); - - it('should fail', function(done) { - var input = 'asdfa\rsdf'; - - pgp.decrypt(input, pubkey, function(err, pt) { - expect(err).to.exist; - expect(pt).to.not.exist; - done(); - }); - }); - it('should work', function(done) { - pgp.decrypt(ciphertext, pubkey, function(err, pt, signValid) { - expect(err).to.not.exist; - expect(pt).to.equal(message); - expect(signValid).to.be.true; - done(); - }); - }); - it('should work without signature', function(done) { - var ct = openpgp.encryptMessage([pgp._publicKey], message); - - pgp.decrypt(ct, undefined, function(err, pt, signValid) { - expect(err).to.not.exist; - expect(pt).to.equal(message); - expect(signValid).to.be.undefined; - done(); - }); - }); - it('should fail to verify if public keys are empty', function(done) { - // setup another public key so that signature verification fails - pgp._publicKey = openpgp.key.readArmored(wrongPubkey).keys[0]; - pgp.decrypt(ciphertext, undefined, function(err, pt, signValid) { - expect(err).to.not.exist; - expect(pt).to.equal(message); - expect(signValid).to.be.null; - done(); - }); - }); - it('should decrypt but signValid should be null for wrong public key', function(done) { - pgp.decrypt(ciphertext, wrongPubkey, function(err, pt, signValid) { - expect(err).to.not.exist; - expect(pt).to.equal(message); - expect(signValid).to.be.null; - done(); - }); - }); - }); - - describe('Verify clearsigned message', function() { - var clearsigned; - - beforeEach(function() { - clearsigned = openpgp.signClearMessage(pgp._privateKey, 'this is a clearsigned message'); - }); - - it('should work', function(done) { - pgp.verifyClearSignedMessage(clearsigned, pubkey, function(err, signaturesValid) { - expect(err).to.not.exist; - expect(signaturesValid).to.be.true; - done(); - }); - }); - - it('should fail', function(done) { - pgp.verifyClearSignedMessage(clearsigned.replace('clearsigned', 'invalid'), pubkey, function(err, signaturesValid) { - expect(err).to.not.exist; - expect(signaturesValid).to.be.false; - done(); - }); - }); - it('should be null for wrong public key', function(done) { - pgp.verifyClearSignedMessage(clearsigned, wrongPubkey, function(err, signaturesValid) { - expect(err).to.not.exist; - expect(signaturesValid).to.be.null; - done(); - }); - }); - }); - - describe('Verify detached signature', function() { - var signedMessage, signature; - - beforeEach(function() { - signedMessage = 'this is a signed message'; - var clearsigned = openpgp.signClearMessage(pgp._privateKey, signedMessage); - var signatureHeader = '-----BEGIN PGP SIGNATURE-----'; - signature = signatureHeader + clearsigned.split(signatureHeader).pop(); - }); - - it('should work', function(done) { - pgp.verifySignedMessage(signedMessage, signature, pubkey, function(err, signaturesValid) { - expect(err).to.not.exist; - expect(signaturesValid).to.be.true; - done(); - }); - }); - - it('should fail', function(done) { - pgp.verifySignedMessage(signedMessage.replace('signed', 'invalid'), signature, pubkey, function(err, signaturesValid) { - expect(err).to.not.exist; - expect(signaturesValid).to.be.false; - done(); - }); - }); - it('should be null for wrong public key', function(done) { - pgp.verifySignedMessage(signedMessage, signature, wrongPubkey, function(err, signaturesValid) { - expect(err).to.not.exist; - expect(signaturesValid).to.be.null; - done(); + pgp.decrypt(ct, keys.publicKeyArmored, function(err, pt, signValid) { + expect(err).to.not.exist; + expect(pt).to.equal('secret'); + expect(signValid).to.be.true; + done(); + }); }); }); }); }); }); + + describe('Import/Export key pair', function() { + it('should fail', function(done) { + pgp.importKeys({ + passphrase: 'asd', + privateKeyArmored: privkey, + publicKeyArmored: pubkey + }, function(err) { + expect(err).to.exist; + expect(err.message).to.equal('Incorrect passphrase!'); + + pgp.exportKeys(function(err, keys) { + expect(err).to.exist; + expect(keys).to.not.exist; + done(); + }); + }); + }); + it('should work', function(done) { + pgp.importKeys({ + passphrase: passphrase, + privateKeyArmored: privkey, + publicKeyArmored: pubkey + }, function(err) { + expect(err).to.not.exist; + + pgp.exportKeys(function(err, keys) { + expect(err).to.not.exist; + expect(keys.keyId).to.equal(keyId); + expect(keys.privateKeyArmored.replace(/\r/g, '')).to.equal(privkey.replace(/\r/g, '')); + expect(keys.publicKeyArmored.replace(/\r/g, '')).to.equal(pubkey.replace(/\r/g, '')); + done(); + }); + }); + }); + }); + + describe('Change passphrase of private key', function() { + it('should work with new passphrase', function(done) { + pgp.changePassphrase({ + privateKeyArmored: privkey, + oldPassphrase: passphrase, + newPassphrase: 'yxcv' + }, function(err, reEncryptedKey) { + expect(err).to.not.exist; + expect(reEncryptedKey).to.exist; + + pgp.importKeys({ + passphrase: 'yxcv', + privateKeyArmored: reEncryptedKey, + publicKeyArmored: pubkey + }, function(err) { + expect(err).to.not.exist; + done(); + }); + }); + }); + it('should work with empty passphrase', function(done) { + pgp.changePassphrase({ + privateKeyArmored: privkey, + oldPassphrase: passphrase, + newPassphrase: undefined + }, function(err, reEncryptedKey) { + expect(err).to.not.exist; + expect(reEncryptedKey).to.exist; + + pgp.importKeys({ + passphrase: undefined, + privateKeyArmored: reEncryptedKey, + publicKeyArmored: pubkey + }, function(err) { + expect(err).to.not.exist; + done(); + }); + }); + }); + it('should fail when passphrases are equal', function(done) { + pgp.changePassphrase({ + privateKeyArmored: privkey, + oldPassphrase: passphrase, + newPassphrase: passphrase + }, function(err, reEncryptedKey) { + expect(err).to.exist; + expect(reEncryptedKey).to.not.exist; + done(); + }); + }); + it('should fail when old passphrase is incorrect', function(done) { + pgp.changePassphrase({ + privateKeyArmored: privkey, + oldPassphrase: 'asd', + newPassphrase: 'yxcv' + }, function(err, reEncryptedKey) { + expect(err).to.exist; + expect(reEncryptedKey).to.not.exist; + done(); + }); + }); + }); + + describe('Encrypt/Sign/Decrypt/Verify', function() { + var message = 'asdfs\n\nThursday, Nov 21, 2013 7:38 PM asdf@example.com wrote:\n' + + '> asdf\n' + + '> \n' + + '> Thursday, Nov 21, 2013 7:32 PM asdf@example.com wrote:\n' + + '> > secret 3'; + var wrongPubkey = '-----BEGIN PGP PUBLIC KEY BLOCK-----\r\nVersion: OpenPGP.js v.1.20131116\r\nComment: Whiteout Mail - http://whiteout.io\r\n\r\nxsBNBFKODs4BB/9iOF4THsjQMY+WEpT7ShgKxj4bHzRRaQkqczS4nZvP0U3g\r\nqeqCnbpagyeKXA+bhWFQW4GmXtgAoeD5PXs6AZYrw3tWNxLKu2Oe6Tp9K/XI\r\nxTMQ2wl4qZKDXHvuPsJ7cmgaWqpPyXtxA4zHHS3WrkI/6VzHAcI/y6x4szSB\r\nKgSuhI3hjh3s7TybUC1U6AfoQGx/S7e3WwlCOrK8GTClirN/2mCPRC5wuIft\r\nnkoMfA6jK8d2OPrJ63shy5cgwHOjQg/xuk46dNS7tkvGmbaa+X0PgqSKB+Hf\r\nYPPNS/ylg911DH9qa8BqYU2QpNh9jUKXSF+HbaOM+plWkCSAL7czV+R3ABEB\r\nAAHNLVdoaXRlb3V0IFVzZXIgPHNhZmV3aXRobWUudGVzdHVzZXJAZ21haWwu\r\nY29tPsLAXAQQAQgAEAUCUo4O2gkQ1/uT/N+/wjwAAN2cB/9gFRmAfvEQ2qz+\r\nWubmT2EsSSnjPMxzG4uyykFoa+TaZCWo2Xa2tQghmU103kEkQb1OEjRjpgwJ\r\nYX9Kghnl8DByM686L5AXnRyHP78qRJCLXSXl0AGicboUDp5sovaa4rswQceH\r\nvcdWgZ/mgHTRoiQeJddy9k+H6MPFiyFaVcFwegVsmpc+dCcC8yT+qh8ZIbyG\r\nRJU60PmKKN7LUusP+8DbSv39zCGJCBlVVKyA4MzdF5uM+sqTdXbKzOrT5DGd\r\nCZaox4s+w16Sq1rHzZKFWfQPfKLDB9pyA0ufCVRA3AF6BUi7G3ZqhZiHNhMP\r\nNvE45V/hS1PbZcfPVoUjE2qc1Ix1\r\n=7Wpe\r\n-----END PGP PUBLIC KEY BLOCK-----'; + + beforeEach(function(done) { + pgp.importKeys({ + passphrase: passphrase, + privateKeyArmored: privkey, + publicKeyArmored: pubkey + }, function(err) { + expect(err).to.not.exist; + done(); + }); + }); + + describe('Get KeyId', function() { + it('should work without param', function() { + var keyId = pgp.getKeyId(); + expect(keyId).to.equal('F6F60E9B42CDFF4C'); + }); + it('should work with param', function() { + var keyId = pgp.getKeyId(pubkey); + expect(keyId).to.equal('F6F60E9B42CDFF4C'); + }); + }); + + describe('Get Fingerprint', function() { + it('should work without param', function() { + var fingerprint = pgp.getFingerprint(); + expect(fingerprint).to.equal('5856CEF789C3A307E8A1B976F6F60E9B42CDFF4C'); + }); + it('should work with param', function() { + var fingerprint = pgp.getFingerprint(pubkey); + expect(fingerprint).to.equal('5856CEF789C3A307E8A1B976F6F60E9B42CDFF4C'); + }); + }); + + describe('getKeyParams', function() { + it('should work with param', function() { + var params = pgp.getKeyParams(pubkey); + expect(params.fingerprint).to.equal('5856CEF789C3A307E8A1B976F6F60E9B42CDFF4C'); + expect(params._id).to.equal("F6F60E9B42CDFF4C"); + expect(params.bitSize).to.equal(keySize); + expect(params.userId).to.equal("whiteout.test@t-online.de"); + expect(params.userIds[0].name).to.equal("Whiteout User"); + expect(params.userIds[0].emailAddress).to.equal("whiteout.test@t-online.de"); + expect(params.algorithm).to.equal("rsa_encrypt_sign"); + }); + + it('should work without param', function() { + var params = pgp.getKeyParams(); + expect(params.fingerprint).to.equal('5856CEF789C3A307E8A1B976F6F60E9B42CDFF4C'); + expect(params._id).to.equal("F6F60E9B42CDFF4C"); + expect(params.bitSize).to.equal(keySize); + expect(params.userId).to.equal("whiteout.test@t-online.de"); + expect(params.userIds[0].name).to.equal("Whiteout User"); + expect(params.userIds[0].emailAddress).to.equal("whiteout.test@t-online.de"); + expect(params.algorithm).to.equal("rsa_encrypt_sign"); + }); + }); + + describe('extractPublicKey', function() { + it('should work', function() { + var pk = pgp.extractPublicKey(privkey); + expect(pk).to.exist; + expect(pk).to.contain('-----BEGIN PGP PUBLIC KEY BLOCK-----'); + }); + }); + + describe('Encrypt and sign', function() { + it('should fail', function(done) { + var input = null; + + pgp.encrypt(input, [pubkey], function(err, ct) { + expect(err).to.exist; + expect(ct).to.not.exist; + done(); + }); + }); + it('should work', function(done) { + pgp.encrypt(message, [pubkey], function(err, ct) { + expect(err).to.not.exist; + expect(ct).to.exist; + done(); + }); + }); + it('should encrypt to myself if public keys are empty', function(done) { + pgp.encrypt(message, undefined, function(err, ct) { + expect(err).to.not.exist; + expect(ct).to.exist; + done(); + }); + }); + }); + + describe('Decrypt and verify', function() { + var ciphertext; + + beforeEach(function(done) { + pgp.encrypt(message, [pubkey], function(err, ct) { + expect(err).to.not.exist; + expect(ct).to.exist; + ciphertext = ct; + done(); + }); + }); + + it('should fail', function(done) { + var input = 'asdfa\rsdf'; + + pgp.decrypt(input, pubkey, function(err, pt) { + expect(err).to.exist; + expect(pt).to.not.exist; + done(); + }); + }); + it('should work', function(done) { + pgp.decrypt(ciphertext, pubkey, function(err, pt, signValid) { + expect(err).to.not.exist; + expect(pt).to.equal(message); + expect(signValid).to.be.true; + done(); + }); + }); + it('should work without signature', function(done) { + var ct = openpgp.encryptMessage([pgp._publicKey], message); + + pgp.decrypt(ct, undefined, function(err, pt, signValid) { + expect(err).to.not.exist; + expect(pt).to.equal(message); + expect(signValid).to.be.undefined; + done(); + }); + }); + it('should fail to verify if public keys are empty', function(done) { + // setup another public key so that signature verification fails + pgp._publicKey = openpgp.key.readArmored(wrongPubkey).keys[0]; + pgp.decrypt(ciphertext, undefined, function(err, pt, signValid) { + expect(err).to.not.exist; + expect(pt).to.equal(message); + expect(signValid).to.be.null; + done(); + }); + }); + it('should decrypt but signValid should be null for wrong public key', function(done) { + pgp.decrypt(ciphertext, wrongPubkey, function(err, pt, signValid) { + expect(err).to.not.exist; + expect(pt).to.equal(message); + expect(signValid).to.be.null; + done(); + }); + }); + }); + + describe('Verify clearsigned message', function() { + var clearsigned; + + beforeEach(function() { + clearsigned = openpgp.signClearMessage(pgp._privateKey, 'this is a clearsigned message'); + }); + + it('should work', function(done) { + pgp.verifyClearSignedMessage(clearsigned, pubkey, function(err, signaturesValid) { + expect(err).to.not.exist; + expect(signaturesValid).to.be.true; + done(); + }); + }); + + it('should fail', function(done) { + pgp.verifyClearSignedMessage(clearsigned.replace('clearsigned', 'invalid'), pubkey, function(err, signaturesValid) { + expect(err).to.not.exist; + expect(signaturesValid).to.be.false; + done(); + }); + }); + it('should be null for wrong public key', function(done) { + pgp.verifyClearSignedMessage(clearsigned, wrongPubkey, function(err, signaturesValid) { + expect(err).to.not.exist; + expect(signaturesValid).to.be.null; + done(); + }); + }); + }); + + describe('Verify detached signature', function() { + var signedMessage, signature; + + beforeEach(function() { + signedMessage = 'this is a signed message'; + var clearsigned = openpgp.signClearMessage(pgp._privateKey, signedMessage); + var signatureHeader = '-----BEGIN PGP SIGNATURE-----'; + signature = signatureHeader + clearsigned.split(signatureHeader).pop(); + }); + + it('should work', function(done) { + pgp.verifySignedMessage(signedMessage, signature, pubkey, function(err, signaturesValid) { + expect(err).to.not.exist; + expect(signaturesValid).to.be.true; + done(); + }); + }); + + it('should fail', function(done) { + pgp.verifySignedMessage(signedMessage.replace('signed', 'invalid'), signature, pubkey, function(err, signaturesValid) { + expect(err).to.not.exist; + expect(signaturesValid).to.be.false; + done(); + }); + }); + it('should be null for wrong public key', function(done) { + pgp.verifySignedMessage(signedMessage, signature, wrongPubkey, function(err, signaturesValid) { + expect(err).to.not.exist; + expect(signaturesValid).to.be.null; + done(); + }); + }); + }); + }); }); \ No newline at end of file diff --git a/test/unit/privatekey-dao-test.js b/test/unit/privatekey-dao-test.js index 7092cfa..63bcd3c 100644 --- a/test/unit/privatekey-dao-test.js +++ b/test/unit/privatekey-dao-test.js @@ -1,226 +1,222 @@ -define(function(require) { - 'use strict'; +'use strict'; - var RestDAO = require('js/dao/rest-dao'), - PrivateKeyDAO = require('js/dao/privatekey-dao'), - expect = chai.expect; +var RestDAO = require('../../src/js/dao/rest-dao'), + PrivateKeyDAO = require('../../src/js/dao/privatekey-dao'); - describe('Private Key DAO unit tests', function() { +describe('Private Key DAO unit tests', function() { - var privkeyDao, restDaoStub, - emailAddress = 'test@example.com', - deviceName = 'iPhone Work'; + var privkeyDao, restDaoStub, + emailAddress = 'test@example.com', + deviceName = 'iPhone Work'; - beforeEach(function() { - restDaoStub = sinon.createStubInstance(RestDAO); - privkeyDao = new PrivateKeyDAO(restDaoStub); - }); + beforeEach(function() { + restDaoStub = sinon.createStubInstance(RestDAO); + privkeyDao = new PrivateKeyDAO(restDaoStub); + }); - afterEach(function() {}); + afterEach(function() {}); - describe('requestDeviceRegistration', function() { - it('should fail due to invalid args', function(done) { - privkeyDao.requestDeviceRegistration({}, function(err, sessionKey) { - expect(err).to.exist; - expect(sessionKey).to.not.exist; - done(); - }); - }); - - it('should work', function(done) { - restDaoStub.post.yields(null, { - encryptedRegSessionKey: 'asdf' - }); - - privkeyDao.requestDeviceRegistration({ - userId: emailAddress, - deviceName: deviceName - }, function(err, sessionKey) { - expect(err).to.not.exist; - expect(sessionKey).to.exist; - done(); - }); + describe('requestDeviceRegistration', function() { + it('should fail due to invalid args', function(done) { + privkeyDao.requestDeviceRegistration({}, function(err, sessionKey) { + expect(err).to.exist; + expect(sessionKey).to.not.exist; + done(); }); }); - describe('uploadDeviceSecret', function() { - it('should fail due to invalid args', function(done) { - privkeyDao.uploadDeviceSecret({}, function(err) { - expect(err).to.exist; - done(); - }); + it('should work', function(done) { + restDaoStub.post.yields(null, { + encryptedRegSessionKey: 'asdf' }); - it('should work', function(done) { - restDaoStub.put.yields(); + privkeyDao.requestDeviceRegistration({ + userId: emailAddress, + deviceName: deviceName + }, function(err, sessionKey) { + expect(err).to.not.exist; + expect(sessionKey).to.exist; + done(); + }); + }); + }); - privkeyDao.uploadDeviceSecret({ - userId: emailAddress, - deviceName: deviceName, - encryptedDeviceSecret: 'asdf', - iv: 'iv' - }, function(err) { - expect(err).to.not.exist; - done(); - }); + describe('uploadDeviceSecret', function() { + it('should fail due to invalid args', function(done) { + privkeyDao.uploadDeviceSecret({}, function(err) { + expect(err).to.exist; + done(); }); }); - describe('requestAuthSessionKey', function() { - it('should fail due to invalid args', function(done) { - privkeyDao.requestAuthSessionKey({}, function(err) { - expect(err).to.exist; - done(); - }); + it('should work', function(done) { + restDaoStub.put.yields(); + + privkeyDao.uploadDeviceSecret({ + userId: emailAddress, + deviceName: deviceName, + encryptedDeviceSecret: 'asdf', + iv: 'iv' + }, function(err) { + expect(err).to.not.exist; + done(); }); + }); + }); - it('should work', function(done) { - restDaoStub.post.withArgs(undefined, '/auth/user/' + emailAddress).yields(); - - privkeyDao.requestAuthSessionKey({ - userId: emailAddress - }, function(err) { - expect(err).to.not.exist; - done(); - }); + describe('requestAuthSessionKey', function() { + it('should fail due to invalid args', function(done) { + privkeyDao.requestAuthSessionKey({}, function(err) { + expect(err).to.exist; + done(); }); }); - describe('verifyAuthentication', function() { - it('should fail due to invalid args', function(done) { - privkeyDao.verifyAuthentication({}, function(err) { - expect(err).to.exist; - done(); - }); + it('should work', function(done) { + restDaoStub.post.withArgs(undefined, '/auth/user/' + emailAddress).yields(); + + privkeyDao.requestAuthSessionKey({ + userId: emailAddress + }, function(err) { + expect(err).to.not.exist; + done(); }); + }); + }); - it('should work', function(done) { - var sessionId = '1'; - - var options = { - userId: emailAddress, - sessionId: sessionId, - encryptedChallenge: 'asdf', - encryptedDeviceSecret: 'qwer', - iv: ' iv' - }; - - restDaoStub.put.withArgs(options, '/auth/user/' + emailAddress + '/session/' + sessionId).yields(); - - privkeyDao.verifyAuthentication(options, function(err) { - expect(err).to.not.exist; - done(); - }); + describe('verifyAuthentication', function() { + it('should fail due to invalid args', function(done) { + privkeyDao.verifyAuthentication({}, function(err) { + expect(err).to.exist; + done(); }); }); - describe('upload', function() { - it('should fail due to invalid args', function(done) { - privkeyDao.upload({}, function(err) { - expect(err).to.exist; - done(); - }); + it('should work', function(done) { + var sessionId = '1'; + + var options = { + userId: emailAddress, + sessionId: sessionId, + encryptedChallenge: 'asdf', + encryptedDeviceSecret: 'qwer', + iv: ' iv' + }; + + restDaoStub.put.withArgs(options, '/auth/user/' + emailAddress + '/session/' + sessionId).yields(); + + privkeyDao.verifyAuthentication(options, function(err) { + expect(err).to.not.exist; + done(); }); + }); + }); - it('should work', function(done) { - var options = { - _id: '12345', - userId: emailAddress, - encryptedPrivateKey: 'asdf', - sessionId: '1', - salt: 'salt', - iv: 'iv' - }; - - restDaoStub.post.withArgs(options, '/privatekey/user/' + emailAddress + '/session/' + options.sessionId).yields(); - - privkeyDao.upload(options, function(err) { - expect(err).to.not.exist; - done(); - }); + describe('upload', function() { + it('should fail due to invalid args', function(done) { + privkeyDao.upload({}, function(err) { + expect(err).to.exist; + done(); }); }); - describe('requestDownload', function() { - it('should fail due to invalid args', function(done) { - privkeyDao.requestDownload({}, function(err) { - expect(err).to.exist; - done(); - }); + it('should work', function(done) { + var options = { + _id: '12345', + userId: emailAddress, + encryptedPrivateKey: 'asdf', + sessionId: '1', + salt: 'salt', + iv: 'iv' + }; + + restDaoStub.post.withArgs(options, '/privatekey/user/' + emailAddress + '/session/' + options.sessionId).yields(); + + privkeyDao.upload(options, function(err) { + expect(err).to.not.exist; + done(); }); + }); + }); - it('should work', function(done) { - var keyId = '12345'; - - restDaoStub.get.withArgs({ - uri: '/privatekey/user/' + emailAddress + '/key/' + keyId - }).yields(); - - privkeyDao.requestDownload({ - userId: emailAddress, - keyId: keyId - }, function(err, found) { - expect(err).to.not.exist; - expect(found).to.be.true; - done(); - }); + describe('requestDownload', function() { + it('should fail due to invalid args', function(done) { + privkeyDao.requestDownload({}, function(err) { + expect(err).to.exist; + done(); }); }); - describe('hasPrivateKey', function() { - it('should fail due to invalid args', function(done) { - privkeyDao.hasPrivateKey({}, function(err) { - expect(err).to.exist; - done(); - }); + it('should work', function(done) { + var keyId = '12345'; + + restDaoStub.get.withArgs({ + uri: '/privatekey/user/' + emailAddress + '/key/' + keyId + }).yields(); + + privkeyDao.requestDownload({ + userId: emailAddress, + keyId: keyId + }, function(err, found) { + expect(err).to.not.exist; + expect(found).to.be.true; + done(); }); + }); + }); - it('should work', function(done) { - var keyId = '12345'; - - restDaoStub.get.withArgs({ - uri: '/privatekey/user/' + emailAddress + '/key/' + keyId + '?ignoreRecovery=true' - }).yields(); - - privkeyDao.hasPrivateKey({ - userId: emailAddress, - keyId: keyId - }, function(err, found) { - expect(err).to.not.exist; - expect(found).to.be.true; - done(); - }); + describe('hasPrivateKey', function() { + it('should fail due to invalid args', function(done) { + privkeyDao.hasPrivateKey({}, function(err) { + expect(err).to.exist; + done(); }); }); - describe('download', function() { - it('should fail due to invalid args', function(done) { - privkeyDao.download({}, function(err) { - expect(err).to.exist; - done(); - }); + it('should work', function(done) { + var keyId = '12345'; + + restDaoStub.get.withArgs({ + uri: '/privatekey/user/' + emailAddress + '/key/' + keyId + '?ignoreRecovery=true' + }).yields(); + + privkeyDao.hasPrivateKey({ + userId: emailAddress, + keyId: keyId + }, function(err, found) { + expect(err).to.not.exist; + expect(found).to.be.true; + done(); }); + }); + }); - it('should work', function(done) { - var key = { - _id: '12345' - }; - - restDaoStub.get.withArgs({ - uri: '/privatekey/user/' + emailAddress + '/key/' + key._id + '/recovery/token' - }).yields(); - - privkeyDao.download({ - userId: emailAddress, - keyId: key._id, - recoveryToken: 'token' - }, function(err) { - expect(err).to.not.exist; - done(); - }); + describe('download', function() { + it('should fail due to invalid args', function(done) { + privkeyDao.download({}, function(err) { + expect(err).to.exist; + done(); }); }); + it('should work', function(done) { + var key = { + _id: '12345' + }; + + restDaoStub.get.withArgs({ + uri: '/privatekey/user/' + emailAddress + '/key/' + key._id + '/recovery/token' + }).yields(); + + privkeyDao.download({ + userId: emailAddress, + keyId: key._id, + recoveryToken: 'token' + }, function(err) { + expect(err).to.not.exist; + done(); + }); + }); }); }); \ No newline at end of file diff --git a/test/unit/privatekey-upload-ctrl-test.js b/test/unit/privatekey-upload-ctrl-test.js index 9dbe3e3..b7649a2 100644 --- a/test/unit/privatekey-upload-ctrl-test.js +++ b/test/unit/privatekey-upload-ctrl-test.js @@ -1,286 +1,281 @@ -define(function(require) { - 'use strict'; +'use strict'; - var expect = chai.expect, - angular = require('angular'), - mocks = require('angularMocks'), - PrivateKeyUploadCtrl = require('js/controller/privatekey-upload'), - appController = require('js/app-controller'), - KeychainDAO = require('js/dao/keychain-dao'), - PGP = require('js/crypto/pgp'); +var mocks = angular.mocks, + PrivateKeyUploadCtrl = require('../../src/js/controller/privatekey-upload'), + appController = require('../../src/js/app-controller'), + KeychainDAO = require('../../src/js/dao/keychain-dao'), + PGP = require('../../src/js/crypto/pgp'); - describe('Private Key Upload Controller unit test', function() { - var scope, location, ctrl, - origEmailDao, emailDaoMock, - origKeychain, keychainMock, - pgpStub, - emailAddress = 'fred@foo.com'; +describe('Private Key Upload Controller unit test', function() { + var scope, location, ctrl, + origEmailDao, emailDaoMock, + origKeychain, keychainMock, + pgpStub, + emailAddress = 'fred@foo.com'; - beforeEach(function(done) { - // remember original module to restore later, then replace it - origEmailDao = appController._emailDao; - appController._emailDao = emailDaoMock = { - _account: { - emailAddress: emailAddress - } + beforeEach(function(done) { + // remember original module to restore later, then replace it + origEmailDao = appController._emailDao; + appController._emailDao = emailDaoMock = { + _account: { + emailAddress: emailAddress + } + }; + origKeychain = appController._keychain; + appController._keychain = keychainMock = sinon.createStubInstance(KeychainDAO); + keychainMock._pgp = pgpStub = sinon.createStubInstance(PGP); + + angular.module('login-privatekey-download-test', []); + mocks.module('login-privatekey-download-test'); + mocks.inject(function($controller, $rootScope) { + scope = $rootScope.$new(); + scope.state = {}; + ctrl = $controller(PrivateKeyUploadCtrl, { + $location: location, + $scope: scope + }); + done(); + }); + }); + + afterEach(function() { + // restore the app controller module + appController._keychain = origKeychain; + appController._emailDao = origEmailDao; + }); + + describe('checkServerForKey', function() { + var keyParams = { + userId: emailAddress, + _id: 'keyId', + }; + + it('should fail', function(done) { + pgpStub.getKeyParams.returns(keyParams); + keychainMock.hasPrivateKey.yields(42); + + scope.onError = function(err) { + expect(err).to.exist; + expect(keychainMock.hasPrivateKey.calledOnce).to.be.true; + done(); }; - origKeychain = appController._keychain; - appController._keychain = keychainMock = sinon.createStubInstance(KeychainDAO); - keychainMock._pgp = pgpStub = sinon.createStubInstance(PGP); - angular.module('login-privatekey-download-test', []); - mocks.module('login-privatekey-download-test'); - mocks.inject(function($controller, $rootScope) { - scope = $rootScope.$new(); - scope.state = {}; - ctrl = $controller(PrivateKeyUploadCtrl, { - $location: location, - $scope: scope - }); + scope.checkServerForKey(); + }); + + it('should return true', function(done) { + pgpStub.getKeyParams.returns(keyParams); + keychainMock.hasPrivateKey.withArgs({ + userId: keyParams.userId, + keyId: keyParams._id + }).yields(null, true); + + scope.checkServerForKey(function(privateKeySynced) { + expect(privateKeySynced).to.be.true; done(); }); }); - afterEach(function() { - // restore the app controller module - appController._keychain = origKeychain; - appController._emailDao = origEmailDao; + it('should return undefined', function(done) { + pgpStub.getKeyParams.returns(keyParams); + keychainMock.hasPrivateKey.withArgs({ + userId: keyParams.userId, + keyId: keyParams._id + }).yields(null, false); + + scope.checkServerForKey(function(privateKeySynced) { + expect(privateKeySynced).to.be.undefined; + done(); + }); + }); + }); + + describe('handlePaste', function() { + it('should work', function() { + scope.handlePaste({ + clipboardData: { + getData: function(val) { + expect(val).to.equal('text/plain'); + return '1qaz-2wsx-3edc-4rfv-5tgb-6yhn'; + } + } + }); + + expect(scope.code0).to.equal('1qaz'); + expect(scope.code1).to.equal('2wsx'); + expect(scope.code2).to.equal('3edc'); + expect(scope.code3).to.equal('4rfv'); + expect(scope.code4).to.equal('5tgb'); + expect(scope.code5).to.equal('6yhn'); + }); + }); + + describe('displayUploadUi', function() { + it('should work', function() { + // add some artifacts from a previous key input + scope.code0 = scope.code1 = scope.code2 = scope.code3 = scope.code4 = scope.code5 = 'asdasd'; + + scope.displayUploadUi(); + expect(scope.step).to.equal(1); + expect(scope.code.length).to.equal(24); + + // artifacts should be cleared + expect(scope.code0).to.be.empty; + expect(scope.code1).to.be.empty; + expect(scope.code2).to.be.empty; + expect(scope.code3).to.be.empty; + expect(scope.code4).to.be.empty; + expect(scope.code5).to.be.empty; + }); + }); + + describe('verifyCode', function() { + it('should fail for wrong code', function() { + scope.code0 = 'b'; + scope.code1 = 'b'; + scope.code2 = 'b'; + scope.code3 = 'b'; + scope.code4 = 'b'; + scope.code5 = 'b'; + scope.code = 'aaaaaa'; + + scope.onError = function() {}; + expect(scope.verifyCode()).to.be.false; }); - describe('checkServerForKey', function() { - var keyParams = { - userId: emailAddress, - _id: 'keyId', + it('should work', function() { + scope.code0 = 'a'; + scope.code1 = 'a'; + scope.code2 = 'a'; + scope.code3 = 'a'; + scope.code4 = 'a'; + scope.code5 = 'a'; + scope.code = 'aaaaaa'; + + scope.onError = function() {}; + expect(scope.verifyCode()).to.be.false; + }); + }); + + describe('setDeviceName', function() { + it('should work', function(done) { + keychainMock.setDeviceName.yields(); + scope.setDeviceName(done); + }); + }); + + describe('encryptAndUploadKey', function() { + it('should fail due to keychain.registerDevice', function(done) { + keychainMock.registerDevice.yields(42); + + scope.onError = function(err) { + expect(err).to.exist; + expect(keychainMock.registerDevice.calledOnce).to.be.true; + done(); }; - it('should fail', function(done) { - pgpStub.getKeyParams.returns(keyParams); - keychainMock.hasPrivateKey.yields(42); - - scope.onError = function(err) { - expect(err).to.exist; - expect(keychainMock.hasPrivateKey.calledOnce).to.be.true; - done(); - }; - - scope.checkServerForKey(); - }); - - it('should return true', function(done) { - pgpStub.getKeyParams.returns(keyParams); - keychainMock.hasPrivateKey.withArgs({ - userId: keyParams.userId, - keyId: keyParams._id - }).yields(null, true); - - scope.checkServerForKey(function(privateKeySynced) { - expect(privateKeySynced).to.be.true; - done(); - }); - }); - - it('should return undefined', function(done) { - pgpStub.getKeyParams.returns(keyParams); - keychainMock.hasPrivateKey.withArgs({ - userId: keyParams.userId, - keyId: keyParams._id - }).yields(null, false); - - scope.checkServerForKey(function(privateKeySynced) { - expect(privateKeySynced).to.be.undefined; - done(); - }); - }); + scope.encryptAndUploadKey(); }); - describe('handlePaste', function() { - it('should work', function() { - scope.handlePaste({ - clipboardData: { - getData: function(val) { - expect(val).to.equal('text/plain'); - return '1qaz-2wsx-3edc-4rfv-5tgb-6yhn'; - } - } - }); + it('should work', function(done) { + keychainMock.registerDevice.yields(); + keychainMock.uploadPrivateKey.yields(); - expect(scope.code0).to.equal('1qaz'); - expect(scope.code1).to.equal('2wsx'); - expect(scope.code2).to.equal('3edc'); - expect(scope.code3).to.equal('4rfv'); - expect(scope.code4).to.equal('5tgb'); - expect(scope.code5).to.equal('6yhn'); + scope.encryptAndUploadKey(function(err) { + expect(err).to.not.exist; + expect(keychainMock.registerDevice.calledOnce).to.be.true; + expect(keychainMock.uploadPrivateKey.calledOnce).to.be.true; + done(); }); }); + }); - describe('displayUploadUi', function() { - it('should work', function() { - // add some artifacts from a previous key input - scope.code0 = scope.code1 = scope.code2 = scope.code3 = scope.code4 = scope.code5 = 'asdasd'; - - scope.displayUploadUi(); - expect(scope.step).to.equal(1); - expect(scope.code.length).to.equal(24); - - // artifacts should be cleared - expect(scope.code0).to.be.empty; - expect(scope.code1).to.be.empty; - expect(scope.code2).to.be.empty; - expect(scope.code3).to.be.empty; - expect(scope.code4).to.be.empty; - expect(scope.code5).to.be.empty; - }); + describe('goBack', function() { + it('should work', function() { + scope.step = 2; + scope.goBack(); + expect(scope.step).to.equal(1); }); - describe('verifyCode', function() { - it('should fail for wrong code', function() { - scope.code0 = 'b'; - scope.code1 = 'b'; - scope.code2 = 'b'; - scope.code3 = 'b'; - scope.code4 = 'b'; - scope.code5 = 'b'; - scope.code = 'aaaaaa'; + it('should not work for < 2', function() { + scope.step = 1; + scope.goBack(); + expect(scope.step).to.equal(1); + }); + }); - scope.onError = function() {}; - expect(scope.verifyCode()).to.be.false; - }); - - it('should work', function() { - scope.code0 = 'a'; - scope.code1 = 'a'; - scope.code2 = 'a'; - scope.code3 = 'a'; - scope.code4 = 'a'; - scope.code5 = 'a'; - scope.code = 'aaaaaa'; - - scope.onError = function() {}; - expect(scope.verifyCode()).to.be.false; - }); + describe('goForward', function() { + var verifyCodeStub, setDeviceNameStub, encryptAndUploadKeyStub; + beforeEach(function() { + verifyCodeStub = sinon.stub(scope, 'verifyCode'); + setDeviceNameStub = sinon.stub(scope, 'setDeviceName'); + encryptAndUploadKeyStub = sinon.stub(scope, 'encryptAndUploadKey'); + }); + afterEach(function() { + verifyCodeStub.restore(); + setDeviceNameStub.restore(); + encryptAndUploadKeyStub.restore(); }); - describe('setDeviceName', function() { - it('should work', function(done) { - keychainMock.setDeviceName.yields(); - scope.setDeviceName(done); - }); + it('should work for < 2', function() { + scope.step = 1; + scope.goForward(); + expect(scope.step).to.equal(2); }); - describe('encryptAndUploadKey', function() { - it('should fail due to keychain.registerDevice', function(done) { - keychainMock.registerDevice.yields(42); - - scope.onError = function(err) { - expect(err).to.exist; - expect(keychainMock.registerDevice.calledOnce).to.be.true; - done(); - }; - - scope.encryptAndUploadKey(); - }); - - it('should work', function(done) { - keychainMock.registerDevice.yields(); - keychainMock.uploadPrivateKey.yields(); - - scope.encryptAndUploadKey(function(err) { - expect(err).to.not.exist; - expect(keychainMock.registerDevice.calledOnce).to.be.true; - expect(keychainMock.uploadPrivateKey.calledOnce).to.be.true; - done(); - }); - }); + it('should work for 2', function() { + verifyCodeStub.returns(true); + scope.step = 2; + scope.goForward(); + expect(scope.step).to.equal(3); }); - describe('goBack', function() { - it('should work', function() { - scope.step = 2; - scope.goBack(); - expect(scope.step).to.equal(1); - }); - - it('should not work for < 2', function() { - scope.step = 1; - scope.goBack(); - expect(scope.step).to.equal(1); - }); + it('should not work for 2 when code invalid', function() { + verifyCodeStub.returns(false); + scope.step = 2; + scope.goForward(); + expect(scope.step).to.equal(2); }); - describe('goForward', function() { - var verifyCodeStub, setDeviceNameStub, encryptAndUploadKeyStub; - beforeEach(function() { - verifyCodeStub = sinon.stub(scope, 'verifyCode'); - setDeviceNameStub = sinon.stub(scope, 'setDeviceName'); - encryptAndUploadKeyStub = sinon.stub(scope, 'encryptAndUploadKey'); - }); - afterEach(function() { - verifyCodeStub.restore(); - setDeviceNameStub.restore(); - encryptAndUploadKeyStub.restore(); - }); + it('should fail for 3 due to error in setDeviceName', function(done) { + scope.step = 3; + setDeviceNameStub.yields(42); - it('should work for < 2', function() { - scope.step = 1; - scope.goForward(); - expect(scope.step).to.equal(2); - }); - - it('should work for 2', function() { - verifyCodeStub.returns(true); - scope.step = 2; - scope.goForward(); + scope.onError = function(err) { + expect(err).to.exist; expect(scope.step).to.equal(3); - }); + done(); + }; - it('should not work for 2 when code invalid', function() { - verifyCodeStub.returns(false); - scope.step = 2; - scope.goForward(); - expect(scope.step).to.equal(2); - }); - - it('should fail for 3 due to error in setDeviceName', function(done) { - scope.step = 3; - setDeviceNameStub.yields(42); - - scope.onError = function(err) { - expect(err).to.exist; - expect(scope.step).to.equal(3); - done(); - }; - - scope.goForward(); - }); - - it('should fail for 3 due to error in encryptAndUploadKey', function(done) { - scope.step = 3; - setDeviceNameStub.yields(); - encryptAndUploadKeyStub.yields(42); - - scope.onError = function(err) { - expect(err).to.exist; - expect(scope.step).to.equal(4); - done(); - }; - - scope.goForward(); - }); - - it('should work for 3', function(done) { - scope.step = 3; - setDeviceNameStub.yields(); - encryptAndUploadKeyStub.yields(); - - scope.onError = function(err) { - expect(err.title).to.equal('Success'); - expect(scope.step).to.equal(4); - done(); - }; - - scope.goForward(); - }); + scope.goForward(); }); + it('should fail for 3 due to error in encryptAndUploadKey', function(done) { + scope.step = 3; + setDeviceNameStub.yields(); + encryptAndUploadKeyStub.yields(42); + + scope.onError = function(err) { + expect(err).to.exist; + expect(scope.step).to.equal(4); + done(); + }; + + scope.goForward(); + }); + + it('should work for 3', function(done) { + scope.step = 3; + setDeviceNameStub.yields(); + encryptAndUploadKeyStub.yields(); + + scope.onError = function(err) { + expect(err.title).to.equal('Success'); + expect(scope.step).to.equal(4); + done(); + }; + + scope.goForward(); + }); }); }); \ No newline at end of file diff --git a/test/unit/publickey-dao-test.js b/test/unit/publickey-dao-test.js index 8e9930f..724fc62 100644 --- a/test/unit/publickey-dao-test.js +++ b/test/unit/publickey-dao-test.js @@ -1,165 +1,161 @@ -define(function(require) { - 'use strict'; +'use strict'; - var RestDAO = require('js/dao/rest-dao'), - PublicKeyDAO = require('js/dao/publickey-dao'), - expect = chai.expect; +var RestDAO = require('../../src/js/dao/rest-dao'), + PublicKeyDAO = require('../../src/js/dao/publickey-dao'); - describe('Public Key DAO unit tests', function() { +describe('Public Key DAO unit tests', function() { - var pubkeyDao, restDaoStub; + var pubkeyDao, restDaoStub; - beforeEach(function() { - restDaoStub = sinon.createStubInstance(RestDAO); - pubkeyDao = new PublicKeyDAO(restDaoStub); - }); + beforeEach(function() { + restDaoStub = sinon.createStubInstance(RestDAO); + pubkeyDao = new PublicKeyDAO(restDaoStub); + }); - afterEach(function() {}); + afterEach(function() {}); - describe('get', function() { - it('should fail', function(done) { - restDaoStub.get.yields(42); + describe('get', function() { + it('should fail', function(done) { + restDaoStub.get.yields(42); - pubkeyDao.get('id', function(err, key) { - expect(err).to.exist; - expect(key).to.not.exist; - expect(restDaoStub.get.calledOnce).to.be.true; - done(); - }); - }); - - it('should work', function(done) { - restDaoStub.get.yields(null, { - _id: '12345', - publicKey: 'asdf' - }); - - pubkeyDao.get('id', function(err, key) { - expect(err).to.not.exist; - expect(key).to.exist; - expect(key._id).to.exist; - expect(key.publicKey).to.exist; - expect(restDaoStub.get.calledOnce).to.be.true; - done(); - }); + pubkeyDao.get('id', function(err, key) { + expect(err).to.exist; + expect(key).to.not.exist; + expect(restDaoStub.get.calledOnce).to.be.true; + done(); }); }); - describe('verify', function() { - it('should fail', function(done) { - restDaoStub.get.yields(42); - - pubkeyDao.verify('id', function(err) { - expect(err).to.exist; - done(); - }); + it('should work', function(done) { + restDaoStub.get.yields(null, { + _id: '12345', + publicKey: 'asdf' }); - it('should not error for 400', function(done) { - restDaoStub.get.yields({ - code: 400 - }); - - pubkeyDao.verify('id', function(err) { - expect(err).to.not.exist; - done(); - }); + pubkeyDao.get('id', function(err, key) { + expect(err).to.not.exist; + expect(key).to.exist; + expect(key._id).to.exist; + expect(key.publicKey).to.exist; + expect(restDaoStub.get.calledOnce).to.be.true; + done(); }); + }); + }); - it('should work', function(done) { - var uuid = 'c621e328-8548-40a1-8309-adf1955e98a9'; - restDaoStub.get.yields(null); + describe('verify', function() { + it('should fail', function(done) { + restDaoStub.get.yields(42); - pubkeyDao.verify(uuid, function(err) { - expect(err).to.not.exist; - expect(restDaoStub.get.calledWith(sinon.match(function(arg){ - return arg.uri === '/verify/' + uuid && arg.type === 'text'; - }))).to.be.true; - done(); - }); + pubkeyDao.verify('id', function(err) { + expect(err).to.exist; + done(); }); }); - describe('get by userId', function() { - it('should fail', function(done) { - restDaoStub.get.yields(42); - - pubkeyDao.getByUserId('userId', function(err, key) { - expect(err).to.exist; - expect(key).to.not.exist; - expect(restDaoStub.get.calledOnce).to.be.true; - done(); - }); + it('should not error for 400', function(done) { + restDaoStub.get.yields({ + code: 400 }); - it('should react to 404', function(done) { - restDaoStub.get.yields({ - code: 404 - }); - - pubkeyDao.getByUserId('userId', function(err, key) { - expect(err).to.not.exist; - expect(key).to.not.exist; - expect(restDaoStub.get.calledOnce).to.be.true; - done(); - }); - }); - - it('should return empty array', function(done) { - restDaoStub.get.yields(null, []); - - pubkeyDao.getByUserId('userId', function(err, key) { - expect(err).to.not.exist; - expect(key).to.not.exist; - expect(restDaoStub.get.calledOnce).to.be.true; - done(); - }); - }); - - it('should work', function(done) { - restDaoStub.get.yields(null, [{ - _id: '12345', - publicKey: 'asdf' - }]); - - pubkeyDao.getByUserId('userId', function(err, key) { - expect(err).to.not.exist; - expect(key).to.exist; - expect(key._id).to.exist; - expect(key.publicKey).to.exist; - expect(restDaoStub.get.calledOnce).to.be.true; - done(); - }); + pubkeyDao.verify('id', function(err) { + expect(err).to.not.exist; + done(); }); }); - describe('put', function() { - it('should fail', function(done) { - restDaoStub.put.yields(); + it('should work', function(done) { + var uuid = 'c621e328-8548-40a1-8309-adf1955e98a9'; + restDaoStub.get.yields(null); - pubkeyDao.put({ - _id: '12345', - publicKey: 'asdf' - }, function(err) { - expect(err).to.not.exist; - expect(restDaoStub.put.calledOnce).to.be.true; - done(); - }); + pubkeyDao.verify(uuid, function(err) { + expect(err).to.not.exist; + expect(restDaoStub.get.calledWith(sinon.match(function(arg) { + return arg.uri === '/verify/' + uuid && arg.type === 'text'; + }))).to.be.true; + done(); + }); + }); + }); + + describe('get by userId', function() { + it('should fail', function(done) { + restDaoStub.get.yields(42); + + pubkeyDao.getByUserId('userId', function(err, key) { + expect(err).to.exist; + expect(key).to.not.exist; + expect(restDaoStub.get.calledOnce).to.be.true; + done(); }); }); - describe('remove', function() { - it('should fail', function(done) { - restDaoStub.remove.yields(); + it('should react to 404', function(done) { + restDaoStub.get.yields({ + code: 404 + }); - pubkeyDao.remove('12345', function(err) { - expect(err).to.not.exist; - expect(restDaoStub.remove.calledOnce).to.be.true; - done(); - }); + pubkeyDao.getByUserId('userId', function(err, key) { + expect(err).to.not.exist; + expect(key).to.not.exist; + expect(restDaoStub.get.calledOnce).to.be.true; + done(); }); }); + it('should return empty array', function(done) { + restDaoStub.get.yields(null, []); + + pubkeyDao.getByUserId('userId', function(err, key) { + expect(err).to.not.exist; + expect(key).to.not.exist; + expect(restDaoStub.get.calledOnce).to.be.true; + done(); + }); + }); + + it('should work', function(done) { + restDaoStub.get.yields(null, [{ + _id: '12345', + publicKey: 'asdf' + }]); + + pubkeyDao.getByUserId('userId', function(err, key) { + expect(err).to.not.exist; + expect(key).to.exist; + expect(key._id).to.exist; + expect(key.publicKey).to.exist; + expect(restDaoStub.get.calledOnce).to.be.true; + done(); + }); + }); + }); + + describe('put', function() { + it('should fail', function(done) { + restDaoStub.put.yields(); + + pubkeyDao.put({ + _id: '12345', + publicKey: 'asdf' + }, function(err) { + expect(err).to.not.exist; + expect(restDaoStub.put.calledOnce).to.be.true; + done(); + }); + }); + }); + + describe('remove', function() { + it('should fail', function(done) { + restDaoStub.remove.yields(); + + pubkeyDao.remove('12345', function(err) { + expect(err).to.not.exist; + expect(restDaoStub.remove.calledOnce).to.be.true; + done(); + }); + }); }); }); \ No newline at end of file diff --git a/test/unit/read-ctrl-test.js b/test/unit/read-ctrl-test.js index a7d8bc5..104b6b1 100644 --- a/test/unit/read-ctrl-test.js +++ b/test/unit/read-ctrl-test.js @@ -1,197 +1,193 @@ -define(function(require) { - 'use strict'; +'use strict'; - var expect = chai.expect, - angular = require('angular'), - mocks = require('angularMocks'), - KeychainDAO = require('js/dao/keychain-dao'), - InvitationDAO = require('js/dao/invitation-dao'), - PGP = require('js/crypto/pgp'), - ReadCtrl = require('js/controller/read'), - OutboxBO = require('js/bo/outbox'), - appController = require('js/app-controller'); +var mocks = angular.mocks, + KeychainDAO = require('../../src/js/dao/keychain-dao'), + InvitationDAO = require('../../src/js/dao/invitation-dao'), + PGP = require('../../src/js/crypto/pgp'), + ReadCtrl = require('../../src/js/controller/read'), + OutboxBO = require('../../src/js/bo/outbox'), + appController = require('../../src/js/app-controller'); - describe('Read Controller unit test', function() { - var scope, ctrl, - origKeychain, keychainMock, - origInvitation, invitationMock, - origCrypto, cryptoMock, - origOutbox, outboxMock, - origEmailDao; +describe('Read Controller unit test', function() { + var scope, ctrl, + origKeychain, keychainMock, + origInvitation, invitationMock, + origCrypto, cryptoMock, + origOutbox, outboxMock, + origEmailDao; - beforeEach(function() { - origKeychain = appController._keychain; - appController._keychain = keychainMock = sinon.createStubInstance(KeychainDAO); + beforeEach(function() { + origKeychain = appController._keychain; + appController._keychain = keychainMock = sinon.createStubInstance(KeychainDAO); - origInvitation = appController._invitationDao; - appController._invitationDao = invitationMock = sinon.createStubInstance(InvitationDAO); + origInvitation = appController._invitationDao; + appController._invitationDao = invitationMock = sinon.createStubInstance(InvitationDAO); - origCrypto = appController._pgp; - appController._pgp = cryptoMock = sinon.createStubInstance(PGP); + origCrypto = appController._pgp; + appController._pgp = cryptoMock = sinon.createStubInstance(PGP); - origOutbox = appController._outboxBo; - appController._outboxBo = outboxMock = sinon.createStubInstance(OutboxBO); + origOutbox = appController._outboxBo; + appController._outboxBo = outboxMock = sinon.createStubInstance(OutboxBO); - origEmailDao = appController._emailDao; - appController._emailDao = { - _account: 'sender@example.com' + origEmailDao = appController._emailDao; + appController._emailDao = { + _account: 'sender@example.com' + }; + + angular.module('readtest', []); + mocks.module('readtest'); + mocks.inject(function($rootScope, $controller) { + scope = $rootScope.$new(); + scope.state = {}; + ctrl = $controller(ReadCtrl, { + $scope: scope + }); + }); + }); + + afterEach(function() { + appController._keychain = origKeychain; + appController._invitationDao = origInvitation; + appController._pgp = origCrypto; + appController._outboxBo = origOutbox; + appController._emailDao = origEmailDao; + }); + + describe('scope variables', function() { + it('should be set correctly', function() { + expect(scope.state.read).to.exist; + expect(scope.state.read.open).to.be.false; + expect(scope.state.read.toggle).to.exist; + }); + }); + + describe('open/close read view', function() { + it('should open/close', function() { + expect(scope.state.read.open).to.be.false; + scope.state.read.toggle(true); + expect(scope.state.read.open).to.be.true; + scope.state.read.toggle(false); + expect(scope.state.read.open).to.be.false; + }); + }); + + describe('getKeyId', function() { + var address = 'asfd@asdf.com'; + + it('should show searching on error', function() { + expect(scope.keyId).to.equal('No key found.'); + keychainMock.getReceiverPublicKey.yields(42); + + scope.onError = function(err) { + expect(err).to.equal(42); + expect(scope.keyId).to.equal('Searching...'); }; - angular.module('readtest', []); - mocks.module('readtest'); - mocks.inject(function($rootScope, $controller) { - scope = $rootScope.$new(); - scope.state = {}; - ctrl = $controller(ReadCtrl, { - $scope: scope - }); - }); + scope.getKeyId(address); }); - afterEach(function() { - appController._keychain = origKeychain; - appController._invitationDao = origInvitation; - appController._pgp = origCrypto; - appController._outboxBo = origOutbox; - appController._emailDao = origEmailDao; + it('should allow invitation on empty key', function() { + keychainMock.getReceiverPublicKey.yields(); + + scope.onError = function(err) { + expect(err).not.exist; + expect(scope.keyId).to.equal('User has no key. Click to invite.'); + }; + + scope.getKeyId(address); }); - describe('scope variables', function() { - it('should be set correctly', function() { - expect(scope.state.read).to.exist; - expect(scope.state.read.open).to.be.false; - expect(scope.state.read.toggle).to.exist; + it('should show searching on error', function() { + keychainMock.getReceiverPublicKey.yields(null, { + publicKey: 'PUBLIC KEY' }); + + cryptoMock.getFingerprint.returns('XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'); + + scope.onError = function(err) { + expect(err).to.not.exist; + expect(scope.keyId).to.equal('PGP key: XXXXXXXX'); + }; + + scope.getKeyId(address); }); - - describe('open/close read view', function() { - it('should open/close', function() { - expect(scope.state.read.open).to.be.false; - scope.state.read.toggle(true); - expect(scope.state.read.open).to.be.true; - scope.state.read.toggle(false); - expect(scope.state.read.open).to.be.false; - }); - }); - - describe('getKeyId', function() { - var address = 'asfd@asdf.com'; - - it('should show searching on error', function() { - expect(scope.keyId).to.equal('No key found.'); - keychainMock.getReceiverPublicKey.yields(42); - - scope.onError = function(err) { - expect(err).to.equal(42); - expect(scope.keyId).to.equal('Searching...'); - }; - - scope.getKeyId(address); - }); - - it('should allow invitation on empty key', function() { - keychainMock.getReceiverPublicKey.yields(); - - scope.onError = function(err) { - expect(err).not.exist; - expect(scope.keyId).to.equal('User has no key. Click to invite.'); - }; - - scope.getKeyId(address); - }); - - it('should show searching on error', function() { - keychainMock.getReceiverPublicKey.yields(null, { - publicKey: 'PUBLIC KEY' - }); - - cryptoMock.getFingerprint.returns('XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'); - - scope.onError = function(err) { - expect(err).to.not.exist; - expect(scope.keyId).to.equal('PGP key: XXXXXXXX'); - }; - - scope.getKeyId(address); - }); - }); - - describe('invite', function() { - it('not allow invitation for secure users', function() { - expect(scope.keyId).to.equal('No key found.'); - - scope.invite({ - secure: true, - address: 'asdf@asdf.de' - }); - - expect(scope.keyId).to.equal('No key found.'); - }); - - it('should show error on invitation dao invite error', function() { - invitationMock.invite.yields(42); - - scope.onError = function(err) { - expect(err).to.equal(42); - }; - - scope.invite({ - address: 'asdf@asdf.de' - }); - }); - - it('should show error on outbox put error', function() { - invitationMock.invite.yields(); - outboxMock.put.yields(42); - - scope.onError = function(err) { - expect(err).to.equal(42); - }; - - scope.invite({ - address: 'asdf@asdf.de' - }); - }); - - it('should work', function() { - invitationMock.invite.yields(); - outboxMock.put.yields(); - - scope.onError = function(err) { - expect(err).to.not.exist; - }; - - scope.invite({ - address: 'asdf@asdf.de' - }); - }); - }); - - describe('parseConversation', function() { - it.skip('should work', function() { - var body = 'foo\n' + - '\n' + - '> bar\n' + - '>\n' + - '> foofoo\n' + - '>> foofoobar\n' + - '\ncomment\n' + - '>> barbar'; - - var nodes = scope.parseConversation({ - body: body - }); - expect(nodes).to.exist; - - var expectedJson = '{"children":["foo\\n",{"children":["bar\\n\\nfoofoo",{"children":["foofoobar"]}]},"\\ncomment",{"children":[{"children":["barbar"]}]}]}'; - var json = JSON.stringify(nodes); - expect(json).to.equal(expectedJson); - - var expectedHtml = '
foo

bar

foofoo
foofoobar

comment
barbar
'; - var html = scope.renderNodes(nodes); - expect(html).to.equal(expectedHtml); - }); - }); - }); + + describe('invite', function() { + it('not allow invitation for secure users', function() { + expect(scope.keyId).to.equal('No key found.'); + + scope.invite({ + secure: true, + address: 'asdf@asdf.de' + }); + + expect(scope.keyId).to.equal('No key found.'); + }); + + it('should show error on invitation dao invite error', function() { + invitationMock.invite.yields(42); + + scope.onError = function(err) { + expect(err).to.equal(42); + }; + + scope.invite({ + address: 'asdf@asdf.de' + }); + }); + + it('should show error on outbox put error', function() { + invitationMock.invite.yields(); + outboxMock.put.yields(42); + + scope.onError = function(err) { + expect(err).to.equal(42); + }; + + scope.invite({ + address: 'asdf@asdf.de' + }); + }); + + it('should work', function() { + invitationMock.invite.yields(); + outboxMock.put.yields(); + + scope.onError = function(err) { + expect(err).to.not.exist; + }; + + scope.invite({ + address: 'asdf@asdf.de' + }); + }); + }); + + describe('parseConversation', function() { + it.skip('should work', function() { + var body = 'foo\n' + + '\n' + + '> bar\n' + + '>\n' + + '> foofoo\n' + + '>> foofoobar\n' + + '\ncomment\n' + + '>> barbar'; + + var nodes = scope.parseConversation({ + body: body + }); + expect(nodes).to.exist; + + var expectedJson = '{"children":["foo\\n",{"children":["bar\\n\\nfoofoo",{"children":["foofoobar"]}]},"\\ncomment",{"children":[{"children":["barbar"]}]}]}'; + var json = JSON.stringify(nodes); + expect(json).to.equal(expectedJson); + + var expectedHtml = '
foo

bar

foofoo
foofoobar

comment
barbar
'; + var html = scope.renderNodes(nodes); + expect(html).to.equal(expectedHtml); + }); + }); + }); \ No newline at end of file diff --git a/test/unit/rest-dao-test.js b/test/unit/rest-dao-test.js index 0bb2e06..2686a04 100644 --- a/test/unit/rest-dao-test.js +++ b/test/unit/rest-dao-test.js @@ -1,225 +1,221 @@ -define(function(require) { - 'use strict'; +'use strict'; - var RestDAO = require('js/dao/rest-dao'), - expect = chai.expect; +var RestDAO = require('../../src/js/dao/rest-dao'); - describe('Rest DAO unit tests', function() { +describe('Rest DAO unit tests', function() { - var restDao, xhrMock, requests; + var restDao, xhrMock, requests; - beforeEach(function() { + beforeEach(function() { + restDao = new RestDAO(); + xhrMock = sinon.useFakeXMLHttpRequest(); + requests = []; + + xhrMock.onCreate = function(xhr) { + requests.push(xhr); + }; + }); + + afterEach(function() { + xhrMock.restore(); + }); + + describe('contructor', function() { + it('should set default base uri', function() { restDao = new RestDAO(); - xhrMock = sinon.useFakeXMLHttpRequest(); - requests = []; - - xhrMock.onCreate = function(xhr) { - requests.push(xhr); - }; + expect(restDao).to.exist; + expect(restDao._baseUri).to.exist; }); - afterEach(function() { - xhrMock.restore(); - }); + it('should accept default base uri', function() { + var baseUri = 'http://custom.com'; - describe('contructor', function() { - it('should set default base uri', function() { - restDao = new RestDAO(); - expect(restDao).to.exist; - expect(restDao._baseUri).to.exist; + restDao = new RestDAO(baseUri); + expect(restDao).to.exist; + expect(restDao._baseUri).to.equal(baseUri); + }); + }); + + describe('get', function() { + it('should work with json as default type', function() { + restDao.get({ + uri: '/asdf' + }, function(err, data, status) { + expect(err).to.not.exist; + expect(data.foo).to.equal('bar'); + var req = requests[0]; + expect(req.requestHeaders.Accept).to.equal('application/json'); + expect(status).to.equal(200); }); - it('should accept default base uri', function() { - var baseUri = 'http://custom.com'; + expect(requests.length).to.equal(1); + requests[0].respond(200, { + "Content-Type": "application/json" + }, '{"foo": "bar"}'); + }); - restDao = new RestDAO(baseUri); - expect(restDao).to.exist; - expect(restDao._baseUri).to.equal(baseUri); + it('should work with jsonz', function() { + restDao.get({ + uri: '/asdf', + type: 'json' + }, function(err, data, status) { + expect(err).to.not.exist; + expect(data.foo).to.equal('bar'); + var req = requests[0]; + expect(req.requestHeaders.Accept).to.equal('application/json'); + expect(status).to.equal(200); + }); + + expect(requests.length).to.equal(1); + requests[0].respond(200, { + "Content-Type": "application/json" + }, '{"foo": "bar"}'); + }); + + it('should work with plain text', function() { + restDao.get({ + uri: '/asdf', + type: 'text' + }, function(err, data, status) { + expect(err).to.not.exist; + expect(data).to.equal('foobar!'); + var req = requests[0]; + expect(req.requestHeaders.Accept).to.equal('text/plain'); + expect(status).to.equal(200); + }); + + expect(requests.length).to.equal(1); + requests[0].respond(200, { + "Content-Type": "text/plain" + }, 'foobar!'); + }); + + it('should work with xml', function() { + restDao.get({ + uri: '/asdf', + type: 'xml' + }, function(err, data, status) { + expect(err).to.not.exist; + expect(data).to.equal('bar'); + var req = requests[0]; + expect(req.requestHeaders.Accept).to.equal('application/xml'); + expect(status).to.equal(200); + }); + + expect(requests.length).to.equal(1); + requests[0].respond(200, { + "Content-Type": "application/xml" + }, 'bar'); + }); + + it('should fail for missing uri parameter', function() { + restDao.get({}, function(err, data) { + expect(err).to.exist; + expect(err.code).to.equal(400); + expect(data).to.not.exist; }); }); - describe('get', function() { - it('should work with json as default type', function() { - restDao.get({ - uri: '/asdf' - }, function(err, data, status) { - expect(err).to.not.exist; - expect(data.foo).to.equal('bar'); - var req = requests[0]; - expect(req.requestHeaders.Accept).to.equal('application/json'); - expect(status).to.equal(200); - }); - - expect(requests.length).to.equal(1); - requests[0].respond(200, { - "Content-Type": "application/json" - }, '{"foo": "bar"}'); - }); - - it('should work with jsonz', function() { - restDao.get({ - uri: '/asdf', - type: 'json' - }, function(err, data, status) { - expect(err).to.not.exist; - expect(data.foo).to.equal('bar'); - var req = requests[0]; - expect(req.requestHeaders.Accept).to.equal('application/json'); - expect(status).to.equal(200); - }); - - expect(requests.length).to.equal(1); - requests[0].respond(200, { - "Content-Type": "application/json" - }, '{"foo": "bar"}'); - }); - - it('should work with plain text', function() { - restDao.get({ - uri: '/asdf', - type: 'text' - }, function(err, data, status) { - expect(err).to.not.exist; - expect(data).to.equal('foobar!'); - var req = requests[0]; - expect(req.requestHeaders.Accept).to.equal('text/plain'); - expect(status).to.equal(200); - }); - - expect(requests.length).to.equal(1); - requests[0].respond(200, { - "Content-Type": "text/plain" - }, 'foobar!'); - }); - - it('should work with xml', function() { - restDao.get({ - uri: '/asdf', - type: 'xml' - }, function(err, data, status) { - expect(err).to.not.exist; - expect(data).to.equal('bar'); - var req = requests[0]; - expect(req.requestHeaders.Accept).to.equal('application/xml'); - expect(status).to.equal(200); - }); - - expect(requests.length).to.equal(1); - requests[0].respond(200, { - "Content-Type": "application/xml" - }, 'bar'); - }); - - it('should fail for missing uri parameter', function() { - restDao.get({}, function(err, data) { - expect(err).to.exist; - expect(err.code).to.equal(400); - expect(data).to.not.exist; - }); - }); - - it('should fail for unhandled data type', function() { - restDao.get({ - uri: '/asdf', - type: 'snafu' - }, function(err, data) { - expect(err).to.exist; - expect(err.code).to.equal(400); - expect(data).to.not.exist; - }); - }); - - it('should fail for server error', function() { - restDao.get({ - uri: '/asdf' - }, function(err, data) { - expect(err).to.exist; - expect(err.code).to.equal(500); - expect(data).to.not.exist; - }); - - expect(requests.length).to.equal(1); - requests[0].respond(500, { - "Content-Type": "text/plain" - }, 'Internal error'); + it('should fail for unhandled data type', function() { + restDao.get({ + uri: '/asdf', + type: 'snafu' + }, function(err, data) { + expect(err).to.exist; + expect(err.code).to.equal(400); + expect(data).to.not.exist; }); }); - describe('post', function() { - it('should fail', function() { - restDao.post('/asdf', {}, function(err) { - expect(err).to.exist; - expect(err.code).to.equal(500); - }); - - expect(requests.length).to.equal(1); - requests[0].respond(500, { - "Content-Type": "text/plain" - }, 'Internal error'); + it('should fail for server error', function() { + restDao.get({ + uri: '/asdf' + }, function(err, data) { + expect(err).to.exist; + expect(err.code).to.equal(500); + expect(data).to.not.exist; }); - it('should work', function() { - restDao.post('/asdf', {}, function(err, res, status) { - expect(err).to.not.exist; - expect(res).to.equal(''); - expect(status).to.equal(201); - }); + expect(requests.length).to.equal(1); + requests[0].respond(500, { + "Content-Type": "text/plain" + }, 'Internal error'); + }); + }); - expect(requests.length).to.equal(1); - requests[0].respond(201); + describe('post', function() { + it('should fail', function() { + restDao.post('/asdf', {}, function(err) { + expect(err).to.exist; + expect(err.code).to.equal(500); }); + + expect(requests.length).to.equal(1); + requests[0].respond(500, { + "Content-Type": "text/plain" + }, 'Internal error'); }); - describe('put', function() { - it('should fail', function() { - restDao.put('/asdf', {}, function(err) { - expect(err).to.exist; - expect(err.code).to.equal(500); - }); - - expect(requests.length).to.equal(1); - requests[0].respond(500, { - "Content-Type": "text/plain" - }, 'Internal error'); + it('should work', function() { + restDao.post('/asdf', {}, function(err, res, status) { + expect(err).to.not.exist; + expect(res).to.equal(''); + expect(status).to.equal(201); }); - it('should work', function() { - restDao.put('/asdf', {}, function(err, res, status) { - expect(err).to.not.exist; - expect(res).to.equal(''); - expect(status).to.equal(201); - }); + expect(requests.length).to.equal(1); + requests[0].respond(201); + }); + }); - expect(requests.length).to.equal(1); - requests[0].respond(201); + describe('put', function() { + it('should fail', function() { + restDao.put('/asdf', {}, function(err) { + expect(err).to.exist; + expect(err.code).to.equal(500); }); + + expect(requests.length).to.equal(1); + requests[0].respond(500, { + "Content-Type": "text/plain" + }, 'Internal error'); }); - describe('remove', function() { - it('should fail', function() { - restDao.remove('/asdf', function(err) { - expect(err).to.exist; - expect(err.code).to.equal(500); - }); - - expect(requests.length).to.equal(1); - requests[0].respond(500, { - "Content-Type": "text/plain" - }, 'Internal error'); + it('should work', function() { + restDao.put('/asdf', {}, function(err, res, status) { + expect(err).to.not.exist; + expect(res).to.equal(''); + expect(status).to.equal(201); }); - it('should work', function() { - restDao.remove('/asdf', function(err, res, status) { - expect(err).to.not.exist; - expect(res).to.equal(''); - expect(status).to.equal(200); - }); + expect(requests.length).to.equal(1); + requests[0].respond(201); + }); + }); - expect(requests.length).to.equal(1); - requests[0].respond(200); + describe('remove', function() { + it('should fail', function() { + restDao.remove('/asdf', function(err) { + expect(err).to.exist; + expect(err.code).to.equal(500); }); + + expect(requests.length).to.equal(1); + requests[0].respond(500, { + "Content-Type": "text/plain" + }, 'Internal error'); }); + it('should work', function() { + restDao.remove('/asdf', function(err, res, status) { + expect(err).to.not.exist; + expect(res).to.equal(''); + expect(status).to.equal(200); + }); + + expect(requests.length).to.equal(1); + requests[0].respond(200); + }); }); }); \ No newline at end of file diff --git a/test/unit/set-passphrase-ctrl-test.js b/test/unit/set-passphrase-ctrl-test.js index 42e0f37..aaa9bc8 100644 --- a/test/unit/set-passphrase-ctrl-test.js +++ b/test/unit/set-passphrase-ctrl-test.js @@ -1,126 +1,122 @@ -define(function(require) { - 'use strict'; +'use strict'; - var expect = chai.expect, - angular = require('angular'), - mocks = require('angularMocks'), - SetPassphraseCtrl = require('js/controller/set-passphrase'), - PGP = require('js/crypto/pgp'), - appController = require('js/app-controller'), - KeychainDAO = require('js/dao/keychain-dao'); +var mocks = angular.mocks, + SetPassphraseCtrl = require('../../src/js/controller/set-passphrase'), + PGP = require('../../src/js/crypto/pgp'), + appController = require('../../src/js/app-controller'), + KeychainDAO = require('../../src/js/dao/keychain-dao'); - describe('Set Passphrase Controller unit test', function() { - var scope, setPassphraseCtrl, - dummyFingerprint, expectedFingerprint, - dummyKeyId, expectedKeyId, - emailAddress, keySize, cryptoMock, keychainMock; +describe('Set Passphrase Controller unit test', function() { + var scope, setPassphraseCtrl, + dummyFingerprint, expectedFingerprint, + dummyKeyId, expectedKeyId, + emailAddress, keySize, cryptoMock, keychainMock; - beforeEach(function() { - appController._pgp = cryptoMock = sinon.createStubInstance(PGP); - appController._keychain = keychainMock = sinon.createStubInstance(KeychainDAO); + beforeEach(function() { + appController._pgp = cryptoMock = sinon.createStubInstance(PGP); + appController._keychain = keychainMock = sinon.createStubInstance(KeychainDAO); - dummyFingerprint = '3A2D39B4E1404190B8B949DE7D7E99036E712926'; - expectedFingerprint = '3A2D 39B4 E140 4190 B8B9 49DE 7D7E 9903 6E71 2926'; - dummyKeyId = '9FEB47936E712926'; - expectedKeyId = '6E712926'; - cryptoMock.getFingerprint.returns(dummyFingerprint); - cryptoMock.getKeyId.returns(dummyKeyId); - emailAddress = 'fred@foo.com'; - keySize = 1234; + dummyFingerprint = '3A2D39B4E1404190B8B949DE7D7E99036E712926'; + expectedFingerprint = '3A2D 39B4 E140 4190 B8B9 49DE 7D7E 9903 6E71 2926'; + dummyKeyId = '9FEB47936E712926'; + expectedKeyId = '6E712926'; + cryptoMock.getFingerprint.returns(dummyFingerprint); + cryptoMock.getKeyId.returns(dummyKeyId); + emailAddress = 'fred@foo.com'; + keySize = 1234; - cryptoMock.getKeyParams.returns({ + cryptoMock.getKeyParams.returns({ + _id: dummyKeyId, + fingerprint: dummyFingerprint, + userId: emailAddress, + userIds: [], + bitSize: keySize + }); + + angular.module('setpassphrasetest', []); + mocks.module('setpassphrasetest'); + mocks.inject(function($rootScope, $controller) { + scope = $rootScope.$new(); + scope.state = {}; + setPassphraseCtrl = $controller(SetPassphraseCtrl, { + $scope: scope + }); + }); + }); + + afterEach(function() {}); + + describe('setPassphrase', function() { + it('should work', function(done) { + scope.oldPassphrase = 'old'; + scope.newPassphrase = 'new'; + + keychainMock.lookupPrivateKey.withArgs(dummyKeyId).yields(null, { + encryptedKey: 'encrypted' + }); + + cryptoMock.changePassphrase.withArgs({ + privateKeyArmored: 'encrypted', + oldPassphrase: 'old', + newPassphrase: 'new' + }).yields(null, 'newArmoredKey'); + + keychainMock.saveLocalPrivateKey.withArgs({ _id: dummyKeyId, - fingerprint: dummyFingerprint, userId: emailAddress, userIds: [], - bitSize: keySize - }); + encryptedKey: 'newArmoredKey' + }).yields(); - angular.module('setpassphrasetest', []); - mocks.module('setpassphrasetest'); - mocks.inject(function($rootScope, $controller) { - scope = $rootScope.$new(); - scope.state = {}; - setPassphraseCtrl = $controller(SetPassphraseCtrl, { - $scope: scope - }); - }); + scope.onError = function(err) { + expect(err.title).to.equal('Success'); + done(); + }; + + scope.setPassphrase(); }); - - afterEach(function() {}); - - describe('setPassphrase', function() { - it('should work', function(done) { - scope.oldPassphrase = 'old'; - scope.newPassphrase = 'new'; - - keychainMock.lookupPrivateKey.withArgs(dummyKeyId).yields(null, { - encryptedKey: 'encrypted' - }); - - cryptoMock.changePassphrase.withArgs({ - privateKeyArmored: 'encrypted', - oldPassphrase: 'old', - newPassphrase: 'new' - }).yields(null, 'newArmoredKey'); - - keychainMock.saveLocalPrivateKey.withArgs({ - _id: dummyKeyId, - userId: emailAddress, - userIds: [], - encryptedKey: 'newArmoredKey' - }).yields(); - - scope.onError = function(err) { - expect(err.title).to.equal('Success'); - done(); - }; - - scope.setPassphrase(); - }); - }); - - describe('check passphrase quality', function() { - it('should be too short', function() { - scope.newPassphrase = '&§DG36'; - scope.checkPassphraseQuality(); - - expect(scope.passphraseMsg).to.equal('Very weak'); - expect(scope.passphraseRating).to.equal(0); - }); - - it('should be very weak', function() { - scope.newPassphrase = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; - scope.checkPassphraseQuality(); - - expect(scope.passphraseMsg).to.equal('Very weak'); - expect(scope.passphraseRating).to.equal(0); - }); - - it('should be weak', function() { - scope.newPassphrase = 'asdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdf'; - scope.checkPassphraseQuality(); - - expect(scope.passphraseMsg).to.equal('Weak'); - expect(scope.passphraseRating).to.equal(1); - }); - - it('should be good', function() { - scope.newPassphrase = 'asdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdf5'; - scope.checkPassphraseQuality(); - - expect(scope.passphraseMsg).to.equal('Good'); - expect(scope.passphraseRating).to.equal(2); - }); - - it('should be strong', function() { - scope.newPassphrase = '&§DG36abcd'; - scope.checkPassphraseQuality(); - - expect(scope.passphraseMsg).to.equal('Strong'); - expect(scope.passphraseRating).to.equal(3); - }); - }); - }); + + describe('check passphrase quality', function() { + it('should be too short', function() { + scope.newPassphrase = '&§DG36'; + scope.checkPassphraseQuality(); + + expect(scope.passphraseMsg).to.equal('Very weak'); + expect(scope.passphraseRating).to.equal(0); + }); + + it('should be very weak', function() { + scope.newPassphrase = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; + scope.checkPassphraseQuality(); + + expect(scope.passphraseMsg).to.equal('Very weak'); + expect(scope.passphraseRating).to.equal(0); + }); + + it('should be weak', function() { + scope.newPassphrase = 'asdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdf'; + scope.checkPassphraseQuality(); + + expect(scope.passphraseMsg).to.equal('Weak'); + expect(scope.passphraseRating).to.equal(1); + }); + + it('should be good', function() { + scope.newPassphrase = 'asdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdf5'; + scope.checkPassphraseQuality(); + + expect(scope.passphraseMsg).to.equal('Good'); + expect(scope.passphraseRating).to.equal(2); + }); + + it('should be strong', function() { + scope.newPassphrase = '&§DG36abcd'; + scope.checkPassphraseQuality(); + + expect(scope.passphraseMsg).to.equal('Strong'); + expect(scope.passphraseRating).to.equal(3); + }); + }); + }); \ No newline at end of file diff --git a/test/unit/update-handler-test.js b/test/unit/update-handler-test.js index 606be07..8267938 100644 --- a/test/unit/update-handler-test.js +++ b/test/unit/update-handler-test.js @@ -1,472 +1,469 @@ -define(function(require) { - 'use strict'; +'use strict'; - var DeviceStorageDAO = require('js/dao/devicestorage-dao'), - Auth = require('js/bo/auth'), - cfg = require('js/app-config').config, - UpdateHandler = require('js/util/update/update-handler'), - config = require('js/app-config').config, - expect = chai.expect; +var DeviceStorageDAO = require('../../src/js/dao/devicestorage-dao'), + Auth = require('../../src/js/bo/auth'), + cfg = require('../../src/js/app-config').config, + UpdateHandler = require('../../src/js/util/update/update-handler'), + config = require('../../src/js/app-config').config; - describe('UpdateHandler', function() { - var updateHandler, appConfigStorageStub, authStub, userStorageStub, origDbVersion; +describe('UpdateHandler', function() { + var updateHandler, appConfigStorageStub, authStub, userStorageStub, origDbVersion; - chai.Assertion.includeStack = true; + chai.Assertion.includeStack = true; - beforeEach(function() { - origDbVersion = cfg.dbVersion; - appConfigStorageStub = sinon.createStubInstance(DeviceStorageDAO); - userStorageStub = sinon.createStubInstance(DeviceStorageDAO); - authStub = sinon.createStubInstance(Auth); - updateHandler = new UpdateHandler(appConfigStorageStub, userStorageStub, authStub); + beforeEach(function() { + origDbVersion = cfg.dbVersion; + appConfigStorageStub = sinon.createStubInstance(DeviceStorageDAO); + userStorageStub = sinon.createStubInstance(DeviceStorageDAO); + authStub = sinon.createStubInstance(Auth); + updateHandler = new UpdateHandler(appConfigStorageStub, userStorageStub, authStub); + }); + + afterEach(function() { + cfg.dbVersion = origDbVersion; + }); + + describe('#constructor', function() { + it('should create instance', function() { + expect(updateHandler).to.exist; + expect(updateHandler._appConfigStorage).to.equal(appConfigStorageStub); + expect(updateHandler._userStorage).to.equal(userStorageStub); + + // the update handler must contain as many db update sripts as there are database versions + expect(updateHandler._updateScripts.length).to.equal(cfg.dbVersion); }); + }); - afterEach(function() { - cfg.dbVersion = origDbVersion; - }); + describe('#update', function() { + var versionDbType = 'dbVersion'; - describe('#constructor', function() { - it('should create instance', function() { - expect(updateHandler).to.exist; - expect(updateHandler._appConfigStorage).to.equal(appConfigStorageStub); - expect(updateHandler._userStorage).to.equal(userStorageStub); + it('should not update when up to date', function(done) { + cfg.dbVersion = 10; // app requires database version 10 + appConfigStorageStub.listItems.withArgs(versionDbType).yieldsAsync(null, ['10']); // database version is 10 - // the update handler must contain as many db update sripts as there are database versions - expect(updateHandler._updateScripts.length).to.equal(cfg.dbVersion); + updateHandler.update(function(error) { + expect(error).to.not.exist; + expect(appConfigStorageStub.listItems.calledOnce).to.be.true; + + done(); }); }); - describe('#update', function() { - var versionDbType = 'dbVersion'; + describe('dummy updates for v2 to v4', function() { + var updateCounter; - it('should not update when up to date', function(done) { - cfg.dbVersion = 10; // app requires database version 10 - appConfigStorageStub.listItems.withArgs(versionDbType).yieldsAsync(null, ['10']); // database version is 10 + beforeEach(function() { + updateCounter = 0; + appConfigStorageStub.listItems.withArgs(versionDbType).yieldsAsync(null, ['2']); // database version is 0 + }); + afterEach(function() { + expect(appConfigStorageStub.listItems.calledOnce).to.be.true; + }); + + + it('should work', function(done) { + cfg.dbVersion = 4; // app requires database version 4 + + // a simple dummy update to executed that only increments the update counter + function dummyUpdate(options, callback) { + updateCounter++; + callback(); + } + + // inject the dummy updates instead of live ones + updateHandler._updateScripts = [dummyUpdate, dummyUpdate, dummyUpdate, dummyUpdate]; + + // execute test updateHandler.update(function(error) { expect(error).to.not.exist; - expect(appConfigStorageStub.listItems.calledOnce).to.be.true; + expect(updateCounter).to.equal(2); done(); }); }); - describe('dummy updates for v2 to v4', function() { - var updateCounter; + it('should fail while updating to v3', function(done) { + cfg.dbVersion = 4; // app requires database version 4 - beforeEach(function() { - updateCounter = 0; - appConfigStorageStub.listItems.withArgs(versionDbType).yieldsAsync(null, ['2']); // database version is 0 - }); + function dummyUpdate(options, callback) { + updateCounter++; + callback(); + } - afterEach(function() { - expect(appConfigStorageStub.listItems.calledOnce).to.be.true; - }); + function failingUpdate(options, callback) { + callback({}); + } + // inject the dummy updates instead of live ones + updateHandler._updateScripts = [dummyUpdate, dummyUpdate, failingUpdate, dummyUpdate]; - it('should work', function(done) { - cfg.dbVersion = 4; // app requires database version 4 + // execute test + updateHandler.update(function(error) { + expect(error).to.exist; + expect(updateCounter).to.equal(0); - // a simple dummy update to executed that only increments the update counter - function dummyUpdate(options, callback) { - updateCounter++; - callback(); - } - - // inject the dummy updates instead of live ones - updateHandler._updateScripts = [dummyUpdate, dummyUpdate, dummyUpdate, dummyUpdate]; - - // execute test - updateHandler.update(function(error) { - expect(error).to.not.exist; - expect(updateCounter).to.equal(2); - - done(); - }); - }); - - it('should fail while updating to v3', function(done) { - cfg.dbVersion = 4; // app requires database version 4 - - function dummyUpdate(options, callback) { - updateCounter++; - callback(); - } - - function failingUpdate(options, callback) { - callback({}); - } - - // inject the dummy updates instead of live ones - updateHandler._updateScripts = [dummyUpdate, dummyUpdate, failingUpdate, dummyUpdate]; - - // execute test - updateHandler.update(function(error) { - expect(error).to.exist; - expect(updateCounter).to.equal(0); - - done(); - }); - }); - - }); - - describe('v0 -> v1', function() { - var emailDbType = 'email_'; - - beforeEach(function() { - cfg.dbVersion = 1; // app requires database version 1 - appConfigStorageStub.listItems.withArgs(versionDbType).yieldsAsync(); // database version is 0 - }); - - afterEach(function() { - // database version is only queried for version checking prior to the update script - // so no need to check this in case-specific tests - expect(appConfigStorageStub.listItems.calledOnce).to.be.true; - }); - - it('should work', function(done) { - userStorageStub.removeList.withArgs(emailDbType).yieldsAsync(); - appConfigStorageStub.storeList.withArgs([1], versionDbType).yieldsAsync(); - - updateHandler.update(function(error) { - expect(error).to.not.exist; - expect(userStorageStub.removeList.calledOnce).to.be.true; - expect(appConfigStorageStub.storeList.calledOnce).to.be.true; - - done(); - }); - }); - - it('should fail when persisting database version fails', function(done) { - userStorageStub.removeList.yieldsAsync(); - appConfigStorageStub.storeList.yieldsAsync(new Error()); - - updateHandler.update(function(error) { - expect(error).to.exist; - expect(userStorageStub.removeList.calledOnce).to.be.true; - expect(appConfigStorageStub.storeList.calledOnce).to.be.true; - - done(); - }); - }); - - it('should fail when wiping emails from database fails', function(done) { - userStorageStub.removeList.yieldsAsync(new Error()); - - updateHandler.update(function(error) { - expect(error).to.exist; - expect(userStorageStub.removeList.calledOnce).to.be.true; - expect(appConfigStorageStub.storeList.called).to.be.false; - - done(); - }); + done(); }); }); - describe('v1 -> v2', function() { - var emailDbType = 'email_'; + }); - beforeEach(function() { - cfg.dbVersion = 2; // app requires database version 2 - appConfigStorageStub.listItems.withArgs(versionDbType).yieldsAsync(null, [1]); // database version is 0 - }); + describe('v0 -> v1', function() { + var emailDbType = 'email_'; - afterEach(function() { - // database version is only queried for version checking prior to the update script - // so no need to check this in case-specific tests - expect(appConfigStorageStub.listItems.calledOnce).to.be.true; - }); + beforeEach(function() { + cfg.dbVersion = 1; // app requires database version 1 + appConfigStorageStub.listItems.withArgs(versionDbType).yieldsAsync(); // database version is 0 + }); - it('should work', function(done) { - userStorageStub.removeList.withArgs(emailDbType).yieldsAsync(); - appConfigStorageStub.storeList.withArgs([2], versionDbType).yieldsAsync(); + afterEach(function() { + // database version is only queried for version checking prior to the update script + // so no need to check this in case-specific tests + expect(appConfigStorageStub.listItems.calledOnce).to.be.true; + }); - updateHandler.update(function(error) { - expect(error).to.not.exist; - expect(userStorageStub.removeList.calledOnce).to.be.true; - expect(appConfigStorageStub.storeList.calledOnce).to.be.true; + it('should work', function(done) { + userStorageStub.removeList.withArgs(emailDbType).yieldsAsync(); + appConfigStorageStub.storeList.withArgs([1], versionDbType).yieldsAsync(); - done(); - }); - }); + updateHandler.update(function(error) { + expect(error).to.not.exist; + expect(userStorageStub.removeList.calledOnce).to.be.true; + expect(appConfigStorageStub.storeList.calledOnce).to.be.true; - it('should fail when persisting database version fails', function(done) { - userStorageStub.removeList.yieldsAsync(); - appConfigStorageStub.storeList.yieldsAsync(new Error()); - - updateHandler.update(function(error) { - expect(error).to.exist; - expect(userStorageStub.removeList.calledOnce).to.be.true; - expect(appConfigStorageStub.storeList.calledOnce).to.be.true; - - done(); - }); - }); - - it('should fail when wiping emails from database fails', function(done) { - userStorageStub.removeList.yieldsAsync(new Error()); - - updateHandler.update(function(error) { - expect(error).to.exist; - expect(userStorageStub.removeList.calledOnce).to.be.true; - expect(appConfigStorageStub.storeList.called).to.be.false; - - done(); - }); + done(); }); }); - describe('v2 -> v3', function() { - var emailDbType = 'email_'; + it('should fail when persisting database version fails', function(done) { + userStorageStub.removeList.yieldsAsync(); + appConfigStorageStub.storeList.yieldsAsync(new Error()); - beforeEach(function() { - cfg.dbVersion = 3; // app requires database version 2 - appConfigStorageStub.listItems.withArgs(versionDbType).yieldsAsync(null, [2]); // database version is 0 - }); + updateHandler.update(function(error) { + expect(error).to.exist; + expect(userStorageStub.removeList.calledOnce).to.be.true; + expect(appConfigStorageStub.storeList.calledOnce).to.be.true; - afterEach(function() { - // database version is only queried for version checking prior to the update script - // so no need to check this in case-specific tests - expect(appConfigStorageStub.listItems.calledOnce).to.be.true; - }); - - it('should work', function(done) { - userStorageStub.removeList.withArgs(emailDbType).yieldsAsync(); - appConfigStorageStub.storeList.withArgs([3], versionDbType).yieldsAsync(); - - updateHandler.update(function(error) { - expect(error).to.not.exist; - expect(userStorageStub.removeList.calledOnce).to.be.true; - expect(appConfigStorageStub.storeList.calledOnce).to.be.true; - - done(); - }); - }); - - it('should fail when persisting database version fails', function(done) { - userStorageStub.removeList.yieldsAsync(); - appConfigStorageStub.storeList.yieldsAsync(new Error()); - - updateHandler.update(function(error) { - expect(error).to.exist; - expect(userStorageStub.removeList.calledOnce).to.be.true; - expect(appConfigStorageStub.storeList.calledOnce).to.be.true; - - done(); - }); - }); - - it('should fail when wiping emails from database fails', function(done) { - userStorageStub.removeList.yieldsAsync(new Error()); - - updateHandler.update(function(error) { - expect(error).to.exist; - expect(userStorageStub.removeList.calledOnce).to.be.true; - expect(appConfigStorageStub.storeList.called).to.be.false; - - done(); - }); - }); - }); - describe('v3 -> v4', function() { - var EMAIL_ADDR_DB_KEY = 'emailaddress'; - var USERNAME_DB_KEY = 'username'; - var PROVIDER_DB_KEY = 'provider'; - var IMAP_DB_KEY = 'imap'; - var SMTP_DB_KEY = 'smtp'; - var REALNAME_DB_KEY = 'realname'; - var emailaddress = 'bla@blubb.io'; - - var imap = config.gmail.imap, - smtp = config.gmail.smtp; - - beforeEach(function() { - cfg.dbVersion = 4; // app requires database version 4 - appConfigStorageStub.listItems.withArgs(versionDbType).yieldsAsync(null, [3]); // database version is 3 - }); - - it('should add gmail as mail service provider with email address and no provider present in db', function(done) { - appConfigStorageStub.listItems.withArgs(EMAIL_ADDR_DB_KEY).yieldsAsync(null, [emailaddress]); - appConfigStorageStub.listItems.withArgs(PROVIDER_DB_KEY).yieldsAsync(null, []); - appConfigStorageStub.storeList.withArgs([4], versionDbType).yieldsAsync(); - appConfigStorageStub.storeList.withArgs(['gmail'], PROVIDER_DB_KEY).yieldsAsync(); - appConfigStorageStub.storeList.withArgs([emailaddress], USERNAME_DB_KEY).yieldsAsync(); - appConfigStorageStub.storeList.withArgs([imap], IMAP_DB_KEY).yieldsAsync(); - appConfigStorageStub.storeList.withArgs([smtp], SMTP_DB_KEY).yieldsAsync(); - appConfigStorageStub.storeList.withArgs([''], REALNAME_DB_KEY).yieldsAsync(); - authStub._loadCredentials.yieldsAsync(); - - updateHandler.update(function(error) { - expect(error).to.not.exist; - expect(appConfigStorageStub.storeList.callCount).to.equal(6); - expect(appConfigStorageStub.listItems.calledThrice).to.be.true; - expect(authStub._loadCredentials.calledOnce).to.be.true; - - done(); - }); - }); - - it('should not add a provider when no email adress is in db', function(done) { - appConfigStorageStub.listItems.withArgs(EMAIL_ADDR_DB_KEY).yieldsAsync(null, []); - appConfigStorageStub.listItems.withArgs(PROVIDER_DB_KEY).yieldsAsync(null, []); - appConfigStorageStub.storeList.withArgs([4], versionDbType).yieldsAsync(); - - updateHandler.update(function(error) { - expect(error).to.not.exist; - expect(appConfigStorageStub.storeList.calledOnce).to.be.true; - expect(appConfigStorageStub.listItems.calledThrice).to.be.true; - - done(); - }); - }); - - it('should fail when appConfigStore write fails', function(done) { - appConfigStorageStub.listItems.yieldsAsync(null, []); - appConfigStorageStub.storeList.yieldsAsync(new Error()); - - updateHandler.update(function(error) { - expect(error).to.exist; - expect(appConfigStorageStub.listItems.calledThrice).to.be.true; - expect(appConfigStorageStub.storeList.calledOnce).to.be.true; - - done(); - }); - }); - - it('should fail when appConfigStore read fails', function(done) { - appConfigStorageStub.listItems.withArgs(EMAIL_ADDR_DB_KEY).yieldsAsync(new Error()); - appConfigStorageStub.storeList.yieldsAsync(new Error()); - - updateHandler.update(function(error) { - expect(error).to.exist; - expect(appConfigStorageStub.listItems.calledTwice).to.be.true; - expect(appConfigStorageStub.storeList.called).to.be.false; - - done(); - }); + done(); }); }); - describe('v4 -> v5', function() { - var FOLDER_TYPE_INBOX = 'Inbox'; - var FOLDER_TYPE_SENT = 'Sent'; - var FOLDER_TYPE_DRAFTS = 'Drafts'; - var FOLDER_TYPE_TRASH = 'Trash'; + it('should fail when wiping emails from database fails', function(done) { + userStorageStub.removeList.yieldsAsync(new Error()); - var FOLDER_DB_TYPE = 'folders'; - var VERSION_DB_TYPE = 'dbVersion'; + updateHandler.update(function(error) { + expect(error).to.exist; + expect(userStorageStub.removeList.calledOnce).to.be.true; + expect(appConfigStorageStub.storeList.called).to.be.false; - var POST_UPDATE_DB_VERSION = 5; - - beforeEach(function() { - cfg.dbVersion = 5; // app requires database version 4 - appConfigStorageStub.listItems.withArgs(VERSION_DB_TYPE).yieldsAsync(null, [4]); // database version is 4 + done(); }); + }); + }); - afterEach(function() { - // database version is only queried for version checking prior to the update script - // so no need to check this in case-specific tests - expect(appConfigStorageStub.listItems.calledOnce).to.be.true; + describe('v1 -> v2', function() { + var emailDbType = 'email_'; + + beforeEach(function() { + cfg.dbVersion = 2; // app requires database version 2 + appConfigStorageStub.listItems.withArgs(versionDbType).yieldsAsync(null, [1]); // database version is 0 + }); + + afterEach(function() { + // database version is only queried for version checking prior to the update script + // so no need to check this in case-specific tests + expect(appConfigStorageStub.listItems.calledOnce).to.be.true; + }); + + it('should work', function(done) { + userStorageStub.removeList.withArgs(emailDbType).yieldsAsync(); + appConfigStorageStub.storeList.withArgs([2], versionDbType).yieldsAsync(); + + updateHandler.update(function(error) { + expect(error).to.not.exist; + expect(userStorageStub.removeList.calledOnce).to.be.true; + expect(appConfigStorageStub.storeList.calledOnce).to.be.true; + + done(); }); + }); - it('should work', function(done) { - userStorageStub.listItems.withArgs(FOLDER_DB_TYPE).yieldsAsync(null, [ - [{ - name: 'inbox1', - type: FOLDER_TYPE_INBOX - }, { - name: 'inbox2', - type: FOLDER_TYPE_INBOX - }, { - name: 'sent1', - type: FOLDER_TYPE_SENT - }, { - name: 'sent2', - type: FOLDER_TYPE_SENT - }, { - name: 'drafts1', - type: FOLDER_TYPE_DRAFTS - }, { - name: 'drafts2', - type: FOLDER_TYPE_DRAFTS - }, { - name: 'trash1', - type: FOLDER_TYPE_TRASH - }, { - name: 'trash2', - type: FOLDER_TYPE_TRASH - }] - ]); + it('should fail when persisting database version fails', function(done) { + userStorageStub.removeList.yieldsAsync(); + appConfigStorageStub.storeList.yieldsAsync(new Error()); - userStorageStub.storeList.withArgs([ - [{ - name: 'inbox1', - type: FOLDER_TYPE_INBOX - }, { - name: 'sent1', - type: FOLDER_TYPE_SENT - }, { - name: 'drafts1', - type: FOLDER_TYPE_DRAFTS - }, { - name: 'trash1', - type: FOLDER_TYPE_TRASH - }] - ], FOLDER_DB_TYPE).yieldsAsync(); + updateHandler.update(function(error) { + expect(error).to.exist; + expect(userStorageStub.removeList.calledOnce).to.be.true; + expect(appConfigStorageStub.storeList.calledOnce).to.be.true; - appConfigStorageStub.storeList.withArgs([POST_UPDATE_DB_VERSION], VERSION_DB_TYPE).yieldsAsync(); - - updateHandler.update(function(error) { - expect(error).to.not.exist; - expect(userStorageStub.listItems.calledOnce).to.be.true; - expect(userStorageStub.storeList.calledOnce).to.be.true; - expect(appConfigStorageStub.storeList.calledOnce).to.be.true; - - done(); - }); + done(); }); + }); - it('should fail when persisting database version fails', function(done) { - userStorageStub.listItems.yieldsAsync(null, []); - userStorageStub.storeList.yieldsAsync(); - appConfigStorageStub.storeList.yieldsAsync(new Error()); + it('should fail when wiping emails from database fails', function(done) { + userStorageStub.removeList.yieldsAsync(new Error()); - updateHandler.update(function(error) { - expect(error).to.exist; - expect(userStorageStub.listItems.calledOnce).to.be.true; - expect(userStorageStub.storeList.calledOnce).to.be.true; - expect(appConfigStorageStub.storeList.calledOnce).to.be.true; + updateHandler.update(function(error) { + expect(error).to.exist; + expect(userStorageStub.removeList.calledOnce).to.be.true; + expect(appConfigStorageStub.storeList.called).to.be.false; - done(); - }); + done(); }); + }); + }); - it('should fail when persisting folders fails', function(done) { - userStorageStub.listItems.yieldsAsync(null, []); - userStorageStub.storeList.yieldsAsync(new Error()); + describe('v2 -> v3', function() { + var emailDbType = 'email_'; - updateHandler.update(function(error) { - expect(error).to.exist; - expect(userStorageStub.listItems.calledOnce).to.be.true; - expect(userStorageStub.storeList.calledOnce).to.be.true; - expect(appConfigStorageStub.storeList.called).to.be.false; + beforeEach(function() { + cfg.dbVersion = 3; // app requires database version 2 + appConfigStorageStub.listItems.withArgs(versionDbType).yieldsAsync(null, [2]); // database version is 0 + }); - done(); - }); + afterEach(function() { + // database version is only queried for version checking prior to the update script + // so no need to check this in case-specific tests + expect(appConfigStorageStub.listItems.calledOnce).to.be.true; + }); + + it('should work', function(done) { + userStorageStub.removeList.withArgs(emailDbType).yieldsAsync(); + appConfigStorageStub.storeList.withArgs([3], versionDbType).yieldsAsync(); + + updateHandler.update(function(error) { + expect(error).to.not.exist; + expect(userStorageStub.removeList.calledOnce).to.be.true; + expect(appConfigStorageStub.storeList.calledOnce).to.be.true; + + done(); }); + }); - it('should fail when listing folders fails', function(done) { - userStorageStub.listItems.yieldsAsync(new Error()); + it('should fail when persisting database version fails', function(done) { + userStorageStub.removeList.yieldsAsync(); + appConfigStorageStub.storeList.yieldsAsync(new Error()); - updateHandler.update(function(error) { - expect(error).to.exist; - expect(userStorageStub.listItems.calledOnce).to.be.true; - expect(userStorageStub.storeList.called).to.be.false; - expect(appConfigStorageStub.storeList.called).to.be.false; + updateHandler.update(function(error) { + expect(error).to.exist; + expect(userStorageStub.removeList.calledOnce).to.be.true; + expect(appConfigStorageStub.storeList.calledOnce).to.be.true; - done(); - }); + done(); + }); + }); + + it('should fail when wiping emails from database fails', function(done) { + userStorageStub.removeList.yieldsAsync(new Error()); + + updateHandler.update(function(error) { + expect(error).to.exist; + expect(userStorageStub.removeList.calledOnce).to.be.true; + expect(appConfigStorageStub.storeList.called).to.be.false; + + done(); + }); + }); + }); + describe('v3 -> v4', function() { + var EMAIL_ADDR_DB_KEY = 'emailaddress'; + var USERNAME_DB_KEY = 'username'; + var PROVIDER_DB_KEY = 'provider'; + var IMAP_DB_KEY = 'imap'; + var SMTP_DB_KEY = 'smtp'; + var REALNAME_DB_KEY = 'realname'; + var emailaddress = 'bla@blubb.io'; + + var imap = config.gmail.imap, + smtp = config.gmail.smtp; + + beforeEach(function() { + cfg.dbVersion = 4; // app requires database version 4 + appConfigStorageStub.listItems.withArgs(versionDbType).yieldsAsync(null, [3]); // database version is 3 + }); + + it('should add gmail as mail service provider with email address and no provider present in db', function(done) { + appConfigStorageStub.listItems.withArgs(EMAIL_ADDR_DB_KEY).yieldsAsync(null, [emailaddress]); + appConfigStorageStub.listItems.withArgs(PROVIDER_DB_KEY).yieldsAsync(null, []); + appConfigStorageStub.storeList.withArgs([4], versionDbType).yieldsAsync(); + appConfigStorageStub.storeList.withArgs(['gmail'], PROVIDER_DB_KEY).yieldsAsync(); + appConfigStorageStub.storeList.withArgs([emailaddress], USERNAME_DB_KEY).yieldsAsync(); + appConfigStorageStub.storeList.withArgs([imap], IMAP_DB_KEY).yieldsAsync(); + appConfigStorageStub.storeList.withArgs([smtp], SMTP_DB_KEY).yieldsAsync(); + appConfigStorageStub.storeList.withArgs([''], REALNAME_DB_KEY).yieldsAsync(); + authStub._loadCredentials.yieldsAsync(); + + updateHandler.update(function(error) { + expect(error).to.not.exist; + expect(appConfigStorageStub.storeList.callCount).to.equal(6); + expect(appConfigStorageStub.listItems.calledThrice).to.be.true; + expect(authStub._loadCredentials.calledOnce).to.be.true; + + done(); + }); + }); + + it('should not add a provider when no email adress is in db', function(done) { + appConfigStorageStub.listItems.withArgs(EMAIL_ADDR_DB_KEY).yieldsAsync(null, []); + appConfigStorageStub.listItems.withArgs(PROVIDER_DB_KEY).yieldsAsync(null, []); + appConfigStorageStub.storeList.withArgs([4], versionDbType).yieldsAsync(); + + updateHandler.update(function(error) { + expect(error).to.not.exist; + expect(appConfigStorageStub.storeList.calledOnce).to.be.true; + expect(appConfigStorageStub.listItems.calledThrice).to.be.true; + + done(); + }); + }); + + it('should fail when appConfigStore write fails', function(done) { + appConfigStorageStub.listItems.yieldsAsync(null, []); + appConfigStorageStub.storeList.yieldsAsync(new Error()); + + updateHandler.update(function(error) { + expect(error).to.exist; + expect(appConfigStorageStub.listItems.calledThrice).to.be.true; + expect(appConfigStorageStub.storeList.calledOnce).to.be.true; + + done(); + }); + }); + + it('should fail when appConfigStore read fails', function(done) { + appConfigStorageStub.listItems.withArgs(EMAIL_ADDR_DB_KEY).yieldsAsync(new Error()); + appConfigStorageStub.storeList.yieldsAsync(new Error()); + + updateHandler.update(function(error) { + expect(error).to.exist; + expect(appConfigStorageStub.listItems.calledTwice).to.be.true; + expect(appConfigStorageStub.storeList.called).to.be.false; + + done(); + }); + }); + }); + + describe('v4 -> v5', function() { + var FOLDER_TYPE_INBOX = 'Inbox'; + var FOLDER_TYPE_SENT = 'Sent'; + var FOLDER_TYPE_DRAFTS = 'Drafts'; + var FOLDER_TYPE_TRASH = 'Trash'; + + var FOLDER_DB_TYPE = 'folders'; + var VERSION_DB_TYPE = 'dbVersion'; + + var POST_UPDATE_DB_VERSION = 5; + + beforeEach(function() { + cfg.dbVersion = 5; // app requires database version 4 + appConfigStorageStub.listItems.withArgs(VERSION_DB_TYPE).yieldsAsync(null, [4]); // database version is 4 + }); + + afterEach(function() { + // database version is only queried for version checking prior to the update script + // so no need to check this in case-specific tests + expect(appConfigStorageStub.listItems.calledOnce).to.be.true; + }); + + it('should work', function(done) { + userStorageStub.listItems.withArgs(FOLDER_DB_TYPE).yieldsAsync(null, [ + [{ + name: 'inbox1', + type: FOLDER_TYPE_INBOX + }, { + name: 'inbox2', + type: FOLDER_TYPE_INBOX + }, { + name: 'sent1', + type: FOLDER_TYPE_SENT + }, { + name: 'sent2', + type: FOLDER_TYPE_SENT + }, { + name: 'drafts1', + type: FOLDER_TYPE_DRAFTS + }, { + name: 'drafts2', + type: FOLDER_TYPE_DRAFTS + }, { + name: 'trash1', + type: FOLDER_TYPE_TRASH + }, { + name: 'trash2', + type: FOLDER_TYPE_TRASH + }] + ]); + + userStorageStub.storeList.withArgs([ + [{ + name: 'inbox1', + type: FOLDER_TYPE_INBOX + }, { + name: 'sent1', + type: FOLDER_TYPE_SENT + }, { + name: 'drafts1', + type: FOLDER_TYPE_DRAFTS + }, { + name: 'trash1', + type: FOLDER_TYPE_TRASH + }] + ], FOLDER_DB_TYPE).yieldsAsync(); + + appConfigStorageStub.storeList.withArgs([POST_UPDATE_DB_VERSION], VERSION_DB_TYPE).yieldsAsync(); + + updateHandler.update(function(error) { + expect(error).to.not.exist; + expect(userStorageStub.listItems.calledOnce).to.be.true; + expect(userStorageStub.storeList.calledOnce).to.be.true; + expect(appConfigStorageStub.storeList.calledOnce).to.be.true; + + done(); + }); + }); + + it('should fail when persisting database version fails', function(done) { + userStorageStub.listItems.yieldsAsync(null, []); + userStorageStub.storeList.yieldsAsync(); + appConfigStorageStub.storeList.yieldsAsync(new Error()); + + updateHandler.update(function(error) { + expect(error).to.exist; + expect(userStorageStub.listItems.calledOnce).to.be.true; + expect(userStorageStub.storeList.calledOnce).to.be.true; + expect(appConfigStorageStub.storeList.calledOnce).to.be.true; + + done(); + }); + }); + + it('should fail when persisting folders fails', function(done) { + userStorageStub.listItems.yieldsAsync(null, []); + userStorageStub.storeList.yieldsAsync(new Error()); + + updateHandler.update(function(error) { + expect(error).to.exist; + expect(userStorageStub.listItems.calledOnce).to.be.true; + expect(userStorageStub.storeList.calledOnce).to.be.true; + expect(appConfigStorageStub.storeList.called).to.be.false; + + done(); + }); + }); + + it('should fail when listing folders fails', function(done) { + userStorageStub.listItems.yieldsAsync(new Error()); + + updateHandler.update(function(error) { + expect(error).to.exist; + expect(userStorageStub.listItems.calledOnce).to.be.true; + expect(userStorageStub.storeList.called).to.be.false; + expect(appConfigStorageStub.storeList.called).to.be.false; + + done(); }); }); }); diff --git a/test/unit/write-ctrl-test.js b/test/unit/write-ctrl-test.js index b53ffd3..2df9468 100644 --- a/test/unit/write-ctrl-test.js +++ b/test/unit/write-ctrl-test.js @@ -1,401 +1,397 @@ -define(function(require) { - 'use strict'; +'use strict'; - var expect = chai.expect, - angular = require('angular'), - mocks = require('angularMocks'), - WriteCtrl = require('js/controller/write'), - EmailDAO = require('js/dao/email-dao'), - OutboxBO = require('js/bo/outbox'), - KeychainDAO = require('js/dao/keychain-dao'), - appController = require('js/app-controller'); +var mocks = angular.mocks, + WriteCtrl = require('../../src/js/controller/write'), + EmailDAO = require('../../src/js/dao/email-dao'), + OutboxBO = require('../../src/js/bo/outbox'), + KeychainDAO = require('../../src/js/dao/keychain-dao'), + appController = require('../../src/js/app-controller'); - describe('Write controller unit test', function() { - var ctrl, scope, - origEmailDao, origOutbox, origKeychain, - emailDaoMock, keychainMock, outboxMock, emailAddress, realname; +describe('Write controller unit test', function() { + var ctrl, scope, + origEmailDao, origOutbox, origKeychain, + emailDaoMock, keychainMock, outboxMock, emailAddress, realname; - beforeEach(function() { - // the app controller is a singleton, we need to remember the - // outbox and email dao to restore it after the tests - origEmailDao = appController._emailDao; - origOutbox = appController._outboxBo; - origKeychain = appController._keychain; + beforeEach(function() { + // the app controller is a singleton, we need to remember the + // outbox and email dao to restore it after the tests + origEmailDao = appController._emailDao; + origOutbox = appController._outboxBo; + origKeychain = appController._keychain; - outboxMock = sinon.createStubInstance(OutboxBO); - appController._outboxBo = outboxMock; + outboxMock = sinon.createStubInstance(OutboxBO); + appController._outboxBo = outboxMock; - emailDaoMock = sinon.createStubInstance(EmailDAO); - appController._emailDao = emailDaoMock; + emailDaoMock = sinon.createStubInstance(EmailDAO); + appController._emailDao = emailDaoMock; - emailAddress = 'fred@foo.com'; - realname = 'Fred Foo'; - emailDaoMock._account = { - emailAddress: emailAddress, - realname: realname - }; + emailAddress = 'fred@foo.com'; + realname = 'Fred Foo'; + emailDaoMock._account = { + emailAddress: emailAddress, + realname: realname + }; - keychainMock = sinon.createStubInstance(KeychainDAO); - appController._keychain = keychainMock; + keychainMock = sinon.createStubInstance(KeychainDAO); + appController._keychain = keychainMock; - angular.module('writetest', []); - mocks.module('writetest'); - mocks.inject(function($rootScope, $controller) { - scope = $rootScope.$new(); - scope.state = {}; - ctrl = $controller(WriteCtrl, { - $scope: scope - }); + angular.module('writetest', []); + mocks.module('writetest'); + mocks.inject(function($rootScope, $controller) { + scope = $rootScope.$new(); + scope.state = {}; + ctrl = $controller(WriteCtrl, { + $scope: scope }); }); + }); - afterEach(function() { - // restore the app controller - appController._emailDao = origEmailDao; - appController._outboxBo = origOutbox; - appController._keychain = origKeychain; + afterEach(function() { + // restore the app controller + appController._emailDao = origEmailDao; + appController._outboxBo = origOutbox; + appController._keychain = origKeychain; + }); + + describe('scope variables', function() { + it('should be set correctly', function() { + expect(scope.state.writer).to.exist; + expect(scope.state.lightbox).to.be.undefined; + expect(scope.state.writer.write).to.exist; + expect(scope.state.writer.close).to.exist; + expect(scope.verify).to.exist; + expect(scope.checkSendStatus).to.exist; + expect(scope.sendToOutbox).to.exist; + expect(scope.tagStyle).to.exist; + expect(scope.lookupAddressBook).to.exist; + }); + }); + + describe('close', function() { + it('should close the writer', function() { + scope.state.lightbox = 'write'; + + scope.state.writer.close(); + + expect(scope.state.lightbox).to.be.undefined; + }); + }); + + describe('write', function() { + it('should prepare write view', function() { + var verifyMock = sinon.stub(scope, 'verify'); + + scope.state.writer.write(); + + expect(scope.writerTitle).to.equal('New email'); + expect(scope.to).to.deep.equal([]); + expect(scope.subject).to.equal(''); + expect(scope.body).to.equal(''); + expect(verifyMock.calledOnce).to.be.true; + + scope.verify.restore(); }); - describe('scope variables', function() { - it('should be set correctly', function() { - expect(scope.state.writer).to.exist; - expect(scope.state.lightbox).to.be.undefined; - expect(scope.state.writer.write).to.exist; - expect(scope.state.writer.close).to.exist; - expect(scope.verify).to.exist; - expect(scope.checkSendStatus).to.exist; - expect(scope.sendToOutbox).to.exist; - expect(scope.tagStyle).to.exist; - expect(scope.lookupAddressBook).to.exist; - }); + it('should prefill write view for response', function() { + var verifyMock = sinon.stub(scope, 'verify'), + address = 'pity@dafool', + subject = 'Ermahgerd!', + body = 'so much body!', + re = { + id: 'abc', + from: [{ + address: address + }], + subject: subject, + sentDate: new Date(), + body: body, + references: ['ghi', 'def'] + }; + + scope.sendBtnSecure = true; + + scope.state.writer.write(re); + + expect(scope.writerTitle).to.equal('Reply'); + expect(scope.to).to.deep.equal([{ + address: address, + }]); + expect(scope.subject).to.equal('Re: ' + subject); + expect(scope.body).to.contain(body); + expect(scope.references).to.deep.equal(['ghi', 'def', 'abc']); + expect(verifyMock.called).to.be.true; + + scope.verify.restore(); }); - describe('close', function() { - it('should close the writer', function() { - scope.state.lightbox = 'write'; - - scope.state.writer.close(); - - expect(scope.state.lightbox).to.be.undefined; - }); - }); - - describe('write', function() { - it('should prepare write view', function() { - var verifyMock = sinon.stub(scope, 'verify'); - - scope.state.writer.write(); - - expect(scope.writerTitle).to.equal('New email'); - expect(scope.to).to.deep.equal([]); - expect(scope.subject).to.equal(''); - expect(scope.body).to.equal(''); - expect(verifyMock.calledOnce).to.be.true; - - scope.verify.restore(); - }); - - it('should prefill write view for response', function() { - var verifyMock = sinon.stub(scope, 'verify'), - address = 'pity@dafool', - subject = 'Ermahgerd!', - body = 'so much body!', - re = { - id: 'abc', - from: [{ - address: address - }], - subject: subject, - sentDate: new Date(), - body: body, - references: ['ghi', 'def'] - }; - - scope.sendBtnSecure = true; - - scope.state.writer.write(re); - - expect(scope.writerTitle).to.equal('Reply'); - expect(scope.to).to.deep.equal([{ - address: address, - }]); - expect(scope.subject).to.equal('Re: ' + subject); - expect(scope.body).to.contain(body); - expect(scope.references).to.deep.equal(['ghi', 'def', 'abc']); - expect(verifyMock.called).to.be.true; - - scope.verify.restore(); - }); - - it('should prefill write view for forward', function() { - var verifyMock = sinon.stub(scope, 'verify'), - address = 'pity@dafool', - subject = 'Ermahgerd!', - body = 'so much body!', - re = { - from: [{ - address: address - }], - to: [{ - address: address - }], - subject: subject, - sentDate: new Date(), - body: body, - attachments: [{}] - }; - - scope.sendBtnSecure = false; - - scope.state.writer.write(re, null, true); - - expect(scope.writerTitle).to.equal('Forward'); - expect(scope.to).to.deep.equal([]); - expect(scope.subject).to.equal('Fwd: ' + subject); - expect(scope.body).to.contain(body); - expect(verifyMock.called).to.be.true; - expect(scope.attachments).to.not.equal(re.attachments); // not the same reference - expect(scope.attachments).to.deep.equal(re.attachments); // but the same content - - scope.verify.restore(); - }); - - }); - - describe('verify', function() { - var checkSendStatusMock; - - beforeEach(function() { - checkSendStatusMock = sinon.stub(scope, 'checkSendStatus'); - }); - - afterEach(function() { - scope.checkSendStatus.restore(); - }); - - it('should do nothing if recipient is not provided', function() { - scope.verify(undefined); - }); - - it('should not work for invalid email addresses', function() { - var recipient = { - address: '' + it('should prefill write view for forward', function() { + var verifyMock = sinon.stub(scope, 'verify'), + address = 'pity@dafool', + subject = 'Ermahgerd!', + body = 'so much body!', + re = { + from: [{ + address: address + }], + to: [{ + address: address + }], + subject: subject, + sentDate: new Date(), + body: body, + attachments: [{}] }; - scope.verify(recipient); + scope.sendBtnSecure = false; - expect(recipient.key).to.be.undefined; - expect(recipient.secure).to.be.undefined; - expect(scope.checkSendStatus.callCount).to.equal(2); - expect(keychainMock.getReceiverPublicKey.called).to.be.false; - }); + scope.state.writer.write(re, null, true); - it('should not work for error in keychain', function(done) { - var recipient = { - address: 'asds@example.com' - }; + expect(scope.writerTitle).to.equal('Forward'); + expect(scope.to).to.deep.equal([]); + expect(scope.subject).to.equal('Fwd: ' + subject); + expect(scope.body).to.contain(body); + expect(verifyMock.called).to.be.true; + expect(scope.attachments).to.not.equal(re.attachments); // not the same reference + expect(scope.attachments).to.deep.equal(re.attachments); // but the same content - keychainMock.refreshKeyForUserId.withArgs(recipient.address).yields({ - errMsg: '404 not found yadda yadda' - }); - - scope.onError = function() { - expect(recipient.key).to.be.undefined; - expect(recipient.secure).to.be.false; - expect(scope.checkSendStatus.callCount).to.equal(1); - expect(keychainMock.refreshKeyForUserId.calledOnce).to.be.true; - done(); - }; - - scope.verify(recipient); - }); - - it('should work for main userId', function(done) { - var recipient = { - address: 'asdf@example.com' - }; - - keychainMock.refreshKeyForUserId.withArgs(recipient.address).yields(null, { - userId: 'asdf@example.com' - }); - - scope.$digest = function() { - expect(recipient.key).to.deep.equal({ - userId: 'asdf@example.com' - }); - expect(recipient.secure).to.be.true; - expect(scope.checkSendStatus.callCount).to.equal(2); - expect(keychainMock.refreshKeyForUserId.calledOnce).to.be.true; - done(); - }; - - scope.verify(recipient); - }); - - it('should work for secondary userId', function(done) { - var recipient = { - address: 'asdf@example.com' - }; - var key = { - userId: 'qwer@example.com', - userIds: [{ - emailAddress: 'asdf@example.com' - }] - }; - - keychainMock.refreshKeyForUserId.withArgs(recipient.address).yields(null, key); - - scope.$digest = function() { - expect(recipient.key).to.deep.equal(key); - expect(recipient.secure).to.be.true; - expect(scope.checkSendStatus.callCount).to.equal(2); - expect(keychainMock.refreshKeyForUserId.calledOnce).to.be.true; - done(); - }; - - scope.verify(recipient); - }); - }); - - describe('checkSendStatus', function() { - beforeEach(function() { - scope.state.writer.write(); - }); - - afterEach(function() {}); - - it('should not be able to send with no recipients', function() { - scope.checkSendStatus(); - - expect(scope.okToSend).to.be.false; - expect(scope.sendBtnText).to.be.undefined; - expect(scope.sendBtnSecure).to.be.undefined; - }); - - it('should be able to send plaintext', function() { - scope.to = [{ - address: 'asdf@asdf.de' - }]; - scope.checkSendStatus(); - - expect(scope.okToSend).to.be.true; - expect(scope.sendBtnText).to.equal('Send'); - expect(scope.sendBtnSecure).to.be.false; - }); - - it('should send plaintext if one receiver is not secure', function() { - scope.to = [{ - address: 'asdf@asdf.de', - secure: true - }, { - address: 'asdf@asdfg.de' - }]; - scope.checkSendStatus(); - - expect(scope.okToSend).to.be.true; - expect(scope.sendBtnText).to.equal('Send'); - expect(scope.sendBtnSecure).to.be.false; - }); - - it('should be able to send securely to multiple recipients', function() { - scope.to = [{ - address: 'asdf@asdf.de', - secure: true - }, { - address: 'asdf@asdfg.de', - secure: true - }]; - scope.checkSendStatus(); - - expect(scope.okToSend).to.be.true; - expect(scope.sendBtnText).to.equal('Send securely'); - expect(scope.sendBtnSecure).to.be.true; - }); - }); - - describe('send to outbox', function() { - it('should work', function() { - scope.to = [{ - address: 'pity@dafool' - }]; - scope.cc = []; - scope.bcc = []; - scope.subject = 'Ermahgerd!'; - scope.body = 'wow. much body! very text!'; - scope.attachments = []; - scope.state.nav = { - currentFolder: 'currentFolder' - }; - - scope.replyTo = {}; - - outboxMock.put.withArgs(sinon.match(function(mail) { - expect(mail.from).to.deep.equal([{ - address: emailAddress, - name: realname - }]); - expect(mail.to).to.deep.equal(scope.to); - expect(mail.cc).to.deep.equal(scope.cc); - expect(mail.bcc).to.deep.equal(scope.bcc); - expect(mail.body).to.contain(scope.body); - expect(mail.subject).to.equal(scope.subject); - expect(mail.attachments).to.be.empty; - expect(mail.sentDate).to.exist; - - - return true; - })).yields(); - emailDaoMock.setFlags.yields(); - - scope.onError = function(err) { - expect(err).to.not.exist; - }; - - scope.sendToOutbox(); - - expect(outboxMock.put.calledOnce).to.be.true; - expect(emailDaoMock.setFlags.calledOnce).to.be.true; - expect(scope.state.lightbox).to.be.undefined; - expect(scope.replyTo.answered).to.be.true; - }); - }); - - describe('lookupAddressBook', function() { - it('should work', function(done) { - keychainMock.listLocalPublicKeys.yields(null, [{ - userId: 'test@asdf.com', - publicKey: 'KEY' - }]); - - var result = scope.lookupAddressBook('test'); - - result.then(function(response) { - expect(response).to.deep.equal([{ - address: 'test@asdf.com' - }]); - done(); - }); - scope.$digest(); - }); - - it('should work with cache', function(done) { - scope.addressBookCache = [{ - address: 'test@asdf.com' - }, { - address: 'tes@asdf.com' - }]; - - var result = scope.lookupAddressBook('test'); - - result.then(function(response) { - expect(response).to.deep.equal([{ - address: 'test@asdf.com' - }]); - done(); - }); - scope.$digest(); - }); + scope.verify.restore(); }); }); + + describe('verify', function() { + var checkSendStatusMock; + + beforeEach(function() { + checkSendStatusMock = sinon.stub(scope, 'checkSendStatus'); + }); + + afterEach(function() { + scope.checkSendStatus.restore(); + }); + + it('should do nothing if recipient is not provided', function() { + scope.verify(undefined); + }); + + it('should not work for invalid email addresses', function() { + var recipient = { + address: '' + }; + + scope.verify(recipient); + + expect(recipient.key).to.be.undefined; + expect(recipient.secure).to.be.undefined; + expect(scope.checkSendStatus.callCount).to.equal(2); + expect(keychainMock.getReceiverPublicKey.called).to.be.false; + }); + + it('should not work for error in keychain', function(done) { + var recipient = { + address: 'asds@example.com' + }; + + keychainMock.refreshKeyForUserId.withArgs(recipient.address).yields({ + errMsg: '404 not found yadda yadda' + }); + + scope.onError = function() { + expect(recipient.key).to.be.undefined; + expect(recipient.secure).to.be.false; + expect(scope.checkSendStatus.callCount).to.equal(1); + expect(keychainMock.refreshKeyForUserId.calledOnce).to.be.true; + done(); + }; + + scope.verify(recipient); + }); + + it('should work for main userId', function(done) { + var recipient = { + address: 'asdf@example.com' + }; + + keychainMock.refreshKeyForUserId.withArgs(recipient.address).yields(null, { + userId: 'asdf@example.com' + }); + + scope.$digest = function() { + expect(recipient.key).to.deep.equal({ + userId: 'asdf@example.com' + }); + expect(recipient.secure).to.be.true; + expect(scope.checkSendStatus.callCount).to.equal(2); + expect(keychainMock.refreshKeyForUserId.calledOnce).to.be.true; + done(); + }; + + scope.verify(recipient); + }); + + it('should work for secondary userId', function(done) { + var recipient = { + address: 'asdf@example.com' + }; + var key = { + userId: 'qwer@example.com', + userIds: [{ + emailAddress: 'asdf@example.com' + }] + }; + + keychainMock.refreshKeyForUserId.withArgs(recipient.address).yields(null, key); + + scope.$digest = function() { + expect(recipient.key).to.deep.equal(key); + expect(recipient.secure).to.be.true; + expect(scope.checkSendStatus.callCount).to.equal(2); + expect(keychainMock.refreshKeyForUserId.calledOnce).to.be.true; + done(); + }; + + scope.verify(recipient); + }); + }); + + describe('checkSendStatus', function() { + beforeEach(function() { + scope.state.writer.write(); + }); + + afterEach(function() {}); + + it('should not be able to send with no recipients', function() { + scope.checkSendStatus(); + + expect(scope.okToSend).to.be.false; + expect(scope.sendBtnText).to.be.undefined; + expect(scope.sendBtnSecure).to.be.undefined; + }); + + it('should be able to send plaintext', function() { + scope.to = [{ + address: 'asdf@asdf.de' + }]; + scope.checkSendStatus(); + + expect(scope.okToSend).to.be.true; + expect(scope.sendBtnText).to.equal('Send'); + expect(scope.sendBtnSecure).to.be.false; + }); + + it('should send plaintext if one receiver is not secure', function() { + scope.to = [{ + address: 'asdf@asdf.de', + secure: true + }, { + address: 'asdf@asdfg.de' + }]; + scope.checkSendStatus(); + + expect(scope.okToSend).to.be.true; + expect(scope.sendBtnText).to.equal('Send'); + expect(scope.sendBtnSecure).to.be.false; + }); + + it('should be able to send securely to multiple recipients', function() { + scope.to = [{ + address: 'asdf@asdf.de', + secure: true + }, { + address: 'asdf@asdfg.de', + secure: true + }]; + scope.checkSendStatus(); + + expect(scope.okToSend).to.be.true; + expect(scope.sendBtnText).to.equal('Send securely'); + expect(scope.sendBtnSecure).to.be.true; + }); + }); + + describe('send to outbox', function() { + it('should work', function() { + scope.to = [{ + address: 'pity@dafool' + }]; + scope.cc = []; + scope.bcc = []; + scope.subject = 'Ermahgerd!'; + scope.body = 'wow. much body! very text!'; + scope.attachments = []; + scope.state.nav = { + currentFolder: 'currentFolder' + }; + + scope.replyTo = {}; + + outboxMock.put.withArgs(sinon.match(function(mail) { + expect(mail.from).to.deep.equal([{ + address: emailAddress, + name: realname + }]); + expect(mail.to).to.deep.equal(scope.to); + expect(mail.cc).to.deep.equal(scope.cc); + expect(mail.bcc).to.deep.equal(scope.bcc); + expect(mail.body).to.contain(scope.body); + expect(mail.subject).to.equal(scope.subject); + expect(mail.attachments).to.be.empty; + expect(mail.sentDate).to.exist; + + + return true; + })).yields(); + emailDaoMock.setFlags.yields(); + + scope.onError = function(err) { + expect(err).to.not.exist; + }; + + scope.sendToOutbox(); + + expect(outboxMock.put.calledOnce).to.be.true; + expect(emailDaoMock.setFlags.calledOnce).to.be.true; + expect(scope.state.lightbox).to.be.undefined; + expect(scope.replyTo.answered).to.be.true; + }); + }); + + describe('lookupAddressBook', function() { + it('should work', function(done) { + keychainMock.listLocalPublicKeys.yields(null, [{ + userId: 'test@asdf.com', + publicKey: 'KEY' + }]); + + var result = scope.lookupAddressBook('test'); + + result.then(function(response) { + expect(response).to.deep.equal([{ + address: 'test@asdf.com' + }]); + done(); + }); + scope.$digest(); + }); + + it('should work with cache', function(done) { + scope.addressBookCache = [{ + address: 'test@asdf.com' + }, { + address: 'tes@asdf.com' + }]; + + var result = scope.lookupAddressBook('test'); + + result.then(function(response) { + expect(response).to.deep.equal([{ + address: 'test@asdf.com' + }]); + done(); + }); + scope.$digest(); + }); + }); + }); \ No newline at end of file From d9a2c77aa444fe51934ec8e5d0b1f02a79eec09d Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Wed, 8 Oct 2014 12:33:23 +0200 Subject: [PATCH 03/27] save package.json to use 4 spaces --- package.json | 142 +++++++++++++++++++++++++-------------------------- 1 file changed, 71 insertions(+), 71 deletions(-) diff --git a/package.json b/package.json index 614c632..8122c21 100644 --- a/package.json +++ b/package.json @@ -1,72 +1,72 @@ { - "name": "whiteout-mail", - "version": "0.0.1", - "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" - }, - "scripts": { - "test": "grunt && grunt test", - "start": "node server.js" - }, - "dependencies": { - "axe-logger": "~0.0.2", - "compression": "^1.0.11", - "config": "^1.0.2", - "crypto-lib": "~0.2.1", - "dompurify": "~0.4.2", - "express": "^4.8.3", - "imap-client": "~0.4.3", - "jquery": "~2.1.1", - "mailreader": "~0.3.5", - "morgan": "^1.2.3", - "ng-infinite-scroll": "~1.1.2", - "npmlog": "^0.1.1", - "pgpbuilder": "~0.4.0", - "pgpmailer": "~0.4.0", - "socket.io": "^1.0.6", - "tcp-socket": "^0.3.9", - "wo-smtpclient": "^0.3.8" - }, - "devDependencies": { - "angular-mocks": "^1.2.25", - "angularjs": "https://github.com/whiteout-io/angular.js/tarball/npm-version", - "browsercrow": "https://github.com/whiteout-io/browsercrow/tarball/master", - "browsersmtp": "https://github.com/whiteout-io/browsersmtp/tarball/master", - "chai": "~1.7.2", - "grunt": "~0.4.1", - "grunt-autoprefixer": "~0.7.2", - "grunt-browserify": "^3.0.1", - "grunt-contrib-clean": "~0.5.0", - "grunt-contrib-compress": "~0.5.2", - "grunt-contrib-connect": "~0.5.0", - "grunt-contrib-copy": "~0.4.1", - "grunt-contrib-jshint": "~0.6.4", - "grunt-contrib-sass": "~0.7.3", - "grunt-contrib-uglify": "^0.6.0", - "grunt-contrib-watch": "~0.5.3", - "grunt-csso": "~0.6.1", - "grunt-manifest": "^0.4.0", - "grunt-mocha": "~0.4.1", - "mocha": "~1.13.0", - "sinon": "~1.7.3", - "time-grunt": "^1.0.0" - } -} + "name": "whiteout-mail", + "version": "0.0.1", + "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" + }, + "scripts": { + "test": "grunt && grunt test", + "start": "node server.js" + }, + "dependencies": { + "axe-logger": "~0.0.2", + "compression": "^1.0.11", + "config": "^1.0.2", + "crypto-lib": "~0.2.1", + "dompurify": "~0.4.2", + "express": "^4.8.3", + "imap-client": "~0.4.3", + "jquery": "~2.1.1", + "mailreader": "~0.3.5", + "morgan": "^1.2.3", + "ng-infinite-scroll": "~1.1.2", + "npmlog": "^0.1.1", + "pgpbuilder": "~0.4.0", + "pgpmailer": "~0.4.0", + "socket.io": "^1.0.6", + "tcp-socket": "^0.3.9", + "wo-smtpclient": "^0.3.8" + }, + "devDependencies": { + "angular-mocks": "^1.2.25", + "angularjs": "https://github.com/whiteout-io/angular.js/tarball/npm-version", + "browsercrow": "https://github.com/whiteout-io/browsercrow/tarball/master", + "browsersmtp": "https://github.com/whiteout-io/browsersmtp/tarball/master", + "chai": "~1.7.2", + "grunt": "~0.4.1", + "grunt-autoprefixer": "~0.7.2", + "grunt-browserify": "^3.0.1", + "grunt-contrib-clean": "~0.5.0", + "grunt-contrib-compress": "~0.5.2", + "grunt-contrib-connect": "~0.5.0", + "grunt-contrib-copy": "~0.4.1", + "grunt-contrib-jshint": "~0.6.4", + "grunt-contrib-sass": "~0.7.3", + "grunt-contrib-uglify": "^0.6.0", + "grunt-contrib-watch": "~0.5.3", + "grunt-csso": "~0.6.1", + "grunt-manifest": "^0.4.0", + "grunt-mocha": "~0.4.1", + "mocha": "~1.13.0", + "sinon": "~1.7.3", + "time-grunt": "^1.0.0" + } +} \ No newline at end of file From 7a20049bbcd169b357323bd97007e45038189ade Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Wed, 8 Oct 2014 12:34:34 +0200 Subject: [PATCH 04/27] Use module.exports instead of exports --- src/js/app-controller.js | 2 +- src/js/bo/auth.js | 2 +- src/js/bo/outbox.js | 2 +- src/js/controller/about.js | 2 +- src/js/controller/account.js | 2 +- src/js/controller/add-account.js | 2 +- src/js/controller/contacts.js | 2 +- src/js/controller/dialog.js | 2 +- src/js/controller/login-existing.js | 2 +- src/js/controller/login-initial.js | 2 +- src/js/controller/login-new-device.js | 2 +- src/js/controller/login-privatekey-download.js | 2 +- src/js/controller/login-set-credentials.js | 2 +- src/js/controller/login.js | 2 +- src/js/controller/mail-list.js | 2 +- src/js/controller/navigation.js | 2 +- src/js/controller/popover.js | 2 +- src/js/controller/privatekey-upload.js | 2 +- src/js/controller/read.js | 2 +- src/js/controller/set-passphrase.js | 2 +- src/js/controller/write.js | 2 +- src/js/crypto/crypto.js | 2 +- src/js/crypto/pbkdf2.js | 2 +- src/js/crypto/pgp.js | 2 +- src/js/dao/admin-dao.js | 2 +- src/js/dao/devicestorage-dao.js | 2 +- src/js/dao/email-dao.js | 2 +- src/js/dao/invitation-dao.js | 2 +- src/js/dao/keychain-dao.js | 2 +- src/js/dao/lawnchair-dao.js | 2 +- src/js/dao/privatekey-dao.js | 2 +- src/js/dao/publickey-dao.js | 2 +- src/js/dao/rest-dao.js | 2 +- src/js/util/backbutton-handler.js | 2 +- src/js/util/connection-doctor.js | 2 +- src/js/util/download.js | 2 +- src/js/util/error.js | 2 +- src/js/util/notification.js | 2 +- src/js/util/oauth.js | 2 +- src/js/util/update/update-handler.js | 2 +- src/js/util/update/update-v1.js | 2 +- src/js/util/update/update-v2.js | 2 +- src/js/util/update/update-v3.js | 2 +- src/js/util/update/update-v4.js | 2 +- src/js/util/update/update-v5.js | 2 +- 45 files changed, 45 insertions(+), 45 deletions(-) diff --git a/src/js/app-controller.js b/src/js/app-controller.js index 82e0408..30218c4 100644 --- a/src/js/app-controller.js +++ b/src/js/app-controller.js @@ -265,4 +265,4 @@ self.onConnect = function(callback) { } }; -exports = self; \ No newline at end of file +module.exports = self; \ No newline at end of file diff --git a/src/js/bo/auth.js b/src/js/bo/auth.js index e7780d1..3916842 100644 --- a/src/js/bo/auth.js +++ b/src/js/bo/auth.js @@ -448,4 +448,4 @@ Auth.prototype.logout = function(callback) { }); }; -exports = Auth; \ No newline at end of file +module.exports = Auth; \ No newline at end of file diff --git a/src/js/bo/outbox.js b/src/js/bo/outbox.js index d67a5e5..12ccc50 100644 --- a/src/js/bo/outbox.js +++ b/src/js/bo/outbox.js @@ -226,4 +226,4 @@ OutboxBO.prototype._processOutbox = function(callback) { } }; -exports = OutboxBO; \ No newline at end of file +module.exports = OutboxBO; \ No newline at end of file diff --git a/src/js/controller/about.js b/src/js/controller/about.js index c88f965..5f2d39c 100644 --- a/src/js/controller/about.js +++ b/src/js/controller/about.js @@ -26,4 +26,4 @@ var AboutCtrl = function($scope) { // }; -exports = AboutCtrl; \ No newline at end of file +module.exports = AboutCtrl; \ No newline at end of file diff --git a/src/js/controller/account.js b/src/js/controller/account.js index 1ca2437..ba1a46b 100644 --- a/src/js/controller/account.js +++ b/src/js/controller/account.js @@ -56,4 +56,4 @@ var AccountCtrl = function($scope) { }; }; -exports = AccountCtrl; \ No newline at end of file +module.exports = AccountCtrl; \ No newline at end of file diff --git a/src/js/controller/add-account.js b/src/js/controller/add-account.js index 622c923..ccc1d17 100644 --- a/src/js/controller/add-account.js +++ b/src/js/controller/add-account.js @@ -115,4 +115,4 @@ var AddAccountCtrl = function($scope, $location, $routeParams) { }; }; -exports = AddAccountCtrl; \ No newline at end of file +module.exports = AddAccountCtrl; \ No newline at end of file diff --git a/src/js/controller/contacts.js b/src/js/controller/contacts.js index cdf11ea..0e72918 100644 --- a/src/js/controller/contacts.js +++ b/src/js/controller/contacts.js @@ -136,4 +136,4 @@ ngModule.directive('keyfileBtn', function() { }; }); -exports = ContactsCtrl; \ No newline at end of file +module.exports = ContactsCtrl; \ No newline at end of file diff --git a/src/js/controller/dialog.js b/src/js/controller/dialog.js index 912f7b9..8e33e22 100644 --- a/src/js/controller/dialog.js +++ b/src/js/controller/dialog.js @@ -11,4 +11,4 @@ var DialogCtrl = function($scope) { }; }; -exports = DialogCtrl; \ No newline at end of file +module.exports = DialogCtrl; \ No newline at end of file diff --git a/src/js/controller/login-existing.js b/src/js/controller/login-existing.js index d520471..006c2ec 100644 --- a/src/js/controller/login-existing.js +++ b/src/js/controller/login-existing.js @@ -68,4 +68,4 @@ var LoginExistingCtrl = function($scope, $location, $routeParams) { } }; -exports = LoginExistingCtrl; \ No newline at end of file +module.exports = LoginExistingCtrl; \ No newline at end of file diff --git a/src/js/controller/login-initial.js b/src/js/controller/login-initial.js index 7d00616..231f74e 100644 --- a/src/js/controller/login-initial.js +++ b/src/js/controller/login-initial.js @@ -116,4 +116,4 @@ var LoginInitialCtrl = function($scope, $location, $routeParams) { }; }; -exports = LoginInitialCtrl; \ No newline at end of file +module.exports = LoginInitialCtrl; \ No newline at end of file diff --git a/src/js/controller/login-new-device.js b/src/js/controller/login-new-device.js index e85c5e5..c9f16d1 100644 --- a/src/js/controller/login-new-device.js +++ b/src/js/controller/login-new-device.js @@ -135,4 +135,4 @@ ngModule.directive('fileReader', function() { }; }); -exports = LoginExistingCtrl; \ No newline at end of file +module.exports = LoginExistingCtrl; \ No newline at end of file diff --git a/src/js/controller/login-privatekey-download.js b/src/js/controller/login-privatekey-download.js index e120d7f..3e7aec5 100644 --- a/src/js/controller/login-privatekey-download.js +++ b/src/js/controller/login-privatekey-download.js @@ -129,4 +129,4 @@ var LoginPrivateKeyDownloadCtrl = function($scope, $location, $routeParams) { }; }; -exports = LoginPrivateKeyDownloadCtrl; \ No newline at end of file +module.exports = LoginPrivateKeyDownloadCtrl; \ No newline at end of file diff --git a/src/js/controller/login-set-credentials.js b/src/js/controller/login-set-credentials.js index 3e40baf..74bbe0b 100644 --- a/src/js/controller/login-set-credentials.js +++ b/src/js/controller/login-set-credentials.js @@ -115,4 +115,4 @@ var SetCredentialsCtrl = function($scope, $location, $routeParams) { }; }; -exports = SetCredentialsCtrl; \ No newline at end of file +module.exports = SetCredentialsCtrl; \ No newline at end of file diff --git a/src/js/controller/login.js b/src/js/controller/login.js index 26a5ee0..5e9d399 100644 --- a/src/js/controller/login.js +++ b/src/js/controller/login.js @@ -98,4 +98,4 @@ var LoginCtrl = function($scope, $location) { } }; -exports = LoginCtrl; \ No newline at end of file +module.exports = LoginCtrl; \ No newline at end of file diff --git a/src/js/controller/mail-list.js b/src/js/controller/mail-list.js index 332a4ae..280c63d 100644 --- a/src/js/controller/mail-list.js +++ b/src/js/controller/mail-list.js @@ -593,4 +593,4 @@ function createDummyMails() { return dummies; } -exports = MailListCtrl; \ No newline at end of file +module.exports = MailListCtrl; \ No newline at end of file diff --git a/src/js/controller/navigation.js b/src/js/controller/navigation.js index ccccaa2..a661f7c 100644 --- a/src/js/controller/navigation.js +++ b/src/js/controller/navigation.js @@ -203,4 +203,4 @@ ngModule.directive('keyShortcuts', function($timeout) { }; }); -exports = NavigationCtrl; \ No newline at end of file +module.exports = NavigationCtrl; \ No newline at end of file diff --git a/src/js/controller/popover.js b/src/js/controller/popover.js index ed6654a..5e40fbb 100644 --- a/src/js/controller/popover.js +++ b/src/js/controller/popover.js @@ -40,4 +40,4 @@ ngModule.directive('popover', function() { }; }); -exports = PopoverCtrl; \ No newline at end of file +module.exports = PopoverCtrl; \ No newline at end of file diff --git a/src/js/controller/privatekey-upload.js b/src/js/controller/privatekey-upload.js index 7c48dad..5a77c72 100644 --- a/src/js/controller/privatekey-upload.js +++ b/src/js/controller/privatekey-upload.js @@ -198,4 +198,4 @@ ngModule.directive('focusNext', function() { }; }); -exports = PrivateKeyUploadCtrl; \ No newline at end of file +module.exports = PrivateKeyUploadCtrl; \ No newline at end of file diff --git a/src/js/controller/read.js b/src/js/controller/read.js index acbfcb8..26aea1c 100644 --- a/src/js/controller/read.js +++ b/src/js/controller/read.js @@ -280,4 +280,4 @@ ngModule.directive('frameLoad', function($timeout, $window) { }; }); -exports = ReadCtrl; \ No newline at end of file +module.exports = ReadCtrl; \ No newline at end of file diff --git a/src/js/controller/set-passphrase.js b/src/js/controller/set-passphrase.js index 9dceaf4..724af8d 100644 --- a/src/js/controller/set-passphrase.js +++ b/src/js/controller/set-passphrase.js @@ -133,4 +133,4 @@ var SetPassphraseCtrl = function($scope) { } }; -exports = SetPassphraseCtrl; \ No newline at end of file +module.exports = SetPassphraseCtrl; \ No newline at end of file diff --git a/src/js/controller/write.js b/src/js/controller/write.js index 81f901d..ac058f3 100644 --- a/src/js/controller/write.js +++ b/src/js/controller/write.js @@ -527,4 +527,4 @@ ngModule.directive('attachmentBtn', function() { }; }); -exports = WriteCtrl; \ No newline at end of file +module.exports = WriteCtrl; \ No newline at end of file diff --git a/src/js/crypto/crypto.js b/src/js/crypto/crypto.js index a2d964e..f5187b8 100644 --- a/src/js/crypto/crypto.js +++ b/src/js/crypto/crypto.js @@ -119,4 +119,4 @@ function startWorker(options) { options.callback(null, result); } -exports = Crypto; \ No newline at end of file +module.exports = Crypto; \ No newline at end of file diff --git a/src/js/crypto/pbkdf2.js b/src/js/crypto/pbkdf2.js index e2d31f4..98842e6 100644 --- a/src/js/crypto/pbkdf2.js +++ b/src/js/crypto/pbkdf2.js @@ -21,4 +21,4 @@ self.getKey = function(password, salt, keySize) { return forge.util.encode64(key); }; -exports = self; \ No newline at end of file +module.exports = self; \ No newline at end of file diff --git a/src/js/crypto/pgp.js b/src/js/crypto/pgp.js index 63a84b7..8fe3d23 100644 --- a/src/js/crypto/pgp.js +++ b/src/js/crypto/pgp.js @@ -447,4 +447,4 @@ function checkSignatureValidity(signatures) { return true; } -exports = PGP; \ No newline at end of file +module.exports = PGP; \ No newline at end of file diff --git a/src/js/dao/admin-dao.js b/src/js/dao/admin-dao.js index 0a60e9e..9209c3b 100644 --- a/src/js/dao/admin-dao.js +++ b/src/js/dao/admin-dao.js @@ -58,4 +58,4 @@ AdminDAO.prototype.validateUser = function(options, callback) { }); }; -exports = AdminDAO; \ No newline at end of file +module.exports = AdminDAO; \ No newline at end of file diff --git a/src/js/dao/devicestorage-dao.js b/src/js/dao/devicestorage-dao.js index 0b2b9a0..8acb14d 100644 --- a/src/js/dao/devicestorage-dao.js +++ b/src/js/dao/devicestorage-dao.js @@ -90,4 +90,4 @@ function createKey(i, type) { return key; } -exports = DeviceStorageDAO; \ No newline at end of file +module.exports = DeviceStorageDAO; \ No newline at end of file diff --git a/src/js/dao/email-dao.js b/src/js/dao/email-dao.js index c7ef1f9..a64d3b1 100644 --- a/src/js/dao/email-dao.js +++ b/src/js/dao/email-dao.js @@ -1724,4 +1724,4 @@ function inlineExternalImages(message) { }); } -exports = EmailDAO; \ No newline at end of file +module.exports = EmailDAO; \ No newline at end of file diff --git a/src/js/dao/invitation-dao.js b/src/js/dao/invitation-dao.js index 3243ae1..41e16ab 100644 --- a/src/js/dao/invitation-dao.js +++ b/src/js/dao/invitation-dao.js @@ -57,4 +57,4 @@ InvitationDAO.prototype.invite = function(options, callback) { } }; -exports = InvitationDAO; \ No newline at end of file +module.exports = InvitationDAO; \ No newline at end of file diff --git a/src/js/dao/keychain-dao.js b/src/js/dao/keychain-dao.js index f847c63..298fff3 100644 --- a/src/js/dao/keychain-dao.js +++ b/src/js/dao/keychain-dao.js @@ -933,4 +933,4 @@ KeychainDAO.prototype.saveLocalPrivateKey = function(privkey, callback) { this._localDbDao.persist(prkLookupKey, privkey, callback); }; -exports = KeychainDAO; \ No newline at end of file +module.exports = KeychainDAO; \ No newline at end of file diff --git a/src/js/dao/lawnchair-dao.js b/src/js/dao/lawnchair-dao.js index d5812ae..685c7f2 100644 --- a/src/js/dao/lawnchair-dao.js +++ b/src/js/dao/lawnchair-dao.js @@ -213,4 +213,4 @@ LawnchairDAO.prototype.clear = function(callback) { this._db.nuke(callback); }; -exports = LawnchairDAO; \ No newline at end of file +module.exports = LawnchairDAO; \ No newline at end of file diff --git a/src/js/dao/privatekey-dao.js b/src/js/dao/privatekey-dao.js index 10979d7..8657790 100644 --- a/src/js/dao/privatekey-dao.js +++ b/src/js/dao/privatekey-dao.js @@ -193,4 +193,4 @@ PrivateKeyDAO.prototype.download = function(options, callback) { }, callback); }; -exports = PrivateKeyDAO; \ No newline at end of file +module.exports = PrivateKeyDAO; \ No newline at end of file diff --git a/src/js/dao/publickey-dao.js b/src/js/dao/publickey-dao.js index f4d6bff..0472871 100644 --- a/src/js/dao/publickey-dao.js +++ b/src/js/dao/publickey-dao.js @@ -100,4 +100,4 @@ PublicKeyDAO.prototype.remove = function(keyId, callback) { this._restDao.remove(uri, callback); }; -exports = PublicKeyDAO; \ No newline at end of file +module.exports = PublicKeyDAO; \ No newline at end of file diff --git a/src/js/dao/rest-dao.js b/src/js/dao/rest-dao.js index 17b3ce1..26da383 100644 --- a/src/js/dao/rest-dao.js +++ b/src/js/dao/rest-dao.js @@ -118,4 +118,4 @@ RestDAO.prototype._processRequest = function(options, callback) { xhr.send(options.payload ? JSON.stringify(options.payload) : undefined); }; -exports = RestDAO; \ No newline at end of file +module.exports = RestDAO; \ No newline at end of file diff --git a/src/js/util/backbutton-handler.js b/src/js/util/backbutton-handler.js index 930e61a..2990bc7 100644 --- a/src/js/util/backbutton-handler.js +++ b/src/js/util/backbutton-handler.js @@ -52,4 +52,4 @@ function handleBackButton(event) { } } -exports = backBtnHandler; \ No newline at end of file +module.exports = backBtnHandler; \ No newline at end of file diff --git a/src/js/util/connection-doctor.js b/src/js/util/connection-doctor.js index acd5a23..d6e3259 100644 --- a/src/js/util/connection-doctor.js +++ b/src/js/util/connection-doctor.js @@ -290,4 +290,4 @@ function createError(code, message, underlyingError) { return error; } -exports = ConnectionDoctor; \ No newline at end of file +module.exports = ConnectionDoctor; \ No newline at end of file diff --git a/src/js/util/download.js b/src/js/util/download.js index 5d9d2c1..a8bf537 100644 --- a/src/js/util/download.js +++ b/src/js/util/download.js @@ -55,4 +55,4 @@ dl.createDownload = function(options) { } }; -exports = dl; \ No newline at end of file +module.exports = dl; \ No newline at end of file diff --git a/src/js/util/error.js b/src/js/util/error.js index 7df0e6b..a7b4e5c 100644 --- a/src/js/util/error.js +++ b/src/js/util/error.js @@ -30,4 +30,4 @@ er.attachHandler = function(scope) { }; }; -exports = er; \ No newline at end of file +module.exports = er; \ No newline at end of file diff --git a/src/js/util/notification.js b/src/js/util/notification.js index e2cdd16..43a085d 100644 --- a/src/js/util/notification.js +++ b/src/js/util/notification.js @@ -55,4 +55,4 @@ self.close = function(notification) { notification.close(); }; -exports = self; \ No newline at end of file +module.exports = self; \ No newline at end of file diff --git a/src/js/util/oauth.js b/src/js/util/oauth.js index acc801a..f67be7c 100644 --- a/src/js/util/oauth.js +++ b/src/js/util/oauth.js @@ -96,4 +96,4 @@ OAuth.prototype.queryEmailAddress = function(token, callback) { }); }; -exports = OAuth; \ No newline at end of file +module.exports = OAuth; \ No newline at end of file diff --git a/src/js/util/update/update-handler.js b/src/js/util/update/update-handler.js index e070cb4..0928cbf 100644 --- a/src/js/util/update/update-handler.js +++ b/src/js/util/update/update-handler.js @@ -127,4 +127,4 @@ UpdateHandler.prototype.checkForUpdate = function(dialog) { } }; -exports = UpdateHandler; \ No newline at end of file +module.exports = UpdateHandler; \ No newline at end of file diff --git a/src/js/util/update/update-v1.js b/src/js/util/update/update-v1.js index 77c2b61..2e04afa 100644 --- a/src/js/util/update/update-v1.js +++ b/src/js/util/update/update-v1.js @@ -24,4 +24,4 @@ function updateV1(options, callback) { }); } -exports = updateV1; \ No newline at end of file +module.exports = updateV1; \ No newline at end of file diff --git a/src/js/util/update/update-v2.js b/src/js/util/update/update-v2.js index 74f62a7..9ebd0d6 100644 --- a/src/js/util/update/update-v2.js +++ b/src/js/util/update/update-v2.js @@ -23,4 +23,4 @@ function updateV2(options, callback) { }); } -exports = updateV2; \ No newline at end of file +module.exports = updateV2; \ No newline at end of file diff --git a/src/js/util/update/update-v3.js b/src/js/util/update/update-v3.js index 82cbf01..3408bf6 100644 --- a/src/js/util/update/update-v3.js +++ b/src/js/util/update/update-v3.js @@ -23,4 +23,4 @@ function update(options, callback) { }); } -exports = update; \ No newline at end of file +module.exports = update; \ No newline at end of file diff --git a/src/js/util/update/update-v4.js b/src/js/util/update/update-v4.js index b481aac..0a02a06 100644 --- a/src/js/util/update/update-v4.js +++ b/src/js/util/update/update-v4.js @@ -100,4 +100,4 @@ function update(options, callback) { } } -exports = update; \ No newline at end of file +module.exports = update; \ No newline at end of file diff --git a/src/js/util/update/update-v5.js b/src/js/util/update/update-v5.js index 338c470..5284b10 100644 --- a/src/js/util/update/update-v5.js +++ b/src/js/util/update/update-v5.js @@ -51,4 +51,4 @@ function update(options, callback) { }); } -exports = update; \ No newline at end of file +module.exports = update; \ No newline at end of file From 0bfef4a710d0cdd51687d005444bd19333ea2c00 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Wed, 8 Oct 2014 18:36:00 +0200 Subject: [PATCH 05/27] Browserify runtime dependencies work --- .jshintrc | 1 + Gruntfile.js | 32 +++++++++++++++++++++++++------- package.json | 2 +- src/index.html | 2 ++ src/js/crypto/pbkdf2-worker.js | 2 ++ 5 files changed, 31 insertions(+), 8 deletions(-) diff --git a/.jshintrc b/.jshintrc index a3064a8..6c4c255 100644 --- a/.jshintrc +++ b/.jshintrc @@ -19,6 +19,7 @@ "predef": [ "self", + "importScripts", "console", "process", "chrome", diff --git a/Gruntfile.js b/Gruntfile.js index ce09983..01c6bc2 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -106,12 +106,17 @@ module.exports = function(grunt) { }, browserify: { - all: { + app: { files: { 'dist/js/app.min.js': ['src/js/app.js'] }, options: { - external: [] + external: ['openpgp', 'node-forge', 'net', 'tls', 'crypto'] // node.js apis not required at build time + } + }, + pbkdf2Worker: { + files: { + 'dist/js/pbkdf2-worker.min.js': ['src/js/crypto/pbkdf2-worker.js'] } }, unitTest: { @@ -122,27 +127,25 @@ module.exports = function(grunt) { external: [] } }, - /* + /* TODO: mailreader-worker: {}, - pbkdf2-worker: {}, integrationTest: {} */ }, uglify: { - all: { + app: { files: { 'dist/js/app.min.js': [ - 'src/lib/openpgp/openpgp.js', 'src/lib/underscore/underscore-min.js', 'node_modules/jquery/dist/jquery.min.js', 'src/lib/angular/angular.min.js', 'src/lib/angular/angular-route.min.js', 'src/lib/angular/angular-animate.min.js', 'src/lib/ngtagsinput/ng-tags-input.min.js', - 'src/lib/fastclick/fastclick.js', 'node_modules/ng-infinite-scroll/build/ng-infinite-scroll.min.js', + 'src/lib/fastclick/fastclick.js', 'src/lib/lawnchair/lawnchair-git.js', 'src/lib/lawnchair/lawnchair-adapter-webkit-sqlite-git.js', 'src/lib/lawnchair/lawnchair-adapter-indexed-db-git.js', @@ -151,6 +154,11 @@ module.exports = function(grunt) { ] } }, + pbkdf2Worker: { + files: { + 'dist/js/pbkdf2-worker.min.js': ['dist/js/pbkdf2-worker.min.js'] + } + }, unitTest: { files: { 'test/unit/index.js': [ @@ -171,6 +179,9 @@ module.exports = function(grunt) { 'test/lib/angular-mocks.js', 'test/unit/index.js' ] + }, + options: { + compress: false } }, options: { @@ -186,6 +197,13 @@ module.exports = function(grunt) { src: ['mocha/mocha.css', 'mocha/mocha.js', 'chai/chai.js', 'sinon/pkg/sinon.js', 'browsercrow/src/*.js', 'browsersmtp/src/*.js'], dest: 'test/lib/' }, + lib: { + expand: true, + flatten: true, + cwd: 'src/lib/', + src: ['openpgp/openpgp.js', 'openpgp/openpgp.worker.js', 'forge/forge.min.js'], + dest: 'dist/js/' + }, font: { expand: true, cwd: 'src/font/', diff --git a/package.json b/package.json index 8122c21..8b8a61c 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "pgpbuilder": "~0.4.0", "pgpmailer": "~0.4.0", "socket.io": "^1.0.6", - "tcp-socket": "^0.3.9", + "tcp-socket": "https://github.com/whiteout-io/tcp-socket/tarball/dev/browserify", "wo-smtpclient": "^0.3.8" }, "devDependencies": { diff --git a/src/index.html b/src/index.html index c1fad5b..7868cc7 100644 --- a/src/index.html +++ b/src/index.html @@ -26,6 +26,8 @@ + + diff --git a/src/js/crypto/pbkdf2-worker.js b/src/js/crypto/pbkdf2-worker.js index 665ddf0..d9107d0 100644 --- a/src/js/crypto/pbkdf2-worker.js +++ b/src/js/crypto/pbkdf2-worker.js @@ -1,5 +1,7 @@ 'use strict'; +importScripts('forge.min.js'); + var pbkdf2 = require('./pbkdf2'); /** From fc613ce501c4eb46c00b3608d7bf94b28c246d79 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Wed, 8 Oct 2014 18:43:41 +0200 Subject: [PATCH 06/27] Correct worker paths --- src/js/crypto/crypto.js | 2 +- src/js/crypto/pgp.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/js/crypto/crypto.js b/src/js/crypto/crypto.js index f5187b8..382f80f 100644 --- a/src/js/crypto/crypto.js +++ b/src/js/crypto/crypto.js @@ -10,7 +10,7 @@ var aes = require('crypto-lib').aes, config = require('../app-config').config, axe = require('axe-logger'); -var PBKDF2_WORKER = '/crypto/pbkdf2-worker.js'; +var PBKDF2_WORKER = config.workerPath + '/pbkdf2-worker.js'; var Crypto = function() {}; diff --git a/src/js/crypto/pgp.js b/src/js/crypto/pgp.js index 8fe3d23..7be0fd4 100644 --- a/src/js/crypto/pgp.js +++ b/src/js/crypto/pgp.js @@ -9,7 +9,7 @@ var util = openpgp.util, var PGP = function() { openpgp.config.prefer_hash_algorithm = openpgp.enums.hash.sha256; - openpgp.initWorker(config.workerPath + '/../lib/openpgp/openpgp.worker.js'); + openpgp.initWorker(config.workerPath + '/openpgp.worker.js'); }; /** From 5d608cad6756c4200c1d8ef317e756220c27dd5a Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Wed, 8 Oct 2014 19:56:25 +0200 Subject: [PATCH 07/27] Starting app works. Added source maps to build --- .gitignore | 1 - Gruntfile.js | 36 +++++++++++++++++++++++++++++++----- src/tpl/read-sandbox.html | 3 +-- 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index babcf49..6d6229e 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,3 @@ release/ test/integration/src/ .elasticbeanstalk/ test/unit/index.js -test/unit/index.js.map diff --git a/Gruntfile.js b/Gruntfile.js index 01c6bc2..7d64888 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -108,7 +108,7 @@ module.exports = function(grunt) { browserify: { app: { files: { - 'dist/js/app.min.js': ['src/js/app.js'] + 'dist/js/app.browserified.js': ['src/js/app.js'] }, options: { external: ['openpgp', 'node-forge', 'net', 'tls', 'crypto'] // node.js apis not required at build time @@ -116,7 +116,7 @@ module.exports = function(grunt) { }, pbkdf2Worker: { files: { - 'dist/js/pbkdf2-worker.min.js': ['src/js/crypto/pbkdf2-worker.js'] + 'dist/js/pbkdf2-worker.browserified.js': ['src/js/crypto/pbkdf2-worker.js'] } }, unitTest: { @@ -150,13 +150,31 @@ module.exports = function(grunt) { 'src/lib/lawnchair/lawnchair-adapter-webkit-sqlite-git.js', 'src/lib/lawnchair/lawnchair-adapter-indexed-db-git.js', 'node_modules/dompurify/purify.js', - 'dist/js/app.min.js' + 'dist/js/app.browserified.js' ] + }, + options: { + mangle: false, + sourceMap: true, + sourceMapName: 'dist/js/app.min.js.map' } }, + readSandbox: { + files: { + 'dist/js/read-sandbox.min.js': [ + 'node_modules/dompurify/purify.js', + 'src/js/controller/read-sandbox.js' + ] + }, + options: { + sourceMap: true, + sourceMapName: 'dist/js/read-sandbox.min.js.map' + } + + }, pbkdf2Worker: { files: { - 'dist/js/pbkdf2-worker.min.js': ['dist/js/pbkdf2-worker.min.js'] + 'dist/js/pbkdf2-worker.min.js': ['dist/js/pbkdf2-worker.browserified.js'] } }, unitTest: { @@ -256,7 +274,15 @@ module.exports = function(grunt) { timestamp: true, hash: true, cache: ['socket.io/socket.io.js'], - exclude: ['appcache.manifest', 'manifest.webapp'], + exclude: [ + 'appcache.manifest', + 'manifest.webapp', + 'js/app.min.js.map', + 'js/app.browserified.js', + 'js/crypto/pbkdf2-worker.browserified.js', + 'js/pbkdf2-worker.browserified.js', + 'js/read-sandbox.min.js.map' + ], master: ['index.html'] }, src: ['**/*.*'], diff --git a/src/tpl/read-sandbox.html b/src/tpl/read-sandbox.html index e7d0b18..19edf4b 100644 --- a/src/tpl/read-sandbox.html +++ b/src/tpl/read-sandbox.html @@ -5,8 +5,7 @@ - - + From b2b39abd66baccd0e0352b2aaca3a8543bf6bed6 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Wed, 8 Oct 2014 20:05:07 +0200 Subject: [PATCH 08/27] Remove DOMpurify from main window --- Gruntfile.js | 1 - 1 file changed, 1 deletion(-) diff --git a/Gruntfile.js b/Gruntfile.js index 7d64888..119f042 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -149,7 +149,6 @@ module.exports = function(grunt) { 'src/lib/lawnchair/lawnchair-git.js', 'src/lib/lawnchair/lawnchair-adapter-webkit-sqlite-git.js', 'src/lib/lawnchair/lawnchair-adapter-indexed-db-git.js', - 'node_modules/dompurify/purify.js', 'dist/js/app.browserified.js' ] }, From 98dfcfcb22c18af1405de468b0d16c5b20e4e25e Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Thu, 9 Oct 2014 10:57:08 +0200 Subject: [PATCH 09/27] Start unit tests --- Gruntfile.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Gruntfile.js b/Gruntfile.js index 119f042..07ea495 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -124,7 +124,7 @@ module.exports = function(grunt) { 'test/unit/index.js': ['test/unit/*-test.js'] }, options: { - external: [] + external: ['openpgp', 'node-forge', 'net', 'tls', 'crypto'] } }, /* @@ -181,6 +181,7 @@ module.exports = function(grunt) { 'test/unit/index.js': [ 'src/lib/underscore/underscore-min.js', 'node_modules/jquery/dist/jquery.min.js', + 'src/lib/forge/forge.min.js', 'src/lib/openpgp/openpgp.js', 'src/lib/angular/angular.min.js', 'node_modules/angular-mocks/angular-mocks.js', From b0a186884a5f2237056fca944559d72ab42ebeef Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Thu, 9 Oct 2014 14:27:10 +0200 Subject: [PATCH 10/27] Fix jshint self errors --- package.json | 2 +- src/js/app-controller.js | 96 ++++++++++++++++++------------------- src/js/crypto/pbkdf2.js | 6 +-- src/js/util/notification.js | 14 +++--- 4 files changed, 59 insertions(+), 59 deletions(-) diff --git a/package.json b/package.json index 8b8a61c..7f82920 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "pgpbuilder": "~0.4.0", "pgpmailer": "~0.4.0", "socket.io": "^1.0.6", - "tcp-socket": "https://github.com/whiteout-io/tcp-socket/tarball/dev/browserify", + "tcp-socket": "^0.3.11", "wo-smtpclient": "^0.3.8" }, "devDependencies": { diff --git a/src/js/app-controller.js b/src/js/app-controller.js index 30218c4..36883e7 100644 --- a/src/js/app-controller.js +++ b/src/js/app-controller.js @@ -29,18 +29,18 @@ var axe = require('axe-logger'), config = appConfig.config, str = appConfig.string; -var self = {}; +var ctrl = {}; /** * Start the application. */ -self.start = function(options, callback) { - if (self.started) { +ctrl.start = function(options, callback) { + if (ctrl.started) { return callback(); } - self.started = true; - self.onError = options.onError; + ctrl.started = true; + ctrl.onError = options.onError; // are we running in a cordova app or in a browser environment? if (window.cordova) { @@ -56,20 +56,20 @@ self.start = function(options, callback) { function onDeviceReady() { axe.debug('Starting app.'); - self.buildModules(); + ctrl.buildModules(); // Handle offline and online gracefully - window.addEventListener('online', self.onConnect.bind(self, self.onError)); - window.addEventListener('offline', self.onDisconnect.bind(self)); + window.addEventListener('online', ctrl.onConnect.bind(ctrl, ctrl.onError)); + window.addEventListener('offline', ctrl.onDisconnect.bind(ctrl)); - self._appConfigStore.init('app-config', callback); + ctrl._appConfigStore.init('app-config', callback); } }; /** * Initialize the dependency tree. */ -self.buildModules = function() { +ctrl.buildModules = function() { var lawnchairDao, restDao, pubkeyDao, privkeyDao, crypto, emailDao, keychain, pgp, userStorage, pgpbuilder, oauth, appConfigStore, auth; // start the mailreader's worker thread @@ -83,13 +83,13 @@ self.buildModules = function() { oauth = new OAuth(new RestDAO('https://www.googleapis.com')); crypto = new Crypto(); - self._pgp = pgp = new PGP(); - self._keychain = keychain = new KeychainDAO(lawnchairDao, pubkeyDao, privkeyDao, crypto, pgp); + ctrl._pgp = pgp = new PGP(); + ctrl._keychain = keychain = new KeychainDAO(lawnchairDao, pubkeyDao, privkeyDao, crypto, pgp); keychain.requestPermissionForKeyUpdate = function(params, callback) { var message = params.newKey ? str.updatePublicKeyMsgNewKey : str.updatePublicKeyMsgRemovedKey; message = message.replace('{0}', params.userId); - self.onError({ + ctrl.onError({ title: str.updatePublicKeyTitle, message: message, positiveBtnStr: str.updatePublicKeyPosBtn, @@ -99,40 +99,40 @@ self.buildModules = function() { }); }; - self._appConfigStore = appConfigStore = new DeviceStorageDAO(new LawnchairDAO()); - self._auth = auth = new Auth(appConfigStore, oauth, pgp); - self._userStorage = userStorage = new DeviceStorageDAO(lawnchairDao); - self._invitationDao = new InvitationDAO(restDao); - self._pgpbuilder = pgpbuilder = new PgpBuilder(); - self._emailDao = emailDao = new EmailDAO(keychain, pgp, userStorage, pgpbuilder, mailreader); - self._outboxBo = new OutboxBO(emailDao, keychain, userStorage); - self._updateHandler = new UpdateHandler(appConfigStore, userStorage, auth); - self._adminDao = new AdminDao(new RestDAO(config.adminUrl)); - self._doctor = new ConnectionDoctor(); + ctrl._appConfigStore = appConfigStore = new DeviceStorageDAO(new LawnchairDAO()); + ctrl._auth = auth = new Auth(appConfigStore, oauth, pgp); + ctrl._userStorage = userStorage = new DeviceStorageDAO(lawnchairDao); + ctrl._invitationDao = new InvitationDAO(restDao); + ctrl._pgpbuilder = pgpbuilder = new PgpBuilder(); + ctrl._emailDao = emailDao = new EmailDAO(keychain, pgp, userStorage, pgpbuilder, mailreader); + ctrl._outboxBo = new OutboxBO(emailDao, keychain, userStorage); + ctrl._updateHandler = new UpdateHandler(appConfigStore, userStorage, auth); + ctrl._adminDao = new AdminDao(new RestDAO(config.adminUrl)); + ctrl._doctor = new ConnectionDoctor(); - emailDao.onError = self.onError; + emailDao.onError = ctrl.onError; }; /** * Calls runtime hooks to check if an app update is available. */ -self.checkForUpdate = function() { - self._updateHandler.checkForUpdate(self.onError); +ctrl.checkForUpdate = function() { + ctrl._updateHandler.checkForUpdate(ctrl.onError); }; /** * Instanciate the mail email data access object and its dependencies. Login to imap on init. */ -self.init = function(options, callback) { +ctrl.init = function(options, callback) { // init user's local database - self._userStorage.init(options.emailAddress, function(err) { + ctrl._userStorage.init(options.emailAddress, function(err) { if (err) { callback(err); return; } // Migrate the databases if necessary - self._updateHandler.update(onUpdate); + ctrl._updateHandler.update(onUpdate); }); function onUpdate(err) { @@ -152,7 +152,7 @@ self.init = function(options, callback) { }; // init email dao - self._emailDao.init({ + ctrl._emailDao.init({ account: account }, function(err, keypair) { if (err) { @@ -168,32 +168,32 @@ self.init = function(options, callback) { /** * Check if the user agent is online. */ -self.isOnline = function() { +ctrl.isOnline = function() { return navigator.onLine; }; /** * Event handler that is called when the user agent goes offline. */ -self.onDisconnect = function() { - self._emailDao.onDisconnect(); +ctrl.onDisconnect = function() { + ctrl._emailDao.onDisconnect(); }; /** * Log the current user out by clear the app config store and deleting instances of imap-client and pgp-mailer. */ -self.logout = function() { +ctrl.logout = function() { // clear app config store - self._auth.logout(function(err) { + ctrl._auth.logout(function(err) { if (err) { - self.onError(err); + ctrl.onError(err); return; } // delete instance of imap-client and pgp-mailer - self._emailDao.onDisconnect(function(err) { + ctrl._emailDao.onDisconnect(function(err) { if (err) { - self.onError(err); + ctrl.onError(err); return; } @@ -206,14 +206,14 @@ self.logout = function() { /** * 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. */ -self.onConnect = function(callback) { - if (!self.isOnline() || !self._emailDao || !self._emailDao._account) { +ctrl.onConnect = function(callback) { + if (!ctrl.isOnline() || !ctrl._emailDao || !ctrl._emailDao._account) { // prevent connection infinite loop callback(); return; } - self._auth.getCredentials(function(err, credentials) { + ctrl._auth.getCredentials(function(err, credentials) { if (err) { callback(err); return; @@ -226,22 +226,22 @@ self.onConnect = function(callback) { // add the maximum update batch size for imap folders to the imap configuration credentials.imap.maxUpdateSize = config.imapUpdateBatchSize; - var pgpMailer = new PgpMailer(credentials.smtp, self._pgpbuilder); + var pgpMailer = new PgpMailer(credentials.smtp, ctrl._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, self.onError); - pgpMailer.onCert = self._auth.handleCertificateUpdate.bind(self._auth, 'smtp', self.onConnect, self.onError); + imapClient.onCert = ctrl._auth.handleCertificateUpdate.bind(ctrl._auth, 'imap', ctrl.onConnect, ctrl.onError); + pgpMailer.onCert = ctrl._auth.handleCertificateUpdate.bind(ctrl._auth, 'smtp', ctrl.onConnect, ctrl.onError); // after-setup configuration depending on the provider: // gmail does not require you to upload to the sent items folder // after successful sending, whereas most other providers do - self._emailDao.ignoreUploadOnSent = !!(config[self._auth.provider] && config[self._auth.provider].ignoreUploadOnSent); + ctrl._emailDao.ignoreUploadOnSent = !!(config[ctrl._auth.provider] && config[ctrl._auth.provider].ignoreUploadOnSent); // connect to clients - self._emailDao.onConnect({ + ctrl._emailDao.onConnect({ imapClient: imapClient, pgpMailer: pgpMailer }, callback); @@ -253,7 +253,7 @@ self.onConnect = function(callback) { setTimeout(function() { axe.debug('Reconnecting...'); // re-init client modules on error - self.onConnect(function(err) { + ctrl.onConnect(function(err) { if (err) { axe.error('Reconnect attempt failed! ' + (err.errMsg || err.message) + (err.stack ? ('\n' + err.stack) : '')); return; @@ -265,4 +265,4 @@ self.onConnect = function(callback) { } }; -module.exports = self; \ No newline at end of file +module.exports = ctrl; \ No newline at end of file diff --git a/src/js/crypto/pbkdf2.js b/src/js/crypto/pbkdf2.js index 98842e6..e4403a2 100644 --- a/src/js/crypto/pbkdf2.js +++ b/src/js/crypto/pbkdf2.js @@ -4,7 +4,7 @@ 'use strict'; -var self = {}; +var pbkdf2 = {}; /** * PBKDF2-HMAC-SHA256 key derivation with a random salt and 10000 iterations @@ -13,7 +13,7 @@ var self = {}; * @param {String} keySize The key size in bits * @return {String} The base64 encoded key */ -self.getKey = function(password, salt, keySize) { +pbkdf2.getKey = function(password, salt, keySize) { var saltUtf8 = forge.util.decode64(salt); var md = forge.md.sha256.create(); var key = forge.pkcs5.pbkdf2(password, saltUtf8, 10000, keySize / 8, md); @@ -21,4 +21,4 @@ self.getKey = function(password, salt, keySize) { return forge.util.encode64(key); }; -module.exports = self; \ No newline at end of file +module.exports = pbkdf2; \ No newline at end of file diff --git a/src/js/util/notification.js b/src/js/util/notification.js index 43a085d..f3663b9 100644 --- a/src/js/util/notification.js +++ b/src/js/util/notification.js @@ -2,10 +2,10 @@ var cfg = require('../app-config').config; -var self = {}; +var notif = {}; if (window.Notification) { - self.hasPermission = Notification.permission === "granted"; + notif.hasPermission = Notification.permission === "granted"; } /** @@ -17,18 +17,18 @@ if (window.Notification) { * @param {Function} options.onClick (optional) callback when the notification is clicked * @returns {Notification} A notification instance */ -self.create = function(options) { +notif.create = function(options) { options.onClick = options.onClick || function() {}; if (!window.Notification) { return; } - if (!self.hasPermission) { + if (!notif.hasPermission) { // don't wait until callback returns Notification.requestPermission(function(permission) { if (permission === "granted") { - self.hasPermission = true; + notif.hasPermission = true; } }); } @@ -51,8 +51,8 @@ self.create = function(options) { return notification; }; -self.close = function(notification) { +notif.close = function(notification) { notification.close(); }; -module.exports = self; \ No newline at end of file +module.exports = notif; \ No newline at end of file From b5b95d1c5c4e72651a82afb4fd2d0e67fdac2879 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Thu, 9 Oct 2014 14:28:47 +0200 Subject: [PATCH 11/27] Browserify build for unit tests works --- Gruntfile.js | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/Gruntfile.js b/Gruntfile.js index 07ea495..f10a976 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -121,7 +121,42 @@ module.exports = function(grunt) { }, unitTest: { files: { - 'test/unit/index.js': ['test/unit/*-test.js'] + 'test/unit/index.js': [ + 'test/unit/oauth-test.js', + 'test/unit/auth-test.js', + 'test/unit/email-dao-test.js', + 'test/unit/app-controller-test.js', + 'test/unit/pgp-test.js', + 'test/unit/crypto-test.js', + 'test/unit/backbutton-handler-test.js', + 'test/unit/rest-dao-test.js', + 'test/unit/admin-dao-test.js', + 'test/unit/publickey-dao-test.js', + 'test/unit/privatekey-dao-test.js', + 'test/unit/lawnchair-dao-test.js', + 'test/unit/keychain-dao-test.js', + 'test/unit/devicestorage-dao-test.js', + 'test/unit/dialog-ctrl-test.js', + 'test/unit/add-account-ctrl-test.js', + 'test/unit/account-ctrl-test.js', + 'test/unit/set-passphrase-ctrl-test.js', + 'test/unit/contacts-ctrl-test.js', + 'test/unit/login-existing-ctrl-test.js', + 'test/unit/login-initial-ctrl-test.js', + 'test/unit/login-new-device-ctrl-test.js', + 'test/unit/login-privatekey-download-ctrl-test.js', + 'test/unit/login-set-credentials-ctrl-test.js', + 'test/unit/privatekey-upload-ctrl-test.js', + 'test/unit/login-ctrl-test.js', + 'test/unit/read-ctrl-test.js', + 'test/unit/navigation-ctrl-test.js', + 'test/unit/mail-list-ctrl-test.js', + 'test/unit/write-ctrl-test.js', + 'test/unit/outbox-bo-test.js', + 'test/unit/invitation-dao-test.js', + 'test/unit/update-handler-test.js', + 'test/unit/connection-doctor-test.js' + ] }, options: { external: ['openpgp', 'node-forge', 'net', 'tls', 'crypto'] From 7e09add0f48ee788ee6d3655e2b2efc53af33254 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Thu, 9 Oct 2014 15:23:41 +0200 Subject: [PATCH 12/27] Dao and crypto unit tests work --- .gitignore | 2 ++ Gruntfile.js | 37 ++++++++++++++++++++++++------------- src/js/app-controller.js | 2 +- src/js/crypto/crypto.js | 6 ++---- test/unit/index.html | 8 +++++--- test/unit/main.js | 4 ++++ 6 files changed, 38 insertions(+), 21 deletions(-) create mode 100644 test/unit/main.js diff --git a/.gitignore b/.gitignore index 6d6229e..e0ebbbc 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ release/ test/integration/src/ .elasticbeanstalk/ test/unit/index.js +**/*.browserified.js +**/*.js.map diff --git a/Gruntfile.js b/Gruntfile.js index f10a976..147da86 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -121,7 +121,7 @@ module.exports = function(grunt) { }, unitTest: { files: { - 'test/unit/index.js': [ + 'test/unit/index.browserified.js': [ 'test/unit/oauth-test.js', 'test/unit/auth-test.js', 'test/unit/email-dao-test.js', @@ -155,7 +155,8 @@ module.exports = function(grunt) { 'test/unit/outbox-bo-test.js', 'test/unit/invitation-dao-test.js', 'test/unit/update-handler-test.js', - 'test/unit/connection-doctor-test.js' + 'test/unit/connection-doctor-test.js', + 'test/unit/main.js' ] }, options: { @@ -216,25 +217,25 @@ module.exports = function(grunt) { 'test/unit/index.js': [ 'src/lib/underscore/underscore-min.js', 'node_modules/jquery/dist/jquery.min.js', - 'src/lib/forge/forge.min.js', - 'src/lib/openpgp/openpgp.js', 'src/lib/angular/angular.min.js', - 'node_modules/angular-mocks/angular-mocks.js', 'src/lib/angular/angular-route.min.js', 'src/lib/angular/angular-animate.min.js', 'src/lib/ngtagsinput/ng-tags-input.min.js', - 'src/lib/fastclick/fastclick.js', 'node_modules/ng-infinite-scroll/build/ng-infinite-scroll.min.js', + 'node_modules/angular-mocks/angular-mocks.js', + 'src/lib/fastclick/fastclick.js', 'src/lib/lawnchair/lawnchair-git.js', 'src/lib/lawnchair/lawnchair-adapter-webkit-sqlite-git.js', 'src/lib/lawnchair/lawnchair-adapter-indexed-db-git.js', - 'node_modules/dompurify/purify.js', 'test/lib/angular-mocks.js', - 'test/unit/index.js' + 'test/unit/index.browserified.js' ] }, options: { - compress: false + mangle: false, + compress: false, + sourceMap: true, + sourceMapName: 'test/unit/index.js.map' } }, options: { @@ -246,8 +247,19 @@ module.exports = function(grunt) { npmDev: { expand: true, flatten: true, - cwd: 'node_modules/', - src: ['mocha/mocha.css', 'mocha/mocha.js', 'chai/chai.js', 'sinon/pkg/sinon.js', 'browsercrow/src/*.js', 'browsersmtp/src/*.js'], + cwd: './', + src: [ + 'node_modules/mocha/mocha.css', + 'node_modules/mocha/mocha.js', + 'node_modules/chai/chai.js', + 'node_modules/sinon/pkg/sinon.js', + 'node_modules/browsercrow/src/*.js', + 'node_modules/browsersmtp/src/*.js', + 'src/lib/openpgp/openpgp.js', + 'src/lib/openpgp/openpgp.worker.js', + 'src/lib/forge/forge.min.js', + 'dist/js/pbkdf2-worker.min.js' + ], dest: 'test/lib/' }, lib: { @@ -343,11 +355,10 @@ module.exports = function(grunt) { grunt.loadNpmTasks('grunt-manifest'); // Build tasks - grunt.registerTask('dist-npm', ['copy:npmDev']); grunt.registerTask('dist-css', ['sass', 'autoprefixer', 'csso']); grunt.registerTask('dist-js', ['browserify', 'uglify']); grunt.registerTask('dist-copy', ['copy']); - grunt.registerTask('dist', ['clean', 'dist-npm', 'dist-css', 'dist-js', 'dist-copy', 'manifest']); + grunt.registerTask('dist', ['clean', 'dist-css', 'dist-js', 'dist-copy', 'manifest']); // Test/Dev tasks grunt.registerTask('dev', ['connect:dev']); diff --git a/src/js/app-controller.js b/src/js/app-controller.js index 36883e7..523fd73 100644 --- a/src/js/app-controller.js +++ b/src/js/app-controller.js @@ -73,7 +73,7 @@ ctrl.buildModules = function() { var lawnchairDao, restDao, pubkeyDao, privkeyDao, crypto, emailDao, keychain, pgp, userStorage, pgpbuilder, oauth, appConfigStore, auth; // start the mailreader's worker thread - mailreader.startWorker(config.workerPath + '/../lib/mailreader-parser-worker.js'); + mailreader.startWorker(config.workerPath + '/mailreader-parser-worker.js'); // init objects and inject dependencies restDao = new RestDAO(); diff --git a/src/js/crypto/crypto.js b/src/js/crypto/crypto.js index 382f80f..f19093a 100644 --- a/src/js/crypto/crypto.js +++ b/src/js/crypto/crypto.js @@ -10,8 +10,6 @@ var aes = require('crypto-lib').aes, config = require('../app-config').config, axe = require('axe-logger'); -var PBKDF2_WORKER = config.workerPath + '/pbkdf2-worker.js'; - var Crypto = function() {}; /** @@ -61,7 +59,7 @@ Crypto.prototype.decrypt = function(ciphertext, key, iv, callback) { */ Crypto.prototype.deriveKey = function(password, salt, keySize, callback) { startWorker({ - script: PBKDF2_WORKER, + script: config.workerPath + '/pbkdf2-worker.min.js', args: { password: password, salt: salt, @@ -82,7 +80,7 @@ function startWorker(options) { // check for WebWorker support if (window.Worker) { // init webworker thread - var worker = new Worker(config.workerPath + options.script); + var worker = new Worker(options.script); worker.onmessage = function(e) { if (e.data.err) { options.callback(e.data.err); diff --git a/test/unit/index.html b/test/unit/index.html index c6366e0..348d2ce 100644 --- a/test/unit/index.html +++ b/test/unit/index.html @@ -13,7 +13,7 @@ // // Polyfills // - + (function() { 'use strict'; // Mozilla bind polyfill because phantomjs is stupid @@ -57,7 +57,7 @@ })(); })(); - + @@ -67,8 +67,10 @@ mocha.setup('bdd'); + + - + - + - - + + + + + + + \ No newline at end of file diff --git a/test/integration/main.js b/test/integration/main.js deleted file mode 100644 index fcec906..0000000 --- a/test/integration/main.js +++ /dev/null @@ -1,61 +0,0 @@ -'use strict'; - -// Mozilla bind polyfill because phantomjs is stupid -if (!Function.prototype.bind) { - Function.prototype.bind = function(oThis) { - if (typeof this !== "function") { - // closest thing possible to the ECMAScript 5 internal IsCallable function - throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable"); - } - - var aArgs = Array.prototype.slice.call(arguments, 1), - fToBind = this, - FNOP = function() {}, - fBound = function() { - return fToBind.apply(this instanceof FNOP && oThis ? this : oThis, aArgs.concat(Array.prototype.slice.call(arguments))); - }; - - FNOP.prototype = this.prototype; - fBound.prototype = new FNOP(); - - return fBound; - }; -} - -require(['src/require-config'], function() { - require.config({ - baseUrl: 'src/lib', - paths: { - 'browsercrow': '../../../lib/browsercrow', - 'browsercrow-envelope': '../../../lib/browsercrow-envelope', - 'browsercrow-bodystructure': '../../../lib/browsercrow-bodystructure', - 'browsercrow-mimeparser': '../../../lib/browsercrow-mimeparser', - 'browsersmtp': '../../../lib/browsersmtp' - } - }); - - // Start the main app logic. - require(['js/app-config', 'axe'], function(app, axe) { - window.Worker = undefined; // disable web workers since mocha doesn't support them - - app.config.workerPath = '../../src/js'; - //app.config.cloudUrl = 'http://localhost:8888'; - - axe.removeAppender(axe.defaultAppender); - - startTests(); - }); -}); - -function startTests() { - mocha.setup('bdd'); - - require( - [ - '../../email-dao-test' - ], function() { - //Tests loaded, run tests - mocha.run(); - } - ); -} \ No newline at end of file diff --git a/test/main.js b/test/main.js new file mode 100644 index 0000000..8c26ce7 --- /dev/null +++ b/test/main.js @@ -0,0 +1,50 @@ +'use strict'; + +// +// Polyfills +// + +// Mozilla bind polyfill because phantomjs is stupid +if (!Function.prototype.bind) { + Function.prototype.bind = function(oThis) { + if (typeof this !== "function") { + // closest thing possible to the ECMAScript 5 internal IsCallable function + throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable"); + } + + var aArgs = Array.prototype.slice.call(arguments, 1), + fToBind = this, + FNOP = function() {}, + fBound = function() { + return fToBind.apply(this instanceof FNOP && oThis ? this : oThis, aArgs.concat(Array.prototype.slice.call(arguments))); + }; + + FNOP.prototype = this.prototype; + fBound.prototype = new FNOP(); + + return fBound; + }; +} + +// a warm round of applause for phantomjs for missing events +(function() { + if (!window.CustomEvent) { + var CustomEvent = function(event, params) { + params = params || { + bubbles: false, + cancelable: false, + detail: undefined + }; + var evt = document.createEvent('CustomEvent'); + evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); + return evt; + }; + + CustomEvent.prototype = window.Event.prototype; + + window.CustomEvent = CustomEvent; + } +})(); + +// set worker path for tests +require('../src/js/app-config').config.workerPath = '../lib'; \ No newline at end of file diff --git a/test/unit/index.html b/test/unit/index.html index 348d2ce..9eeb61c 100644 --- a/test/unit/index.html +++ b/test/unit/index.html @@ -9,55 +9,6 @@
- - @@ -73,7 +24,7 @@ diff --git a/test/unit/main.js b/test/unit/main.js deleted file mode 100644 index 589ba66..0000000 --- a/test/unit/main.js +++ /dev/null @@ -1,4 +0,0 @@ -'use strict'; - -// set worker path for tests -require('../../src/js/app-config').config.workerPath = '../lib'; \ No newline at end of file From c6fb31fd49403994236caad853568de005f22831 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Thu, 9 Oct 2014 17:31:01 +0200 Subject: [PATCH 15/27] Fix phantom mocha tests --- Gruntfile.js | 14 ++++---------- package.json | 4 ++-- test/integration/index.html | 6 +++++- test/unit/index.html | 7 ++++++- 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 08683e3..3f1318c 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -32,19 +32,13 @@ module.exports = function(grunt) { } }, - mocha: { + mocha_phantomjs: { all: { options: { urls: [ 'http://localhost:<%= connect.test.options.port %>/test/unit/index.html', 'http://localhost:<%= connect.test.options.port %>/test/integration/index.html' - ], - run: false, - reporter: 'Spec', - log: false, - - // phanotmjs is soooo slow - timeout: 100000 + ] } } }, @@ -367,7 +361,6 @@ module.exports = function(grunt) { grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.loadNpmTasks('grunt-contrib-connect'); grunt.loadNpmTasks('grunt-contrib-jshint'); - grunt.loadNpmTasks('grunt-mocha'); grunt.loadNpmTasks('grunt-contrib-clean'); grunt.loadNpmTasks('grunt-csso'); grunt.loadNpmTasks('grunt-contrib-sass'); @@ -376,6 +369,7 @@ module.exports = function(grunt) { grunt.loadNpmTasks('grunt-contrib-copy'); grunt.loadNpmTasks('grunt-contrib-compress'); grunt.loadNpmTasks('grunt-manifest'); + grunt.loadNpmTasks('grunt-mocha-phantomjs'); // Build tasks grunt.registerTask('dist-css', ['sass', 'autoprefixer', 'csso']); @@ -385,7 +379,7 @@ module.exports = function(grunt) { // Test/Dev tasks grunt.registerTask('dev', ['connect:dev']); - grunt.registerTask('test', ['jshint', 'connect:test', 'mocha']); + grunt.registerTask('test', ['jshint', 'connect:test', 'mocha_phantomjs']); grunt.registerTask('prod', ['connect:prod']); // diff --git a/package.json b/package.json index fcacaa1..5eb1d35 100644 --- a/package.json +++ b/package.json @@ -63,8 +63,8 @@ "grunt-contrib-watch": "~0.5.3", "grunt-csso": "~0.6.1", "grunt-manifest": "^0.4.0", - "grunt-mocha": "~0.4.1", - "mocha": "~1.13.0", + "grunt-mocha-phantomjs": "^0.6.0", + "mocha": "^1.21.4", "sinon": "~1.7.3", "time-grunt": "^1.0.0" } diff --git a/test/integration/index.html b/test/integration/index.html index f3742da..1cb63de 100644 --- a/test/integration/index.html +++ b/test/integration/index.html @@ -23,7 +23,11 @@ \ No newline at end of file diff --git a/test/unit/index.html b/test/unit/index.html index 9eeb61c..99e9412 100644 --- a/test/unit/index.html +++ b/test/unit/index.html @@ -25,7 +25,12 @@ From 0519b73cdcefe30a0f6272d4dcfe8beb165c4825 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Thu, 9 Oct 2014 18:39:08 +0200 Subject: [PATCH 16/27] Pipe source maps through browserify --- Gruntfile.js | 20 ++++++++++++++++++-- package.json | 1 + 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 3f1318c..d6dcc7d 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -105,7 +105,10 @@ module.exports = function(grunt) { 'dist/js/app.browserified.js': ['src/js/app.js'] }, options: { - external: ['openpgp', 'node-forge', 'net', 'tls', 'crypto'] // node.js apis not required at build time + external: ['openpgp', 'node-forge', 'net', 'tls', 'crypto'], // node.js apis not required at build time + browserifyOptions: { + debug: true + } } }, pbkdf2Worker: { @@ -174,6 +177,15 @@ module.exports = function(grunt) { */ }, + exorcise: { + bundle: { + options: {}, + files: { + 'dist/js/app.browserified.js.map': ['dist/js/app.browserified.js'], + } + } + }, + uglify: { app: { files: { @@ -195,6 +207,8 @@ module.exports = function(grunt) { options: { mangle: false, sourceMap: true, + sourceMapIn: 'dist/js/app.browserified.js.map', + sourceMapIncludeSources: true, sourceMapName: 'dist/js/app.min.js.map' } }, @@ -343,6 +357,7 @@ module.exports = function(grunt) { 'manifest.webapp', 'js/app.min.js.map', 'js/app.browserified.js', + 'js/app.browserified.js.map', 'js/crypto/pbkdf2-worker.browserified.js', 'js/pbkdf2-worker.browserified.js', 'js/read-sandbox.min.js.map' @@ -370,10 +385,11 @@ module.exports = function(grunt) { grunt.loadNpmTasks('grunt-contrib-compress'); grunt.loadNpmTasks('grunt-manifest'); grunt.loadNpmTasks('grunt-mocha-phantomjs'); + grunt.loadNpmTasks('grunt-exorcise'); // Build tasks grunt.registerTask('dist-css', ['sass', 'autoprefixer', 'csso']); - grunt.registerTask('dist-js', ['browserify', 'uglify']); + grunt.registerTask('dist-js', ['browserify', 'exorcise', 'uglify']); grunt.registerTask('dist-copy', ['copy']); grunt.registerTask('dist', ['clean', 'dist-css', 'dist-js', 'dist-copy', 'manifest']); diff --git a/package.json b/package.json index 5eb1d35..3c44a9a 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "grunt-contrib-uglify": "^0.6.0", "grunt-contrib-watch": "~0.5.3", "grunt-csso": "~0.6.1", + "grunt-exorcise": "^0.2.0", "grunt-manifest": "^0.4.0", "grunt-mocha-phantomjs": "^0.6.0", "mocha": "^1.21.4", From 033df2dea83adf489c6ef5fd165025e511c8ebad Mon Sep 17 00:00:00 2001 From: Felix Hammerl Date: Mon, 13 Oct 2014 18:00:01 +0200 Subject: [PATCH 17/27] [WO-567] browserify mailreader worker --- Gruntfile.js | 32 +++++++++++++++++++++----------- package.json | 5 +++-- src/js/app-controller.js | 2 +- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index d6dcc7d..fdfc7a0 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -116,6 +116,18 @@ module.exports = function(grunt) { 'dist/js/pbkdf2-worker.browserified.js': ['src/js/crypto/pbkdf2-worker.js'] } }, + mailreaderWorker: { + files: { + 'dist/js/mailreader-parser-worker.browserified.js': ['node_modules/mailreader/src/mailreader-parser-worker-browserify.js'] + }, + options: { + external: ['buffer'], // node.js apis not required at build time + browserifyOptions: { + debug: true + } + } + + }, unitTest: { files: { 'test/unit/index.browserified.js': [ @@ -171,10 +183,6 @@ module.exports = function(grunt) { external: ['openpgp', 'node-forge', 'net', 'tls', 'crypto'] } } - /* - TODO: - mailreader-worker: {}, - */ }, exorcise: { @@ -223,13 +231,21 @@ module.exports = function(grunt) { sourceMap: true, sourceMapName: 'dist/js/read-sandbox.min.js.map' } - }, pbkdf2Worker: { files: { 'dist/js/pbkdf2-worker.min.js': ['dist/js/pbkdf2-worker.browserified.js'] } }, + mailreaderWorker: { + files: { + 'dist/js/mailreader-parser-worker.min.js': ['dist/js/mailreader-parser-worker.browserified.js'] + }, + options: { + sourceMap: true, + sourceMapName: 'dist/js/mailreader-parser-worker.min.js.map' + } + }, unitTest: { files: { 'test/unit/index.js': [ @@ -323,12 +339,6 @@ module.exports = function(grunt) { cwd: 'src/', src: ['*.html', '*.js', '*.json', 'manifest.*'], dest: 'dist/' - }, - integration: { - expand: true, - cwd: 'src/', - src: ['**'], - dest: 'test/integration/src/' } }, diff --git a/package.json b/package.json index 3c44a9a..89c3197 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ }, "scripts": { "test": "grunt && grunt test", - "start": "node server.js" + "start": "node server.js", + "postinstall": "dir=$(pwd) && cd node_modules/mailreader/ && npm install --production && cd $dir" }, "dependencies": { "axe-logger": "~0.0.2", @@ -35,7 +36,7 @@ "express": "^4.8.3", "imap-client": "~0.4.3", "jquery": "~2.1.1", - "mailreader": "~0.3.5", + "mailreader": "https://github.com/whiteout-io/mailreader/tarball/dev/WO-567", "morgan": "^1.2.3", "ng-infinite-scroll": "~1.1.2", "npmlog": "^0.1.1", diff --git a/src/js/app-controller.js b/src/js/app-controller.js index 523fd73..8cd86c8 100644 --- a/src/js/app-controller.js +++ b/src/js/app-controller.js @@ -73,7 +73,7 @@ ctrl.buildModules = function() { var lawnchairDao, restDao, pubkeyDao, privkeyDao, crypto, emailDao, keychain, pgp, userStorage, pgpbuilder, oauth, appConfigStore, auth; // start the mailreader's worker thread - mailreader.startWorker(config.workerPath + '/mailreader-parser-worker.js'); + mailreader.startWorker(config.workerPath + '/mailreader-parser-worker.min.js'); // init objects and inject dependencies restDao = new RestDAO(); From 6e1ad91f6aec7d2ec84933633d8feaef09e79139 Mon Sep 17 00:00:00 2001 From: Felix Hammerl Date: Mon, 13 Oct 2014 18:38:28 +0200 Subject: [PATCH 18/27] [WO-567] use browserify-friendly tls-client --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 89c3197..bae2b3c 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "pgpbuilder": "~0.4.0", "pgpmailer": "~0.4.0", "socket.io": "^1.0.6", - "tcp-socket": "^0.3.11", + "tcp-socket": "https://github.com/whiteout-io/tcp-socket/tarball/dev/WO-567", "wo-smtpclient": "^0.3.8" }, "devDependencies": { From c120818792350ae744b8ef2e79d8d62b1b69a6d6 Mon Sep 17 00:00:00 2001 From: Felix Hammerl Date: Mon, 13 Oct 2014 19:11:04 +0200 Subject: [PATCH 19/27] [WO-567] use multithreaded tls --- Gruntfile.js | 7 +++++++ src/js/app-controller.js | 3 +++ 2 files changed, 10 insertions(+) diff --git a/Gruntfile.js b/Gruntfile.js index fdfc7a0..8cbfb58 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -316,6 +316,13 @@ module.exports = function(grunt) { src: ['openpgp/openpgp.js', 'openpgp/openpgp.worker.js', 'forge/forge.min.js'], dest: 'dist/js/' }, + tls: { + expand: true, + flatten: true, + cwd: 'node_modules/tcp-socket/src/', + src: ['tcp-socket-tls-worker.js', 'tcp-socket-tls.js'], + dest: 'dist/js/' + }, font: { expand: true, cwd: 'src/font/', diff --git a/src/js/app-controller.js b/src/js/app-controller.js index 8cd86c8..e768105 100644 --- a/src/js/app-controller.js +++ b/src/js/app-controller.js @@ -226,6 +226,9 @@ ctrl.onConnect = function(callback) { // 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.js'; + var pgpMailer = new PgpMailer(credentials.smtp, ctrl._pgpbuilder); var imapClient = new ImapClient(credentials.imap); imapClient.onError = onConnectionError; From 2cb008c8c3b1a544489b8c3c0a62747925d4ad70 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Tue, 14 Oct 2014 12:32:00 +0200 Subject: [PATCH 20/27] Stub node buffer using browserify ignore, src maps in tests --- Gruntfile.js | 53 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 8cbfb58..ae55133 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -6,6 +6,14 @@ module.exports = function(grunt) { var version = grunt.option('release'), zipName = (version) ? version : 'DEV'; + var browserifyOpt = { + exclude: ['openpgp', 'node-forge', 'net', 'tls', 'crypto'], // node apis not required at build time + ignore: ['buffer'], // node apis to be stubbed for runtime + browserifyOptions: { + debug: true + } + }; + // Project configuration. grunt.initConfig({ @@ -104,29 +112,19 @@ module.exports = function(grunt) { files: { 'dist/js/app.browserified.js': ['src/js/app.js'] }, - options: { - external: ['openpgp', 'node-forge', 'net', 'tls', 'crypto'], // node.js apis not required at build time - browserifyOptions: { - debug: true - } - } + options: browserifyOpt }, pbkdf2Worker: { files: { 'dist/js/pbkdf2-worker.browserified.js': ['src/js/crypto/pbkdf2-worker.js'] - } + }, + options: browserifyOpt }, mailreaderWorker: { files: { 'dist/js/mailreader-parser-worker.browserified.js': ['node_modules/mailreader/src/mailreader-parser-worker-browserify.js'] }, - options: { - external: ['buffer'], // node.js apis not required at build time - browserifyOptions: { - debug: true - } - } - + options: browserifyOpt }, unitTest: { files: { @@ -168,9 +166,7 @@ module.exports = function(grunt) { 'test/main.js' ] }, - options: { - external: ['openpgp', 'node-forge', 'net', 'tls', 'crypto'] - } + options: browserifyOpt }, integrationTest: { files: { @@ -180,17 +176,28 @@ module.exports = function(grunt) { ] }, options: { - external: ['openpgp', 'node-forge', 'net', 'tls', 'crypto'] + exclude: browserifyOpt.exclude, + //ignore: ['buffer'], // comment in after browsercrow and browsersmtp use current npm deps + browserifyOptions: browserifyOpt.browserifyOptions } } }, exorcise: { - bundle: { - options: {}, + app: { files: { 'dist/js/app.browserified.js.map': ['dist/js/app.browserified.js'], } + }, + unitTest: { + files: { + 'test/unit/index.browserified.js.map': ['test/unit/index.browserified.js'], + } + }, + integrationTest: { + files: { + 'test/integration/index.browserified.js.map': ['test/integration/index.browserified.js'], + } } }, @@ -263,8 +270,9 @@ module.exports = function(grunt) { }, options: { mangle: false, - compress: false, sourceMap: true, + sourceMapIn: 'test/unit/index.browserified.js.map', + sourceMapIncludeSources: true, sourceMapName: 'test/unit/index.js.map' } }, @@ -280,8 +288,9 @@ module.exports = function(grunt) { }, options: { mangle: false, - compress: false, sourceMap: true, + sourceMapIn: 'test/integration/index.browserified.js.map', + sourceMapIncludeSources: true, sourceMapName: 'test/integration/index.js.map' } }, From 046241e20dc08cbc494cd29fde84aa48bbbc5842 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Tue, 14 Oct 2014 13:01:20 +0200 Subject: [PATCH 21/27] Upgrade forge to v0.6.14 --- src/lib/forge/forge.min.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/forge/forge.min.js b/src/lib/forge/forge.min.js index a0d39aa..848d33e 100644 --- a/src/lib/forge/forge.min.js +++ b/src/lib/forge/forge.min.js @@ -1 +1 @@ -(function(e,t){typeof define=="function"&&define.amd?define([],t):e.forge=t()})(this,function(){var e,t,n;return function(r){function v(e,t){return h.call(e,t)}function m(e,t){var n,r,i,s,o,u,a,f,c,h,p,v=t&&t.split("/"),m=l.map,g=m&&m["*"]||{};if(e&&e.charAt(0)===".")if(t){v=v.slice(0,v.length-1),e=e.split("/"),o=e.length-1,l.nodeIdCompat&&d.test(e[o])&&(e[o]=e[o].replace(d,"")),e=v.concat(e);for(c=0;c0&&(e.splice(c-1,2),c-=2)}}e=e.join("/")}else e.indexOf("./")===0&&(e=e.substring(2));if((v||g)&&m){n=e.split("/");for(c=n.length;c>0;c-=1){r=n.slice(0,c).join("/");if(v)for(h=v.length;h>0;h-=1){i=m[v.slice(0,h).join("/")];if(i){i=i[r];if(i){s=i,u=c;break}}}if(s)break;!a&&g&&g[r]&&(a=g[r],f=c)}!s&&a&&(s=a,u=f),s&&(n.splice(0,u,s),e=n.join("/"))}return e}function g(e,t){return function(){return s.apply(r,p.call(arguments,0).concat([e,t]))}}function y(e){return function(t){return m(t,e)}}function b(e){return function(t){a[e]=t}}function w(e){if(v(f,e)){var t=f[e];delete f[e],c[e]=!0,i.apply(r,t)}if(!v(a,e)&&!v(c,e))throw new Error("No "+e);return a[e]}function E(e){var t,n=e?e.indexOf("!"):-1;return n>-1&&(t=e.substring(0,n),e=e.substring(n+1,e.length)),[t,e]}function S(e){return function(){return l&&l.config&&l.config[e]||{}}}var i,s,o,u,a={},f={},l={},c={},h=Object.prototype.hasOwnProperty,p=[].slice,d=/\.js$/;o=function(e,t){var n,r=E(e),i=r[0];return e=r[1],i&&(i=m(i,t),n=w(i)),i?n&&n.normalize?e=n.normalize(e,y(t)):e=m(e,t):(e=m(e,t),r=E(e),i=r[0],e=r[1],i&&(n=w(i))),{f:i?i+"!"+e:e,n:e,pr:i,p:n}},u={require:function(e){return g(e)},exports:function(e){var t=a[e];return typeof t!="undefined"?t:a[e]={}},module:function(e){return{id:e,uri:"",exports:a[e],config:S(e)}}},i=function(e,t,n,i){var s,l,h,p,d,m=[],y=typeof n,E;i=i||e;if(y==="undefined"||y==="function"){t=!t.length&&n.length?["require","exports","module"]:t;for(d=0;d0)t&1&&(n+=e),t>>>=1,t>0&&(e+=e);return this.data=n,this},t.ByteStringBuffer.prototype.putBytes=function(e){return this.data+=e,this},t.ByteStringBuffer.prototype.putString=function(e){return this.data+=t.encodeUtf8(e),this},t.ByteStringBuffer.prototype.putInt16=function(e){return this.data+=String.fromCharCode(e>>8&255)+String.fromCharCode(e&255),this},t.ByteStringBuffer.prototype.putInt24=function(e){return this.data+=String.fromCharCode(e>>16&255)+String.fromCharCode(e>>8&255)+String.fromCharCode(e&255),this},t.ByteStringBuffer.prototype.putInt32=function(e){return this.data+=String.fromCharCode(e>>24&255)+String.fromCharCode(e>>16&255)+String.fromCharCode(e>>8&255)+String.fromCharCode(e&255),this},t.ByteStringBuffer.prototype.putInt16Le=function(e){return this.data+=String.fromCharCode(e&255)+String.fromCharCode(e>>8&255),this},t.ByteStringBuffer.prototype.putInt24Le=function(e){return this.data+=String.fromCharCode(e&255)+String.fromCharCode(e>>8&255)+String.fromCharCode(e>>16&255),this},t.ByteStringBuffer.prototype.putInt32Le=function(e){return this.data+=String.fromCharCode(e&255)+String.fromCharCode(e>>8&255)+String.fromCharCode(e>>16&255)+String.fromCharCode(e>>24&255),this},t.ByteStringBuffer.prototype.putInt=function(e,t){do t-=8,this.data+=String.fromCharCode(e>>t&255);while(t>0);return this},t.ByteStringBuffer.prototype.putSignedInt=function(e,t){return e<0&&(e+=2<0);return t},t.ByteStringBuffer.prototype.getSignedInt=function(e){var t=this.getInt(e),n=2<=n&&(t-=n<<1),t},t.ByteStringBuffer.prototype.getBytes=function(e){var t;return e?(e=Math.min(this.length(),e),t=this.data.slice(this.read,this.read+e),this.read+=e):e===0?t="":(t=this.read===0?this.data:this.data.slice(this.read),this.clear()),t},t.ByteStringBuffer.prototype.bytes=function(e){return typeof e=="undefined"?this.data.slice(this.read):this.data.slice(this.read,this.read+e)},t.ByteStringBuffer.prototype.at=function(e){return this.data.charCodeAt(this.read+e)},t.ByteStringBuffer.prototype.setAt=function(e,t){return this.data=this.data.substr(0,this.read+e)+String.fromCharCode(t)+this.data.substr(this.read+e+1),this},t.ByteStringBuffer.prototype.last=function(){return this.data.charCodeAt(this.data.length-1)},t.ByteStringBuffer.prototype.copy=function(){var e=t.createBuffer(this.data);return e.read=this.read,e},t.ByteStringBuffer.prototype.compact=function(){return this.read>0&&(this.data=this.data.slice(this.read),this.read=0),this},t.ByteStringBuffer.prototype.clear=function(){return this.data="",this.read=0,this},t.ByteStringBuffer.prototype.truncate=function(e){var t=Math.max(0,this.length()-e);return this.data=this.data.substr(this.read,t),this.read=0,this},t.ByteStringBuffer.prototype.toHex=function(){var e="";for(var t=this.read;t=e)return this;t=Math.max(t||this.growSize,e);var n=new Uint8Array(this.data.buffer,this.data.byteOffset,this.data.byteLength),r=new Uint8Array(this.length()+t);return r.set(n),this.data=new DataView(r.buffer),this},t.DataBuffer.prototype.putByte=function(e){return this.accommodate(1),this.data.setUint8(this.write++,e),this},t.DataBuffer.prototype.fillWithByte=function(e,t){this.accommodate(t);for(var n=0;n>8&65535),this.data.setInt8(this.write,e>>16&255),this.write+=3,this},t.DataBuffer.prototype.putInt32=function(e){return this.accommodate(4),this.data.setInt32(this.write,e),this.write+=4,this},t.DataBuffer.prototype.putInt16Le=function(e){return this.accommodate(2),this.data.setInt16(this.write,e,!0),this.write+=2,this},t.DataBuffer.prototype.putInt24Le=function(e){return this.accommodate(3),this.data.setInt8(this.write,e>>16&255),this.data.setInt16(this.write,e>>8&65535,!0),this.write+=3,this},t.DataBuffer.prototype.putInt32Le=function(e){return this.accommodate(4),this.data.setInt32(this.write,e,!0),this.write+=4,this},t.DataBuffer.prototype.putInt=function(e,t){this.accommodate(t/8);do t-=8,this.data.setInt8(this.write++,e>>t&255);while(t>0);return this},t.DataBuffer.prototype.putSignedInt=function(e,t){return this.accommodate(t/8),e<0&&(e+=2<0);return t},t.DataBuffer.prototype.getSignedInt=function(e){var t=this.getInt(e),n=2<=n&&(t-=n<<1),t},t.DataBuffer.prototype.getBytes=function(e){var t;return e?(e=Math.min(this.length(),e),t=this.data.slice(this.read,this.read+e),this.read+=e):e===0?t="":(t=this.read===0?this.data:this.data.slice(this.read),this.clear()),t},t.DataBuffer.prototype.bytes=function(e){return typeof e=="undefined"?this.data.slice(this.read):this.data.slice(this.read,this.read+e)},t.DataBuffer.prototype.at=function(e){return this.data.getUint8(this.read+e)},t.DataBuffer.prototype.setAt=function(e,t){return this.data.setUint8(e,t),this},t.DataBuffer.prototype.last=function(){return this.data.getUint8(this.write-1)},t.DataBuffer.prototype.copy=function(){return new t.DataBuffer(this)},t.DataBuffer.prototype.compact=function(){if(this.read>0){var e=new Uint8Array(this.data.buffer,this.read),t=new Uint8Array(e.byteLength);t.set(e),this.data=new DataView(t),this.write-=this.read,this.read=0}return this},t.DataBuffer.prototype.clear=function(){return this.data=new DataView(new ArrayBuffer(0)),this.read=this.write=0,this},t.DataBuffer.prototype.truncate=function(e){return this.write=Math.max(0,this.length()-e),this.read=Math.min(this.read,this.write),this},t.DataBuffer.prototype.toHex=function(){var e="";for(var t=this.read;t0)t&1&&(n+=e),t>>>=1,t>0&&(e+=e);return n},t.xorBytes=function(e,t,n){var r="",i="",s="",o=0,u=0;for(;n>0;--n,++o)i=e.charCodeAt(o)^t.charCodeAt(o),u>=10&&(r+=s,s="",u=0),s+=String.fromCharCode(i),++u;return r+=s,r},t.hexToBytes=function(e){var t="",n=0;e.length&!0&&(n=1,t+=String.fromCharCode(parseInt(e[0],16)));for(;n>24&255)+String.fromCharCode(e>>16&255)+String.fromCharCode(e>>8&255)+String.fromCharCode(e&255)};var s="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",o=[62,-1,-1,-1,63,52,53,54,55,56,57,58,59,60,61,-1,-1,-1,64,-1,-1,-1,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,-1,-1,-1,-1,-1,-1,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51];t.encode64=function(e,t){var n="",r="",i,o,u,a=0;while(a>2),n+=s.charAt((i&3)<<4|o>>4),isNaN(o)?n+="==":(n+=s.charAt((o&15)<<2|u>>6),n+=isNaN(u)?"=":s.charAt(u&63)),t&&n.length>t&&(r+=n.substr(0,t)+"\r\n",n=n.substr(t));return r+=n,r},t.decode64=function(e){e=e.replace(/[^A-Za-z0-9\+\/\=]/g,"");var t="",n,r,i,s,u=0;while(u>4),i!==64&&(t+=String.fromCharCode((r&15)<<4|i>>2),s!==64&&(t+=String.fromCharCode((i&3)<<6|s)));return t},t.encodeUtf8=function(e){return unescape(encodeURIComponent(e))},t.decodeUtf8=function(e){return decodeURIComponent(escape(e))},t.binary={raw:{},hex:{},base64:{}},t.binary.raw.encode=function(e){return String.fromCharCode.apply(null,e)},t.binary.raw.decode=function(e,t,n){var r=t;r||(r=new Uint8Array(e.length)),n=n||0;var i=n;for(var s=0;s>2),n+=s.charAt((i&3)<<4|o>>4),isNaN(o)?n+="==":(n+=s.charAt((o&15)<<2|u>>6),n+=isNaN(u)?"=":s.charAt(u&63)),t&&n.length>t&&(r+=n.substr(0,t)+"\r\n",n=n.substr(t));return r+=n,r},t.binary.base64.decode=function(e,t,n){var r=t;r||(r=new Uint8Array(Math.ceil(e.length/4)*3)),e=e.replace(/[^A-Za-z0-9\+\/\=]/g,""),n=n||0;var i,s,u,a,f=0,l=n;while(f>4,u!==64&&(t[l++]=(s&15)<<4|u>>2,a!==64&&(t[l++]=(u&3)<<6|a));return t?l-n:t},t.text={utf8:{},utf16:{}},t.text.utf8.encode=function(e,n,r){e=t.encodeUtf8(e);var i=n;i||(i=new Uint8Array(e.length)),r=r||0;var s=r;for(var o=0;o0?(s=n[r].substring(0,i),o=n[r].substring(i+1)):(s=n[r],o=null),s in t||(t[s]=[]),!(s in Object.prototype)&&o!==null&&t[s].push(unescape(o))}return t},n;return typeof e=="undefined"?(d===null&&(typeof window=="undefined"?d={}:d=t(window.location.search.substring(1))),n=d):n=t(e),n},t.parseFragment=function(e){var n=e,r="",i=e.indexOf("?");i>0&&(n=e.substring(0,i),r=e.substring(i+1));var s=n.split("/");s.length>0&&s[0]===""&&s.shift();var o=r===""?{}:t.getQueryVariables(r);return{pathString:n,queryString:r,path:s,query:o}},t.makeRequest=function(e){var n=t.parseFragment(e),r={path:n.pathString,query:n.queryString,getPath:function(e){return typeof e=="undefined"?n.path:n.path[e]},getQuery:function(e,t){var r;return typeof e=="undefined"?r=n.query:(r=n.query[e],r&&typeof t!="undefined"&&(r=r[t])),r},getQueryLast:function(e,t){var n,i=r.getQuery(e);return i?n=i[i.length-1]:n=t,n}};return r},t.makeLink=function(e,t,n){e=jQuery.isArray(e)?e.join("/"):e;var r=jQuery.param(t||{});return n=n||"",e+(r.length>0?"?"+r:"")+(n.length>0?"#"+n:"")},t.setPath=function(e,t,n){if(typeof e=="object"&&e!==null){var r=0,i=t.length;while(r0&&s.push(r),o=t.lastIndex;var u=n[0][1];switch(u){case"s":case"o":i");break;case"%":s.push("%");break;default:s.push("<%"+u+"?>")}}return s.push(e.substring(o)),s.join("")},t.formatNumber=function(e,t,n,r){var i=e,s=isNaN(t=Math.abs(t))?2:t,o=n===undefined?",":n,u=r===undefined?".":r,a=i<0?"-":"",f=parseInt(i=Math.abs(+i||0).toFixed(s),10)+"",l=f.length>3?f.length%3:0;return a+(l?f.substr(0,l)+u:"")+f.substr(l).replace(/(\d{3})(?=\d)/g,"$1"+u)+(s?o+Math.abs(i-f).toFixed(s).slice(2):"")},t.formatSize=function(e){return e>=1073741824?e=t.formatNumber(e/1073741824,2,".","")+" GiB":e>=1048576?e=t.formatNumber(e/1048576,2,".","")+" MiB":e>=1024?e=t.formatNumber(e/1024,0)+" KiB":e=t.formatNumber(e,0)+" bytes",e},t.bytesFromIP=function(e){return e.indexOf(".")!==-1?t.bytesFromIPv4(e):e.indexOf(":")!==-1?t.bytesFromIPv6(e):null},t.bytesFromIPv4=function(e){e=e.split(".");if(e.length!==4)return null;var n=t.createBuffer();for(var r=0;rr[i].end-r[i].start&&(i=r.length-1))}n.push(o)}if(r.length>0){var f=r[i];f.end-f.start>0&&(n.splice(f.start,f.end-f.start+1,""),f.start===0&&n.unshift(""),f.end===7&&n.push(""))}return n.join(":")},t.estimateCores=function(e,n){function i(e,u,a){if(u===0){var f=Math.floor(e.reduce(function(e,t){return e+t},0)/e.length);return t.cores=Math.max(1,f),URL.revokeObjectURL(r),n(null,t.cores)}s(a,function(t,n){e.push(o(a,n)),i(e,u-1,a)})}function s(e,t){var n=[],i=[];for(var s=0;su.st&&i.sti.st&&u.st=this.blockSize||this._input.length()>0&&this._finish)this._op.call(this.mode,this._input,this.output);this._input.compact()},t.prototype.finish=function(e){e&&this.mode.name==="CBC"&&(this.mode.pad=function(t){return e(this.blockSize,t,!1)},this.mode.unpad=function(t){return e(this.blockSize,t,!0)});var t={};return t.decrypt=this._decrypt,t.overflow=this._input.length()%this.blockSize,!this._decrypt&&this.mode.pad&&!this.mode.pad(this._input,t)?!1:(this._finish=!0,this.update(),this._decrypt&&this.mode.unpad&&!this.mode.unpad(this.output,t)?!1:this.mode.afterFinish&&!this.mode.afterFinish(this.output,t)?!1:!0)}}var r="cipher";if(typeof n!="function"){if(typeof module!="object"||!module.exports)return typeof forge=="undefined"&&(forge={}),e(forge);var i=!0;n=function(e,n){n(t,module)}}var s,o=function(t,n){n.exports=function(n){var i=s.map(function(e){return t(e)}).concat(e);n=n||{},n.defined=n.defined||{};if(n.defined[r])return n[r];n.defined[r]=!0;for(var o=0;o4){var n=t;t=e.util.createBuffer();for(var r=0;r0)return!1;var n=e.length(),r=e.at(n-1);return r>this.blockSize<<2?!1:(e.truncate(r),!0)},t.cbc=function(e){e=e||{},this.name="CBC",this.cipher=e.cipher,this.blockSize=e.blockSize||16,this._blocks=this.blockSize/4,this._inBlock=new Array(this._blocks),this._outBlock=new Array(this._blocks)},t.cbc.prototype.start=function(e){if(e.iv===null){if(!this._prev)throw new Error("Invalid IV parameter.");this._iv=this._prev.slice(0)}else{if(!("iv"in e))throw new Error("Invalid IV parameter.");this._iv=n(e.iv),this._prev=this._iv.slice(0)}},t.cbc.prototype.encrypt=function(e,t){for(var n=0;n0)return!1;var n=e.length(),r=e.at(n-1);return r>this.blockSize<<2?!1:(e.truncate(r),!0)},t.cfb=function(e){e=e||{},this.name="CFB",this.cipher=e.cipher,this.blockSize=e.blockSize||16,this._blocks=this.blockSize/4,this._inBlock=null,this._outBlock=new Array(this._blocks)},t.cfb.prototype.start=function(e){if(!("iv"in e))throw new Error("Invalid IV parameter.");this._iv=n(e.iv),this._inBlock=this._iv.slice(0)},t.cfb.prototype.encrypt=function(e,t){this.cipher.encrypt(this._inBlock,this._outBlock);for(var n=0;n0&&e.truncate(this.blockSize-t.overflow),!0},t.ofb=function(e){e=e||{},this.name="OFB",this.cipher=e.cipher,this.blockSize=e.blockSize||16,this._blocks=this.blockSize/4,this._inBlock=null,this._outBlock=new Array(this._blocks)},t.ofb.prototype.start=function(e){if(!("iv"in e))throw new Error("Invalid IV parameter.");this._iv=n(e.iv),this._inBlock=this._iv.slice(0)},t.ofb.prototype.encrypt=function(e,t){this.cipher.encrypt(this._inBlock,this._outBlock);for(var n=0;n0&&e.truncate(this.blockSize-t.overflow),!0},t.ctr=function(e){e=e||{},this.name="CTR",this.cipher=e.cipher,this.blockSize=e.blockSize||16,this._blocks=this.blockSize/4,this._inBlock=null,this._outBlock=new Array(this._blocks)},t.ctr.prototype.start=function(e){if(!("iv"in e))throw new Error("Invalid IV parameter.");this._iv=n(e.iv),this._inBlock=this._iv.slice(0)},t.ctr.prototype.encrypt=function(e,t){this.cipher.encrypt(this._inBlock,this._outBlock),r(this._inBlock);for(var n=0;n0&&e.truncate(this.blockSize-t.overflow),!0},t.gcm=function(e){e=e||{},this.name="GCM",this.cipher=e.cipher,this.blockSize=e.blockSize||16,this._blocks=this.blockSize/4,this._inBlock=new Array(this._blocks),this._outBlock=new Array(this._blocks),this._R=3774873600},t.gcm.prototype.start=function(t){if(!("iv"in t))throw new Error("Invalid IV parameter.");var n=e.util.createBuffer(t.iv);this._cipherLength=0;var s;"additionalData"in t?s=e.util.createBuffer(t.additionalData):s=e.util.createBuffer(),"tagLength"in t?this._tagLength=t.tagLength:this._tagLength=128,this._tag=null;if(t.decrypt){this._tag=e.util.createBuffer(t.tag).getBytes();if(this._tag.length!==this._tagLength/8)throw new Error("Authentication tag does not match tag length.")}this._hashBlock=new Array(this._blocks),this.tag=null,this._hashSubkey=new Array(this._blocks),this.cipher.encrypt([0,0,0,0],this._hashSubkey),this.componentBits=4,this._m=this.generateHashTable(this._hashSubkey,this.componentBits);var o=n.length();if(o===12)this._j0=[n.getInt32(),n.getInt32(),n.getInt32(),1];else{this._j0=[0,0,0,0];while(n.length()>0)this._j0=this.ghash(this._hashSubkey,this._j0,[n.getInt32(),n.getInt32(),n.getInt32(),n.getInt32()]);this._j0=this.ghash(this._hashSubkey,this._j0,[0,0].concat(i(o*8)))}this._inBlock=this._j0.slice(0),r(this._inBlock),s=e.util.createBuffer(s),this._aDataLength=i(s.length()*8);var u=s.length()%this.blockSize;u&&s.fillWithByte(0,this.blockSize-u),this._s=[0,0,0,0];while(s.length()>0)this._s=this.ghash(this._hashSubkey,this._s,[s.getInt32(),s.getInt32(),s.getInt32(),s.getInt32()])},t.gcm.prototype.encrypt=function(t,n){this.cipher.encrypt(this._inBlock,this._outBlock),r(this._inBlock);var i=t.length();for(var s=0;s0;--r)t[r]=e[r]>>>1|(e[r-1]&1)<<31;t[0]=e[0]>>>1,n&&(t[0]^=this._R)},t.gcm.prototype.tableMultiply=function(e){var t=[0,0,0,0];for(var n=0;n<32;++n){var r=n/8|0,i=e[r]>>>(7-n%8)*4&15,s=this._m[n][i];t[0]^=s[0],t[1]^=s[1],t[2]^=s[2],t[3]^=s[3]}return t},t.gcm.prototype.ghash=function(e,t,n){return t[0]^=n[0],t[1]^=n[1],t[2]^=n[2],t[3]^=n[3],this.tableMultiply(t)},t.gcm.prototype.generateHashTable=function(e,t){var n=8/t,r=4*n,i=16*n,s=new Array(i);for(var o=0;o>>1,i=new Array(n);i[r]=e.slice(0);var s=r>>>1;while(s>0)this.pow(i[2*s],i[s]=[]),s>>=1;s=2;while(s>8^p&255^99,i[r]=p,s[p]=r,d=e[p],l=e[r],c=e[l],h=e[c],v=d<<24^p<<16^p<<8^(p^d),m=(l^c^h)<<24^(r^h)<<16^(r^c^h)<<8^(r^l^h);for(var g=0;g<4;++g)u[g][r]=v,a[g][p]=m,v=v<<24|v>>>8,m=m<<24|m>>>8;r===0?r=f=1:(r=l^e[e[e[l^h]]],f^=e[e[f]])}}function l(e,t){var n=e.slice(0),s,u=1,f=n.length,l=f+6+1,c=r*l;for(var h=f;h>>16&255]<<24^i[s>>>8&255]<<16^i[s&255]<<8^i[s>>>24]^o[u]<<24,u++):f>6&&h%f===4&&(s=i[s>>>24]<<24^i[s>>>16&255]<<16^i[s>>>8&255]<<8^i[s&255]),n[h]=n[h-f]^s;if(t){var p,d=a[0],v=a[1],m=a[2],g=a[3],y=n.slice(0);c=n.length;for(var h=0,b=c-r;h>>24]]^v[i[p>>>16&255]]^m[i[p>>>8&255]]^g[i[p&255]];n=y}return n}function c(e,t,n,r){var o=e.length/4-1,f,l,c,h,p;r?(f=a[0],l=a[1],c=a[2],h=a[3],p=s):(f=u[0],l=u[1],c=u[2],h=u[3],p=i);var d,v,m,g,y,b,w;d=t[0]^e[0],v=t[r?3:1]^e[1],m=t[2]^e[2],g=t[r?1:3]^e[3];var E=3;for(var S=1;S>>24]^l[v>>>16&255]^c[m>>>8&255]^h[g&255]^e[++E],b=f[v>>>24]^l[m>>>16&255]^c[g>>>8&255]^h[d&255]^e[++E],w=f[m>>>24]^l[g>>>16&255]^c[d>>>8&255]^h[v&255]^e[++E],g=f[g>>>24]^l[d>>>16&255]^c[v>>>8&255]^h[m&255]^e[++E],d=y,v=b,m=w;n[0]=p[d>>>24]<<24^p[v>>>16&255]<<16^p[m>>>8&255]<<8^p[g&255]^e[++E],n[r?3:1]=p[v>>>24]<<24^p[m>>>16&255]<<16^p[g>>>8&255]<<8^p[d&255]^e[++E],n[2]=p[m>>>24]<<24^p[g>>>16&255]<<16^p[d>>>8&255]<<8^p[v&255]^e[++E],n[r?1:3]=p[g>>>24]<<24^p[d>>>16&255]<<16^p[v>>>8&255]<<8^p[m&255]^e[++E]}function h(t){t=t||{};var n=(t.mode||"CBC").toUpperCase(),r="AES-"+n,i;t.decrypt?i=e.cipher.createDecipher(r,t.key):i=e.cipher.createCipher(r,t.key);var s=i.start;return i.start=function(t,n){var r=null;n instanceof e.util.ByteBuffer&&(r=n,n={}),n=n||{},n.output=r,n.iv=t,s.call(i,n)},i}e.aes=e.aes||{},e.aes.startEncrypting=function(e,t,n,r){var i=h({key:e,output:n,decrypt:!1,mode:r});return i.start(t),i},e.aes.createEncryptionCipher=function(e,t){return h({key:e,output:null,decrypt:!1,mode:t})},e.aes.startDecrypting=function(e,t,n,r){var i=h({key:e,output:n,decrypt:!0,mode:r});return i.start(t),i},e.aes.createDecryptionCipher=function(e,t){return h({key:e,output:null,decrypt:!0,mode:t})},e.aes.Algorithm=function(e,t){n||f();var r=this;r.name=e,r.mode=new t({blockSize:16,cipher:{encrypt:function(e,t){return c(r._w,e,t,!1)},decrypt:function(e,t){return c(r._w,e,t,!0)}}}),r._init=!1},e.aes.Algorithm.prototype.initialize=function(t){if(this._init)return;var n=t.key,r;if(typeof n!="string"||n.length!==16&&n.length!==24&&n.length!==32){if(e.util.isArray(n)&&(n.length===16||n.length===24||n.length===32)){r=n,n=e.util.createBuffer();for(var i=0;i>>=2;for(var i=0;i1){var p=r.read,d=r.getByte();if(d===0){o=r.getByte();var v=o&192;if(v===t.Class.UNIVERSAL||v===t.Class.CONTEXT_SPECIFIC)try{var m=n(r);h=m===f-(r.read-p),h&&(++p,--f)}catch(g){}}r.read=p}if(h){l=[];if(f===undefined)for(;;){if(r.bytes(2)===String.fromCharCode(0,0)){r.getBytes(2);break}l.push(t.fromDer(r,i))}else{var y=r.length();while(f>0)l.push(t.fromDer(r,i)),f-=y-r.length(),y=r.length()}}else{if(f===undefined){if(i)throw new Error("Non-constructed ASN.1 object of indefinite length.");f=r.length()}if(a===t.Type.BMPSTRING){l="";for(var b=0;b>>=8;while(u>0);r.putByte(a.length|128);for(var o=a.length-1;o>=0;--o)r.putByte(a.charCodeAt(o))}return r.putBuffer(s),r},t.oidToDer=function(t){var n=t.split("."),r=e.util.createBuffer();r.putByte(40*parseInt(n[0],10)+parseInt(n[1],10));var i,s,o,u;for(var a=2;a>>=7,i||(u|=128),s.push(u),i=!1;while(o>0);for(var f=s.length-1;f>=0;--f)r.putByte(s[f])}return r},t.derToOid=function(t){var n;typeof t=="string"&&(t=e.util.createBuffer(t));var r=t.getByte();n=Math.floor(r/40)+"."+r%40;var i=0;while(t.length()>0)r=t.getByte(),i<<=7,r&128?i+=r&127:(n+="."+(i+r),i=0);return n},t.utcTimeToDate=function(e){var t=new Date,n=parseInt(e.substr(0,2),10);n=n>=50?1900+n:2e3+n;var r=parseInt(e.substr(2,2),10)-1,i=parseInt(e.substr(4,2),10),s=parseInt(e.substr(6,2),10),o=parseInt(e.substr(8,2),10),u=0;if(e.length>11){var a=e.charAt(10),f=10;a!=="+"&&a!=="-"&&(u=parseInt(e.substr(10,2),10),f+=2)}t.setUTCFullYear(n,r,i),t.setUTCHours(s,o,u,0);if(f){a=e.charAt(f);if(a==="+"||a==="-"){var l=parseInt(e.substr(f+1,2),10),c=parseInt(e.substr(f+4,2),10),h=l*60+c;h*=6e4,a==="+"?t.setTime(+t-h):t.setTime(+t+h)}}return t},t.generalizedTimeToDate=function(e){var t=new Date,n=parseInt(e.substr(0,4),10),r=parseInt(e.substr(4,2),10)-1,i=parseInt(e.substr(6,2),10),s=parseInt(e.substr(8,2),10),o=parseInt(e.substr(10,2),10),u=parseInt(e.substr(12,2),10),a=0,f=0,l=!1;e.charAt(e.length-1)==="Z"&&(l=!0);var c=e.length-5,h=e.charAt(c);if(h==="+"||h==="-"){var p=parseInt(e.substr(c+1,2),10),d=parseInt(e.substr(c+4,2),10);f=p*60+d,f*=6e4,h==="+"&&(f*=-1),l=!0}return e.charAt(14)==="."&&(a=parseFloat(e.substr(14),10)*1e3),l?(t.setUTCFullYear(n,r,i),t.setUTCHours(s,o,u,a),t.setTime(+t+f)):(t.setFullYear(n,r,i),t.setHours(s,o,u,a)),t},t.dateToUtcTime=function(e){var t="",n=[];n.push((""+e.getUTCFullYear()).substr(2)),n.push(""+(e.getUTCMonth()+1)),n.push(""+e.getUTCDate()),n.push(""+e.getUTCHours()),n.push(""+e.getUTCMinutes()),n.push(""+e.getUTCSeconds());for(var r=0;r=-128&&t<128)return n.putSignedInt(t,8);if(t>=-32768&&t<32768)return n.putSignedInt(t,16);if(t>=-8388608&&t<8388608)return n.putSignedInt(t,24);if(t>=-2147483648&&t<2147483648)return n.putSignedInt(t,32);var r=new Error("Integer too large; max is 32-bits.");throw r.integer=t,r},t.derToInteger=function(t){typeof t=="string"&&(t=e.util.createBuffer(t));var n=t.length()*8;if(n>32)throw new Error("Integer too large; max is 32-bits.");return t.getSignedInt(n)},t.validate=function(n,r,i,s){var o=!1;if(n.tagClass!==r.tagClass&&typeof r.tagClass!="undefined"||n.type!==r.type&&typeof r.type!="undefined")s&&(n.tagClass!==r.tagClass&&s.push("["+r.name+"] "+'Expected tag class "'+r.tagClass+'", got "'+n.tagClass+'"'),n.type!==r.type&&s.push("["+r.name+"] "+'Expected type "'+r.type+'", got "'+n.type+'"'));else if(n.constructed===r.constructed||typeof r.constructed=="undefined"){o=!0;if(r.value&&e.util.isArray(r.value)){var u=0;for(var a=0;o&&a0&&(o+="\n");var u="";for(var a=0;a=64){u=e.h0,a=e.h1,f=e.h2,l=e.h3;for(p=0;p<16;++p)t[p]=n.getInt32Le(),c=l^a&(f^l),o=u+c+s[p]+t[p],h=i[p],u=l,l=f,f=a,a+=o<>>32-h;for(;p<32;++p)c=f^l&(a^f),o=u+c+s[p]+t[r[p]],h=i[p],u=l,l=f,f=a,a+=o<>>32-h;for(;p<48;++p)c=a^f^l,o=u+c+s[p]+t[r[p]],h=i[p],u=l,l=f,f=a,a+=o<>>32-h;for(;p<64;++p)c=f^(a|~l),o=u+c+s[p]+t[r[p]],h=i[p],u=l,l=f,f=a,a+=o<>>32-h;e.h0=e.h0+u|0,e.h1=e.h1+a|0,e.h2=e.h2+f|0,e.h3=e.h3+l|0,d-=64}}var t=e.md5=e.md5||{};e.md=e.md||{},e.md.algorithms=e.md.algorithms||{},e.md.md5=e.md.algorithms.md5=t,t.create=function(){o||u();var t=null,r=e.util.createBuffer(),i=new Array(16),s={algorithm:"md5",blockLength:64,digestLength:16,messageLength:0,messageLength64:[0,0]};return s.start=function(){return s.messageLength=0,s.messageLength64=[0,0],r=e.util.createBuffer(),t={h0:1732584193,h1:4023233417,h2:2562383102,h3:271733878},s},s.start(),s.update=function(n,o){return o==="utf8"&&(n=e.util.encodeUtf8(n)),s.messageLength+=n.length,s.messageLength64[0]+=n.length/4294967296>>>0,s.messageLength64[1]+=n.length>>>0,r.putBytes(n),a(t,i,r),(r.read>2048||r.length()===0)&&r.compact(),s},s.digest=function(){var o=e.util.createBuffer();o.putBytes(r.bytes()),o.putBytes(n.substr(0,64-(s.messageLength64[1]+8&63))),o.putInt32Le(s.messageLength64[1]<<3),o.putInt32Le(s.messageLength64[0]<<3|s.messageLength64[0]>>>28);var u={h0:t.h0,h1:t.h1,h2:t.h2,h3:t.h3};a(u,i,o);var f=e.util.createBuffer();return f.putInt32Le(u.h0),f.putInt32Le(u.h1),f.putInt32Le(u.h2),f.putInt32Le(u.h3),f},s};var n=null,r=null,i=null,s=null,o=!1}var r="md5";if(typeof n!="function"){if(typeof module!="object"||!module.exports)return typeof forge=="undefined"&&(forge={}),e(forge);var i=!0;n=function(e,n){n(t,module)}}var s,o=function(t,n){n.exports=function(n){var i=s.map(function(e){return t(e)}).concat(e);n=n||{},n.defined=n.defined||{};if(n.defined[r])return n[r];n.defined[r]=!0;for(var o=0;o=64){i=e.h0,s=e.h1,o=e.h2,u=e.h3,a=e.h4;for(l=0;l<16;++l)r=n.getInt32(),t[l]=r,f=u^s&(o^u),r=(i<<5|i>>>27)+f+a+1518500249+r,a=u,u=o,o=s<<30|s>>>2,s=i,i=r;for(;l<20;++l)r=t[l-3]^t[l-8]^t[l-14]^t[l-16],r=r<<1|r>>>31,t[l]=r,f=u^s&(o^u),r=(i<<5|i>>>27)+f+a+1518500249+r,a=u,u=o,o=s<<30|s>>>2,s=i,i=r;for(;l<32;++l)r=t[l-3]^t[l-8]^t[l-14]^t[l-16],r=r<<1|r>>>31,t[l]=r,f=s^o^u,r=(i<<5|i>>>27)+f+a+1859775393+r,a=u,u=o,o=s<<30|s>>>2,s=i,i=r;for(;l<40;++l)r=t[l-6]^t[l-16]^t[l-28]^t[l-32],r=r<<2|r>>>30,t[l]=r,f=s^o^u,r=(i<<5|i>>>27)+f+a+1859775393+r,a=u,u=o,o=s<<30|s>>>2,s=i,i=r;for(;l<60;++l)r=t[l-6]^t[l-16]^t[l-28]^t[l-32],r=r<<2|r>>>30,t[l]=r,f=s&o|u&(s^o),r=(i<<5|i>>>27)+f+a+2400959708+r,a=u,u=o,o=s<<30|s>>>2,s=i,i=r;for(;l<80;++l)r=t[l-6]^t[l-16]^t[l-28]^t[l-32],r=r<<2|r>>>30,t[l]=r,f=s^o^u,r=(i<<5|i>>>27)+f+a+3395469782+r,a=u,u=o,o=s<<30|s>>>2,s=i,i=r;e.h0=e.h0+i|0,e.h1=e.h1+s|0,e.h2=e.h2+o|0,e.h3=e.h3+u|0,e.h4=e.h4+a|0,c-=64}}var t=e.sha1=e.sha1||{};e.md=e.md||{},e.md.algorithms=e.md.algorithms||{},e.md.sha1=e.md.algorithms.sha1=t,t.create=function(){r||i();var t=null,o=e.util.createBuffer(),u=new Array(80),a={algorithm:"sha1",blockLength:64,digestLength:20,messageLength:0,messageLength64:[0,0]};return a.start=function(){return a.messageLength=0,a.messageLength64=[0,0],o=e.util.createBuffer(),t={h0:1732584193,h1:4023233417,h2:2562383102,h3:271733878,h4:3285377520},a},a.start(),a.update=function(n,r){return r==="utf8"&&(n=e.util.encodeUtf8(n)),a.messageLength+=n.length,a.messageLength64[0]+=n.length/4294967296>>>0,a.messageLength64[1]+=n.length>>>0,o.putBytes(n),s(t,u,o),(o.read>2048||o.length()===0)&&o.compact(),a},a.digest=function(){var r=e.util.createBuffer();r.putBytes(o.bytes()),r.putBytes(n.substr(0,64-(a.messageLength64[1]+8&63))),r.putInt32(a.messageLength64[0]<<3|a.messageLength64[0]>>>28),r.putInt32(a.messageLength64[1]<<3);var i={h0:t.h0,h1:t.h1,h2:t.h2,h3:t.h3,h4:t.h4};s(i,u,r);var f=e.util.createBuffer();return f.putInt32(i.h0),f.putInt32(i.h1),f.putInt32(i.h2),f.putInt32(i.h3),f.putInt32(i.h4),f},a};var n=null,r=!1}var r="sha1";if(typeof n!="function"){if(typeof module!="object"||!module.exports)return typeof forge=="undefined"&&(forge={}),e(forge);var i=!0;n=function(e,n){n(t,module)}}var s,o=function(t,n){n.exports=function(n){var i=s.map(function(e){return t(e)}).concat(e);n=n||{},n.defined=n.defined||{};if(n.defined[r])return n[r];n.defined[r]=!0;for(var o=0;o=64){for(l=0;l<16;++l)t[l]=n.getInt32();for(;l<64;++l)r=t[l-2],r=(r>>>17|r<<15)^(r>>>19|r<<13)^r>>>10,s=t[l-15],s=(s>>>7|s<<25)^(s>>>18|s<<14)^s>>>3,t[l]=r+t[l-7]+s+t[l-16]|0;c=e.h0,h=e.h1,p=e.h2,d=e.h3,v=e.h4,m=e.h5,g=e.h6,y=e.h7;for(l=0;l<64;++l)u=(v>>>6|v<<26)^(v>>>11|v<<21)^(v>>>25|v<<7),a=g^v&(m^g),o=(c>>>2|c<<30)^(c>>>13|c<<19)^(c>>>22|c<<10),f=c&h|p&(c^h),r=y+u+a+i[l]+t[l],s=o+f,y=g,g=m,m=v,v=d+r|0,d=p,p=h,h=c,c=r+s|0;e.h0=e.h0+c|0,e.h1=e.h1+h|0,e.h2=e.h2+p|0,e.h3=e.h3+d|0,e.h4=e.h4+v|0,e.h5=e.h5+m|0,e.h6=e.h6+g|0,e.h7=e.h7+y|0,b-=64}}var t=e.sha256=e.sha256||{};e.md=e.md||{},e.md.algorithms=e.md.algorithms||{},e.md.sha256=e.md.algorithms.sha256=t,t.create=function(){r||s();var t=null,i=e.util.createBuffer(),u=new Array(64),a={algorithm:"sha256",blockLength:64,digestLength:32,messageLength:0,messageLength64:[0,0]};return a.start=function(){return a.messageLength=0,a.messageLength64=[0,0],i=e.util.createBuffer(),t={h0:1779033703,h1:3144134277,h2:1013904242,h3:2773480762,h4:1359893119,h5:2600822924,h6:528734635,h7:1541459225},a},a.start(),a.update=function(n,r){return r==="utf8"&&(n=e.util.encodeUtf8(n)),a.messageLength+=n.length,a.messageLength64[0]+=n.length/4294967296>>>0,a.messageLength64[1]+=n.length>>>0,i.putBytes(n),o(t,u,i),(i.read>2048||i.length()===0)&&i.compact(),a},a.digest=function(){var r=e.util.createBuffer();r.putBytes(i.bytes()),r.putBytes(n.substr(0,64-(a.messageLength64[1]+8&63))),r.putInt32(a.messageLength64[0]<<3|a.messageLength64[0]>>>28),r.putInt32(a.messageLength64[1]<<3);var s={h0:t.h0,h1:t.h1,h2:t.h2,h3:t.h3,h4:t.h4,h5:t.h5,h6:t.h6,h7:t.h7};o(s,u,r);var f=e.util.createBuffer();return f.putInt32(s.h0),f.putInt32(s.h1),f.putInt32(s.h2),f.putInt32(s.h3),f.putInt32(s.h4),f.putInt32(s.h5),f.putInt32(s.h6),f.putInt32(s.h7),f},a};var n=null,r=!1,i=null}var r="sha256";if(typeof n!="function"){if(typeof module!="object"||!module.exports)return typeof forge=="undefined"&&(forge={}),e(forge);var i=!0;n=function(e,n){n(t,module)}}var s,o=function(t,n){n.exports=function(n){var i=s.map(function(e){return t(e)}).concat(e);n=n||{},n.defined=n.defined||{};if(n.defined[r])return n[r];n.defined[r]=!0;for(var o=0;o=128){for(_=0;_<16;++_)t[_][0]=n.getInt32()>>>0,t[_][1]=n.getInt32()>>>0;for(;_<80;++_)H=t[_-2],D=H[0],P=H[1],r=((D>>>19|P<<13)^(P>>>29|D<<3)^D>>>6)>>>0,i=((D<<13|P>>>19)^(P<<3|D>>>29)^(D<<26|P>>>6))>>>0,j=t[_-15],D=j[0],P=j[1],o=((D>>>1|P<<31)^(D>>>8|P<<24)^D>>>7)>>>0,u=((D<<31|P>>>1)^(D<<24|P>>>8)^(D<<25|P>>>7))>>>0,B=t[_-7],F=t[_-16],P=i+B[1]+u+F[1],t[_][0]=r+B[0]+o+F[0]+(P/4294967296>>>0)>>>0,t[_][1]=P>>>0;m=e[0][0],g=e[0][1],y=e[1][0],b=e[1][1],w=e[2][0],E=e[2][1],S=e[3][0],x=e[3][1],T=e[4][0],N=e[4][1],C=e[5][0],k=e[5][1],L=e[6][0],A=e[6][1],O=e[7][0],M=e[7][1];for(_=0;_<80;++_)l=((T>>>14|N<<18)^(T>>>18|N<<14)^(N>>>9|T<<23))>>>0,c=((T<<18|N>>>14)^(T<<14|N>>>18)^(N<<23|T>>>9))>>>0,h=(L^T&(C^L))>>>0,p=(A^N&(k^A))>>>0,a=((m>>>28|g<<4)^(g>>>2|m<<30)^(g>>>7|m<<25))>>>0,f=((m<<4|g>>>28)^(g<<30|m>>>2)^(g<<25|m>>>7))>>>0,d=(m&y|w&(m^y))>>>0,v=(g&b|E&(g^b))>>>0,P=M+c+p+s[_][1]+t[_][1],r=O+l+h+s[_][0]+t[_][0]+(P/4294967296>>>0)>>>0,i=P>>>0,P=f+v,o=a+d+(P/4294967296>>>0)>>>0,u=P>>>0,O=L,M=A,L=C,A=k,C=T,k=N,P=x+i,T=S+r+(P/4294967296>>>0)>>>0,N=P>>>0,S=w,x=E,w=y,E=b,y=m,b=g,P=i+u,m=r+o+(P/4294967296>>>0)>>>0,g=P>>>0;P=e[0][1]+g,e[0][0]=e[0][0]+m+(P/4294967296>>>0)>>>0,e[0][1]=P>>>0,P=e[1][1]+b,e[1][0]=e[1][0]+y+(P/4294967296>>>0)>>>0,e[1][1]=P>>>0,P=e[2][1]+E,e[2][0]=e[2][0]+w+(P/4294967296>>>0)>>>0,e[2][1]=P>>>0,P=e[3][1]+x,e[3][0]=e[3][0]+S+(P/4294967296>>>0)>>>0,e[3][1]=P>>>0,P=e[4][1]+N,e[4][0]=e[4][0]+T+(P/4294967296>>>0)>>>0,e[4][1]=P>>>0,P=e[5][1]+k,e[5][0]=e[5][0]+C+(P/4294967296>>>0)>>>0,e[5][1]=P>>>0,P=e[6][1]+A,e[6][0]=e[6][0]+L+(P/4294967296>>>0)>>>0,e[6][1]=P>>>0,P=e[7][1]+M,e[7][0]=e[7][0]+O+(P/4294967296>>>0)>>>0,e[7][1]=P>>>0,I-=128}}var t=e.sha512=e.sha512||{};e.md=e.md||{},e.md.algorithms=e.md.algorithms||{},e.md.sha512=e.md.algorithms.sha512=t;var n=e.sha384=e.sha512.sha384=e.sha512.sha384||{};n.create=function(){return t.create("SHA-384")},e.md.sha384=e.md.algorithms.sha384=n,e.sha512.sha256=e.sha512.sha256||{create:function(){return t.create("SHA-512/256")}},e.md["sha512/256"]=e.md.algorithms["sha512/256"]=e.sha512.sha256,e.sha512.sha224=e.sha512.sha224||{create:function(){return t.create("SHA-512/224")}},e.md["sha512/224"]=e.md.algorithms["sha512/224"]=e.sha512.sha224,t.create=function(t){i||u(),typeof t=="undefined"&&(t="SHA-512");if(t in o){var n=o[t],s=null,f=e.util.createBuffer(),l=new Array(80);for(var c=0;c<80;++c)l[c]=new Array(2);var h={algorithm:t.replace("-","").toLowerCase(),blockLength:128,digestLength:64,messageLength:0,messageLength128:[0,0,0,0]};return h.start=function(){h.messageLength=0,h.messageLength128=[0,0,0,0],f=e.util.createBuffer(),s=new Array(n.length);for(var t=0;t>>0,r>>>0];for(var i=3;i>=0;--i)h.messageLength128[i]+=r[1],r[1]=r[0]+(h.messageLength128[i]/4294967296>>>0),h.messageLength128[i]=h.messageLength128[i]>>>0,r[0]=r[1]/4294967296>>>0;return f.putBytes(t),a(s,l,f),(f.read>2048||f.length()===0)&&f.compact(),h},h.digest=function(){var n=e.util.createBuffer();n.putBytes(f.bytes()),n.putBytes(r.substr(0,128-(h.messageLength128[3]+16&127)));var i=[];for(var o=0;o<3;++o)i[o]=h.messageLength128[o]<<3|h.messageLength128[o-1]>>>28;i[3]=h.messageLength128[3]<<3,n.putInt32(i[0]),n.putInt32(i[1]),n.putInt32(i[2]),n.putInt32(i[3]);var u=new Array(s.length);for(var o=0;on.blockLength&&(n.start(),n.update(o.bytes()),o=n.digest()),r=e.util.createBuffer(),i=e.util.createBuffer(),f=o.length();for(var a=0;a65&&o!==-1){var u=t[o];u===","?(++o,t=t.substr(0,o)+"\r\n "+t.substr(o)):t=t.substr(0,o)+"\r\n"+u+t.substr(o+1),s=i-o-1,o=-1,++i}else if(t[i]===" "||t[i]===" "||t[i]===",")o=i;return t}function r(e){return e.replace(/^\s+/,"")}var t=e.pem=e.pem||{};t.encode=function(t,r){r=r||{};var i="-----BEGIN "+t.type+"-----\r\n",s;t.procType&&(s={name:"Proc-Type",values:[String(t.procType.version),t.procType.type]},i+=n(s)),t.contentDomain&&(s={name:"Content-Domain",values:[t.contentDomain]},i+=n(s)),t.dekInfo&&(s={name:"DEK-Info",values:[t.dekInfo.algorithm]},t.dekInfo.parameters&&s.values.push(t.dekInfo.parameters),i+=n(s));if(t.headers)for(var o=0;o8?3:1,m=[],g=[0,0,1,1,1,1,1,1,0,1,1,1,1,1,1,0],y=0,b;for(var w=0;w>>4^S)&252645135,S^=b,E^=b<<4,b=(S>>>-16^E)&65535,E^=b,S^=b<<-16,b=(E>>>2^S)&858993459,S^=b,E^=b<<2,b=(S>>>-16^E)&65535,E^=b,S^=b<<-16,b=(E>>>1^S)&1431655765,S^=b,E^=b<<1,b=(S>>>8^E)&16711935,E^=b,S^=b<<8,b=(E>>>1^S)&1431655765,S^=b,E^=b<<1,b=E<<8|S>>>20&240,E=S<<24|S<<8&16711680|S>>>8&65280|S>>>24&240,S=b;for(var x=0;x>>26,S=S<<2|S>>>26):(E=E<<1|E>>>27,S=S<<1|S>>>27),E&=-15,S&=-15;var T=t[E>>>28]|n[E>>>24&15]|r[E>>>20&15]|i[E>>>16&15]|s[E>>>12&15]|o[E>>>8&15]|u[E>>>4&15],N=a[S>>>28]|f[S>>>24&15]|l[S>>>20&15]|c[S>>>16&15]|h[S>>>12&15]|p[S>>>8&15]|d[S>>>4&15];b=(N>>>16^T)&65535,m[y++]=T^b,m[y++]=N^b<<16}}return m}function c(e,t,l,c){var h=e.length===32?3:9,p;h===3?p=c?[30,-2,-2]:[0,32,2]:p=c?[94,62,-2,32,64,2,30,-2,-2]:[0,32,2,62,30,-2,64,96,2];var d,v=t[0],m=t[1];d=(v>>>4^m)&252645135,m^=d,v^=d<<4,d=(v>>>16^m)&65535,m^=d,v^=d<<16,d=(m>>>2^v)&858993459,v^=d,m^=d<<2,d=(m>>>8^v)&16711935,v^=d,m^=d<<8,d=(v>>>1^m)&1431655765,m^=d,v^=d<<1,v=v<<1|v>>>31,m=m<<1|m>>>31;for(var g=0;g>>4|m<<28)^e[w+1];d=v,v=m,m=d^(r[E>>>24&63]|s[E>>>16&63]|u[E>>>8&63]|f[E&63]|n[S>>>24&63]|i[S>>>16&63]|o[S>>>8&63]|a[S&63])}d=v,v=m,m=d}v=v>>>1|v<<31,m=m>>>1|m<<31,d=(v>>>1^m)&1431655765,m^=d,v^=d<<1,d=(m>>>8^v)&16711935,v^=d,m^=d<<8,d=(m>>>2^v)&858993459,v^=d,m^=d<<2,d=(v>>>16^m)&65535,m^=d,v^=d<<16,d=(v>>>4^m)&252645135,m^=d,v^=d<<4,l[0]=v,l[1]=m}function h(t){t=t||{};var n=(t.mode||"CBC").toUpperCase(),r="DES-"+n,i;t.decrypt?i=e.cipher.createDecipher(r,t.key):i=e.cipher.createCipher(r,t.key);var s=i.start;return i.start=function(t,n){var r=null;n instanceof e.util.ByteBuffer&&(r=n,n={}),n=n||{},n.output=r,n.iv=t,s.call(i,n)},i}e.des=e.des||{},e.des.startEncrypting=function(e,t,n,r){var i=h({key:e,output:n,decrypt:!1,mode:r||(t===null?"ECB":"CBC")});return i.start(t),i},e.des.createEncryptionCipher=function(e,t){return h({key:e,output:null,decrypt:!1,mode:t})},e.des.startDecrypting=function(e,t,n,r){var i=h({key:e,output:n,decrypt:!0,mode:r||(t===null?"ECB":"CBC")});return i.start(t),i},e.des.createDecryptionCipher=function(e,t){return h({key:e,output:null,decrypt:!0,mode:t})},e.des.Algorithm=function(e,t){var n=this;n.name=e,n.mode=new t({blockSize:8,cipher:{encrypt:function(e,t){return c(n._keys,e,t,!1)},decrypt:function(e,t){return c(n._keys,e,t,!0)}}}),n._init=!1},e.des.Algorithm.prototype.initialize=function(t){if(this._init)return;var n=e.util.createBuffer(t.key);if(this.name.indexOf("3DES")===0&&n.length()!==24)throw new Error("Invalid Triple-DES key size: "+n.length()*8);this._keys=l(n),this._init=!0},t("DES-ECB",e.cipher.modes.ecb),t("DES-CBC",e.cipher.modes.cbc),t("DES-CFB",e.cipher.modes.cfb),t("DES-OFB",e.cipher.modes.ofb),t("DES-CTR",e.cipher.modes.ctr),t("3DES-ECB",e.cipher.modes.ecb),t("3DES-CBC",e.cipher.modes.cbc),t("3DES-CFB",e.cipher.modes.cfb),t("3DES-OFB",e.cipher.modes.ofb),t("3DES-CTR",e.cipher.modes.ctr);var n=[16843776,0,65536,16843780,16842756,66564,4,65536,1024,16843776,16843780,1024,16778244,16842756,16777216,4,1028,16778240,16778240,66560,66560,16842752,16842752,16778244,65540,16777220,16777220,65540,0,1028,66564,16777216,65536,16843780,4,16842752,16843776,16777216,16777216,1024,16842756,65536,66560,16777220,1024,4,16778244,66564,16843780,65540,16842752,16778244,16777220,1028,66564,16843776,1028,16778240,16778240,0,65540,66560,0,16842756],r=[-2146402272,-2147450880,32768,1081376,1048576,32,-2146435040,-2147450848,-2147483616,-2146402272,-2146402304,-2147483648,-2147450880,1048576,32,-2146435040,1081344,1048608,-2147450848,0,-2147483648,32768,1081376,-2146435072,1048608,-2147483616,0,1081344,32800,-2146402304,-2146435072,32800,0,1081376,-2146435040,1048576,-2147450848,-2146435072,-2146402304,32768,-2146435072,-2147450880,32,-2146402272,1081376,32,32768,-2147483648,32800,-2146402304,1048576,-2147483616,1048608,-2147450848,-2147483616,1048608,1081344,0,-2147450880,32800,-2147483648,-2146435040,-2146402272,1081344],i=[520,134349312,0,134348808,134218240,0,131592,134218240,131080,134217736,134217736,131072,134349320,131080,134348800,520,134217728,8,134349312,512,131584,134348800,134348808,131592,134218248,131584,131072,134218248,8,134349320,512,134217728,134349312,134217728,131080,520,131072,134349312,134218240,0,512,131080,134349320,134218240,134217736,512,0,134348808,134218248,131072,134217728,134349320,8,131592,131584,134217736,134348800,134218248,520,134348800,131592,8,134348808,131584],s=[8396801,8321,8321,128,8396928,8388737,8388609,8193,0,8396800,8396800,8396929,129,0,8388736,8388609,1,8192,8388608,8396801,128,8388608,8193,8320,8388737,1,8320,8388736,8192,8396928,8396929,129,8388736,8388609,8396800,8396929,129,0,0,8396800,8320,8388736,8388737,1,8396801,8321,8321,128,8396929,129,1,8192,8388609,8193,8396928,8388737,8193,8320,8388608,8396801,128,8388608,8192,8396928],o=[256,34078976,34078720,1107296512,524288,256,1073741824,34078720,1074266368,524288,33554688,1074266368,1107296512,1107820544,524544,1073741824,33554432,1074266112,1074266112,0,1073742080,1107820800,1107820800,33554688,1107820544,1073742080,0,1107296256,34078976,33554432,1107296256,524544,524288,1107296512,256,33554432,1073741824,34078720,1107296512,1074266368,33554688,1073741824,1107820544,34078976,1074266368,256,33554432,1107820544,1107820800,524544,1107296256,1107820800,34078720,0,1074266112,1107296256,524544,33554688,1073742080,524288,0,1074266112,34078976,1073742080],u=[536870928,541065216,16384,541081616,541065216,16,541081616,4194304,536887296,4210704,4194304,536870928,4194320,536887296,536870912,16400,0,4194320,536887312,16384,4210688,536887312,16,541065232,541065232,0,4210704,541081600,16400,4210688,541081600,536870912,536887296,16,541065232,4210688,541081616,4194304,16400,536870928,4194304,536887296,536870912,16400,536870928,541081616,4210688,541065216,4210704,541081600,0,541065232,16,16384,541065216,4210704,16384,4194320,536887312,0,541081600,536870912,4194320,536887312],a=[2097152,69206018,67110914,0,2048,67110914,2099202,69208064,69208066,2097152,0,67108866,2,67108864,69206018,2050,67110912,2099202,2097154,67110912,67108866,69206016,69208064,2097154,69206016,2048,2050,69208066,2099200,2,67108864,2099200,67108864,2099200,2097152,67110914,67110914,69206018,69206018,2,2097154,67108864,67110912,2097152,69208064,2050,2099202,69208064,2050,67108866,69208066,69206016,2099200,0,2,69208066,0,2099202,69206016,2048,67108866,67110912,2048,2097154],f=[268439616,4096,262144,268701760,268435456,268439616,64,268435456,262208,268697600,268701760,266240,268701696,266304,4096,64,268697600,268435520,268439552,4160,266240,262208,268697664,268701696,4160,0,0,268697664,268435520,268439552,266304,262144,266304,262144,268701696,4096,64,268697664,4096,266304,268439552,64,268435520,268697600,268697664,268435456,262144,268439616,0,268701760,262208,268435520,268697600,268439552,268439616,0,268701760,266240,266240,4160,4160,262208,268435456,268701696]}var r="des";if(typeof n!="function"){if(typeof module!="object"||!module.exports)return typeof forge=="undefined"&&(forge={}),e(forge);var i=!0;n=function(e,n){n(t,module)}}var s,o=function(t,n){n.exports=function(n){var i=s.map(function(e){return t(e)}).concat(e);n=n||{},n.defined=n.defined||{};if(n.defined[r])return n[r];n.defined[r]=!0;for(var o=0;o4294967295*o)throw new Error("Derived key is too long.");var u=Math.ceil(i/o),a=i-(u-1)*o,f=e.hmac.create();f.start(s,t);var l="",c,h,p;for(var d=1;d<=u;++d){f.start(null,null),f.update(n),f.update(e.util.int32ToBytes(d)),c=p=f.digest().getBytes();for(var v=2;v<=r;++v)f.start(null,null),f.update(p),h=f.digest().getBytes(),c=e.util.xorBytes(c,h,o),p=h;l+=d=32)return f(),e();var t=32-n.pools[0].messageLength<<5;n.seedFile(t,function(t,r){if(t)return e(t);n.collect(r),f(),e()})}function a(){if(n.pools[0].messageLength>=32)return f();var e=32-n.pools[0].messageLength<<5;n.collect(n.seedFileSync(e)),f()}function f(){var e=n.plugin.md.create();e.update(n.pools[0].digest().getBytes()),n.pools[0].start();var t=1;for(var r=1;r<32;++r)t=t===31?2147483648:t<<2,t%n.reseeds===0&&(e.update(n.pools[r].digest().getBytes()),n.pools[r].start());var i=e.digest().getBytes();e.start(),e.update(i);var s=e.digest().getBytes();n.key=n.plugin.formatKey(i),n.seed=n.plugin.formatSeed(s),n.reseeds=n.reseeds===4294967295?0:n.reseeds+1,n.generated=0}function l(t){var n=null;if(typeof window!="undefined"){var r=window.crypto||window.msCrypto;r&&r.getRandomValues&&(n=function(e){return r.getRandomValues(e)})}var i=e.util.createBuffer();if(n)while(i.length()>16),l+=(f&32767)<<16,l+=f>>15,l=(l&2147483647)+(l>>31),h=l&4294967295;for(var u=0;u<3;++u)c=h>>>(u<<3),c^=Math.floor(Math.random()*256),i.putByte(String.fromCharCode(c&255))}}return i.getBytes(t)}var n={plugin:t,key:null,seed:null,time:null,reseeds:0,generated:0},i=t.md,s=new Array(32);for(var o=0;o<32;++o)s[o]=i.create();return n.pools=s,n.pool=0,n.generate=function(t,r){function l(c){if(c)return r(c);if(f.length()>=t)return r(null,f.getBytes(t));n.generated>1048575&&(n.key=null);if(n.key===null)return e.util.nextTick(function(){u(l)});var h=i(n.key,n.seed);n.generated+=h.length,f.putBytes(h),n.key=o(i(n.key,s(n.seed))),n.seed=a(i(n.key,n.seed)),e.util.setImmediate(l)}if(!r)return n.generateSync(t);var i=n.plugin.cipher,s=n.plugin.increment,o=n.plugin.formatKey,a=n.plugin.formatSeed,f=e.util.createBuffer();n.key=null,l()},n.generateSync=function(t){var r=n.plugin.cipher,i=n.plugin.increment,s=n.plugin.formatKey,o=n.plugin.formatSeed;n.key=null;var u=e.util.createBuffer();while(u.length()1048575&&(n.key=null),n.key===null&&a();var f=r(n.key,n.seed);n.generated+=f.length,u.putBytes(f),n.key=s(r(n.key,i(n.seed))),n.seed=o(r(n.key,n.seed))}return u.getBytes(t)},r?(n.seedFile=function(e,t){r.randomBytes(e,function(e,n){if(e)return t(e);t(null,n.toString())})},n.seedFileSync=function(e){return r.randomBytes(e).toString()}):(n.seedFile=function(e,t){try{t(null,l(e))}catch(n){t(n)}},n.seedFileSync=l),n.collect=function(e){var t=e.length;for(var r=0;r>i&255);n.collect(r)},n.registerWorker=function(e){if(e===self)n.seedFile=function(e,t){function n(e){var r=e.data;r.forge&&r.forge.prng&&(self.removeEventListener("message",n),t(r.forge.prng.err,r.forge.prng.bytes))}self.addEventListener("message",n),self.postMessage({forge:{prng:{needed:e}}})};else{var t=function(t){var r=t.data;r.forge&&r.forge.prng&&n.seedFile(r.forge.prng.needed,function(t,n){e.postMessage({forge:{prng:{err:t,bytes:n}}})})};e.addEventListener("message",t)}},n}}var r="prng";if(typeof n!="function"){if(typeof module!="object"||!module.exports)return typeof forge=="undefined"&&(forge={}),e(forge);var i=!0;n=function(e,n){n(t,module)}}var s,o=function(t,n){n.exports=function(n){var i=s.map(function(e){return t(e)}).concat(e);n=n||{},n.defined=n.defined||{};if(n.defined[r])return n[r];n.defined[r]=!0;for(var o=0;o>16-t},i=function(e,t){return(e&65535)>>t|e<<16-t&65535};e.rc2=e.rc2||{},e.rc2.expandKey=function(n,r){typeof n=="string"&&(n=e.util.createBuffer(n)),r=r||128;var i=n,s=n.length(),o=r,u=Math.ceil(o/8),a=255>>(o&7),f;for(f=s;f<128;f++)i.putByte(t[i.at(f-1)+i.at(f-s)&255]);i.setAt(128-u,t[i.at(128-u)&a]);for(f=127-u;f>=0;f--)i.setAt(f,t[i.at(f+1)^i.at(f+u)]);return i};var s=function(t,s,o){var u=!1,a=null,f=null,l=null,c,h,p,d,v=[];t=e.rc2.expandKey(t,s);for(p=0;p<64;p++)v.push(t.getInt16Le());o?(c=function(e){for(p=0;p<4;p++)e[p]+=v[d]+(e[(p+3)%4]&e[(p+2)%4])+(~e[(p+3)%4]&e[(p+1)%4]),e[p]=r(e[p],n[p]),d++},h=function(e){for(p=0;p<4;p++)e[p]+=v[e[(p+3)%4]&63]}):(c=function(e){for(p=3;p>=0;p--)e[p]=i(e[p],n[p]),e[p]-=v[d]+(e[(p+3)%4]&e[(p+2)%4])+(~e[(p+3)%4]&e[(p+1)%4]),d--},h=function(e){for(p=3;p>=0;p--)e[p]-=v[e[(p+3)%4]&63]});var m=function(e){var t=[];for(p=0;p<4;p++){var n=a.getInt16Le();l!==null&&(o?n^=l.getInt16Le():l.putInt16Le(n)),t.push(n&65535)}d=o?0:63;for(var r=0;r=8)m([[5,c],[1,h],[6,c],[1,h],[5,c]])},finish:function(e){var t=!0;if(o)if(e)t=e(8,a,!o);else{var n=a.length()===8?8:8-a.length();a.fillWithByte(n,n)}t&&(u=!0,g.update());if(!o){t=a.length()===0;if(t)if(e)t=e(8,f,!o);else{var r=f.length(),i=f.at(r-1);i>r?t=!1:f.truncate(i)}}return t}},g};e.rc2.startEncrypting=function(t,n,r){var i=e.rc2.createEncryptionCipher(t,128);return i.start(n,r),i},e.rc2.createEncryptionCipher=function(e,t){return s(e,t,!0)},e.rc2.startDecrypting=function(t,n,r){var i=e.rc2.createDecryptionCipher(t,128);return i.start(n,r),i},e.rc2.createDecryptionCipher=function(e,t){return s(e,t,!1)}}var r="rc2";if(typeof n!="function"){if(typeof module!="object"||!module.exports)return typeof forge=="undefined"&&(forge={}),e(forge);var i=!0;n=function(e,n){n(t,module)}}var s,o=function(t,n){n.exports=function(n){var i=s.map(function(e){return t(e)}).concat(e);n=n||{},n.defined=n.defined||{};if(n.defined[r])return n[r];n.defined[r]=!0;for(var o=0;o=0){var o=t*this.data[e++]+n.data[r]+i;i=Math.floor(o/67108864),n.data[r++]=o&67108863}return i}function u(e,t,n,r,i,s){var o=t&32767,u=t>>15;while(--s>=0){var a=this.data[e]&32767,f=this.data[e++]>>15,l=u*a+f*o;a=o*a+((l&32767)<<15)+n.data[r]+(i&1073741823),i=(a>>>30)+(l>>>15)+u*f+(i>>>30),n.data[r++]=a&1073741823}return i}function a(e,t,n,r,i,s){var o=t&16383,u=t>>14;while(--s>=0){var a=this.data[e]&16383,f=this.data[e++]>>14,l=u*a+f*o;a=o*a+((l&16383)<<14)+n.data[r]+i,i=(a>>28)+(l>>14)+u*f,n.data[r++]=a&268435455}return i}function d(e){return l.charAt(e)}function v(e,t){var n=c[e.charCodeAt(t)];return n==null?-1:n}function m(e){for(var t=this.t-1;t>=0;--t)e.data[t]=this.data[t];e.t=this.t,e.s=this.s}function g(e){this.t=1,this.s=e<0?-1:0,e>0?this.data[0]=e:e<-1?this.data[0]=e+this.DV:this.t=0}function y(e){var t=s();return t.fromInt(e),t}function b(e,t){var n;if(t==16)n=4;else if(t==8)n=3;else if(t==256)n=8;else if(t==2)n=1;else if(t==32)n=5;else{if(t!=4){this.fromRadix(e,t);return}n=2}this.t=0,this.s=0;var r=e.length,s=!1,o=0;while(--r>=0){var u=n==8?e[r]&255:v(e,r);if(u<0){e.charAt(r)=="-"&&(s=!0);continue}s=!1,o==0?this.data[this.t++]=u:o+n>this.DB?(this.data[this.t-1]|=(u&(1<>this.DB-o):this.data[this.t-1]|=u<=this.DB&&(o-=this.DB)}n==8&&(e[0]&128)!=0&&(this.s=-1,o>0&&(this.data[this.t-1]|=(1<0&&this.data[this.t-1]==e)--this.t}function E(e){if(this.s<0)return"-"+this.negate().toString(e);var t;if(e==16)t=4;else if(e==8)t=3;else if(e==2)t=1;else if(e==32)t=5;else{if(e!=4)return this.toRadix(e);t=2}var n=(1<0){u>u)>0&&(i=!0,s=d(r));while(o>=0)u>(u+=this.DB-t)):(r=this.data[o]>>(u-=t)&n,u<=0&&(u+=this.DB,--o)),r>0&&(i=!0),i&&(s+=d(r))}return i?s:"0"}function S(){var e=s();return i.ZERO.subTo(this,e),e}function x(){return this.s<0?this.negate():this}function T(e){var t=this.s-e.s;if(t!=0)return t;var n=this.t;t=n-e.t;if(t!=0)return this.s<0?-t:t;while(--n>=0)if((t=this.data[n]-e.data[n])!=0)return t;return 0}function N(e){var t=1,n;return(n=e>>>16)!=0&&(e=n,t+=16),(n=e>>8)!=0&&(e=n,t+=8),(n=e>>4)!=0&&(e=n,t+=4),(n=e>>2)!=0&&(e=n,t+=2),(n=e>>1)!=0&&(e=n,t+=1),t}function C(){return this.t<=0?0:this.DB*(this.t-1)+N(this.data[this.t-1]^this.s&this.DM)}function k(e,t){var n;for(n=this.t-1;n>=0;--n)t.data[n+e]=this.data[n];for(n=e-1;n>=0;--n)t.data[n]=0;t.t=this.t+e,t.s=this.s}function L(e,t){for(var n=e;n=0;--u)t.data[u+s+1]=this.data[u]>>r|o,o=(this.data[u]&i)<=0;--u)t.data[u]=0;t.data[s]=o,t.t=this.t+s+1,t.s=this.s,t.clamp()}function O(e,t){t.s=this.s;var n=Math.floor(e/this.DB);if(n>=this.t){t.t=0;return}var r=e%this.DB,i=this.DB-r,s=(1<>r;for(var o=n+1;o>r;r>0&&(t.data[this.t-n-1]|=(this.s&s)<>=this.DB;if(e.t>=this.DB;r+=this.s}else{r+=this.s;while(n>=this.DB;r-=e.s}t.s=r<0?-1:0,r<-1?t.data[n++]=this.DV+r:r>0&&(t.data[n++]=r),t.t=n,t.clamp()}function _(e,t){var n=this.abs(),r=e.abs(),s=n.t;t.t=s+r.t;while(--s>=0)t.data[s]=0;for(s=0;s=0)e.data[n]=0;for(n=0;n=t.DV&&(e.data[n+t.t]-=t.DV,e.data[n+t.t+1]=1)}e.t>0&&(e.data[e.t-1]+=t.am(n,t.data[n],e,2*n,0,1)),e.s=0,e.clamp()}function P(e,t,n){var r=e.abs();if(r.t<=0)return;var o=this.abs();if(o.t0?(r.lShiftTo(l,u),o.lShiftTo(l,n)):(r.copyTo(u),o.copyTo(n));var c=u.t,h=u.data[c-1];if(h==0)return;var p=h*(1<1?u.data[c-2]>>this.F2:0),d=this.FV/p,v=(1<=0&&(n.data[n.t++]=1,n.subTo(b,n)),i.ONE.dlShiftTo(c,b),b.subTo(u,u);while(u.t=0){var w=n.data[--g]==h?this.DM:Math.floor(n.data[g]*d+(n.data[g-1]+m)*v);if((n.data[g]+=u.am(0,w,n,y,0,c))0&&n.rShiftTo(l,n),a<0&&i.ZERO.subTo(n,n)}function H(e){var t=s();return this.abs().divRemTo(e,null,t),this.s<0&&t.compareTo(i.ZERO)>0&&e.subTo(t,t),t}function B(e){this.m=e}function j(e){return e.s<0||e.compareTo(this.m)>=0?e.mod(this.m):e}function F(e){return e}function I(e){e.divRemTo(this.m,null,e)}function q(e,t,n){e.multiplyTo(t,n),this.reduce(n)}function R(e,t){e.squareTo(t),this.reduce(t)}function U(){if(this.t<1)return 0;var e=this.data[0];if((e&1)==0)return 0;var t=e&3;return t=t*(2-(e&15)*t)&15,t=t*(2-(e&255)*t)&255,t=t*(2-((e&65535)*t&65535))&65535,t=t*(2-e*t%this.DV)%this.DV,t>0?this.DV-t:-t}function z(e){this.m=e,this.mp=e.invDigit(),this.mpl=this.mp&32767,this.mph=this.mp>>15,this.um=(1<0&&this.m.subTo(t,t),t}function X(e){var t=s();return e.copyTo(t),this.reduce(t),t}function V(e){while(e.t<=this.mt2)e.data[e.t++]=0;for(var t=0;t>15)*this.mpl&this.um)<<15)&e.DM;n=t+this.m.t,e.data[n]+=this.m.am(0,r,e,t,0,this.m.t);while(e.data[n]>=e.DV)e.data[n]-=e.DV,e.data[++n]++}e.clamp(),e.drShiftTo(this.m.t,e),e.compareTo(this.m)>=0&&e.subTo(this.m,e)}function $(e,t){e.squareTo(t),this.reduce(t)}function J(e,t,n){e.multiplyTo(t,n),this.reduce(n)}function K(){return(this.t>0?this.data[0]&1:this.s)==0}function Q(e,t){if(e>4294967295||e<1)return i.ONE;var n=s(),r=s(),o=t.convert(this),u=N(e)-1;o.copyTo(n);while(--u>=0){t.sqrTo(n,r);if((e&1<0)t.mulTo(r,o,n);else{var a=n;n=r,r=a}}return t.revert(n)}function G(e,t){var n;return e<256||t.isEven()?n=new B(t):n=new z(t),this.exp(e,n)}function Y(){var e=s();return this.copyTo(e),e}function Z(){if(this.s<0){if(this.t==1)return this.data[0]-this.DV;if(this.t==0)return-1}else{if(this.t==1)return this.data[0];if(this.t==0)return 0}return(this.data[1]&(1<<32-this.DB)-1)<>24}function tt(){return this.t==0?this.s:this.data[0]<<16>>16}function nt(e){return Math.floor(Math.LN2*this.DB/Math.log(e))}function rt(){return this.s<0?-1:this.t<=0||this.t==1&&this.data[0]<=0?0:1}function it(e){e==null&&(e=10);if(this.signum()==0||e<2||e>36)return"0";var t=this.chunkSize(e),n=Math.pow(e,t),r=y(n),i=s(),o=s(),u="";this.divRemTo(r,i,o);while(i.signum()>0)u=(n+o.intValue()).toString(e).substr(1)+u,i.divRemTo(r,i,o);return o.intValue().toString(e)+u}function st(e,t){this.fromInt(0),t==null&&(t=10);var n=this.chunkSize(t),r=Math.pow(t,n),s=!1,o=0,u=0;for(var a=0;a=n&&(this.dMultiply(r),this.dAddOffset(u,0),o=0,u=0)}o>0&&(this.dMultiply(Math.pow(t,o)),this.dAddOffset(u,0)),s&&i.ZERO.subTo(this,this)}function ot(e,t,n){if("number"==typeof t)if(e<2)this.fromInt(1);else{this.fromNumber(e,n),this.testBit(e-1)||this.bitwiseTo(i.ONE.shiftLeft(e-1),dt,this),this.isEven()&&this.dAddOffset(1,0);while(!this.isProbablePrime(t))this.dAddOffset(2,0),this.bitLength()>e&&this.subTo(i.ONE.shiftLeft(e-1),this)}else{var r=new Array,s=e&7;r.length=(e>>3)+1,t.nextBytes(r),s>0?r[0]&=(1<0){n>n)!=(this.s&this.DM)>>n&&(t[i++]=r|this.s<=0){n<8?(r=(this.data[e]&(1<>(n+=this.DB-8)):(r=this.data[e]>>(n-=8)&255,n<=0&&(n+=this.DB,--e)),(r&128)!=0&&(r|=-256),i==0&&(this.s&128)!=(r&128)&&++i;if(i>0||r!=this.s)t[i++]=r}}return t}function at(e){return this.compareTo(e)==0}function ft(e){return this.compareTo(e)<0?this:e}function lt(e){return this.compareTo(e)>0?this:e}function ct(e,t,n){var r,i,s=Math.min(e.t,this.t);for(r=0;r>=16,t+=16),(e&255)==0&&(e>>=8,t+=8),(e&15)==0&&(e>>=4,t+=4),(e&3)==0&&(e>>=2,t+=2),(e&1)==0&&++t,t}function Tt(){for(var e=0;e=this.t?this.s!=0:(this.data[t]&1<>=this.DB;if(e.t>=this.DB;r+=this.s}else{r+=this.s;while(n>=this.DB;r+=e.s}t.s=r<0?-1:0,r>0?t.data[n++]=r:r<-1&&(t.data[n++]=this.DV+r),t.t=n,t.clamp()}function Dt(e){var t=s();return this.addTo(e,t),t}function Pt(e){var t=s();return this.subTo(e,t),t}function Ht(e){var t=s();return this.multiplyTo(e,t),t}function Bt(e){var t=s();return this.divRemTo(e,t,null),t}function jt(e){var t=s();return this.divRemTo(e,null,t),t}function Ft(e){var t=s(),n=s();return this.divRemTo(e,t,n),new Array(t,n)}function It(e){this.data[this.t]=this.am(0,e-1,this,0,0,this.t),++this.t,this.clamp()}function qt(e,t){if(e==0)return;while(this.t<=t)this.data[this.t++]=0;this.data[t]+=e;while(this.data[t]>=this.DV)this.data[t]-=this.DV,++t>=this.t&&(this.data[this.t++]=0),++this.data[t]}function Rt(){}function Ut(e){return e}function zt(e,t,n){e.multiplyTo(t,n)}function Wt(e,t){e.squareTo(t)}function Xt(e){return this.exp(e,new Rt)}function Vt(e,t,n){var r=Math.min(this.t+e.t,t);n.s=0,n.t=r;while(r>0)n.data[--r]=0;var i;for(i=n.t-this.t;r=0)n.data[r]=0;for(r=Math.max(t-this.t,0);r2*this.m.t)return e.mod(this.m);if(e.compareTo(this.m)<0)return e;var t=s();return e.copyTo(t),this.reduce(t),t}function Qt(e){return e}function Gt(e){e.drShiftTo(this.m.t-1,this.r2),e.t>this.m.t+1&&(e.t=this.m.t+1,e.clamp()),this.mu.multiplyUpperTo(this.r2,this.m.t+1,this.q3),this.m.multiplyLowerTo(this.q3,this.m.t+1,this.r2);while(e.compareTo(this.r2)<0)e.dAddOffset(1,this.m.t+1);e.subTo(this.r2,e);while(e.compareTo(this.m)>=0)e.subTo(this.m,e)}function Yt(e,t){e.squareTo(t),this.reduce(t)}function Zt(e,t,n){e.multiplyTo(t,n),this.reduce(n)}function en(e,t){var n=e.bitLength(),r,i=y(1),o;if(n<=0)return i;n<18?r=1:n<48?r=3:n<144?r=4:n<768?r=5:r=6,n<8?o=new B(t):t.isEven()?o=new Jt(t):o=new z(t);var u=new Array,a=3,f=r-1,l=(1<1){var c=s();o.sqrTo(u[1],c);while(a<=l)u[a]=s(),o.mulTo(c,u[a-2],u[a]),a+=2}var h=e.t-1,p,d=!0,v=s(),m;n=N(e.data[h])-1;while(h>=0){n>=f?p=e.data[h]>>n-f&l:(p=(e.data[h]&(1<0&&(p|=e.data[h-1]>>this.DB+n-f)),a=r;while((p&1)==0)p>>=1,--a;(n-=a)<0&&(n+=this.DB,--h);if(d)u[p].copyTo(i),d=!1;else{while(a>1)o.sqrTo(i,v),o.sqrTo(v,i),a-=2;a>0?o.sqrTo(i,v):(m=i,i=v,v=m),o.mulTo(v,u[p],i)}while(h>=0&&(e.data[h]&1<0&&(t.rShiftTo(s,t),n.rShiftTo(s,n));while(t.signum()>0)(i=t.getLowestSetBit())>0&&t.rShiftTo(i,t),(i=n.getLowestSetBit())>0&&n.rShiftTo(i,n),t.compareTo(n)>=0?(t.subTo(n,t),t.rShiftTo(1,t)):(n.subTo(t,n),n.rShiftTo(1,n));return s>0&&n.lShiftTo(s,n),n}function nn(e){if(e<=0)return 0;var t=this.DV%e,n=this.s<0?e-1:0;if(this.t>0)if(t==0)n=this.data[0]%e;else for(var r=this.t-1;r>=0;--r)n=(t*n+this.data[r])%e;return n}function rn(e){var t=e.isEven();if(this.isEven()&&t||e.signum()==0)return i.ZERO;var n=e.clone(),r=this.clone(),s=y(1),o=y(0),u=y(0),a=y(1);while(n.signum()!=0){while(n.isEven()){n.rShiftTo(1,n);if(t){if(!s.isEven()||!o.isEven())s.addTo(this,s),o.subTo(e,o);s.rShiftTo(1,s)}else o.isEven()||o.subTo(e,o);o.rShiftTo(1,o)}while(r.isEven()){r.rShiftTo(1,r);if(t){if(!u.isEven()||!a.isEven())u.addTo(this,u),a.subTo(e,a);u.rShiftTo(1,u)}else a.isEven()||a.subTo(e,a);a.rShiftTo(1,a)}n.compareTo(r)>=0?(n.subTo(r,n),t&&s.subTo(u,s),o.subTo(a,o)):(r.subTo(n,r),t&&u.subTo(s,u),a.subTo(o,a))}return r.compareTo(i.ONE)!=0?i.ZERO:a.compareTo(e)>=0?a.subtract(e):a.signum()<0?(a.addTo(e,a),a.signum()<0?a.add(e):a):a}function un(e){var t,n=this.abs();if(n.t==1&&n.data[0]<=sn[sn.length-1]){for(t=0;t=0);var a=o.modPow(r,this);if(a.compareTo(i.ONE)!=0&&a.compareTo(t)!=0){var f=1;while(f++>24&255,o>>16&255,o>>8&255,o&255);r.start(),r.update(t+u),i+=r.digest().getBytes()}return i.substring(0,n)}var t=e.pkcs1=e.pkcs1||{};t.encode_rsa_oaep=function(t,r,i){var s,o,u,a;typeof i=="string"?(s=i,o=arguments[3]||undefined,u=arguments[4]||undefined):i&&(s=i.label||undefined,o=i.seed||undefined,u=i.md||undefined,i.mgf1&&i.mgf1.md&&(a=i.mgf1.md)),u?u.start():u=e.md.sha1.create(),a||(a=u);var f=Math.ceil(t.n.bitLength()/8),l=f-2*u.digestLength-2;if(r.length>l){var c=new Error("RSAES-OAEP input message length is too long.");throw c.length=r.length,c.maxLength=l,c}s||(s=""),u.update(s,"raw");var h=u.digest(),p="",d=l-r.length;for(var v=0;vt&&(o=f(t,n));if(o.isProbablePrime(c))return s(null,o);o.dAddOffset(r[a++%8],0)}while(h<0||+(new Date)-pt&&(o=f(t,r));var d=o.toString(16);i.target.postMessage({hex:d,workLoad:l}),o.dAddOffset(c,0)}a=Math.max(1,a);var e=[];for(var i=0;is-11){var o=new Error("Message is too long for PKCS#1 v1.5 padding.");throw o.length=t.length,o.max=s-11,o}i.putByte(0),i.putByte(r);var u=s-3-t.length,a;if(r===0||r===1){a=r===0?0:255;for(var f=0;f0){var l=0,c=e.random.getBytes(u);for(var f=0;f1){if(o.getByte()!==255){--o.read;break}++f}}else if(a===2){f=0;while(o.length()>1){if(o.getByte()===0){--o.read;break}++f}}var c=o.getByte();if(c!==0||f!==s-3-o.length())throw new Error("Encryption block is invalid.");return o.getBytes()}function p(n,i,s){function u(){a(n.pBits,function(e,t){if(e)return s(e);n.p=t;if(n.q!==null)return f(e,n.q);a(n.qBits,f)})}function a(t,n){e.prime.generateProbablePrime(t,o,n)}function f(e,i){if(e)return s(e);n.q=i;if(n.p.compareTo(n.q)<0){var o=n.p;n.p=n.q,n.q=o}if(n.p.subtract(t.ONE).gcd(n.e).compareTo(t.ONE)!==0){n.p=null,u();return}if(n.q.subtract(t.ONE).gcd(n.e).compareTo(t.ONE)!==0){n.q=null,a(n.qBits,f);return}n.p1=n.p.subtract(t.ONE),n.q1=n.q.subtract(t.ONE),n.phi=n.p1.multiply(n.q1);if(n.phi.gcd(n.e).compareTo(t.ONE)!==0){n.p=n.q=null,u();return}n.n=n.p.multiply(n.q);if(n.n.bitLength()!==n.bits){n.q=null,a(n.qBits,f);return}var l=n.e.modInverse(n.phi);n.keys={privateKey:r.rsa.setPrivateKey(n.n,n.e,l,n.p,n.q,l.mod(n.p1),l.mod(n.q1),n.q.modInverse(n.p)),publicKey:r.rsa.setPublicKey(n.n,n.e)},s(null,n.keys)}typeof i=="function"&&(s=i,i={}),i=i||{};var o={algorithm:{name:i.algorithm||"PRIMEINC",options:{workers:i.workers||2,workLoad:i.workLoad||100,workerScript:i.workerScript}}};"prng"in i&&(o.prng=i.prng),u()}function d(t){var n=t.toString(16);return n[0]>="8"&&(n="00"+n),e.util.hexToBytes(n)}function v(e){return e<=100?27:e<=150?18:e<=200?15:e<=250?12:e<=300?9:e<=350?8:e<=400?7:e<=500?6:e<=600?5:e<=800?4:e<=1250?3:2}if(typeof t=="undefined")var t=e.jsbn.BigInteger;var n=e.asn1;e.pki=e.pki||{},e.pki.rsa=e.rsa=e.rsa||{};var r=e.pki,i=[6,4,2,4,2,4,6,2],s={name:"PrivateKeyInfo",tagClass:n.Class.UNIVERSAL,type:n.Type.SEQUENCE,constructed:!0,value:[{name:"PrivateKeyInfo.version",tagClass:n.Class.UNIVERSAL,type:n.Type.INTEGER,constructed:!1,capture:"privateKeyVersion"},{name:"PrivateKeyInfo.privateKeyAlgorithm",tagClass:n.Class.UNIVERSAL,type:n.Type.SEQUENCE,constructed:!0,value:[{name:"AlgorithmIdentifier.algorithm",tagClass:n.Class.UNIVERSAL,type:n.Type.OID,constructed:!1,capture:"privateKeyOid"}]},{name:"PrivateKeyInfo",tagClass:n.Class.UNIVERSAL,type:n.Type.OCTETSTRING,constructed:!1,capture:"privateKey"}]},o={name:"RSAPrivateKey",tagClass:n.Class.UNIVERSAL,type:n.Type.SEQUENCE,constructed:!0,value:[{name:"RSAPrivateKey.version",tagClass:n.Class.UNIVERSAL,type:n.Type.INTEGER,constructed:!1,capture:"privateKeyVersion"},{name:"RSAPrivateKey.modulus",tagClass:n.Class.UNIVERSAL,type:n.Type.INTEGER,constructed:!1,capture:"privateKeyModulus"},{name:"RSAPrivateKey.publicExponent",tagClass:n.Class.UNIVERSAL,type:n.Type.INTEGER,constructed:!1,capture:"privateKeyPublicExponent"},{name:"RSAPrivateKey.privateExponent",tagClass:n.Class.UNIVERSAL,type:n.Type.INTEGER,constructed:!1,capture:"privateKeyPrivateExponent"},{name:"RSAPrivateKey.prime1",tagClass:n.Class.UNIVERSAL,type:n.Type.INTEGER,constructed:!1,capture:"privateKeyPrime1"},{name:"RSAPrivateKey.prime2",tagClass:n.Class.UNIVERSAL,type:n.Type.INTEGER,constructed:!1,capture:"privateKeyPrime2"},{name:"RSAPrivateKey.exponent1",tagClass:n.Class.UNIVERSAL,type:n.Type.INTEGER,constructed:!1,capture:"privateKeyExponent1"},{name:"RSAPrivateKey.exponent2",tagClass:n.Class.UNIVERSAL,type:n.Type.INTEGER,constructed:!1,capture:"privateKeyExponent2"},{name:"RSAPrivateKey.coefficient",tagClass:n.Class.UNIVERSAL,type:n.Type.INTEGER,constructed:!1,capture:"privateKeyCoefficient"}]},u={name:"RSAPublicKey",tagClass:n.Class.UNIVERSAL,type:n.Type.SEQUENCE,constructed:!0,value:[{name:"RSAPublicKey.modulus",tagClass:n.Class.UNIVERSAL,type:n.Type.INTEGER,constructed:!1,capture:"publicKeyModulus"},{name:"RSAPublicKey.exponent",tagClass:n.Class.UNIVERSAL,type:n.Type.INTEGER,constructed:!1,capture:"publicKeyExponent"}]},a=e.pki.rsa.publicKeyValidator={name:"SubjectPublicKeyInfo",tagClass:n.Class.UNIVERSAL,type:n.Type.SEQUENCE,constructed:!0,captureAsn1:"subjectPublicKeyInfo",value:[{name:"SubjectPublicKeyInfo.AlgorithmIdentifier",tagClass:n.Class.UNIVERSAL,type:n.Type.SEQUENCE,constructed:!0,value:[{name:"AlgorithmIdentifier.algorithm",tagClass:n.Class.UNIVERSAL,type:n.Type.OID,constructed:!1,capture:"publicKeyOid"}]},{name:"SubjectPublicKeyInfo.subjectPublicKey",tagClass:n.Class.UNIVERSAL,type:n.Type.BITSTRING,constructed:!1,value:[{name:"SubjectPublicKeyInfo.subjectPublicKey.RSAPublicKey",tagClass:n.Class.UNIVERSAL,type:n.Type.SEQUENCE,constructed:!0,optional:!0,captureAsn1:"rsaPublicKey"}]}]},f=function(e){var t;if(e.algorithm in r.oids){t=r.oids[e.algorithm];var s=n.oidToDer(t).getBytes(),o=n.create(n.Class.UNIVERSAL,n.Type.SEQUENCE,!0,[]),u=n.create(n.Class.UNIVERSAL,n.Type.SEQUENCE,!0,[]);u.value.push(n.create(n.Class.UNIVERSAL,n.Type.OID,!1,s)),u.value.push(n.create(n.Class.UNIVERSAL,n.Type.NULL,!1,""));var a=n.create(n.Class.UNIVERSAL,n.Type.OCTETSTRING,!1,e.digest().getBytes());return o.value.push(u),o.value.push(a),n.toDer(o).getBytes()}var i=new Error("Unknown message digest algorithm.");throw i.algorithm=e.algorithm,i},l=function(n,r,i){if(i)return n.modPow(r.e,r.n);if(!r.p||!r.q)return n.modPow(r.d,r.n);r.dP||(r.dP=r.d.mod(r.p.subtract(t.ONE))),r.dQ||(r.dQ=r.d.mod(r.q.subtract(t.ONE))),r.qInv||(r.qInv=r.q.modInverse(r.p));var s;do s=(new t(e.util.bytesToHex(e.random.getBytes(r.n.bitLength()/8)),16)).mod(r.n);while(s.equals(t.ZERO));n=n.multiply(s.modPow(r.e,r.n)).mod(r.n);var o=n.mod(r.p).modPow(r.dP,r.p),u=n.mod(r.q).modPow(r.dQ,r.q);while(o.compareTo(u)<0)o=o.add(r.p);var a=o.subtract(u).multiply(r.qInv).mod(r.p).multiply(r.q).add(u);return a=a.multiply(s.modInverse(r.n)).mod(r.n),a};r.rsa.encrypt=function(n,r,i){var s=i,o,u=Math.ceil(r.n.bitLength()/8);i!==!1&&i!==!0?(s=i===2,o=c(n,r,i)):(o=e.util.createBuffer(),o.putBytes(n));var a=new t(o.toHex(),16),f=l(a,r,s),h=f.toString(16),p=e.util.createBuffer(),d=u-Math.ceil(h.length/2);while(d>0)p.putByte(0),--d;return p.putBytes(e.util.hexToBytes(h)),p.getBytes()},r.rsa.decrypt=function(n,r,i,s){var o=Math.ceil(r.n.bitLength()/8);if(n.length!==o){var u=new Error("Encrypted message length is invalid.");throw u.length=n.length,u.expected=o,u}var a=new t(e.util.createBuffer(n).toHex(),16);if(a.compareTo(r.n)>=0)throw new Error("Encrypted message is invalid.");var f=l(a,r,i),c=f.toString(16),p=e.util.createBuffer(),d=o-Math.ceil(c.length/2);while(d>0)p.putByte(0),--d;return p.putBytes(e.util.hexToBytes(c)),s!==!1?h(p.getBytes(),r,i):p.getBytes()},r.rsa.createKeyPairGenerationState=function(n,r,i){typeof n=="string"&&(n=parseInt(n,10)),n=n||2048,i=i||{};var s=i.prng||e.random,o={nextBytes:function(e){var t=s.getBytesSync(e.length);for(var n=0;n>1,pBits:n-(n>>1),pqState:0,num:null,keys:null},a.e.fromInt(a.eInt),a},r.rsa.stepKeyPairGenerationState=function(e,n){"algorithm"in e||(e.algorithm="PRIMEINC");var s=new t(null);s.fromInt(30);var o=0,u=function(e,t){return e|t},a=+(new Date),f,l=0;while(e.keys===null&&(n<=0||lc?e.pqState=0:e.num.isProbablePrime(v(e.num.bitLength()))?++e.pqState:e.num.dAddOffset(i[o++%8],0):e.pqState===2?e.pqState=e.num.subtract(t.ONE).gcd(e.e).compareTo(t.ONE)===0?3:0:e.pqState===3&&(e.pqState=0,e.p===null?e.p=e.num:e.q=e.num,e.p!==null&&e.q!==null&&++e.state,e.num=null)}else if(e.state===1)e.p.compareTo(e.q)<0&&(e.num=e.p,e.p=e.q,e.q=e.num),++e.state;else if(e.state===2)e.p1=e.p.subtract(t.ONE),e.q1=e.q.subtract(t.ONE),e.phi=e.p1.multiply(e.q1),++e.state;else if(e.state===3)e.phi.gcd(e.e).compareTo(t.ONE)===0?++e.state:(e.p=null,e.q=null,e.state=0);else if(e.state===4)e.n=e.p.multiply(e.q),e.n.bitLength()===e.bits?++e.state:(e.q=null,e.state=0);else if(e.state===5){var p=e.e.modInverse(e.phi);e.keys={privateKey:r.rsa.setPrivateKey(e.n,e.e,p,e.p,e.q,p.mod(e.p1),p.mod(e.q1),e.q.modInverse(e.p)),publicKey:r.rsa.setPublicKey(e.n,e.e)}}f=+(new Date),l+=f-a,a=f}return e.keys!==null},r.rsa.generateKeyPair=function(e,t,n,i){arguments.length===1?typeof e=="object"?(n=e,e=undefined):typeof e=="function"&&(i=e,e=undefined):arguments.length===2?typeof e=="number"?typeof t=="function"?(i=t,t=undefined):typeof t!="number"&&(n=t,t=undefined):(n=e,i=t,e=undefined,t=undefined):arguments.length===3&&(typeof t=="number"?typeof n=="function"&&(i=n,n=undefined):(i=n,n=t,t=undefined)),n=n||{},e===undefined&&(e=n.bits||2048),t===undefined&&(t=n.e||65537);var s=r.rsa.createKeyPairGenerationState(e,t,n);if(!i)return r.rsa.stepKeyPairGenerationState(s,0),s.keys;p(s,n,i)},r.setRsaPublicKey=r.rsa.setPublicKey=function(t,i){var s={n:t,e:i};return s.encrypt=function(t,n,i){typeof n=="string"?n=n.toUpperCase():n===undefined&&(n="RSAES-PKCS1-V1_5");if(n==="RSAES-PKCS1-V1_5")n={encode:function(e,t,n){return c(e,t,2).getBytes()}};else if(n==="RSA-OAEP"||n==="RSAES-OAEP")n={encode:function(t,n){return e.pkcs1.encode_rsa_oaep(n,t,i)}};else if(["RAW","NONE","NULL",null].indexOf(n)!==-1)n={encode:function(e){return e}};else if(typeof n=="string")throw new Error('Unsupported encryption scheme: "'+n+'".');var o=n.encode(t,s,!0);return r.rsa.encrypt(o,s,!0)},s.verify=function(e,t,i){typeof i=="string"?i=i.toUpperCase():i===undefined&&(i="RSASSA-PKCS1-V1_5");if(i==="RSASSA-PKCS1-V1_5")i={verify:function(e,t){t=h(t,s,!0);var r=n.fromDer(t);return e===r.value[1].value}};else if(i==="NONE"||i==="NULL"||i===null)i={verify:function(e,t){return t=h(t,s,!0),e===t}};var o=r.rsa.decrypt(t,s,!0,!1);return i.verify(e,o,s.n.bitLength())},s},r.setRsaPrivateKey=r.rsa.setPrivateKey=function(t,n,i,s,o,u,a,l){var c={n:t,e:n,d:i,p:s,q:o,dP:u,dQ:a,qInv:l};return c.decrypt=function(t,n,i){typeof n=="string"?n=n.toUpperCase():n===undefined&&(n="RSAES-PKCS1-V1_5");var s=r.rsa.decrypt(t,c,!1,!1);if(n==="RSAES-PKCS1-V1_5")n={decode:h};else if(n==="RSA-OAEP"||n==="RSAES-OAEP")n={decode:function(t,n){return e.pkcs1.decode_rsa_oaep(n,t,i)}};else{if(["RAW","NONE","NULL",null].indexOf(n)===-1)throw new Error('Unsupported encryption scheme: "'+n+'".');n={decode:function(e){return e}}}return n.decode(s,c,!1)},c.sign=function(e,t){var n=!1;typeof t=="string"&&(t=t.toUpperCase());if(t===undefined||t==="RSASSA-PKCS1-V1_5")t={encode:f},n=1;else if(t==="NONE"||t==="NULL"||t===null)t={encode:function(){return e}},n=1;var i=t.encode(e,c.n.bitLength());return r.rsa.encrypt(i,c,n)},c},r.wrapRsaPrivateKey=function(e){return n.create(n.Class.UNIVERSAL,n.Type.SEQUENCE,!0,[n.create(n.Class.UNIVERSAL,n.Type.INTEGER,!1,n.integerToDer(0).getBytes()),n.create(n.Class.UNIVERSAL,n.Type.SEQUENCE,!0,[n.create(n.Class.UNIVERSAL,n.Type.OID,!1,n.oidToDer(r.oids.rsaEncryption).getBytes()),n.create(n.Class.UNIVERSAL,n.Type.NULL,!1,"")]),n.create(n.Class.UNIVERSAL,n.Type.OCTETSTRING,!1,n.toDer(e).getBytes())])},r.privateKeyFromAsn1=function(i){var u={},a=[];n.validate(i,s,u,a)&&(i=n.fromDer(e.util.createBuffer(u.privateKey))),u={},a=[];if(!n.validate(i,o,u,a)){var f=new Error("Cannot read private key. ASN.1 object does not contain an RSAPrivateKey.");throw f.errors=a,f}var l,c,h,p,d,v,m,g;return l=e.util.createBuffer(u.privateKeyModulus).toHex(),c=e.util.createBuffer(u.privateKeyPublicExponent).toHex(),h=e.util.createBuffer(u.privateKeyPrivateExponent).toHex(),p=e.util.createBuffer(u.privateKeyPrime1).toHex(),d=e.util.createBuffer(u.privateKeyPrime2).toHex(),v=e.util.createBuffer(u.privateKeyExponent1).toHex(),m=e.util.createBuffer(u.privateKeyExponent2).toHex(),g=e.util.createBuffer(u.privateKeyCoefficient).toHex(),r.setRsaPrivateKey(new t(l,16),new t(c,16),new t(h,16),new t(p,16),new t(d,16),new t(v,16),new t(m,16),new t(g,16))},r.privateKeyToAsn1=r.privateKeyToRSAPrivateKey=function(e){return n.create(n.Class.UNIVERSAL,n.Type.SEQUENCE,!0,[n.create(n.Class.UNIVERSAL,n.Type.INTEGER,!1,n.integerToDer(0).getBytes()),n.create(n.Class.UNIVERSAL,n.Type.INTEGER,!1,d(e.n)),n.create(n.Class.UNIVERSAL,n.Type.INTEGER,!1,d(e.e)),n.create(n.Class.UNIVERSAL,n.Type.INTEGER,!1,d(e.d)),n.create(n.Class.UNIVERSAL,n.Type.INTEGER,!1,d(e.p)),n.create(n.Class.UNIVERSAL,n.Type.INTEGER,!1,d(e.q)),n.create(n.Class.UNIVERSAL,n.Type.INTEGER,!1,d(e.dP)),n.create(n.Class.UNIVERSAL,n.Type.INTEGER,!1,d(e.dQ)),n.create(n.Class.UNIVERSAL,n.Type.INTEGER,!1,d(e.qInv))])},r.publicKeyFromAsn1=function(i){var s={},o=[];if(n.validate(i,a,s,o)){var f=n.derToOid(s.publicKeyOid);if(f!==r.oids.rsaEncryption){var l=new Error("Cannot read public key. Unknown OID.");throw l.oid=f,l}i=s.rsaPublicKey}o=[];if(!n.validate(i,u,s,o)){var l=new Error("Cannot read public key. ASN.1 object does not contain an RSAPublicKey.");throw l.errors=o,l}var c=e.util.createBuffer(s.publicKeyModulus).toHex(),h=e.util.createBuffer(s.publicKeyExponent).toHex();return r.setRsaPublicKey(new t(c,16),new t(h,16))},r.publicKeyToAsn1=r.publicKeyToSubjectPublicKeyInfo=function(e){return n.create(n.Class.UNIVERSAL,n.Type.SEQUENCE,!0,[n.create(n.Class.UNIVERSAL,n.Type.SEQUENCE,!0,[n.create(n.Class.UNIVERSAL,n.Type.OID,!1,n.oidToDer(r.oids.rsaEncryption).getBytes()),n.create(n.Class.UNIVERSAL,n.Type.NULL,!1,"")]),n.create(n.Class.UNIVERSAL,n.Type.BITSTRING,!1,[r.publicKeyToRSAPublicKey(e)])])},r.publicKeyToRSAPublicKey=function(e){return n.create(n.Class.UNIVERSAL,n.Type.SEQUENCE,!0,[n.create(n.Class.UNIVERSAL,n.Type.INTEGER,!1,d(e.n)),n.create(n.Class.UNIVERSAL,n.Type.INTEGER,!1,d(e.e))])}}var r="rsa";if(typeof n!="function"){if(typeof module!="object"||!module.exports)return typeof forge=="undefined"&&(forge={}),e(forge);var i=!0;n=function(e,n){n(t,module)}}var s,o=function(t,n){n.exports=function(n){var i=s.map(function(e){return t(e)}).concat(e);n=n||{},n.defined=n.defined||{};if(n.defined[r])return n[r];n.defined[r]=!0;for(var o=0;o=0;a--)A>>=8,A+=N.at(a)+L.at(a),L.setAt(a,A&255);k.putBuffer(L)}w=k,c.putBuffer(x)}return c.truncate(c.length()-s),c},r.pbe.getCipher=function(e,t,n){switch(e){case r.oids.pkcs5PBES2:return r.pbe.getCipherForPBES2(e,t,n);case r.oids["pbeWithSHAAnd3-KeyTripleDES-CBC"]:case r.oids["pbewithSHAAnd40BitRC2-CBC"]:return r.pbe.getCipherForPKCS12PBE(e,t,n);default:var i=new Error("Cannot read encrypted PBE data block. Unsupported OID.");throw i.oid=e,i.supportedOids=["pkcs5PBES2","pbeWithSHAAnd3-KeyTripleDES-CBC","pbewithSHAAnd40BitRC2-CBC"],i}},r.pbe.getCipherForPBES2=function(t,i,s){var u={},a=[];if(!n.validate(i,o,u,a)){var f=new Error("Cannot read password-based-encryption algorithm parameters. ASN.1 object is not a supported EncryptedPrivateKeyInfo.");throw f.errors=a,f}t=n.derToOid(u.kdfOid);if(t!==r.oids.pkcs5PBKDF2){var f=new Error("Cannot read encrypted private key. Unsupported key derivation function OID.");throw f.oid=t,f.supportedOids=["pkcs5PBKDF2"],f}t=n.derToOid(u.encOid);if(t!==r.oids["aes128-CBC"]&&t!==r.oids["aes192-CBC"]&&t!==r.oids["aes256-CBC"]&&t!==r.oids["des-EDE3-CBC"]&&t!==r.oids.desCBC){var f=new Error("Cannot read encrypted private key. Unsupported encryption scheme OID.");throw f.oid=t,f.supportedOids=["aes128-CBC","aes192-CBC","aes256-CBC","des-EDE3-CBC","desCBC"],f}var l=u.kdfSalt,c=e.util.createBuffer(u.kdfIterationCount);c=c.getInt(c.length()<<3);var h,p;switch(r.oids[t]){case"aes128-CBC":h=16,p=e.aes.createDecryptionCipher;break;case"aes192-CBC":h=24,p=e.aes.createDecryptionCipher;break;case"aes256-CBC":h=32,p=e.aes.createDecryptionCipher;break;case"des-EDE3-CBC":h=24,p=e.des.createDecryptionCipher;break;case"desCBC":h=8,p=e.des.createDecryptionCipher}var d=e.pkcs5.pbkdf2(s,l,c,h),v=u.encIv,m=p(d);return m.start(v),m},r.pbe.getCipherForPKCS12PBE=function(t,i,s){var o={},a=[];if(!n.validate(i,u,o,a)){var f=new Error("Cannot read password-based-encryption algorithm parameters. ASN.1 object is not a supported EncryptedPrivateKeyInfo.");throw f.errors=a,f}var l=e.util.createBuffer(o.salt),c=e.util.createBuffer(o.iterations);c=c.getInt(c.length()<<3);var h,p,d;switch(t){case r.oids["pbeWithSHAAnd3-KeyTripleDES-CBC"]:h=24,p=8,d=e.des.startDecrypting;break;case r.oids["pbewithSHAAnd40BitRC2-CBC"]:h=5,p=8,d=function(t,n){var r=e.rc2.createDecryptionCipher(t,40);return r.start(n,null),r};break;default:var f=new Error("Cannot read PKCS #12 PBE data block. Unsupported OID.");throw f.oid=t,f}var v=r.pbe.generatePkcs12Key(s,l,1,c,h),m=r.pbe.generatePkcs12Key(s,l,2,c,p);return d(v,m)}}var r="pbe";if(typeof n!="function"){if(typeof module!="object"||!module.exports)return typeof forge=="undefined"&&(forge={}),e(forge);var i=!0;n=function(e,n){n(t,module)}}var s,o=function(t,n){n.exports=function(n){var i=s.map(function(e){return t(e)}).concat(e);n=n||{},n.defined=n.defined||{};if(n.defined[r])return n[r];n.defined[r]=!0;for(var o=0;o>8*c-l&255;return w=String.fromCharCode(w.charCodeAt(0)&~E)+w.substr(1),w+v+String.fromCharCode(188)},a.verify=function(t,s,u){var a,f=u-1,l=Math.ceil(f/8);s=s.substr(-l);if(l>8*l-f&255;if((h.charCodeAt(0)&d)!==0)throw new Error("Bits beyond keysize not zero as expected.");var v=r.generate(p,c),m="";for(a=0;a1&&(c=l.value.charCodeAt(1),h=l.value.length>2?l.value.charCodeAt(2):0),s.digitalSignature=(c&128)===128,s.nonRepudiation=(c&64)===64,s.keyEncipherment=(c&32)===32,s.dataEncipherment=(c&16)===16,s.keyAgreement=(c&8)===8,s.keyCertSign=(c&4)===4,s.cRLSign=(c&2)===2,s.encipherOnly=(c&1)===1,s.decipherOnly=(h&128)===128}else if(s.name==="basicConstraints"){var l=t.fromDer(s.value);l.value.length>0&&l.value[0].type===t.Type.BOOLEAN?s.cA=l.value[0].value.charCodeAt(0)!==0:s.cA=!1;var p=null;l.value.length>0&&l.value[0].type===t.Type.INTEGER?p=l.value[0].value:l.value.length>1&&(p=l.value[1].value),p!==null&&(s.pathLenConstraint=t.derToInteger(p))}else if(s.name==="extKeyUsage"){var l=t.fromDer(s.value);for(var d=0;d1&&(c=l.value.charCodeAt(1)),s.client=(c&128)===128,s.server=(c&64)===64,s.email=(c&32)===32,s.objsign=(c&16)===16,s.reserved=(c&8)===8,s.sslCA=(c&4)===4,s.emailCA=(c&2)===2,s.objCA=(c&1)===1}else if(s.name==="subjectAltName"||s.name==="issuerAltName"){s.altNames=[];var m,l=t.fromDer(s.value);for(var g=0;g2)throw new Error("Cannot read notBefore/notAfter validity times; more than two times were provided in the certificate.");if(w.length<2)throw new Error("Cannot read notBefore/notAfter validity times; they were not provided as either UTCTime or GeneralizedTime.");g.validity.notBefore=w[0],g.validity.notAfter=w[1],g.tbsCertificate=u.tbsCertificate;if(s){g.md=null;if(g.signatureOid in r){var v=r[g.signatureOid];switch(v){case"sha1WithRSAEncryption":g.md=e.md.sha1.create();break;case"md5WithRSAEncryption":g.md=e.md.md5.create();break;case"sha256WithRSAEncryption":g.md=e.md.sha256.create();break;case"RSASSA-PSS":g.md=e.md.sha256.create()}}if(g.md===null){var f=new Error("Could not compute certificate digest. Unknown signature OID.");throw f.signatureOid=g.signatureOid,f}var E=t.toDer(g.tbsCertificate);g.md.update(E.getBytes())}var S=e.md.sha1.create();g.issuer.getField=function(e){return l(g.issuer,e)},g.issuer.addField=function(e){m([e]),g.issuer.attributes.push(e)},g.issuer.attributes=n.RDNAttributesAsArray(u.certIssuer,S),u.certIssuerUniqueId&&(g.issuer.uniqueId=u.certIssuerUniqueId),g.issuer.hash=S.digest().toHex();var x=e.md.sha1.create();return g.subject.getField=function(e){return l(g.subject,e)},g.subject.addField=function(e){m([e]),g.subject.attributes.push(e)},g.subject.attributes=n.RDNAttributesAsArray(u.certSubject,x),u.certSubjectUniqueId&&(g.subject.uniqueId=u.certSubjectUniqueId),g.subject.hash=x.digest().toHex(),u.certExtensions?g.extensions=c(u.certExtensions):g.extensions=[],g.publicKey=n.publicKeyFromAsn1(u.subjectPublicKeyInfo),g},n.certificationRequestFromAsn1=function(i,s){var o={},u=[];if(!t.validate(i,f,o,u)){var a=new Error("Cannot read PKCS#10 certificate request. ASN.1 object is not a PKCS#10 CertificationRequest.");throw a.errors=u,a}if(typeof o.csrSignature!="string"){var c="\0";for(var p=0;p0&&i.value.push(d(r.extensions)),i},n.getCertificationRequestInfo=function(e){var r=t.create(t.Class.UNIVERSAL,t.Type.SEQUENCE,!0,[t.create(t.Class.UNIVERSAL,t.Type.INTEGER,!1,t.integerToDer(e.version).getBytes()),p(e.subject),n.publicKeyToAsn1(e.publicKey),y(e)]);return r},n.distinguishedNameToAsn1=function(e){return p(e)},n.certificateToAsn1=function(e){var r=e.tbsCertificate||n.getTBSCertificate(e);return t.create(t.Class.UNIVERSAL,t.Type.SEQUENCE,!0,[r,t.create(t.Class.UNIVERSAL,t.Type.SEQUENCE,!0,[t.create(t.Class.UNIVERSAL,t.Type.OID,!1,t.oidToDer(e.signatureOid).getBytes()),g(e.signatureOid,e.signatureParameters)]),t.create(t.Class.UNIVERSAL,t.Type.BITSTRING,!1,String.fromCharCode(0)+e.signature)])},n.certificationRequestToAsn1=function(e){var r=e.certificationRequestInfo||n.getCertificationRequestInfo(e);return t.create(t.Class.UNIVERSAL,t.Type.SEQUENCE,!0,[r,t.create(t.Class.UNIVERSAL,t.Type.SEQUENCE,!0,[t.create(t.Class.UNIVERSAL,t.Type.OID,!1,t.oidToDer(e.signatureOid).getBytes()),g(e.signatureOid,e.signatureParameters)]),t.create(t.Class.UNIVERSAL,t.Type.BITSTRING,!1,String.fromCharCode(0)+e.signature)])},n.createCaStore=function(t){var r={certs:{}};r.getIssuer=function(t){var i=null;if(!t.issuer.hash){var s=e.md.sha1.create();t.issuer.attributes=n.RDNAttributesAsArray(p(t.issuer),s),t.issuer.hash=s.digest().toHex()}if(t.issuer.hash in r.certs){i=r.certs[t.issuer.hash];if(e.util.isArray(i))throw new Error("Resolving multiple issuer matches not implemented yet.")}return i},r.addCertificate=function(t){typeof t=="string"&&(t=e.pki.certificateFromPem(t));if(!t.subject.hash){var i=e.md.sha1.create();t.subject.attributes=n.RDNAttributesAsArray(p(t.subject),i),t.subject.hash=i.digest().toHex()}if(t.subject.hash in r.certs){var s=r.certs[t.subject.hash];e.util.isArray(s)||(s=[s]),s.push(t)}else r.certs[t.subject.hash]=t};if(t)for(var i=0;ic.validity.notAfter)a={message:"Certificate is not valid yet or has expired.",error:n.certificateError.certificate_expired,notBefore:c.validity.notBefore,notAfter:c.validity.notAfter,now:o};else{var h=!1;if(r.length>0){l=r[0];try{h=l.verify(c)}catch(p){}}else{var d=t.getIssuer(c);if(d===null)a={message:"Certificate is not trusted.",error:n.certificateError.unknown_ca};else{e.util.isArray(d)||(d=[d]);while(!h&&d.length>0){l=d.shift();try{h=l.verify(c)}catch(p){}}}}a===null&&!h&&(a={message:"Certificate signature is invalid.",error:n.certificateError.bad_certificate})}a===null&&!c.isIssuer(l)&&(a={message:"Certificate issuer is invalid.",error:n.certificateError.bad_certificate});if(a===null){var v={keyUsage:!0,basicConstraints:!0};for(var m=0;a===null&&mE&&(a={message:"Certificate basicConstraints pathLenConstraint violated.",error:n.certificateError.bad_certificate})}}var S=a===null?!0:a.error,x=i?i(S,f,s):S;if(x!==!0){S===!0&&(a={message:"The application rejected the certificate.",error:n.certificateError.bad_certificate});if(x||x===0)typeof x=="object"&&!e.util.isArray(x)?(x.message&&(a.message=x.message),x.error&&(a.error=x.error)):typeof x=="string"&&(a.error=x);throw a}a=null,u=!1,++f}while(r.length>0);return!0}}var r="x509";if(typeof n!="function"){if(typeof module!="object"||!module.exports)return typeof forge=="undefined"&&(forge={}),e(forge);var i=!0;n=function(e,n){n(t,module)}}var s,o=function(t,n){n.exports=function(n){var i=s.map(function(e){return t(e)}).concat(e);n=n||{},n.defined=n.defined||{};if(n.defined[r])return n[r];n.defined[r]=!0;for(var o=0;o=0&&i.push(u)}return i}function l(t){if(t.composed||t.constructed){var n=e.util.createBuffer();for(var r=0;r0&&(f=t.create(t.Class.UNIVERSAL,t.Type.SET,!0,h));var p=[],d=[];s!==null&&(e.util.isArray(s)?d=s:d=[s]);var v=[];for(var m=0;m0){var w=t.create(t.Class.UNIVERSAL,t.Type.SEQUENCE,!0,v),E=t.create(t.Class.UNIVERSAL,t.Type.SEQUENCE,!0,[t.create(t.Class.UNIVERSAL,t.Type.OID,!1,t.oidToDer(n.oids.data).getBytes()),t.create(t.Class.CONTEXT_SPECIFIC,0,!0,[t.create(t.Class.UNIVERSAL,t.Type.OCTETSTRING,!1,t.toDer(w).getBytes())])]);p.push(E)}var S=null;if(i!==null){var x=n.wrapRsaPrivateKey(n.privateKeyToAsn1(i));o===null?S=t.create(t.Class.UNIVERSAL,t.Type.SEQUENCE,!0,[t.create(t.Class.UNIVERSAL,t.Type.OID,!1,t.oidToDer(n.oids.keyBag).getBytes()),t.create(t.Class.CONTEXT_SPECIFIC,0,!0,[x]),f]):S=t.create(t.Class.UNIVERSAL,t.Type.SEQUENCE,!0,[t.create(t.Class.UNIVERSAL,t.Type.OID,!1,t.oidToDer(n.oids.pkcs8ShroudedKeyBag).getBytes()),t.create(t.Class.CONTEXT_SPECIFIC,0,!0,[n.encryptPrivateKeyInfo(x,o,u)]),f]);var T=t.create(t.Class.UNIVERSAL,t.Type.SEQUENCE,!0,[S]),N=t.create(t.Class.UNIVERSAL,t.Type.SEQUENCE,!0,[t.create(t.Class.UNIVERSAL,t.Type.OID,!1,t.oidToDer(n.oids.data).getBytes()),t.create(t.Class.CONTEXT_SPECIFIC,0,!0,[t.create(t.Class.UNIVERSAL,t.Type.OCTETSTRING,!1,t.toDer(T).getBytes())])]);p.push(N)}var C=t.create(t.Class.UNIVERSAL,t.Type.SEQUENCE,!0,p),k;if(u.useMac){var c=e.md.sha1.create(),L=new e.util.ByteBuffer(e.random.getBytes(u.saltSize)),A=u.count,i=r.generateKey(o,L,3,A,20),O=e.hmac.create();O.start(c,i),O.update(t.toDer(C).getBytes());var M=O.getMac();k=t.create(t.Class.UNIVERSAL,t.Type.SEQUENCE,!0,[t.create(t.Class.UNIVERSAL,t.Type.SEQUENCE,!0,[t.create(t.Class.UNIVERSAL,t.Type.SEQUENCE,!0,[t.create(t.Class.UNIVERSAL,t.Type.OID,!1,t.oidToDer(n.oids.sha1).getBytes()),t.create(t.Class.UNIVERSAL,t.Type.NULL,!1,"")]),t.create(t.Class.UNIVERSAL,t.Type.OCTETSTRING,!1,M.getBytes())]),t.create(t.Class.UNIVERSAL,t.Type.OCTETSTRING,!1,L.getBytes()),t.create(t.Class.UNIVERSAL,t.Type.INTEGER,!1,t.integerToDer(A).getBytes())])}return t.create(t.Class.UNIVERSAL,t.Type.SEQUENCE,!0,[t.create(t.Class.UNIVERSAL,t.Type.INTEGER,!1,t.integerToDer(3).getBytes()),t.create(t.Class.UNIVERSAL,t.Type.SEQUENCE,!0,[t.create(t.Class.UNIVERSAL,t.Type.OID,!1,t.oidToDer(n.oids.data).getBytes()),t.create(t.Class.CONTEXT_SPECIFIC,0,!0,[t.create(t.Class.UNIVERSAL,t.Type.OCTETSTRING,!1,t.toDer(C).getBytes())])]),k])},r.generateKey=e.pbe.generatePkcs12Key}var r="pkcs12";if(typeof n!="function"){if(typeof module!="object"||!module.exports)return typeof forge=="undefined"&&(forge={}),e(forge);var i=!0;n=function(e,n){n(t,module)}}var s,o=function(t,n){n.exports=function(n){var i=s.map(function(e){return t(e)}).concat(e);n=n||{},n.defined=n.defined||{};if(n.defined[r])return n[r];n.defined[r]=!0;for(var o=0;o>1,u=o+(t.length&1),a=t.substr(0,u),f=t.substr(o,u),l=e.util.createBuffer(),c=e.hmac.create();r=n+r;var h=Math.ceil(i/16),p=Math.ceil(i/20);c.start("MD5",a);var d=e.util.createBuffer();l.putBytes(r);for(var v=0;v0&&(a.queue(e,a.createAlert(e,{level:a.Alert.Level.warning,description:a.Alert.Description.no_renegotiation})),a.flush(e)),e.process()},a.parseHelloMessage=function(t,n,r){var i=null,s=t.entity===a.ConnectionEnd.client;if(r<38)t.error(t,{message:s?"Invalid ServerHello message. Message too short.":"Invalid ClientHello message. Message too short.",send:!0,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.illegal_parameter}});else{var u=n.fragment,f=u.length();i={version:{major:u.getByte(),minor:u.getByte()},random:e.util.createBuffer(u.getBytes(32)),session_id:o(u,1),extensions:[]},s?(i.cipher_suite=u.getBytes(2),i.compression_method=u.getByte()):(i.cipher_suites=o(u,2),i.compression_methods=o(u,1)),f=r-(f-u.length());if(f>0){var l=o(u,2);while(l.length()>0)i.extensions.push({type:[l.getByte(),l.getByte()],data:o(l,2)});if(!s)for(var c=0;c0){var d=p.getByte();if(d!==0)break;t.session.extensions.server_name.serverNameList.push(o(p,2).getBytes())}}}}if(t.session.version)if(i.version.major!==t.session.version.major||i.version.minor!==t.session.version.minor)return t.error(t,{message:"TLS version change is disallowed during renegotiation.",send:!0,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.protocol_version}});if(s)t.session.cipherSuite=a.getCipherSuite(i.cipher_suite);else{var v=e.util.createBuffer(i.cipher_suites.bytes());while(v.length()>0){t.session.cipherSuite=a.getCipherSuite(v.getBytes(2));if(t.session.cipherSuite!==null)break}}if(t.session.cipherSuite===null)return t.error(t,{message:"No cipher suites in common.",send:!0,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.handshake_failure},cipherSuite:e.util.bytesToHex(i.cipher_suite)});s?t.session.compressionMethod=i.compression_method:t.session.compressionMethod=a.CompressionMethod.none}return i},a.createSecurityParameters=function(e,t){var n=e.entity===a.ConnectionEnd.client,r=t.random.bytes(),i=n?e.session.sp.client_random:r,s=n?r:a.createRandom().getBytes();e.session.sp={entity:e.entity,prf_algorithm:a.PRFAlgorithm.tls_prf_sha256,bulk_cipher_algorithm:null,cipher_type:null,enc_key_length:null,block_length:null,fixed_iv_length:null,record_iv_length:null,mac_algorithm:null,mac_length:null,mac_key_length:null,compression_algorithm:e.session.compressionMethod,pre_master_secret:null,master_secret:null,client_random:i,server_random:s}},a.handleServerHello=function(e,t,n){var r=a.parseHelloMessage(e,t,n);if(e.fail)return;if(!(r.version.minor<=e.version.minor))return e.error(e,{message:"Incompatible TLS version.",send:!0,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.protocol_version}});e.version.minor=r.version.minor,e.session.version=e.version;var i=r.session_id.bytes();i.length>0&&i===e.session.id?(e.expect=d,e.session.resuming=!0,e.session.sp.server_random=r.random.bytes()):(e.expect=l,e.session.resuming=!1,a.createSecurityParameters(e,r)),e.session.id=i,e.process()},a.handleClientHello=function(t,n,r){var i=a.parseHelloMessage(t,n,r);if(t.fail)return;var s=i.session_id.bytes(),o=null;if(t.sessionCache){o=t.sessionCache.getSession(s);if(o===null)s="";else if(o.version.major!==i.version.major||o.version.minor>i.version.minor)o=null,s=""}s.length===0&&(s=e.random.getBytes(32)),t.session.id=s,t.session.clientHelloVersion=i.version,t.session.sp={};if(o)t.version=t.session.version=o.version,t.session.sp=o.sp;else{var u;for(var f=1;f0)u=o(s.certificate_list,3),f=e.asn1.fromDer(u),u=e.pki.certificateFromAsn1(f,!0),l.push(u)}catch(h){return t.error(t,{message:"Could not parse certificate list.",cause:h,send:!0,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.bad_certificate}})}var p=t.entity===a.ConnectionEnd.client;!p&&t.verifyClient!==!0||l.length!==0?l.length===0?t.expect=p?c:w:(p?t.session.serverCertificate=l[0]:t.session.clientCertificate=l[0],a.verifyCertificateChain(t,l)&&(t.expect=p?c:w)):t.error(t,{message:p?"No server certificate provided.":"No client certificate provided.",send:!0,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.illegal_parameter}}),t.process()},a.handleServerKeyExchange=function(e,t,n){if(n>0)return e.error(e,{message:"Invalid key parameters. Only RSA is supported.",send:!0,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.unsupported_certificate}});e.expect=h,e.process()},a.handleClientKeyExchange=function(t,n,r){if(r<48)return t.error(t,{message:"Invalid key parameters. Only RSA is supported.",send:!0,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.unsupported_certificate}});var i=n.fragment,s={enc_pre_master_secret:o(i,2).getBytes()},u=null;if(t.getPrivateKey)try{u=t.getPrivateKey(t,t.session.serverCertificate),u=e.pki.privateKeyFromPem(u)}catch(f){t.error(t,{message:"Could not get private key.",cause:f,send:!0,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.internal_error}})}if(u===null)return t.error(t,{message:"No private key set.",send:!0,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.internal_error}});try{var l=t.session.sp;l.pre_master_secret=u.decrypt(s.enc_pre_master_secret);var c=t.session.clientHelloVersion;if(c.major!==l.pre_master_secret.charCodeAt(0)||c.minor!==l.pre_master_secret.charCodeAt(1))throw new Error("TLS version rollback attack detected.")}catch(f){l.pre_master_secret=e.random.getBytes(48)}t.expect=S,t.session.clientCertificate!==null&&(t.expect=E),t.process()},a.handleCertificateRequest=function(e,t,n){if(n<3)return e.error(e,{message:"Invalid CertificateRequest. Message too short.",send:!0,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.illegal_parameter}});var r=t.fragment,i={certificate_types:o(r,1),certificate_authorities:o(r,2)};e.session.certificateRequest=i,e.expect=p,e.process()},a.handleCertificateVerify=function(t,n,r){if(r<2)return t.error(t,{message:"Invalid CertificateVerify. Message too short.",send:!0,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.illegal_parameter}});var i=n.fragment;i.read-=4;var s=i.bytes();i.read+=4;var u={signature:o(i,2).getBytes()},f=e.util.createBuffer();f.putBuffer(t.session.md5.digest()),f.putBuffer(t.session.sha1.digest()),f=f.getBytes();try{var l=t.session.clientCertificate;if(!l.publicKey.verify(f,u.signature,"NONE"))throw new Error("CertificateVerify signature does not match.");t.session.md5.update(s),t.session.sha1.update(s)}catch(c){return t.error(t,{message:"Bad signature in CertificateVerify.",send:!0,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.handshake_failure}})}t.expect=S,t.process()},a.handleServerHelloDone=function(t,n,r){if(r>0)return t.error(t,{message:"Invalid ServerHelloDone message. Invalid length.",send:!0,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.record_overflow}});if(t.serverCertificate===null){var i={message:"No server certificate provided. Not enough security.",send:!0,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.insufficient_security}},s=0,o=t.verify(t,i.alert.description,s,[]);if(o!==!0){if(o||o===0)typeof o=="object"&&!e.util.isArray(o)?(o.message&&(i.message=o.message),o.alert&&(i.alert.description=o.alert)):typeof o=="number"&&(i.alert.description=o);return t.error(t,i)}}t.session.certificateRequest!==null&&(n=a.createRecord(t,{type:a.ContentType.handshake,data:a.createCertificate(t)}),a.queue(t,n)),n=a.createRecord(t,{type:a.ContentType.handshake,data:a.createClientKeyExchange(t)}),a.queue(t,n),t.expect=g;var u=function(e,t){e.session.certificateRequest!==null&&e.session.clientCertificate!==null&&a.queue(e,a.createRecord(e,{type:a.ContentType.handshake,data:a.createCertificateVerify(e,t)})),a.queue(e,a.createRecord(e,{type:a.ContentType.change_cipher_spec,data:a.createChangeCipherSpec()})),e.state.pending=a.createConnectionState(e),e.state.current.write=e.state.pending.write,a.queue(e,a.createRecord(e,{type:a.ContentType.handshake,data:a.createFinished(e)})),e.expect=d,a.flush(e),e.process()};if(t.session.certificateRequest===null||t.session.clientCertificate===null)return u(t,null);a.getClientSignature(t,u)},a.handleChangeCipherSpec=function(e,t){if(t.fragment.getByte()!==1)return e.error(e,{message:"Invalid ChangeCipherSpec message received.",send:!0,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.illegal_parameter}});var n=e.entity===a.ConnectionEnd.client;if(e.session.resuming&&n||!e.session.resuming&&!n)e.state.pending=a.createConnectionState(e);e.state.current.read=e.state.pending.read;if(!e.session.resuming&&n||e.session.resuming&&!n)e.state.pending=null;e.expect=n?v:x,e.process()},a.handleFinished=function(n,r,i){var s=r.fragment;s.read-=4;var o=s.bytes();s.read+=4;var u=r.fragment.getBytes();s=e.util.createBuffer(),s.putBuffer(n.session.md5.digest()),s.putBuffer(n.session.sha1.digest());var f=n.entity===a.ConnectionEnd.client,l=f?"server finished":"client finished",c=n.session.sp,h=12,p=t;s=p(c.master_secret,l,s.getBytes(),h);if(s.getBytes()!==u)return n.error(n,{message:"Invalid verify_data in Finished message.",send:!0,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.decrypt_error}});n.session.md5.update(o),n.session.sha1.update(o);if(n.session.resuming&&f||!n.session.resuming&&!f)a.queue(n,a.createRecord(n,{type:a.ContentType.change_cipher_spec,data:a.createChangeCipherSpec()})),n.state.current.write=n.state.pending.write,n.state.pending=null,a.queue(n,a.createRecord(n,{type:a.ContentType.handshake,data:a.createFinished(n)}));n.expect=f?m:T,n.handshaking=!1,++n.handshakes,n.peerCertificate=f?n.session.serverCertificate:n.session.clientCertificate,a.flush(n),n.isConnected=!0,n.connected(n),n.process()},a.handleAlert=function(e,t){var n=t.fragment,r={level:n.getByte(),description:n.getByte()},i;switch(r.description){case a.Alert.Description.close_notify:i="Connection closed.";break;case a.Alert.Description.unexpected_message:i="Unexpected message.";break;case a.Alert.Description.bad_record_mac:i="Bad record MAC.";break;case a.Alert.Description.decryption_failed:i="Decryption failed.";break;case a.Alert.Description.record_overflow:i="Record overflow.";break;case a.Alert.Description.decompression_failure:i="Decompression failed.";break;case a.Alert.Description.handshake_failure:i="Handshake failure.";break;case a.Alert.Description.bad_certificate:i="Bad certificate.";break;case a.Alert.Description.unsupported_certificate:i="Unsupported certificate.";break;case a.Alert.Description.certificate_revoked:i="Certificate revoked.";break;case a.Alert.Description.certificate_expired:i="Certificate expired.";break;case a.Alert.Description.certificate_unknown:i="Certificate unknown.";break;case a.Alert.Description.illegal_parameter:i="Illegal parameter.";break;case a.Alert.Description.unknown_ca:i="Unknown certificate authority.";break;case a.Alert.Description.access_denied:i="Access denied.";break;case a.Alert.Description.decode_error:i="Decode error.";break;case a.Alert.Description.decrypt_error:i="Decrypt error.";break;case a.Alert.Description.export_restriction:i="Export restriction.";break;case a.Alert.Description.protocol_version:i="Unsupported protocol version.";break;case a.Alert.Description.insufficient_security:i="Insufficient security.";break;case a.Alert.Description.internal_error:i="Internal error.";break;case a.Alert.Description.user_canceled:i="User canceled.";break;case a.Alert.Description.no_renegotiation:i="Renegotiation not supported.";break;default:i="Unknown error."}if(r.description===a.Alert.Description.close_notify)return e.close();e.error(e,{message:i,send:!1,origin:e.entity===a.ConnectionEnd.client?"server":"client",alert:r}),e.process()},a.handleHandshake=function(t,n){var r=n.fragment,i=r.getByte(),s=r.getInt24();if(s>r.length())return t.fragmented=n,n.fragment=e.util.createBuffer(),r.read-=4,t.process();t.fragmented=null,r.read-=4;var o=r.bytes(s+4);r.read+=4,i in q[t.entity][t.expect]?(t.entity===a.ConnectionEnd.server&&!t.open&&!t.fail&&(t.handshaking=!0,t.session={version:null,extensions:{server_name:{serverNameList:[]}},cipherSuite:null,compressionMethod:null,serverCertificate:null,clientCertificate:null,md5:e.md.md5.create(),sha1:e.md.sha1.create()}),i!==a.HandshakeType.hello_request&&i!==a.HandshakeType.certificate_verify&&i!==a.HandshakeType.finished&&(t.session.md5.update(o),t.session.sha1.update(o)),q[t.entity][t.expect][i](t,n,s)):a.handleUnexpected(t,n)},a.handleApplicationData=function(e,t){e.data.putBuffer(t.fragment),e.dataReady(e),e.process()},a.handleHeartbeat=function(t,n){var r=n.fragment,i=r.getByte(),s=r.getInt16(),o=r.getBytes(s);if(i===a.HeartbeatMessageType.heartbeat_request){if(t.handshaking||s>o.length)return t.process();a.queue(t,a.createRecord(t,{type:a.ContentType.heartbeat,data:a.createHeartbeat(a.HeartbeatMessageType.heartbeat_response,o)})),a.flush(t)}else if(i===a.HeartbeatMessageType.heartbeat_response){if(o!==t.expectedHeartbeatPayload)return t.process();t.heartbeatReceived&&t.heartbeatReceived(t,e.util.createBuffer(o))}t.process()};var f=0,l=1,c=2,h=3,p=4,d=5,v=6,m=7,g=8,y=0,b=1,w=2,E=3,S=4,x=5,T=6,N=7,C=a.handleUnexpected,k=a.handleChangeCipherSpec,L=a.handleAlert,A=a.handleHandshake,O=a.handleApplicationData,M=a.handleHeartbeat,_=[];_[a.ConnectionEnd.client]=[[C,L,A,C,M],[C,L,A,C,M],[C,L,A,C,M],[C,L,A,C,M],[C,L,A,C,M],[k,L,C,C,M],[C,L,A,C,M],[C,L,A,O,M],[C,L,A,C,M]],_[a.ConnectionEnd.server]=[[C,L,A,C,M],[C,L,A,C,M],[C,L,A,C,M],[C,L,A,C,M],[k,L,C,C,M],[C,L,A,C,M],[C,L,A,O,M],[C,L,A,C,M]];var D=a.handleHelloRequest,P=a.handleServerHello,H=a.handleCertificate,B=a.handleServerKeyExchange,j=a.handleCertificateRequest,F=a.handleServerHelloDone,I=a.handleFinished,q=[];q[a.ConnectionEnd.client]=[[C,C,P,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C],[D,C,C,C,C,C,C,C,C,C,C,H,B,j,F,C,C,C,C,C,C],[D,C,C,C,C,C,C,C,C,C,C,C,B,j,F,C,C,C,C,C,C],[D,C,C,C,C,C,C,C,C,C,C,C,C,j,F,C,C,C,C,C,C],[D,C,C,C,C,C,C,C,C,C,C,C,C,C,F,C,C,C,C,C,C],[D,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C],[D,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,I],[D,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C],[D,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C]];var R=a.handleClientHello,U=a.handleClientKeyExchange,z=a.handleCertificateVerify;q[a.ConnectionEnd.server]=[[C,R,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C],[C,C,C,C,C,C,C,C,C,C,C,H,C,C,C,C,C,C,C,C,C],[C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,U,C,C,C,C],[C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,z,C,C,C,C,C],[C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C],[C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,I],[C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C],[C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C]],a.generateKeys=function(e,n){var r=t,i=n.client_random+n.server_random;e.session.resuming||(n.master_secret=r(n.pre_master_secret,"master secret",i,48).bytes(),n.pre_master_secret=null),i=n.server_random+n.client_random;var s=2*n.mac_key_length+2*n.enc_key_length,o=e.version.major===a.Versions.TLS_1_0.major&&e.version.minor===a.Versions.TLS_1_0.minor;o&&(s+=2*n.fixed_iv_length);var u=r(n.master_secret,"key expansion",i,s),f={client_write_MAC_key:u.getBytes(n.mac_key_length),server_write_MAC_key:u.getBytes(n.mac_key_length),client_write_key:u.getBytes(n.enc_key_length),server_write_key:u.getBytes(n.enc_key_length)};return o&&(f.client_write_IV=u.getBytes(n.fixed_iv_length),f.server_write_IV=u.getBytes(n.fixed_iv_length)),f},a.createConnectionState=function(e){var t=e.entity===a.ConnectionEnd.client,n=function(){var e={sequenceNumber:[0,0],macKey:null,macLength:0,macFunction:null,cipherState:null,cipherFunction:function(e){return!0},compressionState:null,compressFunction:function(e){return!0},updateSequenceNumber:function(){e.sequenceNumber[1]===4294967295?(e.sequenceNumber[1]=0,++e.sequenceNumber[0]):++e.sequenceNumber[1]}};return e},r={read:n(),write:n()};r.read.update=function(e,t){return r.read.cipherFunction(t,r.read)?r.read.compressFunction(e,t,r.read)||e.error(e,{message:"Could not decompress record.",send:!0,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.decompression_failure}}):e.error(e,{message:"Could not decrypt record or bad MAC.",send:!0,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.bad_record_mac}}),!e.fail},r.write.update=function(e,t){return r.write.compressFunction(e,t,r.write)?r.write.cipherFunction(t,r.write)||e.error(e,{message:"Could not encrypt record.",send:!1,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.internal_error}}):e.error(e,{message:"Could not compress record.",send:!1,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.internal_error}}),!e.fail};if(e.session){var o=e.session.sp;e.session.cipherSuite.initSecurityParameters(o),o.keys=a.generateKeys(e,o),r.read.macKey=t?o.keys.server_write_MAC_key:o.keys.client_write_MAC_key,r.write.macKey=t?o.keys.client_write_MAC_key:o.keys.server_write_MAC_key,e.session.cipherSuite.initConnectionState(r,e,o);switch(o.compression_algorithm){case a.CompressionMethod.none:break;case a.CompressionMethod.deflate:r.read.compressFunction=s,r.write.compressFunction=i;break;default:throw new Error("Unsupported compression algorithm.")}}return r},a.createRandom=function(){var t=new Date,n=+t+t.getTimezoneOffset()*6e4,r=e.util.createBuffer();return r.putInt32(n),r.putBytes(e.random.getBytes(28)),r},a.createRecord=function(e,t){if(!t.data)return null;var n={type:t.type,version:{major:e.version.major,minor:e.version.minor},length:t.data.length(),fragment:t.data};return n},a.createAlert=function(t,n){var r=e.util.createBuffer();return r.putByte(n.level),r.putByte(n.description),a.createRecord(t,{type:a.ContentType.alert,data:r})},a.createClientHello=function(t){t.session.clientHelloVersion={major:t.version.major,minor:t.version.minor};var n=e.util.createBuffer();for(var r=0;r0&&(d+=2);var v=t.session.id,m=v.length+1+2+4+28+2+s+1+f+d,g=e.util.createBuffer();return g.putByte(a.HandshakeType.client_hello),g.putInt24(m),g.putByte(t.version.major),g.putByte(t.version.minor),g.putBytes(t.session.sp.client_random),u(g,1,e.util.createBuffer(v)),u(g,2,n),u(g,1,o),d>0&&u(g,2,l),g},a.createServerHello=function(t){var n=t.session.id,r=n.length+1+2+4+28+2+1,i=e.util.createBuffer();return i.putByte(a.HandshakeType.server_hello),i.putInt24(r),i.putByte(t.version.major),i.putByte(t.version.minor),i.putBytes(t.session.sp.server_random),u(i,1,e.util.createBuffer(n)),i.putByte(t.session.cipherSuite.id[0]),i.putByte(t.session.cipherSuite.id[1]),i.putByte(t.session.compressionMethod),i},a.createCertificate=function(t){var n=t.entity===a.ConnectionEnd.client,r=null;if(t.getCertificate){var i;n?i=t.session.certificateRequest:i=t.session.extensions.server_name.serverNameList,r=t.getCertificate(t,i)}var s=e.util.createBuffer();if(r!==null)try{e.util.isArray(r)||(r=[r]);var o=null;for(var f=0;f0&&(r.putByte(a.HandshakeType.server_key_exchange),r.putInt24(n)),r},a.getClientSignature=function(t,n){var r=e.util.createBuffer();r.putBuffer(t.session.md5.digest()),r.putBuffer(t.session.sha1.digest()),r=r.getBytes(),t.getSignature=t.getSignature||function(t,n,r){var i=null;if(t.getPrivateKey)try{i=t.getPrivateKey(t,t.session.clientCertificate),i=e.pki.privateKeyFromPem(i)}catch(s){t.error(t,{message:"Could not get private key.",cause:s,send:!0,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.internal_error}})}i===null?t.error(t,{message:"No private key set.",send:!0,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.internal_error}}):n=i.sign(n,null),r(t,n)},t.getSignature(t,r,n)},a.createCertificateVerify=function(t,n){var r=n.length+2,i=e.util.createBuffer();return i.putByte(a.HandshakeType.certificate_verify),i.putInt24(r),i.putInt16(n.length),i.putBytes(n),i},a.createCertificateRequest=function(t){var n=e.util.createBuffer();n.putByte(1);var r=e.util.createBuffer();for(var i in t.caStore.certs){var s=t.caStore.certs[i],o=e.pki.distinguishedNameToAsn1(s.subject);r.putBuffer(e.asn1.toDer(o))}var f=1+n.length()+2+r.length(),l=e.util.createBuffer();return l.putByte(a.HandshakeType.certificate_request),l.putInt24(f),u(l,1,n),u(l,2,r),l},a.createServerHelloDone=function(t){var n=e.util.createBuffer();return n.putByte(a.HandshakeType.server_hello_done),n.putInt24(0),n},a.createChangeCipherSpec=function(){var t=e.util.createBuffer();return t.putByte(1),t},a.createFinished=function(n){var r=e.util.createBuffer();r.putBuffer(n.session.md5.digest()),r.putBuffer(n.session.sha1.digest());var i=n.entity===a.ConnectionEnd.client,s=n.session.sp,o=12,u=t,f=i?"client finished":"server finished";r=u(s.master_secret,f,r.getBytes(),o);var l=e.util.createBuffer();return l.putByte(a.HandshakeType.finished),l.putInt24(r.length()),l.putBuffer(r),l},a.createHeartbeat=function(t,n,r){typeof r=="undefined"&&(r=n.length);var i=e.util.createBuffer();i.putByte(t),i.putInt16(r),i.putBytes(n);var s=i.length(),o=Math.max(16,s-r-3);return i.putBytes(e.random.getBytes(o)),i},a.queue=function(t,n){if(!n)return;if(n.type===a.ContentType.handshake){var r=n.fragment.bytes();t.session.md5.update(r),t.session.sha1.update(r),r=null}var i;if(n.fragment.length()<=a.MaxFragment)i=[n];else{i=[];var s=n.fragment.bytes();while(s.length>a.MaxFragment)i.push(a.createRecord(t,{type:n.type,data:e.util.createBuffer(s.slice(0,a.MaxFragment))})),s=s.slice(a.MaxFragment);s.length>0&&i.push(a.createRecord(t,{type:n.type,data:e.util.createBuffer(s)}))}for(var o=0;o0&&(i=r.order[0]);if(i!==null&&i in r.cache){n=r.cache[i],delete r.cache[i];for(var s in r.order)if(r.order[s]===i){r.order.splice(s,1);break}}return n},r.setSession=function(t,n){if(r.order.length===r.capacity){var i=r.order.shift();delete r.cache[i]}var i=e.util.bytesToHex(t);r.order.push(i),r.cache[i]=n}}return r},a.createConnection=function(t){var n=null;t.caStore?e.util.isArray(t.caStore)?n=e.pki.createCaStore(t.caStore):n=t.caStore:n=e.pki.createCaStore();var r=t.cipherSuites||null;if(r===null){r=[];for(var i in a.CipherSuites)r.push(a.CipherSuites[i])}var s=t.server||!1?a.ConnectionEnd.server:a.ConnectionEnd.client,o=t.sessionCache?a.createSessionCache(t.sessionCache):null,u={version:{major:a.Version.major,minor:a.Version.minor},entity:s,sessionId:t.sessionId,caStore:n,sessionCache:o,cipherSuites:r,connected:t.connected,virtualHost:t.virtualHost||null,verifyClient:t.verifyClient||!1,verify:t.verify||function(e,t,n,r){return t},getCertificate:t.getCertificate||null,getPrivateKey:t.getPrivateKey||null,getSignature:t.getSignature||null,input:e.util.createBuffer(),tlsData:e.util.createBuffer(),data:e.util.createBuffer(),tlsDataReady:t.tlsDataReady,dataReady:t.dataReady,heartbeatReceived:t.heartbeatReceived,closed:t.closed,error:function(e,n){n.origin=n.origin||(e.entity===a.ConnectionEnd.client?"client":"server"),n.send&&(a.queue(e,a.createAlert(e,n.alert)),a.flush(e));var r=n.fatal!==!1;r&&(e.fail=!0),t.error(e,n),r&&e.close(!1)},deflate:t.deflate||null,inflate:t.inflate||null};u.reset=function(e){u.version={major:a.Version.major,minor:a.Version.minor},u.record=null,u.session=null,u.peerCertificate=null,u.state={pending:null,current:null},u.expect=u.entity===a.ConnectionEnd.client?f:y,u.fragmented=null,u.records=[],u.open=!1,u.handshakes=0,u.handshaking=!1,u.isConnected=!1,u.fail=!e&&typeof e!="undefined",u.input.clear(),u.tlsData.clear(),u.data.clear(),u.state.current=a.createConnectionState(u)},u.reset();var l=function(e,t){var n=t.type-a.ContentType.change_cipher_spec,r=_[e.entity][e.expect];n in r?r[n](e,t):a.handleUnexpected(e,t)},c=function(t){var n=0,r=t.input,i=r.length();if(i<5)n=5-i;else{t.record={type:r.getByte(),version:{major:r.getByte(),minor:r.getByte()},length:r.getInt16(),fragment:e.util.createBuffer(),ready:!1};var s=t.record.version.major===t.version.major;s&&t.session&&t.session.version&&(s=t.record.version.minor===t.version.minor),s||t.error(t,{message:"Incompatible TLS version.",send:!0,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.protocol_version}})}return n},h=function(e){var t=0,n=e.input,r=n.length();if(r0&&(u.sessionCache&&(n=u.sessionCache.getSession(t)),n===null&&(t="")),t.length===0&&u.sessionCache&&(n=u.sessionCache.getSession(),n!==null&&(t=n.id)),u.session={id:t,version:null,cipherSuite:null,compressionMethod:null,serverCertificate:null,certificateRequest:null,clientCertificate:null,sp:{},md5:e.md.md5.create(),sha1:e.md.sha1.create()},n&&(u.version=n.version,u.session.sp=n.sp),u.session.sp.client_random=a.createRandom().getBytes(),u.open=!0,a.queue(u,a.createRecord(u,{type:a.ContentType.handshake,data:a.createClientHello(u)})),a.flush(u)}},u.process=function(e){var t=0;return e&&u.input.putBytes(e),u.fail||(u.record!==null&&u.record.ready&&u.record.fragment.isEmpty()&&(u.record=null),u.record===null&&(t=c(u)),!u.fail&&u.record!==null&&!u.record.ready&&(t=h(u)),!u.fail&&u.record!==null&&u.record.ready&&l(u,u.record)),t},u.prepare=function(t){return a.queue(u,a.createRecord(u,{type:a.ContentType.application_data,data:e.util.createBuffer(t)})),a.flush(u)},u.prepareHeartbeatRequest=function(t,n){return t instanceof e.util.ByteBuffer&&(t=t.bytes()),typeof n=="undefined"&&(n=t.length),u.expectedHeartbeatPayload=t,a.queue(u,a.createRecord(u,{type:a.ContentType.heartbeat,data:a.createHeartbeat(a.HeartbeatMessageType.heartbeat_request,t,n)})),a.flush(u)},u.close=function(e){if(!u.fail&&u.sessionCache&&u.session){var t={id:u.session.id,version:u.session.version,sp:u.session.sp};t.sp.keys=null,u.sessionCache.setSession(t.id,t)}if(u.open){u.open=!1,u.input.clear();if(u.isConnected||u.handshaking)u.isConnected=u.handshaking=!1,a.queue(u,a.createAlert(u,{level:a.Alert.Level.warning,description:a.Alert.Description.close_notify})),a.flush(u);u.closed(u)}u.reset(e)},u},e.tls=e.tls||{};for(var V in a)typeof a[V]!="function"&&(e.tls[V]=a[V]);e.tls.prf_tls1=t,e.tls.hmac_sha1=r,e.tls.createSessionCache=a.createSessionCache,e.tls.createConnection=a.createConnection}var r="tls";if(typeof n!="function"){if(typeof module!="object"||!module.exports)return typeof forge=="undefined"&&(forge={}),e(forge);var i=!0;n=function(e,n){n(t,module)}}var s,o=function(t,n){n.exports=function(n){var i=s.map(function(e){return t(e)}).concat(e);n=n||{},n.defined=n.defined||{};if(n.defined[r])return n[r];n.defined[r]=!0;for(var o=0;o=t.Versions.TLS_1_1.minor&&a.output.putBytes(u),a.update(n.fragment),a.finish(i)&&(n.fragment=a.output,n.length=n.fragment.length(),s=!0),s}function i(e,t,n){if(!n){var r=e-t.length()%e;t.fillWithByte(r-1,r)}return!0}function s(e,t,n){var r=!0;if(n){var i=t.length(),s=t.last();for(var o=i-1-s;o=f?(n.fragment=a.output.getBytes(h-f),l=a.output.getBytes(f)):n.fragment=a.output.getBytes(),n.fragment=e.util.createBuffer(n.fragment),n.length=n.fragment.length();var p=r.macFunction(r.macKey,r.sequenceNumber,n);return r.updateSequenceNumber(),i=p===l&&i,i}var t=e.tls;t.CipherSuites.TLS_RSA_WITH_AES_128_CBC_SHA={id:[0,47],name:"TLS_RSA_WITH_AES_128_CBC_SHA",initSecurityParameters:function(e){e.bulk_cipher_algorithm=t.BulkCipherAlgorithm.aes,e.cipher_type=t.CipherType.block,e.enc_key_length=16,e.block_length=16,e.fixed_iv_length=16,e.record_iv_length=16,e.mac_algorithm=t.MACAlgorithm.hmac_sha1,e.mac_length=20,e.mac_key_length=20},initConnectionState:n},t.CipherSuites.TLS_RSA_WITH_AES_256_CBC_SHA={id:[0,53],name:"TLS_RSA_WITH_AES_256_CBC_SHA",initSecurityParameters:function(e){e.bulk_cipher_algorithm=t.BulkCipherAlgorithm.aes,e.cipher_type=t.CipherType.block,e.enc_key_length=32,e.block_length=16,e.fixed_iv_length=16,e.record_iv_length=16,e.mac_algorithm=t.MACAlgorithm.hmac_sha1,e.mac_length=20,e.mac_key_length=20},initConnectionState:n};var o=0}var r="aesCipherSuites";if(typeof n!="function"){if(typeof module!="object"||!module.exports)return typeof forge=="undefined"&&(forge={}),e(forge);var i=!0;n=function(e,n){n(t,module)}}var s,o=function(t,n){n.exports=function(n){var i=s.map(function(e){return t(e)}).concat(e);n=n||{},n.defined=n.defined||{};if(n.defined[r])return n[r];n.defined[r]=!0;for(var o=0;o="8"&&(r="00"+r);var i=e.util.hexToBytes(r);t.putInt32(i.length),t.putBytes(i)}function r(e,t){e.putInt32(t.length),e.putString(t)}function i(){var t=e.md.sha1.create(),n=arguments.length;for(var r=0;r=1&&e.log.verbose(t,"[%s][%s] init",this.id,this.name,this)};T.prototype.debug=function(n){n=n||"",e.log.debug(t,n,"[%s][%s] task:",this.id,this.name,this,"subtasks:",this.subtasks.length,"queue:",s)},T.prototype.next=function(e,t){typeof e=="function"&&(t=e,e=this.name);var n=new T({run:t,name:e,parent:this});return n.state=l,n.type=this.type,n.successCallback=this.successCallback||null,n.failureCallback=this.failureCallback||null,this.subtasks.push(n),this},T.prototype.parallel=function(t,n){return e.util.isArray(t)&&(n=t,t=this.name),this.next(t,function(r){var i=r;i.block(n.length);var s=function(t,r){e.task.start({type:t,run:function(e){n[r](e)},success:function(e){i.unblock()},failure:function(e){i.unblock()}})};for(var o=0;o0&&(this.state=x[this.state][g])},T.prototype.unblock=function(e){return e=typeof e=="undefined"?1:e,this.blocks-=e,this.blocks===0&&this.state!==p&&(this.state=l,C(this,0)),this.blocks},T.prototype.sleep=function(e){e=typeof e=="undefined"?0:e,this.state=x[this.state][b];var t=this;this.timeoutId=setTimeout(function(){t.timeoutId=null,t.state=l,C(t,0)},e)},T.prototype.wait=function(e){e.wait(this)},T.prototype.wakeup=function(){this.state===h&&(cancelTimeout(this.timeoutId),this.timeoutId=null,this.state=l,C(this,0))},T.prototype.cancel=function(){this.state=x[this.state][E],this.permitsNeeded=0,this.timeoutId!==null&&(cancelTimeout(this.timeoutId),this.timeoutId=null),this.subtasks=[]},T.prototype.fail=function(e){this.error=!0,k(this,!0);if(e)e.error=this.error,e.swapTime=this.swapTime,e.userData=this.userData,C(e,0);else{if(this.parent!==null){var t=this.parent;while(t.parent!==null)t.error=this.error,t.swapTime=this.swapTime,t.userData=this.userData,t=t.parent;k(t,!0)}this.failureCallback&&this.failureCallback(this)}};var N=function(e){e.error=!1,e.state=x[e.state][m],setTimeout(function(){e.state===l&&(e.swapTime=+(new Date),e.run(e),C(e,0))},0)},C=function(e,t){var n=t>u||+(new Date)-e.swapTime>a,r=function(t){t++;if(e.state===l){n&&(e.swapTime=+(new Date));if(e.subtasks.length>0){var r=e.subtasks.shift();r.error=e.error,r.swapTime=e.swapTime,r.userData=e.userData,r.run(r),r.error||C(r,t)}else k(e),e.error||e.parent!==null&&(e.parent.error=e.error,e.parent.swapTime=e.swapTime,e.parent.userData=e.userData,C(e.parent,t))}};n?setTimeout(r,0):r(t)},k=function(i,o){i.state=p,delete r[i.id],n>=1&&e.log.verbose(t,"[%s][%s] finish",i.id,i.name,i),i.parent===null&&(i.type in s?s[i.type].length===0?e.log.error(t,"[%s][%s] task queue empty [%s]",i.id,i.name,i.type):s[i.type][0]!==i?e.log.error(t,"[%s][%s] task not first in queue [%s]",i.id,i.name,i.type):(s[i.type].shift(),s[i.type].length===0?(n>=1&&e.log.verbose(t,"[%s][%s] delete queue [%s]",i.id,i.name,i.type),delete s[i.type]):(n>=1&&e.log.verbose(t,"[%s][%s] queue start next [%s] remain:%s",i.id,i.name,i.type,s[i.type].length),s[i.type][0].start())):e.log.error(t,"[%s][%s] task queue missing [%s]",i.id,i.name,i.type),o||(i.error&&i.failureCallback?i.failureCallback(i):!i.error&&i.successCallback&&i.successCallback(i)))};e.task=e.task||{},e.task.start=function(r){var i=new T({run:r.run,name:r.name||o});i.type=r.type,i.successCallback=r.success||null,i.failureCallback=r.failure||null,i.type in s?s[r.type].push(i):(n>=1&&e.log.verbose(t,"[%s][%s] create queue [%s]",i.id,i.name,i.type),s[i.type]=[i],N(i))},e.task.cancel=function(e){e in s&&(s[e]=[s[e][0]])},e.task.createCondition=function(){var e={tasks:{}};return e.wait=function(t){t.id in e.tasks||(t.block(),e.tasks[t.id]=t)},e.notify=function(){var t=e.tasks;e.tasks={};for(var n in t)t[n].unblock()},e}}var r="task";if(typeof n!="function"){if(typeof module!="object"||!module.exports)return typeof forge=="undefined"&&(forge={}),e(forge);var i=!0;n=function(e,n){n(t,module)}}var s,o=function(t,n){n.exports=function(n){var i=s.map(function(e){return t(e)}).concat(e);n=n||{},n.defined=n.defined||{};if(n.defined[r])return n[r];n.defined[r]=!0;for(var o=0;o0&&(e.splice(c-1,2),c-=2)}}e=e.join("/")}else e.indexOf("./")===0&&(e=e.substring(2));if((v||g)&&m){n=e.split("/");for(c=n.length;c>0;c-=1){r=n.slice(0,c).join("/");if(v)for(h=v.length;h>0;h-=1){i=m[v.slice(0,h).join("/")];if(i){i=i[r];if(i){s=i,u=c;break}}}if(s)break;!a&&g&&g[r]&&(a=g[r],f=c)}!s&&a&&(s=a,u=f),s&&(n.splice(0,u,s),e=n.join("/"))}return e}function g(e,t){return function(){return s.apply(r,p.call(arguments,0).concat([e,t]))}}function y(e){return function(t){return m(t,e)}}function b(e){return function(t){a[e]=t}}function w(e){if(v(f,e)){var t=f[e];delete f[e],c[e]=!0,i.apply(r,t)}if(!v(a,e)&&!v(c,e))throw new Error("No "+e);return a[e]}function E(e){var t,n=e?e.indexOf("!"):-1;return n>-1&&(t=e.substring(0,n),e=e.substring(n+1,e.length)),[t,e]}function S(e){return function(){return l&&l.config&&l.config[e]||{}}}var i,s,o,u,a={},f={},l={},c={},h=Object.prototype.hasOwnProperty,p=[].slice,d=/\.js$/;o=function(e,t){var n,r=E(e),i=r[0];return e=r[1],i&&(i=m(i,t),n=w(i)),i?n&&n.normalize?e=n.normalize(e,y(t)):e=m(e,t):(e=m(e,t),r=E(e),i=r[0],e=r[1],i&&(n=w(i))),{f:i?i+"!"+e:e,n:e,pr:i,p:n}},u={require:function(e){return g(e)},exports:function(e){var t=a[e];return typeof t!="undefined"?t:a[e]={}},module:function(e){return{id:e,uri:"",exports:a[e],config:S(e)}}},i=function(e,t,n,i){var s,l,h,p,d,m=[],y=typeof n,E;i=i||e;if(y==="undefined"||y==="function"){t=!t.length&&n.length?["require","exports","module"]:t;for(d=0;d0)t&1&&(n+=e),t>>>=1,t>0&&(e+=e);return this.data=n,this},t.ByteStringBuffer.prototype.putBytes=function(e){return this.data+=e,this},t.ByteStringBuffer.prototype.putString=function(e){return this.data+=t.encodeUtf8(e),this},t.ByteStringBuffer.prototype.putInt16=function(e){return this.data+=String.fromCharCode(e>>8&255)+String.fromCharCode(e&255),this},t.ByteStringBuffer.prototype.putInt24=function(e){return this.data+=String.fromCharCode(e>>16&255)+String.fromCharCode(e>>8&255)+String.fromCharCode(e&255),this},t.ByteStringBuffer.prototype.putInt32=function(e){return this.data+=String.fromCharCode(e>>24&255)+String.fromCharCode(e>>16&255)+String.fromCharCode(e>>8&255)+String.fromCharCode(e&255),this},t.ByteStringBuffer.prototype.putInt16Le=function(e){return this.data+=String.fromCharCode(e&255)+String.fromCharCode(e>>8&255),this},t.ByteStringBuffer.prototype.putInt24Le=function(e){return this.data+=String.fromCharCode(e&255)+String.fromCharCode(e>>8&255)+String.fromCharCode(e>>16&255),this},t.ByteStringBuffer.prototype.putInt32Le=function(e){return this.data+=String.fromCharCode(e&255)+String.fromCharCode(e>>8&255)+String.fromCharCode(e>>16&255)+String.fromCharCode(e>>24&255),this},t.ByteStringBuffer.prototype.putInt=function(e,t){do t-=8,this.data+=String.fromCharCode(e>>t&255);while(t>0);return this},t.ByteStringBuffer.prototype.putSignedInt=function(e,t){return e<0&&(e+=2<0);return t},t.ByteStringBuffer.prototype.getSignedInt=function(e){var t=this.getInt(e),n=2<=n&&(t-=n<<1),t},t.ByteStringBuffer.prototype.getBytes=function(e){var t;return e?(e=Math.min(this.length(),e),t=this.data.slice(this.read,this.read+e),this.read+=e):e===0?t="":(t=this.read===0?this.data:this.data.slice(this.read),this.clear()),t},t.ByteStringBuffer.prototype.bytes=function(e){return typeof e=="undefined"?this.data.slice(this.read):this.data.slice(this.read,this.read+e)},t.ByteStringBuffer.prototype.at=function(e){return this.data.charCodeAt(this.read+e)},t.ByteStringBuffer.prototype.setAt=function(e,t){return this.data=this.data.substr(0,this.read+e)+String.fromCharCode(t)+this.data.substr(this.read+e+1),this},t.ByteStringBuffer.prototype.last=function(){return this.data.charCodeAt(this.data.length-1)},t.ByteStringBuffer.prototype.copy=function(){var e=t.createBuffer(this.data);return e.read=this.read,e},t.ByteStringBuffer.prototype.compact=function(){return this.read>0&&(this.data=this.data.slice(this.read),this.read=0),this},t.ByteStringBuffer.prototype.clear=function(){return this.data="",this.read=0,this},t.ByteStringBuffer.prototype.truncate=function(e){var t=Math.max(0,this.length()-e);return this.data=this.data.substr(this.read,t),this.read=0,this},t.ByteStringBuffer.prototype.toHex=function(){var e="";for(var t=this.read;t=e)return this;t=Math.max(t||this.growSize,e);var n=new Uint8Array(this.data.buffer,this.data.byteOffset,this.data.byteLength),r=new Uint8Array(this.length()+t);return r.set(n),this.data=new DataView(r.buffer),this},t.DataBuffer.prototype.putByte=function(e){return this.accommodate(1),this.data.setUint8(this.write++,e),this},t.DataBuffer.prototype.fillWithByte=function(e,t){this.accommodate(t);for(var n=0;n>8&65535),this.data.setInt8(this.write,e>>16&255),this.write+=3,this},t.DataBuffer.prototype.putInt32=function(e){return this.accommodate(4),this.data.setInt32(this.write,e),this.write+=4,this},t.DataBuffer.prototype.putInt16Le=function(e){return this.accommodate(2),this.data.setInt16(this.write,e,!0),this.write+=2,this},t.DataBuffer.prototype.putInt24Le=function(e){return this.accommodate(3),this.data.setInt8(this.write,e>>16&255),this.data.setInt16(this.write,e>>8&65535,!0),this.write+=3,this},t.DataBuffer.prototype.putInt32Le=function(e){return this.accommodate(4),this.data.setInt32(this.write,e,!0),this.write+=4,this},t.DataBuffer.prototype.putInt=function(e,t){this.accommodate(t/8);do t-=8,this.data.setInt8(this.write++,e>>t&255);while(t>0);return this},t.DataBuffer.prototype.putSignedInt=function(e,t){return this.accommodate(t/8),e<0&&(e+=2<0);return t},t.DataBuffer.prototype.getSignedInt=function(e){var t=this.getInt(e),n=2<=n&&(t-=n<<1),t},t.DataBuffer.prototype.getBytes=function(e){var t;return e?(e=Math.min(this.length(),e),t=this.data.slice(this.read,this.read+e),this.read+=e):e===0?t="":(t=this.read===0?this.data:this.data.slice(this.read),this.clear()),t},t.DataBuffer.prototype.bytes=function(e){return typeof e=="undefined"?this.data.slice(this.read):this.data.slice(this.read,this.read+e)},t.DataBuffer.prototype.at=function(e){return this.data.getUint8(this.read+e)},t.DataBuffer.prototype.setAt=function(e,t){return this.data.setUint8(e,t),this},t.DataBuffer.prototype.last=function(){return this.data.getUint8(this.write-1)},t.DataBuffer.prototype.copy=function(){return new t.DataBuffer(this)},t.DataBuffer.prototype.compact=function(){if(this.read>0){var e=new Uint8Array(this.data.buffer,this.read),t=new Uint8Array(e.byteLength);t.set(e),this.data=new DataView(t),this.write-=this.read,this.read=0}return this},t.DataBuffer.prototype.clear=function(){return this.data=new DataView(new ArrayBuffer(0)),this.read=this.write=0,this},t.DataBuffer.prototype.truncate=function(e){return this.write=Math.max(0,this.length()-e),this.read=Math.min(this.read,this.write),this},t.DataBuffer.prototype.toHex=function(){var e="";for(var t=this.read;t0)t&1&&(n+=e),t>>>=1,t>0&&(e+=e);return n},t.xorBytes=function(e,t,n){var r="",i="",s="",o=0,u=0;for(;n>0;--n,++o)i=e.charCodeAt(o)^t.charCodeAt(o),u>=10&&(r+=s,s="",u=0),s+=String.fromCharCode(i),++u;return r+=s,r},t.hexToBytes=function(e){var t="",n=0;e.length&!0&&(n=1,t+=String.fromCharCode(parseInt(e[0],16)));for(;n>24&255)+String.fromCharCode(e>>16&255)+String.fromCharCode(e>>8&255)+String.fromCharCode(e&255)};var s="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",o=[62,-1,-1,-1,63,52,53,54,55,56,57,58,59,60,61,-1,-1,-1,64,-1,-1,-1,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,-1,-1,-1,-1,-1,-1,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51];t.encode64=function(e,t){var n="",r="",i,o,u,a=0;while(a>2),n+=s.charAt((i&3)<<4|o>>4),isNaN(o)?n+="==":(n+=s.charAt((o&15)<<2|u>>6),n+=isNaN(u)?"=":s.charAt(u&63)),t&&n.length>t&&(r+=n.substr(0,t)+"\r\n",n=n.substr(t));return r+=n,r},t.decode64=function(e){e=e.replace(/[^A-Za-z0-9\+\/\=]/g,"");var t="",n,r,i,s,u=0;while(u>4),i!==64&&(t+=String.fromCharCode((r&15)<<4|i>>2),s!==64&&(t+=String.fromCharCode((i&3)<<6|s)));return t},t.encodeUtf8=function(e){return unescape(encodeURIComponent(e))},t.decodeUtf8=function(e){return decodeURIComponent(escape(e))},t.binary={raw:{},hex:{},base64:{}},t.binary.raw.encode=function(e){return String.fromCharCode.apply(null,e)},t.binary.raw.decode=function(e,t,n){var r=t;r||(r=new Uint8Array(e.length)),n=n||0;var i=n;for(var s=0;s>2),n+=s.charAt((i&3)<<4|o>>4),isNaN(o)?n+="==":(n+=s.charAt((o&15)<<2|u>>6),n+=isNaN(u)?"=":s.charAt(u&63)),t&&n.length>t&&(r+=n.substr(0,t)+"\r\n",n=n.substr(t));return r+=n,r},t.binary.base64.decode=function(e,t,n){var r=t;r||(r=new Uint8Array(Math.ceil(e.length/4)*3)),e=e.replace(/[^A-Za-z0-9\+\/\=]/g,""),n=n||0;var i,s,u,a,f=0,l=n;while(f>4,u!==64&&(r[l++]=(s&15)<<4|u>>2,a!==64&&(r[l++]=(u&3)<<6|a));return t?l-n:r},t.text={utf8:{},utf16:{}},t.text.utf8.encode=function(e,n,r){e=t.encodeUtf8(e);var i=n;i||(i=new Uint8Array(e.length)),r=r||0;var s=r;for(var o=0;o0?(s=n[r].substring(0,i),o=n[r].substring(i+1)):(s=n[r],o=null),s in t||(t[s]=[]),!(s in Object.prototype)&&o!==null&&t[s].push(unescape(o))}return t},n;return typeof e=="undefined"?(d===null&&(typeof window=="undefined"?d={}:d=t(window.location.search.substring(1))),n=d):n=t(e),n},t.parseFragment=function(e){var n=e,r="",i=e.indexOf("?");i>0&&(n=e.substring(0,i),r=e.substring(i+1));var s=n.split("/");s.length>0&&s[0]===""&&s.shift();var o=r===""?{}:t.getQueryVariables(r);return{pathString:n,queryString:r,path:s,query:o}},t.makeRequest=function(e){var n=t.parseFragment(e),r={path:n.pathString,query:n.queryString,getPath:function(e){return typeof e=="undefined"?n.path:n.path[e]},getQuery:function(e,t){var r;return typeof e=="undefined"?r=n.query:(r=n.query[e],r&&typeof t!="undefined"&&(r=r[t])),r},getQueryLast:function(e,t){var n,i=r.getQuery(e);return i?n=i[i.length-1]:n=t,n}};return r},t.makeLink=function(e,t,n){e=jQuery.isArray(e)?e.join("/"):e;var r=jQuery.param(t||{});return n=n||"",e+(r.length>0?"?"+r:"")+(n.length>0?"#"+n:"")},t.setPath=function(e,t,n){if(typeof e=="object"&&e!==null){var r=0,i=t.length;while(r0&&s.push(r),o=t.lastIndex;var u=n[0][1];switch(u){case"s":case"o":i");break;case"%":s.push("%");break;default:s.push("<%"+u+"?>")}}return s.push(e.substring(o)),s.join("")},t.formatNumber=function(e,t,n,r){var i=e,s=isNaN(t=Math.abs(t))?2:t,o=n===undefined?",":n,u=r===undefined?".":r,a=i<0?"-":"",f=parseInt(i=Math.abs(+i||0).toFixed(s),10)+"",l=f.length>3?f.length%3:0;return a+(l?f.substr(0,l)+u:"")+f.substr(l).replace(/(\d{3})(?=\d)/g,"$1"+u)+(s?o+Math.abs(i-f).toFixed(s).slice(2):"")},t.formatSize=function(e){return e>=1073741824?e=t.formatNumber(e/1073741824,2,".","")+" GiB":e>=1048576?e=t.formatNumber(e/1048576,2,".","")+" MiB":e>=1024?e=t.formatNumber(e/1024,0)+" KiB":e=t.formatNumber(e,0)+" bytes",e},t.bytesFromIP=function(e){return e.indexOf(".")!==-1?t.bytesFromIPv4(e):e.indexOf(":")!==-1?t.bytesFromIPv6(e):null},t.bytesFromIPv4=function(e){e=e.split(".");if(e.length!==4)return null;var n=t.createBuffer();for(var r=0;rr[i].end-r[i].start&&(i=r.length-1))}n.push(o)}if(r.length>0){var f=r[i];f.end-f.start>0&&(n.splice(f.start,f.end-f.start+1,""),f.start===0&&n.unshift(""),f.end===7&&n.push(""))}return n.join(":")},t.estimateCores=function(e,n){function i(e,u,a){if(u===0){var f=Math.floor(e.reduce(function(e,t){return e+t},0)/e.length);return t.cores=Math.max(1,f),URL.revokeObjectURL(r),n(null,t.cores)}s(a,function(t,n){e.push(o(a,n)),i(e,u-1,a)})}function s(e,t){var n=[],i=[];for(var s=0;su.st&&i.sti.st&&u.st0)return t.cores=navigator.hardwareConcurrency,n(null,t.cores);if(typeof Worker=="undefined")return t.cores=1,n(null,t.cores);if(typeof Blob=="undefined")return t.cores=2,n(null,t.cores);var r=URL.createObjectURL(new Blob(["(",function(){self.addEventListener("message",function(e){var t=Date.now(),n=t+4;while(Date.now()=this.blockSize||this._input.length()>0&&this._finish)this._op.call(this.mode,this._input,this.output);this._input.compact()},t.prototype.finish=function(e){e&&this.mode.name==="CBC"&&(this.mode.pad=function(t){return e(this.blockSize,t,!1)},this.mode.unpad=function(t){return e(this.blockSize,t,!0)});var t={};return t.decrypt=this._decrypt,t.overflow=this._input.length()%this.blockSize,!this._decrypt&&this.mode.pad&&!this.mode.pad(this._input,t)?!1:(this._finish=!0,this.update(),this._decrypt&&this.mode.unpad&&!this.mode.unpad(this.output,t)?!1:this.mode.afterFinish&&!this.mode.afterFinish(this.output,t)?!1:!0)}}var r="cipher";if(typeof n!="function"){if(typeof module!="object"||!module.exports)return typeof forge=="undefined"&&(forge={}),e(forge);var i=!0;n=function(e,n){n(t,module)}}var s,o=function(t,n){n.exports=function(n){var i=s.map(function(e){return t(e)}).concat(e);n=n||{},n.defined=n.defined||{};if(n.defined[r])return n[r];n.defined[r]=!0;for(var o=0;o4){var n=t;t=e.util.createBuffer();for(var r=0;r0)return!1;var n=e.length(),r=e.at(n-1);return r>this.blockSize<<2?!1:(e.truncate(r),!0)},t.cbc=function(e){e=e||{},this.name="CBC",this.cipher=e.cipher,this.blockSize=e.blockSize||16,this._blocks=this.blockSize/4,this._inBlock=new Array(this._blocks),this._outBlock=new Array(this._blocks)},t.cbc.prototype.start=function(e){if(e.iv===null){if(!this._prev)throw new Error("Invalid IV parameter.");this._iv=this._prev.slice(0)}else{if(!("iv"in e))throw new Error("Invalid IV parameter.");this._iv=n(e.iv),this._prev=this._iv.slice(0)}},t.cbc.prototype.encrypt=function(e,t){for(var n=0;n0)return!1;var n=e.length(),r=e.at(n-1);return r>this.blockSize<<2?!1:(e.truncate(r),!0)},t.cfb=function(e){e=e||{},this.name="CFB",this.cipher=e.cipher,this.blockSize=e.blockSize||16,this._blocks=this.blockSize/4,this._inBlock=null,this._outBlock=new Array(this._blocks)},t.cfb.prototype.start=function(e){if(!("iv"in e))throw new Error("Invalid IV parameter.");this._iv=n(e.iv),this._inBlock=this._iv.slice(0)},t.cfb.prototype.encrypt=function(e,t){this.cipher.encrypt(this._inBlock,this._outBlock);for(var n=0;n0&&e.truncate(this.blockSize-t.overflow),!0},t.ofb=function(e){e=e||{},this.name="OFB",this.cipher=e.cipher,this.blockSize=e.blockSize||16,this._blocks=this.blockSize/4,this._inBlock=null,this._outBlock=new Array(this._blocks)},t.ofb.prototype.start=function(e){if(!("iv"in e))throw new Error("Invalid IV parameter.");this._iv=n(e.iv),this._inBlock=this._iv.slice(0)},t.ofb.prototype.encrypt=function(e,t){this.cipher.encrypt(this._inBlock,this._outBlock);for(var n=0;n0&&e.truncate(this.blockSize-t.overflow),!0},t.ctr=function(e){e=e||{},this.name="CTR",this.cipher=e.cipher,this.blockSize=e.blockSize||16,this._blocks=this.blockSize/4,this._inBlock=null,this._outBlock=new Array(this._blocks)},t.ctr.prototype.start=function(e){if(!("iv"in e))throw new Error("Invalid IV parameter.");this._iv=n(e.iv),this._inBlock=this._iv.slice(0)},t.ctr.prototype.encrypt=function(e,t){this.cipher.encrypt(this._inBlock,this._outBlock),r(this._inBlock);for(var n=0;n0&&e.truncate(this.blockSize-t.overflow),!0},t.gcm=function(e){e=e||{},this.name="GCM",this.cipher=e.cipher,this.blockSize=e.blockSize||16,this._blocks=this.blockSize/4,this._inBlock=new Array(this._blocks),this._outBlock=new Array(this._blocks),this._R=3774873600},t.gcm.prototype.start=function(t){if(!("iv"in t))throw new Error("Invalid IV parameter.");var n=e.util.createBuffer(t.iv);this._cipherLength=0;var s;"additionalData"in t?s=e.util.createBuffer(t.additionalData):s=e.util.createBuffer(),"tagLength"in t?this._tagLength=t.tagLength:this._tagLength=128,this._tag=null;if(t.decrypt){this._tag=e.util.createBuffer(t.tag).getBytes();if(this._tag.length!==this._tagLength/8)throw new Error("Authentication tag does not match tag length.")}this._hashBlock=new Array(this._blocks),this.tag=null,this._hashSubkey=new Array(this._blocks),this.cipher.encrypt([0,0,0,0],this._hashSubkey),this.componentBits=4,this._m=this.generateHashTable(this._hashSubkey,this.componentBits);var o=n.length();if(o===12)this._j0=[n.getInt32(),n.getInt32(),n.getInt32(),1];else{this._j0=[0,0,0,0];while(n.length()>0)this._j0=this.ghash(this._hashSubkey,this._j0,[n.getInt32(),n.getInt32(),n.getInt32(),n.getInt32()]);this._j0=this.ghash(this._hashSubkey,this._j0,[0,0].concat(i(o*8)))}this._inBlock=this._j0.slice(0),r(this._inBlock),s=e.util.createBuffer(s),this._aDataLength=i(s.length()*8);var u=s.length()%this.blockSize;u&&s.fillWithByte(0,this.blockSize-u),this._s=[0,0,0,0];while(s.length()>0)this._s=this.ghash(this._hashSubkey,this._s,[s.getInt32(),s.getInt32(),s.getInt32(),s.getInt32()])},t.gcm.prototype.encrypt=function(t,n){this.cipher.encrypt(this._inBlock,this._outBlock),r(this._inBlock);var i=t.length();for(var s=0;s0;--r)t[r]=e[r]>>>1|(e[r-1]&1)<<31;t[0]=e[0]>>>1,n&&(t[0]^=this._R)},t.gcm.prototype.tableMultiply=function(e){var t=[0,0,0,0];for(var n=0;n<32;++n){var r=n/8|0,i=e[r]>>>(7-n%8)*4&15,s=this._m[n][i];t[0]^=s[0],t[1]^=s[1],t[2]^=s[2],t[3]^=s[3]}return t},t.gcm.prototype.ghash=function(e,t,n){return t[0]^=n[0],t[1]^=n[1],t[2]^=n[2],t[3]^=n[3],this.tableMultiply(t)},t.gcm.prototype.generateHashTable=function(e,t){var n=8/t,r=4*n,i=16*n,s=new Array(i);for(var o=0;o>>1,i=new Array(n);i[r]=e.slice(0);var s=r>>>1;while(s>0)this.pow(i[2*s],i[s]=[]),s>>=1;s=2;while(s>8^p&255^99,i[r]=p,s[p]=r,d=e[p],l=e[r],c=e[l],h=e[c],v=d<<24^p<<16^p<<8^(p^d),m=(l^c^h)<<24^(r^h)<<16^(r^c^h)<<8^(r^l^h);for(var g=0;g<4;++g)u[g][r]=v,a[g][p]=m,v=v<<24|v>>>8,m=m<<24|m>>>8;r===0?r=f=1:(r=l^e[e[e[l^h]]],f^=e[e[f]])}}function l(e,t){var n=e.slice(0),s,u=1,f=n.length,l=f+6+1,c=r*l;for(var h=f;h>>16&255]<<24^i[s>>>8&255]<<16^i[s&255]<<8^i[s>>>24]^o[u]<<24,u++):f>6&&h%f===4&&(s=i[s>>>24]<<24^i[s>>>16&255]<<16^i[s>>>8&255]<<8^i[s&255]),n[h]=n[h-f]^s;if(t){var p,d=a[0],v=a[1],m=a[2],g=a[3],y=n.slice(0);c=n.length;for(var h=0,b=c-r;h>>24]]^v[i[p>>>16&255]]^m[i[p>>>8&255]]^g[i[p&255]];n=y}return n}function c(e,t,n,r){var o=e.length/4-1,f,l,c,h,p;r?(f=a[0],l=a[1],c=a[2],h=a[3],p=s):(f=u[0],l=u[1],c=u[2],h=u[3],p=i);var d,v,m,g,y,b,w;d=t[0]^e[0],v=t[r?3:1]^e[1],m=t[2]^e[2],g=t[r?1:3]^e[3];var E=3;for(var S=1;S>>24]^l[v>>>16&255]^c[m>>>8&255]^h[g&255]^e[++E],b=f[v>>>24]^l[m>>>16&255]^c[g>>>8&255]^h[d&255]^e[++E],w=f[m>>>24]^l[g>>>16&255]^c[d>>>8&255]^h[v&255]^e[++E],g=f[g>>>24]^l[d>>>16&255]^c[v>>>8&255]^h[m&255]^e[++E],d=y,v=b,m=w;n[0]=p[d>>>24]<<24^p[v>>>16&255]<<16^p[m>>>8&255]<<8^p[g&255]^e[++E],n[r?3:1]=p[v>>>24]<<24^p[m>>>16&255]<<16^p[g>>>8&255]<<8^p[d&255]^e[++E],n[2]=p[m>>>24]<<24^p[g>>>16&255]<<16^p[d>>>8&255]<<8^p[v&255]^e[++E],n[r?1:3]=p[g>>>24]<<24^p[d>>>16&255]<<16^p[v>>>8&255]<<8^p[m&255]^e[++E]}function h(t){t=t||{};var n=(t.mode||"CBC").toUpperCase(),r="AES-"+n,i;t.decrypt?i=e.cipher.createDecipher(r,t.key):i=e.cipher.createCipher(r,t.key);var s=i.start;return i.start=function(t,n){var r=null;n instanceof e.util.ByteBuffer&&(r=n,n={}),n=n||{},n.output=r,n.iv=t,s.call(i,n)},i}e.aes=e.aes||{},e.aes.startEncrypting=function(e,t,n,r){var i=h({key:e,output:n,decrypt:!1,mode:r});return i.start(t),i},e.aes.createEncryptionCipher=function(e,t){return h({key:e,output:null,decrypt:!1,mode:t})},e.aes.startDecrypting=function(e,t,n,r){var i=h({key:e,output:n,decrypt:!0,mode:r});return i.start(t),i},e.aes.createDecryptionCipher=function(e,t){return h({key:e,output:null,decrypt:!0,mode:t})},e.aes.Algorithm=function(e,t){n||f();var r=this;r.name=e,r.mode=new t({blockSize:16,cipher:{encrypt:function(e,t){return c(r._w,e,t,!1)},decrypt:function(e,t){return c(r._w,e,t,!0)}}}),r._init=!1},e.aes.Algorithm.prototype.initialize=function(t){if(this._init)return;var n=t.key,r;if(typeof n!="string"||n.length!==16&&n.length!==24&&n.length!==32){if(e.util.isArray(n)&&(n.length===16||n.length===24||n.length===32)){r=n,n=e.util.createBuffer();for(var i=0;i>>=2;for(var i=0;i1){var p=r.read,d=r.getByte();if(d===0){o=r.getByte();var v=o&192;if(v===t.Class.UNIVERSAL||v===t.Class.CONTEXT_SPECIFIC)try{var m=n(r);h=m===f-(r.read-p),h&&(++p,--f)}catch(g){}}r.read=p}if(h){l=[];if(f===undefined)for(;;){if(r.bytes(2)===String.fromCharCode(0,0)){r.getBytes(2);break}l.push(t.fromDer(r,i))}else{var y=r.length();while(f>0)l.push(t.fromDer(r,i)),f-=y-r.length(),y=r.length()}}else{if(f===undefined){if(i)throw new Error("Non-constructed ASN.1 object of indefinite length.");f=r.length()}if(a===t.Type.BMPSTRING){l="";for(var b=0;b>>=8;while(u>0);r.putByte(a.length|128);for(var o=a.length-1;o>=0;--o)r.putByte(a.charCodeAt(o))}return r.putBuffer(s),r},t.oidToDer=function(t){var n=t.split("."),r=e.util.createBuffer();r.putByte(40*parseInt(n[0],10)+parseInt(n[1],10));var i,s,o,u;for(var a=2;a>>=7,i||(u|=128),s.push(u),i=!1;while(o>0);for(var f=s.length-1;f>=0;--f)r.putByte(s[f])}return r},t.derToOid=function(t){var n;typeof t=="string"&&(t=e.util.createBuffer(t));var r=t.getByte();n=Math.floor(r/40)+"."+r%40;var i=0;while(t.length()>0)r=t.getByte(),i<<=7,r&128?i+=r&127:(n+="."+(i+r),i=0);return n},t.utcTimeToDate=function(e){var t=new Date,n=parseInt(e.substr(0,2),10);n=n>=50?1900+n:2e3+n;var r=parseInt(e.substr(2,2),10)-1,i=parseInt(e.substr(4,2),10),s=parseInt(e.substr(6,2),10),o=parseInt(e.substr(8,2),10),u=0;if(e.length>11){var a=e.charAt(10),f=10;a!=="+"&&a!=="-"&&(u=parseInt(e.substr(10,2),10),f+=2)}t.setUTCFullYear(n,r,i),t.setUTCHours(s,o,u,0);if(f){a=e.charAt(f);if(a==="+"||a==="-"){var l=parseInt(e.substr(f+1,2),10),c=parseInt(e.substr(f+4,2),10),h=l*60+c;h*=6e4,a==="+"?t.setTime(+t-h):t.setTime(+t+h)}}return t},t.generalizedTimeToDate=function(e){var t=new Date,n=parseInt(e.substr(0,4),10),r=parseInt(e.substr(4,2),10)-1,i=parseInt(e.substr(6,2),10),s=parseInt(e.substr(8,2),10),o=parseInt(e.substr(10,2),10),u=parseInt(e.substr(12,2),10),a=0,f=0,l=!1;e.charAt(e.length-1)==="Z"&&(l=!0);var c=e.length-5,h=e.charAt(c);if(h==="+"||h==="-"){var p=parseInt(e.substr(c+1,2),10),d=parseInt(e.substr(c+4,2),10);f=p*60+d,f*=6e4,h==="+"&&(f*=-1),l=!0}return e.charAt(14)==="."&&(a=parseFloat(e.substr(14),10)*1e3),l?(t.setUTCFullYear(n,r,i),t.setUTCHours(s,o,u,a),t.setTime(+t+f)):(t.setFullYear(n,r,i),t.setHours(s,o,u,a)),t},t.dateToUtcTime=function(e){var t="",n=[];n.push((""+e.getUTCFullYear()).substr(2)),n.push(""+(e.getUTCMonth()+1)),n.push(""+e.getUTCDate()),n.push(""+e.getUTCHours()),n.push(""+e.getUTCMinutes()),n.push(""+e.getUTCSeconds());for(var r=0;r=-128&&t<128)return n.putSignedInt(t,8);if(t>=-32768&&t<32768)return n.putSignedInt(t,16);if(t>=-8388608&&t<8388608)return n.putSignedInt(t,24);if(t>=-2147483648&&t<2147483648)return n.putSignedInt(t,32);var r=new Error("Integer too large; max is 32-bits.");throw r.integer=t,r},t.derToInteger=function(t){typeof t=="string"&&(t=e.util.createBuffer(t));var n=t.length()*8;if(n>32)throw new Error("Integer too large; max is 32-bits.");return t.getSignedInt(n)},t.validate=function(n,r,i,s){var o=!1;if(n.tagClass!==r.tagClass&&typeof r.tagClass!="undefined"||n.type!==r.type&&typeof r.type!="undefined")s&&(n.tagClass!==r.tagClass&&s.push("["+r.name+"] "+'Expected tag class "'+r.tagClass+'", got "'+n.tagClass+'"'),n.type!==r.type&&s.push("["+r.name+"] "+'Expected type "'+r.type+'", got "'+n.type+'"'));else if(n.constructed===r.constructed||typeof r.constructed=="undefined"){o=!0;if(r.value&&e.util.isArray(r.value)){var u=0;for(var a=0;o&&a0&&(o+="\n");var u="";for(var a=0;a=64){u=e.h0,a=e.h1,f=e.h2,l=e.h3;for(p=0;p<16;++p)t[p]=n.getInt32Le(),c=l^a&(f^l),o=u+c+s[p]+t[p],h=i[p],u=l,l=f,f=a,a+=o<>>32-h;for(;p<32;++p)c=f^l&(a^f),o=u+c+s[p]+t[r[p]],h=i[p],u=l,l=f,f=a,a+=o<>>32-h;for(;p<48;++p)c=a^f^l,o=u+c+s[p]+t[r[p]],h=i[p],u=l,l=f,f=a,a+=o<>>32-h;for(;p<64;++p)c=f^(a|~l),o=u+c+s[p]+t[r[p]],h=i[p],u=l,l=f,f=a,a+=o<>>32-h;e.h0=e.h0+u|0,e.h1=e.h1+a|0,e.h2=e.h2+f|0,e.h3=e.h3+l|0,d-=64}}var t=e.md5=e.md5||{};e.md=e.md||{},e.md.algorithms=e.md.algorithms||{},e.md.md5=e.md.algorithms.md5=t,t.create=function(){o||u();var t=null,r=e.util.createBuffer(),i=new Array(16),s={algorithm:"md5",blockLength:64,digestLength:16,messageLength:0,messageLength64:[0,0]};return s.start=function(){return s.messageLength=0,s.messageLength64=[0,0],r=e.util.createBuffer(),t={h0:1732584193,h1:4023233417,h2:2562383102,h3:271733878},s},s.start(),s.update=function(n,o){return o==="utf8"&&(n=e.util.encodeUtf8(n)),s.messageLength+=n.length,s.messageLength64[0]+=n.length/4294967296>>>0,s.messageLength64[1]+=n.length>>>0,r.putBytes(n),a(t,i,r),(r.read>2048||r.length()===0)&&r.compact(),s},s.digest=function(){var o=e.util.createBuffer();o.putBytes(r.bytes()),o.putBytes(n.substr(0,64-(s.messageLength64[1]+8&63))),o.putInt32Le(s.messageLength64[1]<<3),o.putInt32Le(s.messageLength64[0]<<3|s.messageLength64[0]>>>28);var u={h0:t.h0,h1:t.h1,h2:t.h2,h3:t.h3};a(u,i,o);var f=e.util.createBuffer();return f.putInt32Le(u.h0),f.putInt32Le(u.h1),f.putInt32Le(u.h2),f.putInt32Le(u.h3),f},s};var n=null,r=null,i=null,s=null,o=!1}var r="md5";if(typeof n!="function"){if(typeof module!="object"||!module.exports)return typeof forge=="undefined"&&(forge={}),e(forge);var i=!0;n=function(e,n){n(t,module)}}var s,o=function(t,n){n.exports=function(n){var i=s.map(function(e){return t(e)}).concat(e);n=n||{},n.defined=n.defined||{};if(n.defined[r])return n[r];n.defined[r]=!0;for(var o=0;o=64){i=e.h0,s=e.h1,o=e.h2,u=e.h3,a=e.h4;for(l=0;l<16;++l)r=n.getInt32(),t[l]=r,f=u^s&(o^u),r=(i<<5|i>>>27)+f+a+1518500249+r,a=u,u=o,o=s<<30|s>>>2,s=i,i=r;for(;l<20;++l)r=t[l-3]^t[l-8]^t[l-14]^t[l-16],r=r<<1|r>>>31,t[l]=r,f=u^s&(o^u),r=(i<<5|i>>>27)+f+a+1518500249+r,a=u,u=o,o=s<<30|s>>>2,s=i,i=r;for(;l<32;++l)r=t[l-3]^t[l-8]^t[l-14]^t[l-16],r=r<<1|r>>>31,t[l]=r,f=s^o^u,r=(i<<5|i>>>27)+f+a+1859775393+r,a=u,u=o,o=s<<30|s>>>2,s=i,i=r;for(;l<40;++l)r=t[l-6]^t[l-16]^t[l-28]^t[l-32],r=r<<2|r>>>30,t[l]=r,f=s^o^u,r=(i<<5|i>>>27)+f+a+1859775393+r,a=u,u=o,o=s<<30|s>>>2,s=i,i=r;for(;l<60;++l)r=t[l-6]^t[l-16]^t[l-28]^t[l-32],r=r<<2|r>>>30,t[l]=r,f=s&o|u&(s^o),r=(i<<5|i>>>27)+f+a+2400959708+r,a=u,u=o,o=s<<30|s>>>2,s=i,i=r;for(;l<80;++l)r=t[l-6]^t[l-16]^t[l-28]^t[l-32],r=r<<2|r>>>30,t[l]=r,f=s^o^u,r=(i<<5|i>>>27)+f+a+3395469782+r,a=u,u=o,o=s<<30|s>>>2,s=i,i=r;e.h0=e.h0+i|0,e.h1=e.h1+s|0,e.h2=e.h2+o|0,e.h3=e.h3+u|0,e.h4=e.h4+a|0,c-=64}}var t=e.sha1=e.sha1||{};e.md=e.md||{},e.md.algorithms=e.md.algorithms||{},e.md.sha1=e.md.algorithms.sha1=t,t.create=function(){r||i();var t=null,o=e.util.createBuffer(),u=new Array(80),a={algorithm:"sha1",blockLength:64,digestLength:20,messageLength:0,messageLength64:[0,0]};return a.start=function(){return a.messageLength=0,a.messageLength64=[0,0],o=e.util.createBuffer(),t={h0:1732584193,h1:4023233417,h2:2562383102,h3:271733878,h4:3285377520},a},a.start(),a.update=function(n,r){return r==="utf8"&&(n=e.util.encodeUtf8(n)),a.messageLength+=n.length,a.messageLength64[0]+=n.length/4294967296>>>0,a.messageLength64[1]+=n.length>>>0,o.putBytes(n),s(t,u,o),(o.read>2048||o.length()===0)&&o.compact(),a},a.digest=function(){var r=e.util.createBuffer();r.putBytes(o.bytes()),r.putBytes(n.substr(0,64-(a.messageLength64[1]+8&63))),r.putInt32(a.messageLength64[0]<<3|a.messageLength64[0]>>>28),r.putInt32(a.messageLength64[1]<<3);var i={h0:t.h0,h1:t.h1,h2:t.h2,h3:t.h3,h4:t.h4};s(i,u,r);var f=e.util.createBuffer();return f.putInt32(i.h0),f.putInt32(i.h1),f.putInt32(i.h2),f.putInt32(i.h3),f.putInt32(i.h4),f},a};var n=null,r=!1}var r="sha1";if(typeof n!="function"){if(typeof module!="object"||!module.exports)return typeof forge=="undefined"&&(forge={}),e(forge);var i=!0;n=function(e,n){n(t,module)}}var s,o=function(t,n){n.exports=function(n){var i=s.map(function(e){return t(e)}).concat(e);n=n||{},n.defined=n.defined||{};if(n.defined[r])return n[r];n.defined[r]=!0;for(var o=0;o=64){for(l=0;l<16;++l)t[l]=n.getInt32();for(;l<64;++l)r=t[l-2],r=(r>>>17|r<<15)^(r>>>19|r<<13)^r>>>10,s=t[l-15],s=(s>>>7|s<<25)^(s>>>18|s<<14)^s>>>3,t[l]=r+t[l-7]+s+t[l-16]|0;c=e.h0,h=e.h1,p=e.h2,d=e.h3,v=e.h4,m=e.h5,g=e.h6,y=e.h7;for(l=0;l<64;++l)u=(v>>>6|v<<26)^(v>>>11|v<<21)^(v>>>25|v<<7),a=g^v&(m^g),o=(c>>>2|c<<30)^(c>>>13|c<<19)^(c>>>22|c<<10),f=c&h|p&(c^h),r=y+u+a+i[l]+t[l],s=o+f,y=g,g=m,m=v,v=d+r|0,d=p,p=h,h=c,c=r+s|0;e.h0=e.h0+c|0,e.h1=e.h1+h|0,e.h2=e.h2+p|0,e.h3=e.h3+d|0,e.h4=e.h4+v|0,e.h5=e.h5+m|0,e.h6=e.h6+g|0,e.h7=e.h7+y|0,b-=64}}var t=e.sha256=e.sha256||{};e.md=e.md||{},e.md.algorithms=e.md.algorithms||{},e.md.sha256=e.md.algorithms.sha256=t,t.create=function(){r||s();var t=null,i=e.util.createBuffer(),u=new Array(64),a={algorithm:"sha256",blockLength:64,digestLength:32,messageLength:0,messageLength64:[0,0]};return a.start=function(){return a.messageLength=0,a.messageLength64=[0,0],i=e.util.createBuffer(),t={h0:1779033703,h1:3144134277,h2:1013904242,h3:2773480762,h4:1359893119,h5:2600822924,h6:528734635,h7:1541459225},a},a.start(),a.update=function(n,r){return r==="utf8"&&(n=e.util.encodeUtf8(n)),a.messageLength+=n.length,a.messageLength64[0]+=n.length/4294967296>>>0,a.messageLength64[1]+=n.length>>>0,i.putBytes(n),o(t,u,i),(i.read>2048||i.length()===0)&&i.compact(),a},a.digest=function(){var r=e.util.createBuffer();r.putBytes(i.bytes()),r.putBytes(n.substr(0,64-(a.messageLength64[1]+8&63))),r.putInt32(a.messageLength64[0]<<3|a.messageLength64[0]>>>28),r.putInt32(a.messageLength64[1]<<3);var s={h0:t.h0,h1:t.h1,h2:t.h2,h3:t.h3,h4:t.h4,h5:t.h5,h6:t.h6,h7:t.h7};o(s,u,r);var f=e.util.createBuffer();return f.putInt32(s.h0),f.putInt32(s.h1),f.putInt32(s.h2),f.putInt32(s.h3),f.putInt32(s.h4),f.putInt32(s.h5),f.putInt32(s.h6),f.putInt32(s.h7),f},a};var n=null,r=!1,i=null}var r="sha256";if(typeof n!="function"){if(typeof module!="object"||!module.exports)return typeof forge=="undefined"&&(forge={}),e(forge);var i=!0;n=function(e,n){n(t,module)}}var s,o=function(t,n){n.exports=function(n){var i=s.map(function(e){return t(e)}).concat(e);n=n||{},n.defined=n.defined||{};if(n.defined[r])return n[r];n.defined[r]=!0;for(var o=0;o=128){for(_=0;_<16;++_)t[_][0]=n.getInt32()>>>0,t[_][1]=n.getInt32()>>>0;for(;_<80;++_)H=t[_-2],D=H[0],P=H[1],r=((D>>>19|P<<13)^(P>>>29|D<<3)^D>>>6)>>>0,i=((D<<13|P>>>19)^(P<<3|D>>>29)^(D<<26|P>>>6))>>>0,j=t[_-15],D=j[0],P=j[1],o=((D>>>1|P<<31)^(D>>>8|P<<24)^D>>>7)>>>0,u=((D<<31|P>>>1)^(D<<24|P>>>8)^(D<<25|P>>>7))>>>0,B=t[_-7],F=t[_-16],P=i+B[1]+u+F[1],t[_][0]=r+B[0]+o+F[0]+(P/4294967296>>>0)>>>0,t[_][1]=P>>>0;m=e[0][0],g=e[0][1],y=e[1][0],b=e[1][1],w=e[2][0],E=e[2][1],S=e[3][0],x=e[3][1],T=e[4][0],N=e[4][1],C=e[5][0],k=e[5][1],L=e[6][0],A=e[6][1],O=e[7][0],M=e[7][1];for(_=0;_<80;++_)l=((T>>>14|N<<18)^(T>>>18|N<<14)^(N>>>9|T<<23))>>>0,c=((T<<18|N>>>14)^(T<<14|N>>>18)^(N<<23|T>>>9))>>>0,h=(L^T&(C^L))>>>0,p=(A^N&(k^A))>>>0,a=((m>>>28|g<<4)^(g>>>2|m<<30)^(g>>>7|m<<25))>>>0,f=((m<<4|g>>>28)^(g<<30|m>>>2)^(g<<25|m>>>7))>>>0,d=(m&y|w&(m^y))>>>0,v=(g&b|E&(g^b))>>>0,P=M+c+p+s[_][1]+t[_][1],r=O+l+h+s[_][0]+t[_][0]+(P/4294967296>>>0)>>>0,i=P>>>0,P=f+v,o=a+d+(P/4294967296>>>0)>>>0,u=P>>>0,O=L,M=A,L=C,A=k,C=T,k=N,P=x+i,T=S+r+(P/4294967296>>>0)>>>0,N=P>>>0,S=w,x=E,w=y,E=b,y=m,b=g,P=i+u,m=r+o+(P/4294967296>>>0)>>>0,g=P>>>0;P=e[0][1]+g,e[0][0]=e[0][0]+m+(P/4294967296>>>0)>>>0,e[0][1]=P>>>0,P=e[1][1]+b,e[1][0]=e[1][0]+y+(P/4294967296>>>0)>>>0,e[1][1]=P>>>0,P=e[2][1]+E,e[2][0]=e[2][0]+w+(P/4294967296>>>0)>>>0,e[2][1]=P>>>0,P=e[3][1]+x,e[3][0]=e[3][0]+S+(P/4294967296>>>0)>>>0,e[3][1]=P>>>0,P=e[4][1]+N,e[4][0]=e[4][0]+T+(P/4294967296>>>0)>>>0,e[4][1]=P>>>0,P=e[5][1]+k,e[5][0]=e[5][0]+C+(P/4294967296>>>0)>>>0,e[5][1]=P>>>0,P=e[6][1]+A,e[6][0]=e[6][0]+L+(P/4294967296>>>0)>>>0,e[6][1]=P>>>0,P=e[7][1]+M,e[7][0]=e[7][0]+O+(P/4294967296>>>0)>>>0,e[7][1]=P>>>0,I-=128}}var t=e.sha512=e.sha512||{};e.md=e.md||{},e.md.algorithms=e.md.algorithms||{},e.md.sha512=e.md.algorithms.sha512=t;var n=e.sha384=e.sha512.sha384=e.sha512.sha384||{};n.create=function(){return t.create("SHA-384")},e.md.sha384=e.md.algorithms.sha384=n,e.sha512.sha256=e.sha512.sha256||{create:function(){return t.create("SHA-512/256")}},e.md["sha512/256"]=e.md.algorithms["sha512/256"]=e.sha512.sha256,e.sha512.sha224=e.sha512.sha224||{create:function(){return t.create("SHA-512/224")}},e.md["sha512/224"]=e.md.algorithms["sha512/224"]=e.sha512.sha224,t.create=function(t){i||u(),typeof t=="undefined"&&(t="SHA-512");if(t in o){var n=o[t],s=null,f=e.util.createBuffer(),l=new Array(80);for(var c=0;c<80;++c)l[c]=new Array(2);var h={algorithm:t.replace("-","").toLowerCase(),blockLength:128,digestLength:64,messageLength:0,messageLength128:[0,0,0,0]};return h.start=function(){h.messageLength=0,h.messageLength128=[0,0,0,0],f=e.util.createBuffer(),s=new Array(n.length);for(var t=0;t>>0,r>>>0];for(var i=3;i>=0;--i)h.messageLength128[i]+=r[1],r[1]=r[0]+(h.messageLength128[i]/4294967296>>>0),h.messageLength128[i]=h.messageLength128[i]>>>0,r[0]=r[1]/4294967296>>>0;return f.putBytes(t),a(s,l,f),(f.read>2048||f.length()===0)&&f.compact(),h},h.digest=function(){var n=e.util.createBuffer();n.putBytes(f.bytes()),n.putBytes(r.substr(0,128-(h.messageLength128[3]+16&127)));var i=[];for(var o=0;o<3;++o)i[o]=h.messageLength128[o]<<3|h.messageLength128[o-1]>>>28;i[3]=h.messageLength128[3]<<3,n.putInt32(i[0]),n.putInt32(i[1]),n.putInt32(i[2]),n.putInt32(i[3]);var u=new Array(s.length);for(var o=0;on.blockLength&&(n.start(),n.update(o.bytes()),o=n.digest()),r=e.util.createBuffer(),i=e.util.createBuffer(),f=o.length();for(var a=0;a65&&o!==-1){var u=t[o];u===","?(++o,t=t.substr(0,o)+"\r\n "+t.substr(o)):t=t.substr(0,o)+"\r\n"+u+t.substr(o+1),s=i-o-1,o=-1,++i}else if(t[i]===" "||t[i]===" "||t[i]===",")o=i;return t}function r(e){return e.replace(/^\s+/,"")}var t=e.pem=e.pem||{};t.encode=function(t,r){r=r||{};var i="-----BEGIN "+t.type+"-----\r\n",s;t.procType&&(s={name:"Proc-Type",values:[String(t.procType.version),t.procType.type]},i+=n(s)),t.contentDomain&&(s={name:"Content-Domain",values:[t.contentDomain]},i+=n(s)),t.dekInfo&&(s={name:"DEK-Info",values:[t.dekInfo.algorithm]},t.dekInfo.parameters&&s.values.push(t.dekInfo.parameters),i+=n(s));if(t.headers)for(var o=0;o8?3:1,m=[],g=[0,0,1,1,1,1,1,1,0,1,1,1,1,1,1,0],y=0,b;for(var w=0;w>>4^S)&252645135,S^=b,E^=b<<4,b=(S>>>-16^E)&65535,E^=b,S^=b<<-16,b=(E>>>2^S)&858993459,S^=b,E^=b<<2,b=(S>>>-16^E)&65535,E^=b,S^=b<<-16,b=(E>>>1^S)&1431655765,S^=b,E^=b<<1,b=(S>>>8^E)&16711935,E^=b,S^=b<<8,b=(E>>>1^S)&1431655765,S^=b,E^=b<<1,b=E<<8|S>>>20&240,E=S<<24|S<<8&16711680|S>>>8&65280|S>>>24&240,S=b;for(var x=0;x>>26,S=S<<2|S>>>26):(E=E<<1|E>>>27,S=S<<1|S>>>27),E&=-15,S&=-15;var T=t[E>>>28]|n[E>>>24&15]|r[E>>>20&15]|i[E>>>16&15]|s[E>>>12&15]|o[E>>>8&15]|u[E>>>4&15],N=a[S>>>28]|f[S>>>24&15]|l[S>>>20&15]|c[S>>>16&15]|h[S>>>12&15]|p[S>>>8&15]|d[S>>>4&15];b=(N>>>16^T)&65535,m[y++]=T^b,m[y++]=N^b<<16}}return m}function c(e,t,l,c){var h=e.length===32?3:9,p;h===3?p=c?[30,-2,-2]:[0,32,2]:p=c?[94,62,-2,32,64,2,30,-2,-2]:[0,32,2,62,30,-2,64,96,2];var d,v=t[0],m=t[1];d=(v>>>4^m)&252645135,m^=d,v^=d<<4,d=(v>>>16^m)&65535,m^=d,v^=d<<16,d=(m>>>2^v)&858993459,v^=d,m^=d<<2,d=(m>>>8^v)&16711935,v^=d,m^=d<<8,d=(v>>>1^m)&1431655765,m^=d,v^=d<<1,v=v<<1|v>>>31,m=m<<1|m>>>31;for(var g=0;g>>4|m<<28)^e[w+1];d=v,v=m,m=d^(r[E>>>24&63]|s[E>>>16&63]|u[E>>>8&63]|f[E&63]|n[S>>>24&63]|i[S>>>16&63]|o[S>>>8&63]|a[S&63])}d=v,v=m,m=d}v=v>>>1|v<<31,m=m>>>1|m<<31,d=(v>>>1^m)&1431655765,m^=d,v^=d<<1,d=(m>>>8^v)&16711935,v^=d,m^=d<<8,d=(m>>>2^v)&858993459,v^=d,m^=d<<2,d=(v>>>16^m)&65535,m^=d,v^=d<<16,d=(v>>>4^m)&252645135,m^=d,v^=d<<4,l[0]=v,l[1]=m}function h(t){t=t||{};var n=(t.mode||"CBC").toUpperCase(),r="DES-"+n,i;t.decrypt?i=e.cipher.createDecipher(r,t.key):i=e.cipher.createCipher(r,t.key);var s=i.start;return i.start=function(t,n){var r=null;n instanceof e.util.ByteBuffer&&(r=n,n={}),n=n||{},n.output=r,n.iv=t,s.call(i,n)},i}e.des=e.des||{},e.des.startEncrypting=function(e,t,n,r){var i=h({key:e,output:n,decrypt:!1,mode:r||(t===null?"ECB":"CBC")});return i.start(t),i},e.des.createEncryptionCipher=function(e,t){return h({key:e,output:null,decrypt:!1,mode:t})},e.des.startDecrypting=function(e,t,n,r){var i=h({key:e,output:n,decrypt:!0,mode:r||(t===null?"ECB":"CBC")});return i.start(t),i},e.des.createDecryptionCipher=function(e,t){return h({key:e,output:null,decrypt:!0,mode:t})},e.des.Algorithm=function(e,t){var n=this;n.name=e,n.mode=new t({blockSize:8,cipher:{encrypt:function(e,t){return c(n._keys,e,t,!1)},decrypt:function(e,t){return c(n._keys,e,t,!0)}}}),n._init=!1},e.des.Algorithm.prototype.initialize=function(t){if(this._init)return;var n=e.util.createBuffer(t.key);if(this.name.indexOf("3DES")===0&&n.length()!==24)throw new Error("Invalid Triple-DES key size: "+n.length()*8);this._keys=l(n),this._init=!0},t("DES-ECB",e.cipher.modes.ecb),t("DES-CBC",e.cipher.modes.cbc),t("DES-CFB",e.cipher.modes.cfb),t("DES-OFB",e.cipher.modes.ofb),t("DES-CTR",e.cipher.modes.ctr),t("3DES-ECB",e.cipher.modes.ecb),t("3DES-CBC",e.cipher.modes.cbc),t("3DES-CFB",e.cipher.modes.cfb),t("3DES-OFB",e.cipher.modes.ofb),t("3DES-CTR",e.cipher.modes.ctr);var n=[16843776,0,65536,16843780,16842756,66564,4,65536,1024,16843776,16843780,1024,16778244,16842756,16777216,4,1028,16778240,16778240,66560,66560,16842752,16842752,16778244,65540,16777220,16777220,65540,0,1028,66564,16777216,65536,16843780,4,16842752,16843776,16777216,16777216,1024,16842756,65536,66560,16777220,1024,4,16778244,66564,16843780,65540,16842752,16778244,16777220,1028,66564,16843776,1028,16778240,16778240,0,65540,66560,0,16842756],r=[-2146402272,-2147450880,32768,1081376,1048576,32,-2146435040,-2147450848,-2147483616,-2146402272,-2146402304,-2147483648,-2147450880,1048576,32,-2146435040,1081344,1048608,-2147450848,0,-2147483648,32768,1081376,-2146435072,1048608,-2147483616,0,1081344,32800,-2146402304,-2146435072,32800,0,1081376,-2146435040,1048576,-2147450848,-2146435072,-2146402304,32768,-2146435072,-2147450880,32,-2146402272,1081376,32,32768,-2147483648,32800,-2146402304,1048576,-2147483616,1048608,-2147450848,-2147483616,1048608,1081344,0,-2147450880,32800,-2147483648,-2146435040,-2146402272,1081344],i=[520,134349312,0,134348808,134218240,0,131592,134218240,131080,134217736,134217736,131072,134349320,131080,134348800,520,134217728,8,134349312,512,131584,134348800,134348808,131592,134218248,131584,131072,134218248,8,134349320,512,134217728,134349312,134217728,131080,520,131072,134349312,134218240,0,512,131080,134349320,134218240,134217736,512,0,134348808,134218248,131072,134217728,134349320,8,131592,131584,134217736,134348800,134218248,520,134348800,131592,8,134348808,131584],s=[8396801,8321,8321,128,8396928,8388737,8388609,8193,0,8396800,8396800,8396929,129,0,8388736,8388609,1,8192,8388608,8396801,128,8388608,8193,8320,8388737,1,8320,8388736,8192,8396928,8396929,129,8388736,8388609,8396800,8396929,129,0,0,8396800,8320,8388736,8388737,1,8396801,8321,8321,128,8396929,129,1,8192,8388609,8193,8396928,8388737,8193,8320,8388608,8396801,128,8388608,8192,8396928],o=[256,34078976,34078720,1107296512,524288,256,1073741824,34078720,1074266368,524288,33554688,1074266368,1107296512,1107820544,524544,1073741824,33554432,1074266112,1074266112,0,1073742080,1107820800,1107820800,33554688,1107820544,1073742080,0,1107296256,34078976,33554432,1107296256,524544,524288,1107296512,256,33554432,1073741824,34078720,1107296512,1074266368,33554688,1073741824,1107820544,34078976,1074266368,256,33554432,1107820544,1107820800,524544,1107296256,1107820800,34078720,0,1074266112,1107296256,524544,33554688,1073742080,524288,0,1074266112,34078976,1073742080],u=[536870928,541065216,16384,541081616,541065216,16,541081616,4194304,536887296,4210704,4194304,536870928,4194320,536887296,536870912,16400,0,4194320,536887312,16384,4210688,536887312,16,541065232,541065232,0,4210704,541081600,16400,4210688,541081600,536870912,536887296,16,541065232,4210688,541081616,4194304,16400,536870928,4194304,536887296,536870912,16400,536870928,541081616,4210688,541065216,4210704,541081600,0,541065232,16,16384,541065216,4210704,16384,4194320,536887312,0,541081600,536870912,4194320,536887312],a=[2097152,69206018,67110914,0,2048,67110914,2099202,69208064,69208066,2097152,0,67108866,2,67108864,69206018,2050,67110912,2099202,2097154,67110912,67108866,69206016,69208064,2097154,69206016,2048,2050,69208066,2099200,2,67108864,2099200,67108864,2099200,2097152,67110914,67110914,69206018,69206018,2,2097154,67108864,67110912,2097152,69208064,2050,2099202,69208064,2050,67108866,69208066,69206016,2099200,0,2,69208066,0,2099202,69206016,2048,67108866,67110912,2048,2097154],f=[268439616,4096,262144,268701760,268435456,268439616,64,268435456,262208,268697600,268701760,266240,268701696,266304,4096,64,268697600,268435520,268439552,4160,266240,262208,268697664,268701696,4160,0,0,268697664,268435520,268439552,266304,262144,266304,262144,268701696,4096,64,268697664,4096,266304,268439552,64,268435520,268697600,268697664,268435456,262144,268439616,0,268701760,262208,268435520,268697600,268439552,268439616,0,268701760,266240,266240,4160,4160,262208,268435456,268701696]}var r="des";if(typeof n!="function"){if(typeof module!="object"||!module.exports)return typeof forge=="undefined"&&(forge={}),e(forge);var i=!0;n=function(e,n){n(t,module)}}var s,o=function(t,n){n.exports=function(n){var i=s.map(function(e){return t(e)}).concat(e);n=n||{},n.defined=n.defined||{};if(n.defined[r])return n[r];n.defined[r]=!0;for(var o=0;of)return o(null,h);c.start(null,null),c.update(n),c.update(e.util.int32ToBytes(m)),p=v=c.digest().getBytes(),g=2,b()}function b(){if(g<=r)return c.start(null,null),c.update(v),d=c.digest().getBytes(),p=e.util.xorBytes(p,d,u),v=d,++g,e.util.setImmediate(b);h+=m4294967295*u){var a=new Error("Derived key is too long.");if(o)return o(a);throw a}var f=Math.ceil(i/u),l=i-(f-1)*u,c=e.hmac.create();c.start(s,t);var h="",p,d,v;if(!o){for(var m=1;m<=f;++m){c.start(null,null),c.update(n),c.update(e.util.int32ToBytes(m)),p=v=c.digest().getBytes();for(var g=2;g<=r;++g)c.start(null,null),c.update(v),d=c.digest().getBytes(),p=e.util.xorBytes(p,d,u),v=d;h+=m=32)return f(),e();var t=32-n.pools[0].messageLength<<5;n.seedFile(t,function(t,r){if(t)return e(t);n.collect(r),f(),e()})}function a(){if(n.pools[0].messageLength>=32)return f();var e=32-n.pools[0].messageLength<<5;n.collect(n.seedFileSync(e)),f()}function f(){var e=n.plugin.md.create();e.update(n.pools[0].digest().getBytes()),n.pools[0].start();var t=1;for(var r=1;r<32;++r)t=t===31?2147483648:t<<2,t%n.reseeds===0&&(e.update(n.pools[r].digest().getBytes()),n.pools[r].start());var i=e.digest().getBytes();e.start(),e.update(i);var s=e.digest().getBytes();n.key=n.plugin.formatKey(i),n.seed=n.plugin.formatSeed(s),n.reseeds=n.reseeds===4294967295?0:n.reseeds+1,n.generated=0}function l(t){var n=null;if(typeof window!="undefined"){var r=window.crypto||window.msCrypto;r&&r.getRandomValues&&(n=function(e){return r.getRandomValues(e)})}var i=e.util.createBuffer();if(n)while(i.length()>16),l+=(f&32767)<<16,l+=f>>15,l=(l&2147483647)+(l>>31),h=l&4294967295;for(var u=0;u<3;++u)c=h>>>(u<<3),c^=Math.floor(Math.random()*256),i.putByte(String.fromCharCode(c&255))}}return i.getBytes(t)}var n={plugin:t,key:null,seed:null,time:null,reseeds:0,generated:0},i=t.md,s=new Array(32);for(var o=0;o<32;++o)s[o]=i.create();return n.pools=s,n.pool=0,n.generate=function(t,r){function l(c){if(c)return r(c);if(f.length()>=t)return r(null,f.getBytes(t));n.generated>1048575&&(n.key=null);if(n.key===null)return e.util.nextTick(function(){u(l)});var h=i(n.key,n.seed);n.generated+=h.length,f.putBytes(h),n.key=o(i(n.key,s(n.seed))),n.seed=a(i(n.key,n.seed)),e.util.setImmediate(l)}if(!r)return n.generateSync(t);var i=n.plugin.cipher,s=n.plugin.increment,o=n.plugin.formatKey,a=n.plugin.formatSeed,f=e.util.createBuffer();n.key=null,l()},n.generateSync=function(t){var r=n.plugin.cipher,i=n.plugin.increment,s=n.plugin.formatKey,o=n.plugin.formatSeed;n.key=null;var u=e.util.createBuffer();while(u.length()1048575&&(n.key=null),n.key===null&&a();var f=r(n.key,n.seed);n.generated+=f.length,u.putBytes(f),n.key=s(r(n.key,i(n.seed))),n.seed=o(r(n.key,n.seed))}return u.getBytes(t)},r?(n.seedFile=function(e,t){r.randomBytes(e,function(e,n){if(e)return t(e);t(null,n.toString())})},n.seedFileSync=function(e){return r.randomBytes(e).toString()}):(n.seedFile=function(e,t){try{t(null,l(e))}catch(n){t(n)}},n.seedFileSync=l),n.collect=function(e){var t=e.length;for(var r=0;r>i&255);n.collect(r)},n.registerWorker=function(e){if(e===self)n.seedFile=function(e,t){function n(e){var r=e.data;r.forge&&r.forge.prng&&(self.removeEventListener("message",n),t(r.forge.prng.err,r.forge.prng.bytes))}self.addEventListener("message",n),self.postMessage({forge:{prng:{needed:e}}})};else{var t=function(t){var r=t.data;r.forge&&r.forge.prng&&n.seedFile(r.forge.prng.needed,function(t,n){e.postMessage({forge:{prng:{err:t,bytes:n}}})})};e.addEventListener("message",t)}},n}}var r="prng";if(typeof n!="function"){if(typeof module!="object"||!module.exports)return typeof forge=="undefined"&&(forge={}),e(forge);var i=!0;n=function(e,n){n(t,module)}}var s,o=function(t,n){n.exports=function(n){var i=s.map(function(e){return t(e)}).concat(e);n=n||{},n.defined=n.defined||{};if(n.defined[r])return n[r];n.defined[r]=!0;for(var o=0;o>16-t},i=function(e,t){return(e&65535)>>t|e<<16-t&65535};e.rc2=e.rc2||{},e.rc2.expandKey=function(n,r){typeof n=="string"&&(n=e.util.createBuffer(n)),r=r||128;var i=n,s=n.length(),o=r,u=Math.ceil(o/8),a=255>>(o&7),f;for(f=s;f<128;f++)i.putByte(t[i.at(f-1)+i.at(f-s)&255]);i.setAt(128-u,t[i.at(128-u)&a]);for(f=127-u;f>=0;f--)i.setAt(f,t[i.at(f+1)^i.at(f+u)]);return i};var s=function(t,s,o){var u=!1,a=null,f=null,l=null,c,h,p,d,v=[];t=e.rc2.expandKey(t,s);for(p=0;p<64;p++)v.push(t.getInt16Le());o?(c=function(e){for(p=0;p<4;p++)e[p]+=v[d]+(e[(p+3)%4]&e[(p+2)%4])+(~e[(p+3)%4]&e[(p+1)%4]),e[p]=r(e[p],n[p]),d++},h=function(e){for(p=0;p<4;p++)e[p]+=v[e[(p+3)%4]&63]}):(c=function(e){for(p=3;p>=0;p--)e[p]=i(e[p],n[p]),e[p]-=v[d]+(e[(p+3)%4]&e[(p+2)%4])+(~e[(p+3)%4]&e[(p+1)%4]),d--},h=function(e){for(p=3;p>=0;p--)e[p]-=v[e[(p+3)%4]&63]});var m=function(e){var t=[];for(p=0;p<4;p++){var n=a.getInt16Le();l!==null&&(o?n^=l.getInt16Le():l.putInt16Le(n)),t.push(n&65535)}d=o?0:63;for(var r=0;r=8)m([[5,c],[1,h],[6,c],[1,h],[5,c]])},finish:function(e){var t=!0;if(o)if(e)t=e(8,a,!o);else{var n=a.length()===8?8:8-a.length();a.fillWithByte(n,n)}t&&(u=!0,g.update());if(!o){t=a.length()===0;if(t)if(e)t=e(8,f,!o);else{var r=f.length(),i=f.at(r-1);i>r?t=!1:f.truncate(i)}}return t}},g};e.rc2.startEncrypting=function(t,n,r){var i=e.rc2.createEncryptionCipher(t,128);return i.start(n,r),i},e.rc2.createEncryptionCipher=function(e,t){return s(e,t,!0)},e.rc2.startDecrypting=function(t,n,r){var i=e.rc2.createDecryptionCipher(t,128);return i.start(n,r),i},e.rc2.createDecryptionCipher=function(e,t){return s(e,t,!1)}}var r="rc2";if(typeof n!="function"){if(typeof module!="object"||!module.exports)return typeof forge=="undefined"&&(forge={}),e(forge);var i=!0;n=function(e,n){n(t,module)}}var s,o=function(t,n){n.exports=function(n){var i=s.map(function(e){return t(e)}).concat(e);n=n||{},n.defined=n.defined||{};if(n.defined[r])return n[r];n.defined[r]=!0;for(var o=0;o=0){var o=t*this.data[e++]+n.data[r]+i;i=Math.floor(o/67108864),n.data[r++]=o&67108863}return i}function u(e,t,n,r,i,s){var o=t&32767,u=t>>15;while(--s>=0){var a=this.data[e]&32767,f=this.data[e++]>>15,l=u*a+f*o;a=o*a+((l&32767)<<15)+n.data[r]+(i&1073741823),i=(a>>>30)+(l>>>15)+u*f+(i>>>30),n.data[r++]=a&1073741823}return i}function a(e,t,n,r,i,s){var o=t&16383,u=t>>14;while(--s>=0){var a=this.data[e]&16383,f=this.data[e++]>>14,l=u*a+f*o;a=o*a+((l&16383)<<14)+n.data[r]+i,i=(a>>28)+(l>>14)+u*f,n.data[r++]=a&268435455}return i}function d(e){return l.charAt(e)}function v(e,t){var n=c[e.charCodeAt(t)];return n==null?-1:n}function m(e){for(var t=this.t-1;t>=0;--t)e.data[t]=this.data[t];e.t=this.t,e.s=this.s}function g(e){this.t=1,this.s=e<0?-1:0,e>0?this.data[0]=e:e<-1?this.data[0]=e+this.DV:this.t=0}function y(e){var t=s();return t.fromInt(e),t}function b(e,t){var n;if(t==16)n=4;else if(t==8)n=3;else if(t==256)n=8;else if(t==2)n=1;else if(t==32)n=5;else{if(t!=4){this.fromRadix(e,t);return}n=2}this.t=0,this.s=0;var r=e.length,s=!1,o=0;while(--r>=0){var u=n==8?e[r]&255:v(e,r);if(u<0){e.charAt(r)=="-"&&(s=!0);continue}s=!1,o==0?this.data[this.t++]=u:o+n>this.DB?(this.data[this.t-1]|=(u&(1<>this.DB-o):this.data[this.t-1]|=u<=this.DB&&(o-=this.DB)}n==8&&(e[0]&128)!=0&&(this.s=-1,o>0&&(this.data[this.t-1]|=(1<0&&this.data[this.t-1]==e)--this.t}function E(e){if(this.s<0)return"-"+this.negate().toString(e);var t;if(e==16)t=4;else if(e==8)t=3;else if(e==2)t=1;else if(e==32)t=5;else{if(e!=4)return this.toRadix(e);t=2}var n=(1<0){u>u)>0&&(i=!0,s=d(r));while(o>=0)u>(u+=this.DB-t)):(r=this.data[o]>>(u-=t)&n,u<=0&&(u+=this.DB,--o)),r>0&&(i=!0),i&&(s+=d(r))}return i?s:"0"}function S(){var e=s();return i.ZERO.subTo(this,e),e}function x(){return this.s<0?this.negate():this}function T(e){var t=this.s-e.s;if(t!=0)return t;var n=this.t;t=n-e.t;if(t!=0)return this.s<0?-t:t;while(--n>=0)if((t=this.data[n]-e.data[n])!=0)return t;return 0}function N(e){var t=1,n;return(n=e>>>16)!=0&&(e=n,t+=16),(n=e>>8)!=0&&(e=n,t+=8),(n=e>>4)!=0&&(e=n,t+=4),(n=e>>2)!=0&&(e=n,t+=2),(n=e>>1)!=0&&(e=n,t+=1),t}function C(){return this.t<=0?0:this.DB*(this.t-1)+N(this.data[this.t-1]^this.s&this.DM)}function k(e,t){var n;for(n=this.t-1;n>=0;--n)t.data[n+e]=this.data[n];for(n=e-1;n>=0;--n)t.data[n]=0;t.t=this.t+e,t.s=this.s}function L(e,t){for(var n=e;n=0;--u)t.data[u+s+1]=this.data[u]>>r|o,o=(this.data[u]&i)<=0;--u)t.data[u]=0;t.data[s]=o,t.t=this.t+s+1,t.s=this.s,t.clamp()}function O(e,t){t.s=this.s;var n=Math.floor(e/this.DB);if(n>=this.t){t.t=0;return}var r=e%this.DB,i=this.DB-r,s=(1<>r;for(var o=n+1;o>r;r>0&&(t.data[this.t-n-1]|=(this.s&s)<>=this.DB;if(e.t>=this.DB;r+=this.s}else{r+=this.s;while(n>=this.DB;r-=e.s}t.s=r<0?-1:0,r<-1?t.data[n++]=this.DV+r:r>0&&(t.data[n++]=r),t.t=n,t.clamp()}function _(e,t){var n=this.abs(),r=e.abs(),s=n.t;t.t=s+r.t;while(--s>=0)t.data[s]=0;for(s=0;s=0)e.data[n]=0;for(n=0;n=t.DV&&(e.data[n+t.t]-=t.DV,e.data[n+t.t+1]=1)}e.t>0&&(e.data[e.t-1]+=t.am(n,t.data[n],e,2*n,0,1)),e.s=0,e.clamp()}function P(e,t,n){var r=e.abs();if(r.t<=0)return;var o=this.abs();if(o.t0?(r.lShiftTo(l,u),o.lShiftTo(l,n)):(r.copyTo(u),o.copyTo(n));var c=u.t,h=u.data[c-1];if(h==0)return;var p=h*(1<1?u.data[c-2]>>this.F2:0),d=this.FV/p,v=(1<=0&&(n.data[n.t++]=1,n.subTo(b,n)),i.ONE.dlShiftTo(c,b),b.subTo(u,u);while(u.t=0){var w=n.data[--g]==h?this.DM:Math.floor(n.data[g]*d+(n.data[g-1]+m)*v);if((n.data[g]+=u.am(0,w,n,y,0,c))0&&n.rShiftTo(l,n),a<0&&i.ZERO.subTo(n,n)}function H(e){var t=s();return this.abs().divRemTo(e,null,t),this.s<0&&t.compareTo(i.ZERO)>0&&e.subTo(t,t),t}function B(e){this.m=e}function j(e){return e.s<0||e.compareTo(this.m)>=0?e.mod(this.m):e}function F(e){return e}function I(e){e.divRemTo(this.m,null,e)}function q(e,t,n){e.multiplyTo(t,n),this.reduce(n)}function R(e,t){e.squareTo(t),this.reduce(t)}function U(){if(this.t<1)return 0;var e=this.data[0];if((e&1)==0)return 0;var t=e&3;return t=t*(2-(e&15)*t)&15,t=t*(2-(e&255)*t)&255,t=t*(2-((e&65535)*t&65535))&65535,t=t*(2-e*t%this.DV)%this.DV,t>0?this.DV-t:-t}function z(e){this.m=e,this.mp=e.invDigit(),this.mpl=this.mp&32767,this.mph=this.mp>>15,this.um=(1<0&&this.m.subTo(t,t),t}function X(e){var t=s();return e.copyTo(t),this.reduce(t),t}function V(e){while(e.t<=this.mt2)e.data[e.t++]=0;for(var t=0;t>15)*this.mpl&this.um)<<15)&e.DM;n=t+this.m.t,e.data[n]+=this.m.am(0,r,e,t,0,this.m.t);while(e.data[n]>=e.DV)e.data[n]-=e.DV,e.data[++n]++}e.clamp(),e.drShiftTo(this.m.t,e),e.compareTo(this.m)>=0&&e.subTo(this.m,e)}function $(e,t){e.squareTo(t),this.reduce(t)}function J(e,t,n){e.multiplyTo(t,n),this.reduce(n)}function K(){return(this.t>0?this.data[0]&1:this.s)==0}function Q(e,t){if(e>4294967295||e<1)return i.ONE;var n=s(),r=s(),o=t.convert(this),u=N(e)-1;o.copyTo(n);while(--u>=0){t.sqrTo(n,r);if((e&1<0)t.mulTo(r,o,n);else{var a=n;n=r,r=a}}return t.revert(n)}function G(e,t){var n;return e<256||t.isEven()?n=new B(t):n=new z(t),this.exp(e,n)}function Y(){var e=s();return this.copyTo(e),e}function Z(){if(this.s<0){if(this.t==1)return this.data[0]-this.DV;if(this.t==0)return-1}else{if(this.t==1)return this.data[0];if(this.t==0)return 0}return(this.data[1]&(1<<32-this.DB)-1)<>24}function tt(){return this.t==0?this.s:this.data[0]<<16>>16}function nt(e){return Math.floor(Math.LN2*this.DB/Math.log(e))}function rt(){return this.s<0?-1:this.t<=0||this.t==1&&this.data[0]<=0?0:1}function it(e){e==null&&(e=10);if(this.signum()==0||e<2||e>36)return"0";var t=this.chunkSize(e),n=Math.pow(e,t),r=y(n),i=s(),o=s(),u="";this.divRemTo(r,i,o);while(i.signum()>0)u=(n+o.intValue()).toString(e).substr(1)+u,i.divRemTo(r,i,o);return o.intValue().toString(e)+u}function st(e,t){this.fromInt(0),t==null&&(t=10);var n=this.chunkSize(t),r=Math.pow(t,n),s=!1,o=0,u=0;for(var a=0;a=n&&(this.dMultiply(r),this.dAddOffset(u,0),o=0,u=0)}o>0&&(this.dMultiply(Math.pow(t,o)),this.dAddOffset(u,0)),s&&i.ZERO.subTo(this,this)}function ot(e,t,n){if("number"==typeof t)if(e<2)this.fromInt(1);else{this.fromNumber(e,n),this.testBit(e-1)||this.bitwiseTo(i.ONE.shiftLeft(e-1),dt,this),this.isEven()&&this.dAddOffset(1,0);while(!this.isProbablePrime(t))this.dAddOffset(2,0),this.bitLength()>e&&this.subTo(i.ONE.shiftLeft(e-1),this)}else{var r=new Array,s=e&7;r.length=(e>>3)+1,t.nextBytes(r),s>0?r[0]&=(1<0){n>n)!=(this.s&this.DM)>>n&&(t[i++]=r|this.s<=0){n<8?(r=(this.data[e]&(1<>(n+=this.DB-8)):(r=this.data[e]>>(n-=8)&255,n<=0&&(n+=this.DB,--e)),(r&128)!=0&&(r|=-256),i==0&&(this.s&128)!=(r&128)&&++i;if(i>0||r!=this.s)t[i++]=r}}return t}function at(e){return this.compareTo(e)==0}function ft(e){return this.compareTo(e)<0?this:e}function lt(e){return this.compareTo(e)>0?this:e}function ct(e,t,n){var r,i,s=Math.min(e.t,this.t);for(r=0;r>=16,t+=16),(e&255)==0&&(e>>=8,t+=8),(e&15)==0&&(e>>=4,t+=4),(e&3)==0&&(e>>=2,t+=2),(e&1)==0&&++t,t}function Tt(){for(var e=0;e=this.t?this.s!=0:(this.data[t]&1<>=this.DB;if(e.t>=this.DB;r+=this.s}else{r+=this.s;while(n>=this.DB;r+=e.s}t.s=r<0?-1:0,r>0?t.data[n++]=r:r<-1&&(t.data[n++]=this.DV+r),t.t=n,t.clamp()}function Dt(e){var t=s();return this.addTo(e,t),t}function Pt(e){var t=s();return this.subTo(e,t),t}function Ht(e){var t=s();return this.multiplyTo(e,t),t}function Bt(e){var t=s();return this.divRemTo(e,t,null),t}function jt(e){var t=s();return this.divRemTo(e,null,t),t}function Ft(e){var t=s(),n=s();return this.divRemTo(e,t,n),new Array(t,n)}function It(e){this.data[this.t]=this.am(0,e-1,this,0,0,this.t),++this.t,this.clamp()}function qt(e,t){if(e==0)return;while(this.t<=t)this.data[this.t++]=0;this.data[t]+=e;while(this.data[t]>=this.DV)this.data[t]-=this.DV,++t>=this.t&&(this.data[this.t++]=0),++this.data[t]}function Rt(){}function Ut(e){return e}function zt(e,t,n){e.multiplyTo(t,n)}function Wt(e,t){e.squareTo(t)}function Xt(e){return this.exp(e,new Rt)}function Vt(e,t,n){var r=Math.min(this.t+e.t,t);n.s=0,n.t=r;while(r>0)n.data[--r]=0;var i;for(i=n.t-this.t;r=0)n.data[r]=0;for(r=Math.max(t-this.t,0);r2*this.m.t)return e.mod(this.m);if(e.compareTo(this.m)<0)return e;var t=s();return e.copyTo(t),this.reduce(t),t}function Qt(e){return e}function Gt(e){e.drShiftTo(this.m.t-1,this.r2),e.t>this.m.t+1&&(e.t=this.m.t+1,e.clamp()),this.mu.multiplyUpperTo(this.r2,this.m.t+1,this.q3),this.m.multiplyLowerTo(this.q3,this.m.t+1,this.r2);while(e.compareTo(this.r2)<0)e.dAddOffset(1,this.m.t+1);e.subTo(this.r2,e);while(e.compareTo(this.m)>=0)e.subTo(this.m,e)}function Yt(e,t){e.squareTo(t),this.reduce(t)}function Zt(e,t,n){e.multiplyTo(t,n),this.reduce(n)}function en(e,t){var n=e.bitLength(),r,i=y(1),o;if(n<=0)return i;n<18?r=1:n<48?r=3:n<144?r=4:n<768?r=5:r=6,n<8?o=new B(t):t.isEven()?o=new Jt(t):o=new z(t);var u=new Array,a=3,f=r-1,l=(1<1){var c=s();o.sqrTo(u[1],c);while(a<=l)u[a]=s(),o.mulTo(c,u[a-2],u[a]),a+=2}var h=e.t-1,p,d=!0,v=s(),m;n=N(e.data[h])-1;while(h>=0){n>=f?p=e.data[h]>>n-f&l:(p=(e.data[h]&(1<0&&(p|=e.data[h-1]>>this.DB+n-f)),a=r;while((p&1)==0)p>>=1,--a;(n-=a)<0&&(n+=this.DB,--h);if(d)u[p].copyTo(i),d=!1;else{while(a>1)o.sqrTo(i,v),o.sqrTo(v,i),a-=2;a>0?o.sqrTo(i,v):(m=i,i=v,v=m),o.mulTo(v,u[p],i)}while(h>=0&&(e.data[h]&1<0&&(t.rShiftTo(s,t),n.rShiftTo(s,n));while(t.signum()>0)(i=t.getLowestSetBit())>0&&t.rShiftTo(i,t),(i=n.getLowestSetBit())>0&&n.rShiftTo(i,n),t.compareTo(n)>=0?(t.subTo(n,t),t.rShiftTo(1,t)):(n.subTo(t,n),n.rShiftTo(1,n));return s>0&&n.lShiftTo(s,n),n}function nn(e){if(e<=0)return 0;var t=this.DV%e,n=this.s<0?e-1:0;if(this.t>0)if(t==0)n=this.data[0]%e;else for(var r=this.t-1;r>=0;--r)n=(t*n+this.data[r])%e;return n}function rn(e){var t=e.isEven();if(this.isEven()&&t||e.signum()==0)return i.ZERO;var n=e.clone(),r=this.clone(),s=y(1),o=y(0),u=y(0),a=y(1);while(n.signum()!=0){while(n.isEven()){n.rShiftTo(1,n);if(t){if(!s.isEven()||!o.isEven())s.addTo(this,s),o.subTo(e,o);s.rShiftTo(1,s)}else o.isEven()||o.subTo(e,o);o.rShiftTo(1,o)}while(r.isEven()){r.rShiftTo(1,r);if(t){if(!u.isEven()||!a.isEven())u.addTo(this,u),a.subTo(e,a);u.rShiftTo(1,u)}else a.isEven()||a.subTo(e,a);a.rShiftTo(1,a)}n.compareTo(r)>=0?(n.subTo(r,n),t&&s.subTo(u,s),o.subTo(a,o)):(r.subTo(n,r),t&&u.subTo(s,u),a.subTo(o,a))}return r.compareTo(i.ONE)!=0?i.ZERO:a.compareTo(e)>=0?a.subtract(e):a.signum()<0?(a.addTo(e,a),a.signum()<0?a.add(e):a):a}function un(e){var t,n=this.abs();if(n.t==1&&n.data[0]<=sn[sn.length-1]){for(t=0;t=0);var a=o.modPow(r,this);if(a.compareTo(i.ONE)!=0&&a.compareTo(t)!=0){var f=1;while(f++>24&255,o>>16&255,o>>8&255,o&255);r.start(),r.update(t+u),i+=r.digest().getBytes()}return i.substring(0,n)}var t=e.pkcs1=e.pkcs1||{};t.encode_rsa_oaep=function(t,r,i){var s,o,u,a;typeof i=="string"?(s=i,o=arguments[3]||undefined,u=arguments[4]||undefined):i&&(s=i.label||undefined,o=i.seed||undefined,u=i.md||undefined,i.mgf1&&i.mgf1.md&&(a=i.mgf1.md)),u?u.start():u=e.md.sha1.create(),a||(a=u);var f=Math.ceil(t.n.bitLength()/8),l=f-2*u.digestLength-2;if(r.length>l){var c=new Error("RSAES-OAEP input message length is too long.");throw c.length=r.length,c.maxLength=l,c}s||(s=""),u.update(s,"raw");var h=u.digest(),p="",d=l-r.length;for(var v=0;vt&&(o=f(t,n));if(o.isProbablePrime(c))return s(null,o);o.dAddOffset(r[a++%8],0)}while(h<0||+(new Date)-pt&&(o=f(t,r));var d=o.toString(16);i.target.postMessage({hex:d,workLoad:l}),o.dAddOffset(c,0)}a=Math.max(1,a);var e=[];for(var i=0;is-11){var o=new Error("Message is too long for PKCS#1 v1.5 padding.");throw o.length=t.length,o.max=s-11,o}i.putByte(0),i.putByte(r);var u=s-3-t.length,a;if(r===0||r===1){a=r===0?0:255;for(var f=0;f0){var l=0,c=e.random.getBytes(u);for(var f=0;f1){if(o.getByte()!==255){--o.read;break}++f}}else if(a===2){f=0;while(o.length()>1){if(o.getByte()===0){--o.read;break}++f}}var c=o.getByte();if(c!==0||f!==s-3-o.length())throw new Error("Encryption block is invalid.");return o.getBytes()}function p(n,i,s){function u(){a(n.pBits,function(e,t){if(e)return s(e);n.p=t;if(n.q!==null)return f(e,n.q);a(n.qBits,f)})}function a(t,n){e.prime.generateProbablePrime(t,o,n)}function f(e,i){if(e)return s(e);n.q=i;if(n.p.compareTo(n.q)<0){var o=n.p;n.p=n.q,n.q=o}if(n.p.subtract(t.ONE).gcd(n.e).compareTo(t.ONE)!==0){n.p=null,u();return}if(n.q.subtract(t.ONE).gcd(n.e).compareTo(t.ONE)!==0){n.q=null,a(n.qBits,f);return}n.p1=n.p.subtract(t.ONE),n.q1=n.q.subtract(t.ONE),n.phi=n.p1.multiply(n.q1);if(n.phi.gcd(n.e).compareTo(t.ONE)!==0){n.p=n.q=null,u();return}n.n=n.p.multiply(n.q);if(n.n.bitLength()!==n.bits){n.q=null,a(n.qBits,f);return}var l=n.e.modInverse(n.phi);n.keys={privateKey:r.rsa.setPrivateKey(n.n,n.e,l,n.p,n.q,l.mod(n.p1),l.mod(n.q1),n.q.modInverse(n.p)),publicKey:r.rsa.setPublicKey(n.n,n.e)},s(null,n.keys)}typeof i=="function"&&(s=i,i={}),i=i||{};var o={algorithm:{name:i.algorithm||"PRIMEINC",options:{workers:i.workers||2,workLoad:i.workLoad||100,workerScript:i.workerScript}}};"prng"in i&&(o.prng=i.prng),u()}function d(t){var n=t.toString(16);return n[0]>="8"&&(n="00"+n),e.util.hexToBytes(n)}function v(e){return e<=100?27:e<=150?18:e<=200?15:e<=250?12:e<=300?9:e<=350?8:e<=400?7:e<=500?6:e<=600?5:e<=800?4:e<=1250?3:2}if(typeof t=="undefined")var t=e.jsbn.BigInteger;var n=e.asn1;e.pki=e.pki||{},e.pki.rsa=e.rsa=e.rsa||{};var r=e.pki,i=[6,4,2,4,2,4,6,2],s={name:"PrivateKeyInfo",tagClass:n.Class.UNIVERSAL,type:n.Type.SEQUENCE,constructed:!0,value:[{name:"PrivateKeyInfo.version",tagClass:n.Class.UNIVERSAL,type:n.Type.INTEGER,constructed:!1,capture:"privateKeyVersion"},{name:"PrivateKeyInfo.privateKeyAlgorithm",tagClass:n.Class.UNIVERSAL,type:n.Type.SEQUENCE,constructed:!0,value:[{name:"AlgorithmIdentifier.algorithm",tagClass:n.Class.UNIVERSAL,type:n.Type.OID,constructed:!1,capture:"privateKeyOid"}]},{name:"PrivateKeyInfo",tagClass:n.Class.UNIVERSAL,type:n.Type.OCTETSTRING,constructed:!1,capture:"privateKey"}]},o={name:"RSAPrivateKey",tagClass:n.Class.UNIVERSAL,type:n.Type.SEQUENCE,constructed:!0,value:[{name:"RSAPrivateKey.version",tagClass:n.Class.UNIVERSAL,type:n.Type.INTEGER,constructed:!1,capture:"privateKeyVersion"},{name:"RSAPrivateKey.modulus",tagClass:n.Class.UNIVERSAL,type:n.Type.INTEGER,constructed:!1,capture:"privateKeyModulus"},{name:"RSAPrivateKey.publicExponent",tagClass:n.Class.UNIVERSAL,type:n.Type.INTEGER,constructed:!1,capture:"privateKeyPublicExponent"},{name:"RSAPrivateKey.privateExponent",tagClass:n.Class.UNIVERSAL,type:n.Type.INTEGER,constructed:!1,capture:"privateKeyPrivateExponent"},{name:"RSAPrivateKey.prime1",tagClass:n.Class.UNIVERSAL,type:n.Type.INTEGER,constructed:!1,capture:"privateKeyPrime1"},{name:"RSAPrivateKey.prime2",tagClass:n.Class.UNIVERSAL,type:n.Type.INTEGER,constructed:!1,capture:"privateKeyPrime2"},{name:"RSAPrivateKey.exponent1",tagClass:n.Class.UNIVERSAL,type:n.Type.INTEGER,constructed:!1,capture:"privateKeyExponent1"},{name:"RSAPrivateKey.exponent2",tagClass:n.Class.UNIVERSAL,type:n.Type.INTEGER,constructed:!1,capture:"privateKeyExponent2"},{name:"RSAPrivateKey.coefficient",tagClass:n.Class.UNIVERSAL,type:n.Type.INTEGER,constructed:!1,capture:"privateKeyCoefficient"}]},u={name:"RSAPublicKey",tagClass:n.Class.UNIVERSAL,type:n.Type.SEQUENCE,constructed:!0,value:[{name:"RSAPublicKey.modulus",tagClass:n.Class.UNIVERSAL,type:n.Type.INTEGER,constructed:!1,capture:"publicKeyModulus"},{name:"RSAPublicKey.exponent",tagClass:n.Class.UNIVERSAL,type:n.Type.INTEGER,constructed:!1,capture:"publicKeyExponent"}]},a=e.pki.rsa.publicKeyValidator={name:"SubjectPublicKeyInfo",tagClass:n.Class.UNIVERSAL,type:n.Type.SEQUENCE,constructed:!0,captureAsn1:"subjectPublicKeyInfo",value:[{name:"SubjectPublicKeyInfo.AlgorithmIdentifier",tagClass:n.Class.UNIVERSAL,type:n.Type.SEQUENCE,constructed:!0,value:[{name:"AlgorithmIdentifier.algorithm",tagClass:n.Class.UNIVERSAL,type:n.Type.OID,constructed:!1,capture:"publicKeyOid"}]},{name:"SubjectPublicKeyInfo.subjectPublicKey",tagClass:n.Class.UNIVERSAL,type:n.Type.BITSTRING,constructed:!1,value:[{name:"SubjectPublicKeyInfo.subjectPublicKey.RSAPublicKey",tagClass:n.Class.UNIVERSAL,type:n.Type.SEQUENCE,constructed:!0,optional:!0,captureAsn1:"rsaPublicKey"}]}]},f=function(e){var t;if(e.algorithm in r.oids){t=r.oids[e.algorithm];var s=n.oidToDer(t).getBytes(),o=n.create(n.Class.UNIVERSAL,n.Type.SEQUENCE,!0,[]),u=n.create(n.Class.UNIVERSAL,n.Type.SEQUENCE,!0,[]);u.value.push(n.create(n.Class.UNIVERSAL,n.Type.OID,!1,s)),u.value.push(n.create(n.Class.UNIVERSAL,n.Type.NULL,!1,""));var a=n.create(n.Class.UNIVERSAL,n.Type.OCTETSTRING,!1,e.digest().getBytes());return o.value.push(u),o.value.push(a),n.toDer(o).getBytes()}var i=new Error("Unknown message digest algorithm.");throw i.algorithm=e.algorithm,i},l=function(n,r,i){if(i)return n.modPow(r.e,r.n);if(!r.p||!r.q)return n.modPow(r.d,r.n);r.dP||(r.dP=r.d.mod(r.p.subtract(t.ONE))),r.dQ||(r.dQ=r.d.mod(r.q.subtract(t.ONE))),r.qInv||(r.qInv=r.q.modInverse(r.p));var s;do s=(new t(e.util.bytesToHex(e.random.getBytes(r.n.bitLength()/8)),16)).mod(r.n);while(s.equals(t.ZERO));n=n.multiply(s.modPow(r.e,r.n)).mod(r.n);var o=n.mod(r.p).modPow(r.dP,r.p),u=n.mod(r.q).modPow(r.dQ,r.q);while(o.compareTo(u)<0)o=o.add(r.p);var a=o.subtract(u).multiply(r.qInv).mod(r.p).multiply(r.q).add(u);return a=a.multiply(s.modInverse(r.n)).mod(r.n),a};r.rsa.encrypt=function(n,r,i){var s=i,o,u=Math.ceil(r.n.bitLength()/8);i!==!1&&i!==!0?(s=i===2,o=c(n,r,i)):(o=e.util.createBuffer(),o.putBytes(n));var a=new t(o.toHex(),16),f=l(a,r,s),h=f.toString(16),p=e.util.createBuffer(),d=u-Math.ceil(h.length/2);while(d>0)p.putByte(0),--d;return p.putBytes(e.util.hexToBytes(h)),p.getBytes()},r.rsa.decrypt=function(n,r,i,s){var o=Math.ceil(r.n.bitLength()/8);if(n.length!==o){var u=new Error("Encrypted message length is invalid.");throw u.length=n.length,u.expected=o,u}var a=new t(e.util.createBuffer(n).toHex(),16);if(a.compareTo(r.n)>=0)throw new Error("Encrypted message is invalid.");var f=l(a,r,i),c=f.toString(16),p=e.util.createBuffer(),d=o-Math.ceil(c.length/2);while(d>0)p.putByte(0),--d;return p.putBytes(e.util.hexToBytes(c)),s!==!1?h(p.getBytes(),r,i):p.getBytes()},r.rsa.createKeyPairGenerationState=function(n,r,i){typeof n=="string"&&(n=parseInt(n,10)),n=n||2048,i=i||{};var s=i.prng||e.random,o={nextBytes:function(e){var t=s.getBytesSync(e.length);for(var n=0;n>1,pBits:n-(n>>1),pqState:0,num:null,keys:null},a.e.fromInt(a.eInt),a},r.rsa.stepKeyPairGenerationState=function(e,n){"algorithm"in e||(e.algorithm="PRIMEINC");var s=new t(null);s.fromInt(30);var o=0,u=function(e,t){return e|t},a=+(new Date),f,l=0;while(e.keys===null&&(n<=0||lc?e.pqState=0:e.num.isProbablePrime(v(e.num.bitLength()))?++e.pqState:e.num.dAddOffset(i[o++%8],0):e.pqState===2?e.pqState=e.num.subtract(t.ONE).gcd(e.e).compareTo(t.ONE)===0?3:0:e.pqState===3&&(e.pqState=0,e.p===null?e.p=e.num:e.q=e.num,e.p!==null&&e.q!==null&&++e.state,e.num=null)}else if(e.state===1)e.p.compareTo(e.q)<0&&(e.num=e.p,e.p=e.q,e.q=e.num),++e.state;else if(e.state===2)e.p1=e.p.subtract(t.ONE),e.q1=e.q.subtract(t.ONE),e.phi=e.p1.multiply(e.q1),++e.state;else if(e.state===3)e.phi.gcd(e.e).compareTo(t.ONE)===0?++e.state:(e.p=null,e.q=null,e.state=0);else if(e.state===4)e.n=e.p.multiply(e.q),e.n.bitLength()===e.bits?++e.state:(e.q=null,e.state=0);else if(e.state===5){var p=e.e.modInverse(e.phi);e.keys={privateKey:r.rsa.setPrivateKey(e.n,e.e,p,e.p,e.q,p.mod(e.p1),p.mod(e.q1),e.q.modInverse(e.p)),publicKey:r.rsa.setPublicKey(e.n,e.e)}}f=+(new Date),l+=f-a,a=f}return e.keys!==null},r.rsa.generateKeyPair=function(e,t,n,i){arguments.length===1?typeof e=="object"?(n=e,e=undefined):typeof e=="function"&&(i=e,e=undefined):arguments.length===2?typeof e=="number"?typeof t=="function"?(i=t,t=undefined):typeof t!="number"&&(n=t,t=undefined):(n=e,i=t,e=undefined,t=undefined):arguments.length===3&&(typeof t=="number"?typeof n=="function"&&(i=n,n=undefined):(i=n,n=t,t=undefined)),n=n||{},e===undefined&&(e=n.bits||2048),t===undefined&&(t=n.e||65537);var s=r.rsa.createKeyPairGenerationState(e,t,n);if(!i)return r.rsa.stepKeyPairGenerationState(s,0),s.keys;p(s,n,i)},r.setRsaPublicKey=r.rsa.setPublicKey=function(t,i){var s={n:t,e:i};return s.encrypt=function(t,n,i){typeof n=="string"?n=n.toUpperCase():n===undefined&&(n="RSAES-PKCS1-V1_5");if(n==="RSAES-PKCS1-V1_5")n={encode:function(e,t,n){return c(e,t,2).getBytes()}};else if(n==="RSA-OAEP"||n==="RSAES-OAEP")n={encode:function(t,n){return e.pkcs1.encode_rsa_oaep(n,t,i)}};else if(["RAW","NONE","NULL",null].indexOf(n)!==-1)n={encode:function(e){return e}};else if(typeof n=="string")throw new Error('Unsupported encryption scheme: "'+n+'".');var o=n.encode(t,s,!0);return r.rsa.encrypt(o,s,!0)},s.verify=function(e,t,i){typeof i=="string"?i=i.toUpperCase():i===undefined&&(i="RSASSA-PKCS1-V1_5");if(i==="RSASSA-PKCS1-V1_5")i={verify:function(e,t){t=h(t,s,!0);var r=n.fromDer(t);return e===r.value[1].value}};else if(i==="NONE"||i==="NULL"||i===null)i={verify:function(e,t){return t=h(t,s,!0),e===t}};var o=r.rsa.decrypt(t,s,!0,!1);return i.verify(e,o,s.n.bitLength())},s},r.setRsaPrivateKey=r.rsa.setPrivateKey=function(t,n,i,s,o,u,a,l){var c={n:t,e:n,d:i,p:s,q:o,dP:u,dQ:a,qInv:l};return c.decrypt=function(t,n,i){typeof n=="string"?n=n.toUpperCase():n===undefined&&(n="RSAES-PKCS1-V1_5");var s=r.rsa.decrypt(t,c,!1,!1);if(n==="RSAES-PKCS1-V1_5")n={decode:h};else if(n==="RSA-OAEP"||n==="RSAES-OAEP")n={decode:function(t,n){return e.pkcs1.decode_rsa_oaep(n,t,i)}};else{if(["RAW","NONE","NULL",null].indexOf(n)===-1)throw new Error('Unsupported encryption scheme: "'+n+'".');n={decode:function(e){return e}}}return n.decode(s,c,!1)},c.sign=function(e,t){var n=!1;typeof t=="string"&&(t=t.toUpperCase());if(t===undefined||t==="RSASSA-PKCS1-V1_5")t={encode:f},n=1;else if(t==="NONE"||t==="NULL"||t===null)t={encode:function(){return e}},n=1;var i=t.encode(e,c.n.bitLength());return r.rsa.encrypt(i,c,n)},c},r.wrapRsaPrivateKey=function(e){return n.create(n.Class.UNIVERSAL,n.Type.SEQUENCE,!0,[n.create(n.Class.UNIVERSAL,n.Type.INTEGER,!1,n.integerToDer(0).getBytes()),n.create(n.Class.UNIVERSAL,n.Type.SEQUENCE,!0,[n.create(n.Class.UNIVERSAL,n.Type.OID,!1,n.oidToDer(r.oids.rsaEncryption).getBytes()),n.create(n.Class.UNIVERSAL,n.Type.NULL,!1,"")]),n.create(n.Class.UNIVERSAL,n.Type.OCTETSTRING,!1,n.toDer(e).getBytes())])},r.privateKeyFromAsn1=function(i){var u={},a=[];n.validate(i,s,u,a)&&(i=n.fromDer(e.util.createBuffer(u.privateKey))),u={},a=[];if(!n.validate(i,o,u,a)){var f=new Error("Cannot read private key. ASN.1 object does not contain an RSAPrivateKey.");throw f.errors=a,f}var l,c,h,p,d,v,m,g;return l=e.util.createBuffer(u.privateKeyModulus).toHex(),c=e.util.createBuffer(u.privateKeyPublicExponent).toHex(),h=e.util.createBuffer(u.privateKeyPrivateExponent).toHex(),p=e.util.createBuffer(u.privateKeyPrime1).toHex(),d=e.util.createBuffer(u.privateKeyPrime2).toHex(),v=e.util.createBuffer(u.privateKeyExponent1).toHex(),m=e.util.createBuffer(u.privateKeyExponent2).toHex(),g=e.util.createBuffer(u.privateKeyCoefficient).toHex(),r.setRsaPrivateKey(new t(l,16),new t(c,16),new t(h,16),new t(p,16),new t(d,16),new t(v,16),new t(m,16),new t(g,16))},r.privateKeyToAsn1=r.privateKeyToRSAPrivateKey=function(e){return n.create(n.Class.UNIVERSAL,n.Type.SEQUENCE,!0,[n.create(n.Class.UNIVERSAL,n.Type.INTEGER,!1,n.integerToDer(0).getBytes()),n.create(n.Class.UNIVERSAL,n.Type.INTEGER,!1,d(e.n)),n.create(n.Class.UNIVERSAL,n.Type.INTEGER,!1,d(e.e)),n.create(n.Class.UNIVERSAL,n.Type.INTEGER,!1,d(e.d)),n.create(n.Class.UNIVERSAL,n.Type.INTEGER,!1,d(e.p)),n.create(n.Class.UNIVERSAL,n.Type.INTEGER,!1,d(e.q)),n.create(n.Class.UNIVERSAL,n.Type.INTEGER,!1,d(e.dP)),n.create(n.Class.UNIVERSAL,n.Type.INTEGER,!1,d(e.dQ)),n.create(n.Class.UNIVERSAL,n.Type.INTEGER,!1,d(e.qInv))])},r.publicKeyFromAsn1=function(i){var s={},o=[];if(n.validate(i,a,s,o)){var f=n.derToOid(s.publicKeyOid);if(f!==r.oids.rsaEncryption){var l=new Error("Cannot read public key. Unknown OID.");throw l.oid=f,l}i=s.rsaPublicKey}o=[];if(!n.validate(i,u,s,o)){var l=new Error("Cannot read public key. ASN.1 object does not contain an RSAPublicKey.");throw l.errors=o,l}var c=e.util.createBuffer(s.publicKeyModulus).toHex(),h=e.util.createBuffer(s.publicKeyExponent).toHex();return r.setRsaPublicKey(new t(c,16),new t(h,16))},r.publicKeyToAsn1=r.publicKeyToSubjectPublicKeyInfo=function(e){return n.create(n.Class.UNIVERSAL,n.Type.SEQUENCE,!0,[n.create(n.Class.UNIVERSAL,n.Type.SEQUENCE,!0,[n.create(n.Class.UNIVERSAL,n.Type.OID,!1,n.oidToDer(r.oids.rsaEncryption).getBytes()),n.create(n.Class.UNIVERSAL,n.Type.NULL,!1,"")]),n.create(n.Class.UNIVERSAL,n.Type.BITSTRING,!1,[r.publicKeyToRSAPublicKey(e)])])},r.publicKeyToRSAPublicKey=function(e){return n.create(n.Class.UNIVERSAL,n.Type.SEQUENCE,!0,[n.create(n.Class.UNIVERSAL,n.Type.INTEGER,!1,d(e.n)),n.create(n.Class.UNIVERSAL,n.Type.INTEGER,!1,d(e.e))])}}var r="rsa";if(typeof n!="function"){if(typeof module!="object"||!module.exports)return typeof forge=="undefined"&&(forge={}),e(forge);var i=!0;n=function(e,n){n(t,module)}}var s,o=function(t,n){n.exports=function(n){var i=s.map(function(e){return t(e)}).concat(e);n=n||{},n.defined=n.defined||{};if(n.defined[r])return n[r];n.defined[r]=!0;for(var o=0;o=0;a--)A>>=8,A+=N.at(a)+L.at(a),L.setAt(a,A&255);k.putBuffer(L)}w=k,c.putBuffer(x)}return c.truncate(c.length()-s),c},r.pbe.getCipher=function(e,t,n){switch(e){case r.oids.pkcs5PBES2:return r.pbe.getCipherForPBES2(e,t,n);case r.oids["pbeWithSHAAnd3-KeyTripleDES-CBC"]:case r.oids["pbewithSHAAnd40BitRC2-CBC"]:return r.pbe.getCipherForPKCS12PBE(e,t,n);default:var i=new Error("Cannot read encrypted PBE data block. Unsupported OID.");throw i.oid=e,i.supportedOids=["pkcs5PBES2","pbeWithSHAAnd3-KeyTripleDES-CBC","pbewithSHAAnd40BitRC2-CBC"],i}},r.pbe.getCipherForPBES2=function(t,i,s){var u={},a=[];if(!n.validate(i,o,u,a)){var f=new Error("Cannot read password-based-encryption algorithm parameters. ASN.1 object is not a supported EncryptedPrivateKeyInfo.");throw f.errors=a,f}t=n.derToOid(u.kdfOid);if(t!==r.oids.pkcs5PBKDF2){var f=new Error("Cannot read encrypted private key. Unsupported key derivation function OID.");throw f.oid=t,f.supportedOids=["pkcs5PBKDF2"],f}t=n.derToOid(u.encOid);if(t!==r.oids["aes128-CBC"]&&t!==r.oids["aes192-CBC"]&&t!==r.oids["aes256-CBC"]&&t!==r.oids["des-EDE3-CBC"]&&t!==r.oids.desCBC){var f=new Error("Cannot read encrypted private key. Unsupported encryption scheme OID.");throw f.oid=t,f.supportedOids=["aes128-CBC","aes192-CBC","aes256-CBC","des-EDE3-CBC","desCBC"],f}var l=u.kdfSalt,c=e.util.createBuffer(u.kdfIterationCount);c=c.getInt(c.length()<<3);var h,p;switch(r.oids[t]){case"aes128-CBC":h=16,p=e.aes.createDecryptionCipher;break;case"aes192-CBC":h=24,p=e.aes.createDecryptionCipher;break;case"aes256-CBC":h=32,p=e.aes.createDecryptionCipher;break;case"des-EDE3-CBC":h=24,p=e.des.createDecryptionCipher;break;case"desCBC":h=8,p=e.des.createDecryptionCipher}var d=e.pkcs5.pbkdf2(s,l,c,h),v=u.encIv,m=p(d);return m.start(v),m},r.pbe.getCipherForPKCS12PBE=function(t,i,s){var o={},a=[];if(!n.validate(i,u,o,a)){var f=new Error("Cannot read password-based-encryption algorithm parameters. ASN.1 object is not a supported EncryptedPrivateKeyInfo.");throw f.errors=a,f}var l=e.util.createBuffer(o.salt),c=e.util.createBuffer(o.iterations);c=c.getInt(c.length()<<3);var h,p,d;switch(t){case r.oids["pbeWithSHAAnd3-KeyTripleDES-CBC"]:h=24,p=8,d=e.des.startDecrypting;break;case r.oids["pbewithSHAAnd40BitRC2-CBC"]:h=5,p=8,d=function(t,n){var r=e.rc2.createDecryptionCipher(t,40);return r.start(n,null),r};break;default:var f=new Error("Cannot read PKCS #12 PBE data block. Unsupported OID.");throw f.oid=t,f}var v=r.pbe.generatePkcs12Key(s,l,1,c,h),m=r.pbe.generatePkcs12Key(s,l,2,c,p);return d(v,m)}}var r="pbe";if(typeof n!="function"){if(typeof module!="object"||!module.exports)return typeof forge=="undefined"&&(forge={}),e(forge);var i=!0;n=function(e,n){n(t,module)}}var s,o=function(t,n){n.exports=function(n){var i=s.map(function(e){return t(e)}).concat(e);n=n||{},n.defined=n.defined||{};if(n.defined[r])return n[r];n.defined[r]=!0;for(var o=0;o>8*c-l&255;return w=String.fromCharCode(w.charCodeAt(0)&~E)+w.substr(1),w+v+String.fromCharCode(188)},a.verify=function(t,s,u){var a,f=u-1,l=Math.ceil(f/8);s=s.substr(-l);if(l>8*l-f&255;if((h.charCodeAt(0)&d)!==0)throw new Error("Bits beyond keysize not zero as expected.");var v=r.generate(p,c),m="";for(a=0;a2)throw new Error("Cannot read notBefore/notAfter validity times; more than two times were provided in the certificate.");if(b.length<2)throw new Error("Cannot read notBefore/notAfter validity times; they were not provided as either UTCTime or GeneralizedTime.");m.validity.notBefore=b[0],m.validity.notAfter=b[1],m.tbsCertificate=u.tbsCertificate;if(s){m.md=null;if(m.signatureOid in r){var v=r[m.signatureOid];switch(v){case"sha1WithRSAEncryption":m.md=e.md.sha1.create();break;case"md5WithRSAEncryption":m.md=e.md.md5.create();break;case"sha256WithRSAEncryption":m.md=e.md.sha256.create();break;case"RSASSA-PSS":m.md=e.md.sha256.create()}}if(m.md===null){var f=new Error("Could not compute certificate digest. Unknown signature OID.");throw f.signatureOid=m.signatureOid,f}var w=t.toDer(m.tbsCertificate);m.md.update(w.getBytes())}var E=e.md.sha1.create();m.issuer.getField=function(e){return l(m.issuer,e)},m.issuer.addField=function(e){d([e]),m.issuer.attributes.push(e)},m.issuer.attributes=n.RDNAttributesAsArray(u.certIssuer,E),u.certIssuerUniqueId&&(m.issuer.uniqueId=u.certIssuerUniqueId),m.issuer.hash=E.digest().toHex();var S=e.md.sha1.create();return m.subject.getField=function(e){return l(m.subject,e)},m.subject.addField=function(e){d([e]),m.subject.attributes.push(e)},m.subject.attributes=n.RDNAttributesAsArray(u.certSubject,S),u.certSubjectUniqueId&&(m.subject.uniqueId=u.certSubjectUniqueId),m.subject.hash=S.digest().toHex(),u.certExtensions?m.extensions=n.certificateExtensionsFromAsn1(u.certExtensions):m.extensions=[],m.publicKey=n.publicKeyFromAsn1(u.subjectPublicKeyInfo),m},n.certificateExtensionsFromAsn1=function(e){var t=[];for(var r=0;r1&&(o=s.value.charCodeAt(1),u=s.value.length>2?s.value.charCodeAt(2):0),i.digitalSignature=(o&128)===128,i.nonRepudiation=(o&64)===64,i.keyEncipherment=(o&32)===32,i.dataEncipherment=(o&16)===16,i.keyAgreement=(o&8)===8,i.keyCertSign=(o&4)===4,i.cRLSign=(o&2)===2,i.encipherOnly=(o&1)===1,i.decipherOnly=(u&128)===128}else if(i.name==="basicConstraints"){var s=t.fromDer(i.value);s.value.length>0&&s.value[0].type===t.Type.BOOLEAN?i.cA=s.value[0].value.charCodeAt(0)!==0:i.cA=!1;var a=null;s.value.length>0&&s.value[0].type===t.Type.INTEGER?a=s.value[0].value:s.value.length>1&&(a=s.value[1].value),a!==null&&(i.pathLenConstraint=t.derToInteger(a))}else if(i.name==="extKeyUsage"){var s=t.fromDer(i.value);for(var f=0;f1&&(o=s.value.charCodeAt(1)),i.client=(o&128)===128,i.server=(o&64)===64,i.email=(o&32)===32,i.objsign=(o&16)===16,i.reserved=(o&8)===8,i.sslCA=(o&4)===4,i.emailCA=(o&2)===2,i.objCA=(o&1)===1}else if(i.name==="subjectAltName"||i.name==="issuerAltName"){i.altNames=[];var c,s=t.fromDer(i.value);for(var h=0;h0&&i.value.push(n.certificateExtensionsToAsn1(r.extensions)),i},n.getCertificationRequestInfo=function(e){var r=t.create(t.Class.UNIVERSAL,t.Type.SEQUENCE,!0,[t.create(t.Class.UNIVERSAL,t.Type.INTEGER,!1,t.integerToDer(e.version).getBytes()),h(e.subject),n.publicKeyToAsn1(e.publicKey),g(e)]);return r},n.distinguishedNameToAsn1=function(e){return h(e)},n.certificateToAsn1=function(e){var r=e.tbsCertificate||n.getTBSCertificate(e);return t.create(t.Class.UNIVERSAL,t.Type.SEQUENCE,!0,[r,t.create(t.Class.UNIVERSAL,t.Type.SEQUENCE,!0,[t.create(t.Class.UNIVERSAL,t.Type.OID,!1,t.oidToDer(e.signatureOid).getBytes()),m(e.signatureOid,e.signatureParameters)]),t.create(t.Class.UNIVERSAL,t.Type.BITSTRING,!1,String.fromCharCode(0)+e.signature)])},n.certificateExtensionsToAsn1=function(e){var r=t.create(t.Class.CONTEXT_SPECIFIC,3,!0,[]),i=t.create(t.Class.UNIVERSAL,t.Type.SEQUENCE,!0,[]);r.value.push(i);for(var s=0;sc.validity.notAfter)a={message:"Certificate is not valid yet or has expired.",error:n.certificateError.certificate_expired,notBefore:c.validity.notBefore,notAfter:c.validity.notAfter,now:o};else{var h=!1;if(r.length>0){l=r[0];try{h=l.verify(c)}catch(p){}}else{var d=t.getIssuer(c);if(d===null)a={message:"Certificate is not trusted.",error:n.certificateError.unknown_ca};else{e.util.isArray(d)||(d=[d]);while(!h&&d.length>0){l=d.shift();try{h=l.verify(c)}catch(p){}}}}a===null&&!h&&(a={message:"Certificate signature is invalid.",error:n.certificateError.bad_certificate})}a===null&&!c.isIssuer(l)&&(a={message:"Certificate issuer is invalid.",error:n.certificateError.bad_certificate});if(a===null){var v={keyUsage:!0,basicConstraints:!0};for(var m=0;a===null&&mE&&(a={message:"Certificate basicConstraints pathLenConstraint violated.",error:n.certificateError.bad_certificate})}}var S=a===null?!0:a.error,x=i?i(S,f,s):S;if(x!==!0){S===!0&&(a={message:"The application rejected the certificate.",error:n.certificateError.bad_certificate});if(x||x===0)typeof x=="object"&&!e.util.isArray(x)?(x.message&&(a.message=x.message),x.error&&(a.error=x.error)):typeof x=="string"&&(a.error=x);throw a}a=null,u=!1,++f}while(r.length>0);return!0}}var r="x509";if(typeof n!="function"){if(typeof module!="object"||!module.exports)return typeof forge=="undefined"&&(forge={}),e(forge);var i=!0;n=function(e,n){n(t,module)}}var s,o=function(t,n){n.exports=function(n){var i=s.map(function(e){return t(e)}).concat(e);n=n||{},n.defined=n.defined||{};if(n.defined[r])return n[r];n.defined[r]=!0;for(var o=0;o=0&&i.push(u)}return i}function l(t){if(t.composed||t.constructed){var n=e.util.createBuffer();for(var r=0;r0&&(f=t.create(t.Class.UNIVERSAL,t.Type.SET,!0,h));var p=[],d=[];s!==null&&(e.util.isArray(s)?d=s:d=[s]);var v=[];for(var m=0;m0){var w=t.create(t.Class.UNIVERSAL,t.Type.SEQUENCE,!0,v),E=t.create(t.Class.UNIVERSAL,t.Type.SEQUENCE,!0,[t.create(t.Class.UNIVERSAL,t.Type.OID,!1,t.oidToDer(n.oids.data).getBytes()),t.create(t.Class.CONTEXT_SPECIFIC,0,!0,[t.create(t.Class.UNIVERSAL,t.Type.OCTETSTRING,!1,t.toDer(w).getBytes())])]);p.push(E)}var S=null;if(i!==null){var x=n.wrapRsaPrivateKey(n.privateKeyToAsn1(i));o===null?S=t.create(t.Class.UNIVERSAL,t.Type.SEQUENCE,!0,[t.create(t.Class.UNIVERSAL,t.Type.OID,!1,t.oidToDer(n.oids.keyBag).getBytes()),t.create(t.Class.CONTEXT_SPECIFIC,0,!0,[x]),f]):S=t.create(t.Class.UNIVERSAL,t.Type.SEQUENCE,!0,[t.create(t.Class.UNIVERSAL,t.Type.OID,!1,t.oidToDer(n.oids.pkcs8ShroudedKeyBag).getBytes()),t.create(t.Class.CONTEXT_SPECIFIC,0,!0,[n.encryptPrivateKeyInfo(x,o,u)]),f]);var T=t.create(t.Class.UNIVERSAL,t.Type.SEQUENCE,!0,[S]),N=t.create(t.Class.UNIVERSAL,t.Type.SEQUENCE,!0,[t.create(t.Class.UNIVERSAL,t.Type.OID,!1,t.oidToDer(n.oids.data).getBytes()),t.create(t.Class.CONTEXT_SPECIFIC,0,!0,[t.create(t.Class.UNIVERSAL,t.Type.OCTETSTRING,!1,t.toDer(T).getBytes())])]);p.push(N)}var C=t.create(t.Class.UNIVERSAL,t.Type.SEQUENCE,!0,p),k;if(u.useMac){var c=e.md.sha1.create(),L=new e.util.ByteBuffer(e.random.getBytes(u.saltSize)),A=u.count,i=r.generateKey(o,L,3,A,20),O=e.hmac.create();O.start(c,i),O.update(t.toDer(C).getBytes());var M=O.getMac();k=t.create(t.Class.UNIVERSAL,t.Type.SEQUENCE,!0,[t.create(t.Class.UNIVERSAL,t.Type.SEQUENCE,!0,[t.create(t.Class.UNIVERSAL,t.Type.SEQUENCE,!0,[t.create(t.Class.UNIVERSAL,t.Type.OID,!1,t.oidToDer(n.oids.sha1).getBytes()),t.create(t.Class.UNIVERSAL,t.Type.NULL,!1,"")]),t.create(t.Class.UNIVERSAL,t.Type.OCTETSTRING,!1,M.getBytes())]),t.create(t.Class.UNIVERSAL,t.Type.OCTETSTRING,!1,L.getBytes()),t.create(t.Class.UNIVERSAL,t.Type.INTEGER,!1,t.integerToDer(A).getBytes())])}return t.create(t.Class.UNIVERSAL,t.Type.SEQUENCE,!0,[t.create(t.Class.UNIVERSAL,t.Type.INTEGER,!1,t.integerToDer(3).getBytes()),t.create(t.Class.UNIVERSAL,t.Type.SEQUENCE,!0,[t.create(t.Class.UNIVERSAL,t.Type.OID,!1,t.oidToDer(n.oids.data).getBytes()),t.create(t.Class.CONTEXT_SPECIFIC,0,!0,[t.create(t.Class.UNIVERSAL,t.Type.OCTETSTRING,!1,t.toDer(C).getBytes())])]),k])},r.generateKey=e.pbe.generatePkcs12Key}var r="pkcs12";if(typeof n!="function"){if(typeof module!="object"||!module.exports)return typeof forge=="undefined"&&(forge={}),e(forge);var i=!0;n=function(e,n){n(t,module)}}var s,o=function(t,n){n.exports=function(n){var i=s.map(function(e){return t(e)}).concat(e);n=n||{},n.defined=n.defined||{};if(n.defined[r])return n[r];n.defined[r]=!0;for(var o=0;o>1,u=o+(t.length&1),a=t.substr(0,u),f=t.substr(o,u),l=e.util.createBuffer(),c=e.hmac.create();r=n+r;var h=Math.ceil(i/16),p=Math.ceil(i/20);c.start("MD5",a);var d=e.util.createBuffer();l.putBytes(r);for(var v=0;v0&&(a.queue(e,a.createAlert(e,{level:a.Alert.Level.warning,description:a.Alert.Description.no_renegotiation})),a.flush(e)),e.process()},a.parseHelloMessage=function(t,n,r){var i=null,s=t.entity===a.ConnectionEnd.client;if(r<38)t.error(t,{message:s?"Invalid ServerHello message. Message too short.":"Invalid ClientHello message. Message too short.",send:!0,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.illegal_parameter}});else{var u=n.fragment,f=u.length();i={version:{major:u.getByte(),minor:u.getByte()},random:e.util.createBuffer(u.getBytes(32)),session_id:o(u,1),extensions:[]},s?(i.cipher_suite=u.getBytes(2),i.compression_method=u.getByte()):(i.cipher_suites=o(u,2),i.compression_methods=o(u,1)),f=r-(f-u.length());if(f>0){var l=o(u,2);while(l.length()>0)i.extensions.push({type:[l.getByte(),l.getByte()],data:o(l,2)});if(!s)for(var c=0;c0){var d=p.getByte();if(d!==0)break;t.session.extensions.server_name.serverNameList.push(o(p,2).getBytes())}}}}if(t.session.version)if(i.version.major!==t.session.version.major||i.version.minor!==t.session.version.minor)return t.error(t,{message:"TLS version change is disallowed during renegotiation.",send:!0,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.protocol_version}});if(s)t.session.cipherSuite=a.getCipherSuite(i.cipher_suite);else{var v=e.util.createBuffer(i.cipher_suites.bytes());while(v.length()>0){t.session.cipherSuite=a.getCipherSuite(v.getBytes(2));if(t.session.cipherSuite!==null)break}}if(t.session.cipherSuite===null)return t.error(t,{message:"No cipher suites in common.",send:!0,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.handshake_failure},cipherSuite:e.util.bytesToHex(i.cipher_suite)});s?t.session.compressionMethod=i.compression_method:t.session.compressionMethod=a.CompressionMethod.none}return i},a.createSecurityParameters=function(e,t){var n=e.entity===a.ConnectionEnd.client,r=t.random.bytes(),i=n?e.session.sp.client_random:r,s=n?r:a.createRandom().getBytes();e.session.sp={entity:e.entity,prf_algorithm:a.PRFAlgorithm.tls_prf_sha256,bulk_cipher_algorithm:null,cipher_type:null,enc_key_length:null,block_length:null,fixed_iv_length:null,record_iv_length:null,mac_algorithm:null,mac_length:null,mac_key_length:null,compression_algorithm:e.session.compressionMethod,pre_master_secret:null,master_secret:null,client_random:i,server_random:s}},a.handleServerHello=function(e,t,n){var r=a.parseHelloMessage(e,t,n);if(e.fail)return;if(!(r.version.minor<=e.version.minor))return e.error(e,{message:"Incompatible TLS version.",send:!0,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.protocol_version}});e.version.minor=r.version.minor,e.session.version=e.version;var i=r.session_id.bytes();i.length>0&&i===e.session.id?(e.expect=d,e.session.resuming=!0,e.session.sp.server_random=r.random.bytes()):(e.expect=l,e.session.resuming=!1,a.createSecurityParameters(e,r)),e.session.id=i,e.process()},a.handleClientHello=function(t,n,r){var i=a.parseHelloMessage(t,n,r);if(t.fail)return;var s=i.session_id.bytes(),o=null;if(t.sessionCache){o=t.sessionCache.getSession(s);if(o===null)s="";else if(o.version.major!==i.version.major||o.version.minor>i.version.minor)o=null,s=""}s.length===0&&(s=e.random.getBytes(32)),t.session.id=s,t.session.clientHelloVersion=i.version,t.session.sp={};if(o)t.version=t.session.version=o.version,t.session.sp=o.sp;else{var u;for(var f=1;f0)u=o(s.certificate_list,3),f=e.asn1.fromDer(u),u=e.pki.certificateFromAsn1(f,!0),l.push(u)}catch(h){return t.error(t,{message:"Could not parse certificate list.",cause:h,send:!0,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.bad_certificate}})}var p=t.entity===a.ConnectionEnd.client;!p&&t.verifyClient!==!0||l.length!==0?l.length===0?t.expect=p?c:w:(p?t.session.serverCertificate=l[0]:t.session.clientCertificate=l[0],a.verifyCertificateChain(t,l)&&(t.expect=p?c:w)):t.error(t,{message:p?"No server certificate provided.":"No client certificate provided.",send:!0,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.illegal_parameter}}),t.process()},a.handleServerKeyExchange=function(e,t,n){if(n>0)return e.error(e,{message:"Invalid key parameters. Only RSA is supported.",send:!0,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.unsupported_certificate}});e.expect=h,e.process()},a.handleClientKeyExchange=function(t,n,r){if(r<48)return t.error(t,{message:"Invalid key parameters. Only RSA is supported.",send:!0,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.unsupported_certificate}});var i=n.fragment,s={enc_pre_master_secret:o(i,2).getBytes()},u=null;if(t.getPrivateKey)try{u=t.getPrivateKey(t,t.session.serverCertificate),u=e.pki.privateKeyFromPem(u)}catch(f){t.error(t,{message:"Could not get private key.",cause:f,send:!0,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.internal_error}})}if(u===null)return t.error(t,{message:"No private key set.",send:!0,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.internal_error}});try{var l=t.session.sp;l.pre_master_secret=u.decrypt(s.enc_pre_master_secret);var c=t.session.clientHelloVersion;if(c.major!==l.pre_master_secret.charCodeAt(0)||c.minor!==l.pre_master_secret.charCodeAt(1))throw new Error("TLS version rollback attack detected.")}catch(f){l.pre_master_secret=e.random.getBytes(48)}t.expect=S,t.session.clientCertificate!==null&&(t.expect=E),t.process()},a.handleCertificateRequest=function(e,t,n){if(n<3)return e.error(e,{message:"Invalid CertificateRequest. Message too short.",send:!0,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.illegal_parameter}});var r=t.fragment,i={certificate_types:o(r,1),certificate_authorities:o(r,2)};e.session.certificateRequest=i,e.expect=p,e.process()},a.handleCertificateVerify=function(t,n,r){if(r<2)return t.error(t,{message:"Invalid CertificateVerify. Message too short.",send:!0,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.illegal_parameter}});var i=n.fragment;i.read-=4;var s=i.bytes();i.read+=4;var u={signature:o(i,2).getBytes()},f=e.util.createBuffer();f.putBuffer(t.session.md5.digest()),f.putBuffer(t.session.sha1.digest()),f=f.getBytes();try{var l=t.session.clientCertificate;if(!l.publicKey.verify(f,u.signature,"NONE"))throw new Error("CertificateVerify signature does not match.");t.session.md5.update(s),t.session.sha1.update(s)}catch(c){return t.error(t,{message:"Bad signature in CertificateVerify.",send:!0,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.handshake_failure}})}t.expect=S,t.process()},a.handleServerHelloDone=function(t,n,r){if(r>0)return t.error(t,{message:"Invalid ServerHelloDone message. Invalid length.",send:!0,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.record_overflow}});if(t.serverCertificate===null){var i={message:"No server certificate provided. Not enough security.",send:!0,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.insufficient_security}},s=0,o=t.verify(t,i.alert.description,s,[]);if(o!==!0){if(o||o===0)typeof o=="object"&&!e.util.isArray(o)?(o.message&&(i.message=o.message),o.alert&&(i.alert.description=o.alert)):typeof o=="number"&&(i.alert.description=o);return t.error(t,i)}}t.session.certificateRequest!==null&&(n=a.createRecord(t,{type:a.ContentType.handshake,data:a.createCertificate(t)}),a.queue(t,n)),n=a.createRecord(t,{type:a.ContentType.handshake,data:a.createClientKeyExchange(t)}),a.queue(t,n),t.expect=g;var u=function(e,t){e.session.certificateRequest!==null&&e.session.clientCertificate!==null&&a.queue(e,a.createRecord(e,{type:a.ContentType.handshake,data:a.createCertificateVerify(e,t)})),a.queue(e,a.createRecord(e,{type:a.ContentType.change_cipher_spec,data:a.createChangeCipherSpec()})),e.state.pending=a.createConnectionState(e),e.state.current.write=e.state.pending.write,a.queue(e,a.createRecord(e,{type:a.ContentType.handshake,data:a.createFinished(e)})),e.expect=d,a.flush(e),e.process()};if(t.session.certificateRequest===null||t.session.clientCertificate===null)return u(t,null);a.getClientSignature(t,u)},a.handleChangeCipherSpec=function(e,t){if(t.fragment.getByte()!==1)return e.error(e,{message:"Invalid ChangeCipherSpec message received.",send:!0,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.illegal_parameter}});var n=e.entity===a.ConnectionEnd.client;if(e.session.resuming&&n||!e.session.resuming&&!n)e.state.pending=a.createConnectionState(e);e.state.current.read=e.state.pending.read;if(!e.session.resuming&&n||e.session.resuming&&!n)e.state.pending=null;e.expect=n?v:x,e.process()},a.handleFinished=function(n,r,i){var s=r.fragment;s.read-=4;var o=s.bytes();s.read+=4;var u=r.fragment.getBytes();s=e.util.createBuffer(),s.putBuffer(n.session.md5.digest()),s.putBuffer(n.session.sha1.digest());var f=n.entity===a.ConnectionEnd.client,l=f?"server finished":"client finished",c=n.session.sp,h=12,p=t;s=p(c.master_secret,l,s.getBytes(),h);if(s.getBytes()!==u)return n.error(n,{message:"Invalid verify_data in Finished message.",send:!0,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.decrypt_error}});n.session.md5.update(o),n.session.sha1.update(o);if(n.session.resuming&&f||!n.session.resuming&&!f)a.queue(n,a.createRecord(n,{type:a.ContentType.change_cipher_spec,data:a.createChangeCipherSpec()})),n.state.current.write=n.state.pending.write,n.state.pending=null,a.queue(n,a.createRecord(n,{type:a.ContentType.handshake,data:a.createFinished(n)}));n.expect=f?m:T,n.handshaking=!1,++n.handshakes,n.peerCertificate=f?n.session.serverCertificate:n.session.clientCertificate,a.flush(n),n.isConnected=!0,n.connected(n),n.process()},a.handleAlert=function(e,t){var n=t.fragment,r={level:n.getByte(),description:n.getByte()},i;switch(r.description){case a.Alert.Description.close_notify:i="Connection closed.";break;case a.Alert.Description.unexpected_message:i="Unexpected message.";break;case a.Alert.Description.bad_record_mac:i="Bad record MAC.";break;case a.Alert.Description.decryption_failed:i="Decryption failed.";break;case a.Alert.Description.record_overflow:i="Record overflow.";break;case a.Alert.Description.decompression_failure:i="Decompression failed.";break;case a.Alert.Description.handshake_failure:i="Handshake failure.";break;case a.Alert.Description.bad_certificate:i="Bad certificate.";break;case a.Alert.Description.unsupported_certificate:i="Unsupported certificate.";break;case a.Alert.Description.certificate_revoked:i="Certificate revoked.";break;case a.Alert.Description.certificate_expired:i="Certificate expired.";break;case a.Alert.Description.certificate_unknown:i="Certificate unknown.";break;case a.Alert.Description.illegal_parameter:i="Illegal parameter.";break;case a.Alert.Description.unknown_ca:i="Unknown certificate authority.";break;case a.Alert.Description.access_denied:i="Access denied.";break;case a.Alert.Description.decode_error:i="Decode error.";break;case a.Alert.Description.decrypt_error:i="Decrypt error.";break;case a.Alert.Description.export_restriction:i="Export restriction.";break;case a.Alert.Description.protocol_version:i="Unsupported protocol version.";break;case a.Alert.Description.insufficient_security:i="Insufficient security.";break;case a.Alert.Description.internal_error:i="Internal error.";break;case a.Alert.Description.user_canceled:i="User canceled.";break;case a.Alert.Description.no_renegotiation:i="Renegotiation not supported.";break;default:i="Unknown error."}if(r.description===a.Alert.Description.close_notify)return e.close();e.error(e,{message:i,send:!1,origin:e.entity===a.ConnectionEnd.client?"server":"client",alert:r}),e.process()},a.handleHandshake=function(t,n){var r=n.fragment,i=r.getByte(),s=r.getInt24();if(s>r.length())return t.fragmented=n,n.fragment=e.util.createBuffer(),r.read-=4,t.process();t.fragmented=null,r.read-=4;var o=r.bytes(s+4);r.read+=4,i in q[t.entity][t.expect]?(t.entity===a.ConnectionEnd.server&&!t.open&&!t.fail&&(t.handshaking=!0,t.session={version:null,extensions:{server_name:{serverNameList:[]}},cipherSuite:null,compressionMethod:null,serverCertificate:null,clientCertificate:null,md5:e.md.md5.create(),sha1:e.md.sha1.create()}),i!==a.HandshakeType.hello_request&&i!==a.HandshakeType.certificate_verify&&i!==a.HandshakeType.finished&&(t.session.md5.update(o),t.session.sha1.update(o)),q[t.entity][t.expect][i](t,n,s)):a.handleUnexpected(t,n)},a.handleApplicationData=function(e,t){e.data.putBuffer(t.fragment),e.dataReady(e),e.process()},a.handleHeartbeat=function(t,n){var r=n.fragment,i=r.getByte(),s=r.getInt16(),o=r.getBytes(s);if(i===a.HeartbeatMessageType.heartbeat_request){if(t.handshaking||s>o.length)return t.process();a.queue(t,a.createRecord(t,{type:a.ContentType.heartbeat,data:a.createHeartbeat(a.HeartbeatMessageType.heartbeat_response,o)})),a.flush(t)}else if(i===a.HeartbeatMessageType.heartbeat_response){if(o!==t.expectedHeartbeatPayload)return t.process();t.heartbeatReceived&&t.heartbeatReceived(t,e.util.createBuffer(o))}t.process()};var f=0,l=1,c=2,h=3,p=4,d=5,v=6,m=7,g=8,y=0,b=1,w=2,E=3,S=4,x=5,T=6,N=7,C=a.handleUnexpected,k=a.handleChangeCipherSpec,L=a.handleAlert,A=a.handleHandshake,O=a.handleApplicationData,M=a.handleHeartbeat,_=[];_[a.ConnectionEnd.client]=[[C,L,A,C,M],[C,L,A,C,M],[C,L,A,C,M],[C,L,A,C,M],[C,L,A,C,M],[k,L,C,C,M],[C,L,A,C,M],[C,L,A,O,M],[C,L,A,C,M]],_[a.ConnectionEnd.server]=[[C,L,A,C,M],[C,L,A,C,M],[C,L,A,C,M],[C,L,A,C,M],[k,L,C,C,M],[C,L,A,C,M],[C,L,A,O,M],[C,L,A,C,M]];var D=a.handleHelloRequest,P=a.handleServerHello,H=a.handleCertificate,B=a.handleServerKeyExchange,j=a.handleCertificateRequest,F=a.handleServerHelloDone,I=a.handleFinished,q=[];q[a.ConnectionEnd.client]=[[C,C,P,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C],[D,C,C,C,C,C,C,C,C,C,C,H,B,j,F,C,C,C,C,C,C],[D,C,C,C,C,C,C,C,C,C,C,C,B,j,F,C,C,C,C,C,C],[D,C,C,C,C,C,C,C,C,C,C,C,C,j,F,C,C,C,C,C,C],[D,C,C,C,C,C,C,C,C,C,C,C,C,C,F,C,C,C,C,C,C],[D,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C],[D,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,I],[D,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C],[D,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C]];var R=a.handleClientHello,U=a.handleClientKeyExchange,z=a.handleCertificateVerify;q[a.ConnectionEnd.server]=[[C,R,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C],[C,C,C,C,C,C,C,C,C,C,C,H,C,C,C,C,C,C,C,C,C],[C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,U,C,C,C,C],[C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,z,C,C,C,C,C],[C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C],[C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,I],[C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C],[C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C,C]],a.generateKeys=function(e,n){var r=t,i=n.client_random+n.server_random;e.session.resuming||(n.master_secret=r(n.pre_master_secret,"master secret",i,48).bytes(),n.pre_master_secret=null),i=n.server_random+n.client_random;var s=2*n.mac_key_length+2*n.enc_key_length,o=e.version.major===a.Versions.TLS_1_0.major&&e.version.minor===a.Versions.TLS_1_0.minor;o&&(s+=2*n.fixed_iv_length);var u=r(n.master_secret,"key expansion",i,s),f={client_write_MAC_key:u.getBytes(n.mac_key_length),server_write_MAC_key:u.getBytes(n.mac_key_length),client_write_key:u.getBytes(n.enc_key_length),server_write_key:u.getBytes(n.enc_key_length)};return o&&(f.client_write_IV=u.getBytes(n.fixed_iv_length),f.server_write_IV=u.getBytes(n.fixed_iv_length)),f},a.createConnectionState=function(e){var t=e.entity===a.ConnectionEnd.client,n=function(){var e={sequenceNumber:[0,0],macKey:null,macLength:0,macFunction:null,cipherState:null,cipherFunction:function(e){return!0},compressionState:null,compressFunction:function(e){return!0},updateSequenceNumber:function(){e.sequenceNumber[1]===4294967295?(e.sequenceNumber[1]=0,++e.sequenceNumber[0]):++e.sequenceNumber[1]}};return e},r={read:n(),write:n()};r.read.update=function(e,t){return r.read.cipherFunction(t,r.read)?r.read.compressFunction(e,t,r.read)||e.error(e,{message:"Could not decompress record.",send:!0,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.decompression_failure}}):e.error(e,{message:"Could not decrypt record or bad MAC.",send:!0,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.bad_record_mac}}),!e.fail},r.write.update=function(e,t){return r.write.compressFunction(e,t,r.write)?r.write.cipherFunction(t,r.write)||e.error(e,{message:"Could not encrypt record.",send:!1,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.internal_error}}):e.error(e,{message:"Could not compress record.",send:!1,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.internal_error}}),!e.fail};if(e.session){var o=e.session.sp;e.session.cipherSuite.initSecurityParameters(o),o.keys=a.generateKeys(e,o),r.read.macKey=t?o.keys.server_write_MAC_key:o.keys.client_write_MAC_key,r.write.macKey=t?o.keys.client_write_MAC_key:o.keys.server_write_MAC_key,e.session.cipherSuite.initConnectionState(r,e,o);switch(o.compression_algorithm){case a.CompressionMethod.none:break;case a.CompressionMethod.deflate:r.read.compressFunction=s,r.write.compressFunction=i;break;default:throw new Error("Unsupported compression algorithm.")}}return r},a.createRandom=function(){var t=new Date,n=+t+t.getTimezoneOffset()*6e4,r=e.util.createBuffer();return r.putInt32(n),r.putBytes(e.random.getBytes(28)),r},a.createRecord=function(e,t){if(!t.data)return null;var n={type:t.type,version:{major:e.version.major,minor:e.version.minor},length:t.data.length(),fragment:t.data};return n},a.createAlert=function(t,n){var r=e.util.createBuffer();return r.putByte(n.level),r.putByte(n.description),a.createRecord(t,{type:a.ContentType.alert,data:r})},a.createClientHello=function(t){t.session.clientHelloVersion={major:t.version.major,minor:t.version.minor};var n=e.util.createBuffer();for(var r=0;r0&&(d+=2);var v=t.session.id,m=v.length+1+2+4+28+2+s+1+f+d,g=e.util.createBuffer();return g.putByte(a.HandshakeType.client_hello),g.putInt24(m),g.putByte(t.version.major),g.putByte(t.version.minor),g.putBytes(t.session.sp.client_random),u(g,1,e.util.createBuffer(v)),u(g,2,n),u(g,1,o),d>0&&u(g,2,l),g},a.createServerHello=function(t){var n=t.session.id,r=n.length+1+2+4+28+2+1,i=e.util.createBuffer();return i.putByte(a.HandshakeType.server_hello),i.putInt24(r),i.putByte(t.version.major),i.putByte(t.version.minor),i.putBytes(t.session.sp.server_random),u(i,1,e.util.createBuffer(n)),i.putByte(t.session.cipherSuite.id[0]),i.putByte(t.session.cipherSuite.id[1]),i.putByte(t.session.compressionMethod),i},a.createCertificate=function(t){var n=t.entity===a.ConnectionEnd.client,r=null;if(t.getCertificate){var i;n?i=t.session.certificateRequest:i=t.session.extensions.server_name.serverNameList,r=t.getCertificate(t,i)}var s=e.util.createBuffer();if(r!==null)try{e.util.isArray(r)||(r=[r]);var o=null;for(var f=0;f0&&(r.putByte(a.HandshakeType.server_key_exchange),r.putInt24(n)),r},a.getClientSignature=function(t,n){var r=e.util.createBuffer();r.putBuffer(t.session.md5.digest()),r.putBuffer(t.session.sha1.digest()),r=r.getBytes(),t.getSignature=t.getSignature||function(t,n,r){var i=null;if(t.getPrivateKey)try{i=t.getPrivateKey(t,t.session.clientCertificate),i=e.pki.privateKeyFromPem(i)}catch(s){t.error(t,{message:"Could not get private key.",cause:s,send:!0,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.internal_error}})}i===null?t.error(t,{message:"No private key set.",send:!0,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.internal_error}}):n=i.sign(n,null),r(t,n)},t.getSignature(t,r,n)},a.createCertificateVerify=function(t,n){var r=n.length+2,i=e.util.createBuffer();return i.putByte(a.HandshakeType.certificate_verify),i.putInt24(r),i.putInt16(n.length),i.putBytes(n),i},a.createCertificateRequest=function(t){var n=e.util.createBuffer();n.putByte(1);var r=e.util.createBuffer();for(var i in t.caStore.certs){var s=t.caStore.certs[i],o=e.pki.distinguishedNameToAsn1(s.subject);r.putBuffer(e.asn1.toDer(o))}var f=1+n.length()+2+r.length(),l=e.util.createBuffer();return l.putByte(a.HandshakeType.certificate_request),l.putInt24(f),u(l,1,n),u(l,2,r),l},a.createServerHelloDone=function(t){var n=e.util.createBuffer();return n.putByte(a.HandshakeType.server_hello_done),n.putInt24(0),n},a.createChangeCipherSpec=function(){var t=e.util.createBuffer();return t.putByte(1),t},a.createFinished=function(n){var r=e.util.createBuffer();r.putBuffer(n.session.md5.digest()),r.putBuffer(n.session.sha1.digest());var i=n.entity===a.ConnectionEnd.client,s=n.session.sp,o=12,u=t,f=i?"client finished":"server finished";r=u(s.master_secret,f,r.getBytes(),o);var l=e.util.createBuffer();return l.putByte(a.HandshakeType.finished),l.putInt24(r.length()),l.putBuffer(r),l},a.createHeartbeat=function(t,n,r){typeof r=="undefined"&&(r=n.length);var i=e.util.createBuffer();i.putByte(t),i.putInt16(r),i.putBytes(n);var s=i.length(),o=Math.max(16,s-r-3);return i.putBytes(e.random.getBytes(o)),i},a.queue=function(t,n){if(!n)return;if(n.type===a.ContentType.handshake){var r=n.fragment.bytes();t.session.md5.update(r),t.session.sha1.update(r),r=null}var i;if(n.fragment.length()<=a.MaxFragment)i=[n];else{i=[];var s=n.fragment.bytes();while(s.length>a.MaxFragment)i.push(a.createRecord(t,{type:n.type,data:e.util.createBuffer(s.slice(0,a.MaxFragment))})),s=s.slice(a.MaxFragment);s.length>0&&i.push(a.createRecord(t,{type:n.type,data:e.util.createBuffer(s)}))}for(var o=0;o0&&(i=r.order[0]);if(i!==null&&i in r.cache){n=r.cache[i],delete r.cache[i];for(var s in r.order)if(r.order[s]===i){r.order.splice(s,1);break}}return n},r.setSession=function(t,n){if(r.order.length===r.capacity){var i=r.order.shift();delete r.cache[i]}var i=e.util.bytesToHex(t);r.order.push(i),r.cache[i]=n}}return r},a.createConnection=function(t){var n=null;t.caStore?e.util.isArray(t.caStore)?n=e.pki.createCaStore(t.caStore):n=t.caStore:n=e.pki.createCaStore();var r=t.cipherSuites||null;if(r===null){r=[];for(var i in a.CipherSuites)r.push(a.CipherSuites[i])}var s=t.server||!1?a.ConnectionEnd.server:a.ConnectionEnd.client,o=t.sessionCache?a.createSessionCache(t.sessionCache):null,u={version:{major:a.Version.major,minor:a.Version.minor},entity:s,sessionId:t.sessionId,caStore:n,sessionCache:o,cipherSuites:r,connected:t.connected,virtualHost:t.virtualHost||null,verifyClient:t.verifyClient||!1,verify:t.verify||function(e,t,n,r){return t},getCertificate:t.getCertificate||null,getPrivateKey:t.getPrivateKey||null,getSignature:t.getSignature||null,input:e.util.createBuffer(),tlsData:e.util.createBuffer(),data:e.util.createBuffer(),tlsDataReady:t.tlsDataReady,dataReady:t.dataReady,heartbeatReceived:t.heartbeatReceived,closed:t.closed,error:function(e,n){n.origin=n.origin||(e.entity===a.ConnectionEnd.client?"client":"server"),n.send&&(a.queue(e,a.createAlert(e,n.alert)),a.flush(e));var r=n.fatal!==!1;r&&(e.fail=!0),t.error(e,n),r&&e.close(!1)},deflate:t.deflate||null,inflate:t.inflate||null};u.reset=function(e){u.version={major:a.Version.major,minor:a.Version.minor},u.record=null,u.session=null,u.peerCertificate=null,u.state={pending:null,current:null},u.expect=u.entity===a.ConnectionEnd.client?f:y,u.fragmented=null,u.records=[],u.open=!1,u.handshakes=0,u.handshaking=!1,u.isConnected=!1,u.fail=!e&&typeof e!="undefined",u.input.clear(),u.tlsData.clear(),u.data.clear(),u.state.current=a.createConnectionState(u)},u.reset();var l=function(e,t){var n=t.type-a.ContentType.change_cipher_spec,r=_[e.entity][e.expect];n in r?r[n](e,t):a.handleUnexpected(e,t)},c=function(t){var n=0,r=t.input,i=r.length();if(i<5)n=5-i;else{t.record={type:r.getByte(),version:{major:r.getByte(),minor:r.getByte()},length:r.getInt16(),fragment:e.util.createBuffer(),ready:!1};var s=t.record.version.major===t.version.major;s&&t.session&&t.session.version&&(s=t.record.version.minor===t.version.minor),s||t.error(t,{message:"Incompatible TLS version.",send:!0,alert:{level:a.Alert.Level.fatal,description:a.Alert.Description.protocol_version}})}return n},h=function(e){var t=0,n=e.input,r=n.length();if(r0&&(u.sessionCache&&(n=u.sessionCache.getSession(t)),n===null&&(t="")),t.length===0&&u.sessionCache&&(n=u.sessionCache.getSession(),n!==null&&(t=n.id)),u.session={id:t,version:null,cipherSuite:null,compressionMethod:null,serverCertificate:null,certificateRequest:null,clientCertificate:null,sp:{},md5:e.md.md5.create(),sha1:e.md.sha1.create()},n&&(u.version=n.version,u.session.sp=n.sp),u.session.sp.client_random=a.createRandom().getBytes(),u.open=!0,a.queue(u,a.createRecord(u,{type:a.ContentType.handshake,data:a.createClientHello(u)})),a.flush(u)}},u.process=function(e){var t=0;return e&&u.input.putBytes(e),u.fail||(u.record!==null&&u.record.ready&&u.record.fragment.isEmpty()&&(u.record=null),u.record===null&&(t=c(u)),!u.fail&&u.record!==null&&!u.record.ready&&(t=h(u)),!u.fail&&u.record!==null&&u.record.ready&&l(u,u.record)),t},u.prepare=function(t){return a.queue(u,a.createRecord(u,{type:a.ContentType.application_data,data:e.util.createBuffer(t)})),a.flush(u)},u.prepareHeartbeatRequest=function(t,n){return t instanceof e.util.ByteBuffer&&(t=t.bytes()),typeof n=="undefined"&&(n=t.length),u.expectedHeartbeatPayload=t,a.queue(u,a.createRecord(u,{type:a.ContentType.heartbeat,data:a.createHeartbeat(a.HeartbeatMessageType.heartbeat_request,t,n)})),a.flush(u)},u.close=function(e){if(!u.fail&&u.sessionCache&&u.session){var t={id:u.session.id,version:u.session.version,sp:u.session.sp};t.sp.keys=null,u.sessionCache.setSession(t.id,t)}if(u.open){u.open=!1,u.input.clear();if(u.isConnected||u.handshaking)u.isConnected=u.handshaking=!1,a.queue(u,a.createAlert(u,{level:a.Alert.Level.warning,description:a.Alert.Description.close_notify})),a.flush(u);u.closed(u)}u.reset(e)},u},e.tls=e.tls||{};for(var V in a)typeof a[V]!="function"&&(e.tls[V]=a[V]);e.tls.prf_tls1=t,e.tls.hmac_sha1=r,e.tls.createSessionCache=a.createSessionCache,e.tls.createConnection=a.createConnection}var r="tls";if(typeof n!="function"){if(typeof module!="object"||!module.exports)return typeof forge=="undefined"&&(forge={}),e(forge);var i=!0;n=function(e,n){n(t,module)}}var s,o=function(t,n){n.exports=function(n){var i=s.map(function(e){return t(e)}).concat(e);n=n||{},n.defined=n.defined||{};if(n.defined[r])return n[r];n.defined[r]=!0;for(var o=0;o=t.Versions.TLS_1_1.minor&&a.output.putBytes(u),a.update(n.fragment),a.finish(i)&&(n.fragment=a.output,n.length=n.fragment.length(),s=!0),s}function i(e,t,n){if(!n){var r=e-t.length()%e;t.fillWithByte(r-1,r)}return!0}function s(e,t,n){var r=!0;if(n){var i=t.length(),s=t.last();for(var o=i-1-s;o=f?(n.fragment=a.output.getBytes(h-f),l=a.output.getBytes(f)):n.fragment=a.output.getBytes(),n.fragment=e.util.createBuffer(n.fragment),n.length=n.fragment.length();var p=r.macFunction(r.macKey,r.sequenceNumber,n);return r.updateSequenceNumber(),i=p===l&&i,i}var t=e.tls;t.CipherSuites.TLS_RSA_WITH_AES_128_CBC_SHA={id:[0,47],name:"TLS_RSA_WITH_AES_128_CBC_SHA",initSecurityParameters:function(e){e.bulk_cipher_algorithm=t.BulkCipherAlgorithm.aes,e.cipher_type=t.CipherType.block,e.enc_key_length=16,e.block_length=16,e.fixed_iv_length=16,e.record_iv_length=16,e.mac_algorithm=t.MACAlgorithm.hmac_sha1,e.mac_length=20,e.mac_key_length=20},initConnectionState:n},t.CipherSuites.TLS_RSA_WITH_AES_256_CBC_SHA={id:[0,53],name:"TLS_RSA_WITH_AES_256_CBC_SHA",initSecurityParameters:function(e){e.bulk_cipher_algorithm=t.BulkCipherAlgorithm.aes,e.cipher_type=t.CipherType.block,e.enc_key_length=32,e.block_length=16,e.fixed_iv_length=16,e.record_iv_length=16,e.mac_algorithm=t.MACAlgorithm.hmac_sha1,e.mac_length=20,e.mac_key_length=20},initConnectionState:n};var o=0}var r="aesCipherSuites";if(typeof n!="function"){if(typeof module!="object"||!module.exports)return typeof forge=="undefined"&&(forge={}),e(forge);var i=!0;n=function(e,n){n(t,module)}}var s,o=function(t,n){n.exports=function(n){var i=s.map(function(e){return t(e)}).concat(e);n=n||{},n.defined=n.defined||{};if(n.defined[r])return n[r];n.defined[r]=!0;for(var o=0;o0&&(u=e.util.fillString(String.fromCharCode(0),a)+u);var f=r.encrypt(u,"NONE"),l=n.generate(u,s);return{encapsulation:f,key:l}},s.decrypt=function(e,t,r){var i=e.decrypt(t,"NONE");return n.generate(i,r)},s},e.kem.kdf1=function(e,t){n(this,e,0,t||e.digestLength)},e.kem.kdf2=function(e,t){n(this,e,1,t||e.digestLength)}}var r="kem";if(typeof n!="function"){if(typeof module!="object"||!module.exports)return typeof forge=="undefined"&&(forge={}),e(forge);var i=!0;n=function(e,n){n(t,module)}}var s,o=function(t,n){n.exports=function(n){var i=s.map(function(e){return t(e)}).concat(e);n=n||{},n.defined=n.defined||{};if(n.defined[r])return n[r];n.defined[r]=!0;for(var o=0;o="8"&&(r="00"+r);var i=e.util.hexToBytes(r);t.putInt32(i.length),t.putBytes(i)}function r(e,t){e.putInt32(t.length),e.putString(t)}function i(){var t=e.md.sha1.create(),n=arguments.length;for(var r=0;r=1&&e.log.verbose(t,"[%s][%s] init",this.id,this.name,this)};T.prototype.debug=function(n){n=n||"",e.log.debug(t,n,"[%s][%s] task:",this.id,this.name,this,"subtasks:",this.subtasks.length,"queue:",s)},T.prototype.next=function(e,t){typeof e=="function"&&(t=e,e=this.name);var n=new T({run:t,name:e,parent:this});return n.state=l,n.type=this.type,n.successCallback=this.successCallback||null,n.failureCallback=this.failureCallback||null,this.subtasks.push(n),this},T.prototype.parallel=function(t,n){return e.util.isArray(t)&&(n=t,t=this.name),this.next(t,function(r){var i=r;i.block(n.length);var s=function(t,r){e.task.start({type:t,run:function(e){n[r](e)},success:function(e){i.unblock()},failure:function(e){i.unblock()}})};for(var o=0;o0&&(this.state=x[this.state][g])},T.prototype.unblock=function(e){return e=typeof e=="undefined"?1:e,this.blocks-=e,this.blocks===0&&this.state!==p&&(this.state=l,C(this,0)),this.blocks},T.prototype.sleep=function(e){e=typeof e=="undefined"?0:e,this.state=x[this.state][b];var t=this;this.timeoutId=setTimeout(function(){t.timeoutId=null,t.state=l,C(t,0)},e)},T.prototype.wait=function(e){e.wait(this)},T.prototype.wakeup=function(){this.state===h&&(cancelTimeout(this.timeoutId),this.timeoutId=null,this.state=l,C(this,0))},T.prototype.cancel=function(){this.state=x[this.state][E],this.permitsNeeded=0,this.timeoutId!==null&&(cancelTimeout(this.timeoutId),this.timeoutId=null),this.subtasks=[]},T.prototype.fail=function(e){this.error=!0,k(this,!0);if(e)e.error=this.error,e.swapTime=this.swapTime,e.userData=this.userData,C(e,0);else{if(this.parent!==null){var t=this.parent;while(t.parent!==null)t.error=this.error,t.swapTime=this.swapTime,t.userData=this.userData,t=t.parent;k(t,!0)}this.failureCallback&&this.failureCallback(this)}};var N=function(e){e.error=!1,e.state=x[e.state][m],setTimeout(function(){e.state===l&&(e.swapTime=+(new Date),e.run(e),C(e,0))},0)},C=function(e,t){var n=t>u||+(new Date)-e.swapTime>a,r=function(t){t++;if(e.state===l){n&&(e.swapTime=+(new Date));if(e.subtasks.length>0){var r=e.subtasks.shift();r.error=e.error,r.swapTime=e.swapTime,r.userData=e.userData,r.run(r),r.error||C(r,t)}else k(e),e.error||e.parent!==null&&(e.parent.error=e.error,e.parent.swapTime=e.swapTime,e.parent.userData=e.userData,C(e.parent,t))}};n?setTimeout(r,0):r(t)},k=function(i,o){i.state=p,delete r[i.id],n>=1&&e.log.verbose(t,"[%s][%s] finish",i.id,i.name,i),i.parent===null&&(i.type in s?s[i.type].length===0?e.log.error(t,"[%s][%s] task queue empty [%s]",i.id,i.name,i.type):s[i.type][0]!==i?e.log.error(t,"[%s][%s] task not first in queue [%s]",i.id,i.name,i.type):(s[i.type].shift(),s[i.type].length===0?(n>=1&&e.log.verbose(t,"[%s][%s] delete queue [%s]",i.id,i.name,i.type),delete s[i.type]):(n>=1&&e.log.verbose(t,"[%s][%s] queue start next [%s] remain:%s",i.id,i.name,i.type,s[i.type].length),s[i.type][0].start())):e.log.error(t,"[%s][%s] task queue missing [%s]",i.id,i.name,i.type),o||(i.error&&i.failureCallback?i.failureCallback(i):!i.error&&i.successCallback&&i.successCallback(i)))};e.task=e.task||{},e.task.start=function(r){var i=new T({run:r.run,name:r.name||o});i.type=r.type,i.successCallback=r.success||null,i.failureCallback=r.failure||null,i.type in s?s[r.type].push(i):(n>=1&&e.log.verbose(t,"[%s][%s] create queue [%s]",i.id,i.name,i.type),s[i.type]=[i],N(i))},e.task.cancel=function(e){e in s&&(s[e]=[s[e][0]])},e.task.createCondition=function(){var e={tasks:{}};return e.wait=function(t){t.id in e.tasks||(t.block(),e.tasks[t.id]=t)},e.notify=function(){var t=e.tasks;e.tasks={};for(var n in t)t[n].unblock()},e}}var r="task";if(typeof n!="function"){if(typeof module!="object"||!module.exports)return typeof forge=="undefined"&&(forge={}),e(forge);var i=!0;n=function(e,n){n(t,module)}}var s,o=function(t,n){n.exports=function(n){var i=s.map(function(e){return t(e)}).concat(e);n=n||{},n.defined=n.defined||{};if(n.defined[r])return n[r];n.defined[r]=!0;for(var o=0;o Date: Tue, 14 Oct 2014 13:12:59 +0200 Subject: [PATCH 22/27] [WO-567] ignore buffer in tests --- Gruntfile.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index ae55133..80aa375 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -177,7 +177,7 @@ module.exports = function(grunt) { }, options: { exclude: browserifyOpt.exclude, - //ignore: ['buffer'], // comment in after browsercrow and browsersmtp use current npm deps + ignore: ['buffer'], browserifyOptions: browserifyOpt.browserifyOptions } } @@ -493,4 +493,4 @@ module.exports = function(grunt) { grunt.registerTask('release-stable', ['dist', 'manifest-stable', 'compress']); grunt.registerTask('default', ['release-dev']); -}; \ No newline at end of file +}; From c6c058c2dc96e7797254f09cddfc1cfc96bc4f3f Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Tue, 14 Oct 2014 13:21:59 +0200 Subject: [PATCH 23/27] Use same browserify options for each build --- Gruntfile.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 80aa375..58f6223 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -175,11 +175,7 @@ module.exports = function(grunt) { 'test/main.js' ] }, - options: { - exclude: browserifyOpt.exclude, - ignore: ['buffer'], - browserifyOptions: browserifyOpt.browserifyOptions - } + options: browserifyOpt } }, @@ -493,4 +489,4 @@ module.exports = function(grunt) { grunt.registerTask('release-stable', ['dist', 'manifest-stable', 'compress']); grunt.registerTask('default', ['release-dev']); -}; +}; \ No newline at end of file From 7be6c63060240dab767e13a384fae5356b067ed2 Mon Sep 17 00:00:00 2001 From: Felix Hammerl Date: Tue, 14 Oct 2014 15:53:09 +0200 Subject: [PATCH 24/27] Use worker thread in connection doctor --- src/js/util/connection-doctor.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/js/util/connection-doctor.js b/src/js/util/connection-doctor.js index d6e3259..4556f4c 100644 --- a/src/js/util/connection-doctor.js +++ b/src/js/util/connection-doctor.js @@ -52,6 +52,7 @@ ConnectionDoctor.prototype.configure = function(credentials) { secure: this.credentials.imap.secure, ignoreTLS: this.credentials.imap.ignoreTLS, ca: this.credentials.imap.ca, + tlsWorkerPath: cfg.workerPath + '/tcp-socket-tls-worker.js', auth: { user: this.credentials.username, pass: this.credentials.password, @@ -63,6 +64,7 @@ ConnectionDoctor.prototype.configure = function(credentials) { useSecureTransport: this.credentials.smtp.secure, ignoreTLS: this.credentials.smtp.ignoreTLS, ca: this.credentials.smtp.ca, + tlsWorkerPath: cfg.workerPath + '/tcp-socket-tls-worker.js', auth: { user: this.credentials.username, pass: this.credentials.password, @@ -156,7 +158,8 @@ ConnectionDoctor.prototype._checkReachable = function(options, callback) { socket = TCPSocket.open(options.host, options.port, { binaryType: 'arraybuffer', useSecureTransport: options.secure, - ca: options.ca + ca: options.ca, + tlsWorkerPath: cfg.workerPath + '/tcp-socket-tls-worker.js' }); socket.ondata = function() {}; // we don't actually care about the data @@ -290,4 +293,4 @@ function createError(code, message, underlyingError) { return error; } -module.exports = ConnectionDoctor; \ No newline at end of file +module.exports = ConnectionDoctor; From 3a6e0463fb35eec40f49d28ec7eefb927dac18e2 Mon Sep 17 00:00:00 2001 From: Felix Hammerl Date: Wed, 15 Oct 2014 19:58:26 +0200 Subject: [PATCH 25/27] [WO-567] fix TLS worker handling --- Gruntfile.js | 24 ++++++++++++++++-------- package.json | 4 ++-- src/js/app-controller.js | 4 ++-- src/js/util/connection-doctor.js | 8 +++++--- test/unit/connection-doctor-test.js | 14 +++++++++----- 5 files changed, 34 insertions(+), 20 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 58f6223..9e66d74 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -126,6 +126,12 @@ module.exports = function(grunt) { }, options: browserifyOpt }, + tlsWorker: { + files: { + 'dist/js/tcp-socket-tls-worker.browserified.js': ['node_modules/tcp-socket/src/tcp-socket-tls-worker.js'] + }, + options: browserifyOpt + }, unitTest: { files: { 'test/unit/index.browserified.js': [ @@ -249,6 +255,15 @@ module.exports = function(grunt) { sourceMapName: 'dist/js/mailreader-parser-worker.min.js.map' } }, + tlsWorker: { + files: { + 'dist/js/tcp-socket-tls-worker.min.js': ['dist/js/tcp-socket-tls-worker.browserified.js'] + }, + options: { + sourceMap: true, + sourceMapName: 'dist/js/tcp-socket-tls-worker.min.js.map' + } + }, unitTest: { files: { 'test/unit/index.js': [ @@ -321,13 +336,6 @@ module.exports = function(grunt) { src: ['openpgp/openpgp.js', 'openpgp/openpgp.worker.js', 'forge/forge.min.js'], dest: 'dist/js/' }, - tls: { - expand: true, - flatten: true, - cwd: 'node_modules/tcp-socket/src/', - src: ['tcp-socket-tls-worker.js', 'tcp-socket-tls.js'], - dest: 'dist/js/' - }, font: { expand: true, cwd: 'src/font/', @@ -489,4 +497,4 @@ module.exports = function(grunt) { grunt.registerTask('release-stable', ['dist', 'manifest-stable', 'compress']); grunt.registerTask('default', ['release-dev']); -}; \ No newline at end of file +}; diff --git a/package.json b/package.json index bae2b3c..a1c7156 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "pgpbuilder": "~0.4.0", "pgpmailer": "~0.4.0", "socket.io": "^1.0.6", - "tcp-socket": "https://github.com/whiteout-io/tcp-socket/tarball/dev/WO-567", + "tcp-socket": "~0.3.13", "wo-smtpclient": "^0.3.8" }, "devDependencies": { @@ -70,4 +70,4 @@ "sinon": "~1.7.3", "time-grunt": "^1.0.0" } -} \ No newline at end of file +} diff --git a/src/js/app-controller.js b/src/js/app-controller.js index e768105..96bf770 100644 --- a/src/js/app-controller.js +++ b/src/js/app-controller.js @@ -227,7 +227,7 @@ ctrl.onConnect = function(callback) { 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.js'; + credentials.imap.tlsWorkerPath = credentials.smtp.tlsWorkerPath = config.workerPath + '/tcp-socket-tls-worker.min.js'; var pgpMailer = new PgpMailer(credentials.smtp, ctrl._pgpbuilder); var imapClient = new ImapClient(credentials.imap); @@ -268,4 +268,4 @@ ctrl.onConnect = function(callback) { } }; -module.exports = ctrl; \ No newline at end of file +module.exports = ctrl; diff --git a/src/js/util/connection-doctor.js b/src/js/util/connection-doctor.js index 4556f4c..eeeccb5 100644 --- a/src/js/util/connection-doctor.js +++ b/src/js/util/connection-doctor.js @@ -30,6 +30,8 @@ var NO_INBOX = ConnectionDoctor.NO_INBOX = 47; var GENERIC_ERROR = ConnectionDoctor.GENERIC_ERROR = 48; +var WORKER_PATH = cfg.workerPath + '/tcp-socket-tls-worker.min.js'; + // // Public API // @@ -52,7 +54,7 @@ ConnectionDoctor.prototype.configure = function(credentials) { secure: this.credentials.imap.secure, ignoreTLS: this.credentials.imap.ignoreTLS, ca: this.credentials.imap.ca, - tlsWorkerPath: cfg.workerPath + '/tcp-socket-tls-worker.js', + tlsWorkerPath: WORKER_PATH, auth: { user: this.credentials.username, pass: this.credentials.password, @@ -64,7 +66,7 @@ ConnectionDoctor.prototype.configure = function(credentials) { useSecureTransport: this.credentials.smtp.secure, ignoreTLS: this.credentials.smtp.ignoreTLS, ca: this.credentials.smtp.ca, - tlsWorkerPath: cfg.workerPath + '/tcp-socket-tls-worker.js', + tlsWorkerPath: WORKER_PATH, auth: { user: this.credentials.username, pass: this.credentials.password, @@ -159,7 +161,7 @@ ConnectionDoctor.prototype._checkReachable = function(options, callback) { binaryType: 'arraybuffer', useSecureTransport: options.secure, ca: options.ca, - tlsWorkerPath: cfg.workerPath + '/tcp-socket-tls-worker.js' + tlsWorkerPath: WORKER_PATH }); socket.ondata = function() {}; // we don't actually care about the data diff --git a/test/unit/connection-doctor-test.js b/test/unit/connection-doctor-test.js index 7a50499..8a431d5 100644 --- a/test/unit/connection-doctor-test.js +++ b/test/unit/connection-doctor-test.js @@ -8,7 +8,7 @@ var TCPSocket = require('tcp-socket'), describe('Connection Doctor', function() { var doctor; - var socketStub, imapStub, smtpStub, credentials; + var socketStub, imapStub, smtpStub, credentials, workerPath; beforeEach(function() { // @@ -22,6 +22,7 @@ describe('Connection Doctor', function() { } }; + workerPath = 'js/tcp-socket-tls-worker.min.js'; imapStub = sinon.createStubInstance(ImapClient); smtpStub = sinon.createStubInstance(SmtpClient); @@ -84,7 +85,8 @@ describe('Connection Doctor', function() { expect(TCPSocket.open.calledWith(credentials.imap.host, credentials.imap.port, { binaryType: 'arraybuffer', useSecureTransport: credentials.imap.secure, - ca: credentials.imap.ca + ca: credentials.imap.ca, + tlsWorkerPath: workerPath })).to.be.true; done(); @@ -108,7 +110,8 @@ describe('Connection Doctor', function() { expect(TCPSocket.open.calledWith(credentials.imap.host, credentials.imap.port, { binaryType: 'arraybuffer', useSecureTransport: credentials.imap.secure, - ca: credentials.imap.ca + ca: credentials.imap.ca, + tlsWorkerPath: workerPath })).to.be.true; done(); @@ -125,7 +128,8 @@ describe('Connection Doctor', function() { expect(TCPSocket.open.calledWith(credentials.imap.host, credentials.imap.port, { binaryType: 'arraybuffer', useSecureTransport: credentials.imap.secure, - ca: credentials.imap.ca + ca: credentials.imap.ca, + tlsWorkerPath: workerPath })).to.be.true; done(); @@ -408,4 +412,4 @@ describe('Connection Doctor', function() { }); }); }); -}); \ No newline at end of file +}); From 9723d6f0cde5d59782fa91188a55a3631fe4e0df Mon Sep 17 00:00:00 2001 From: Felix Hammerl Date: Thu, 16 Oct 2014 09:48:22 +0200 Subject: [PATCH 26/27] [WO-567] use mailreader v0.4.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a1c7156..9d8ca7f 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "express": "^4.8.3", "imap-client": "~0.4.3", "jquery": "~2.1.1", - "mailreader": "https://github.com/whiteout-io/mailreader/tarball/dev/WO-567", + "mailreader": "~0.4.0", "morgan": "^1.2.3", "ng-infinite-scroll": "~1.1.2", "npmlog": "^0.1.1", From d971969b5faa152690a92382bc374ba704993151 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Thu, 16 Oct 2014 12:02:22 +0200 Subject: [PATCH 27/27] Stub GET requests to key server in integration test --- test/integration/email-dao-test.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/integration/email-dao-test.js b/test/integration/email-dao-test.js index 8694fc8..b59b73a 100644 --- a/test/integration/email-dao-test.js +++ b/test/integration/email-dao-test.js @@ -269,6 +269,10 @@ describe('Email DAO integration tests', function() { }, function(err) { expect(err).to.not.exist; + // stub rest request to key server + sinon.stub(appController._emailDao._keychain._publicKeyDao, 'get').yields(null, mockKeyPair.publicKey); + sinon.stub(appController._emailDao._keychain._publicKeyDao, 'getByUserId').yields(null, mockKeyPair.publicKey); + userStorage = appController._userStorage; appController.init({