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 | 467 ++- src/js/app.js | 228 +- src/js/bo/auth.js | 690 ++-- src/js/bo/outbox.js | 381 ++- src/js/controller/about.js | 44 +- src/js/controller/account.js | 102 +- src/js/controller/add-account.js | 194 +- src/js/controller/contacts.js | 226 +- src/js/controller/dialog.js | 22 +- src/js/controller/login-existing.js | 108 +- src/js/controller/login-initial.js | 196 +- src/js/controller/login-new-device.js | 215 +- .../controller/login-privatekey-download.js | 190 +- src/js/controller/login-set-credentials.js | 218 +- src/js/controller/login.js | 156 +- src/js/controller/mail-list.js | 1000 +++--- src/js/controller/navigation.js | 366 +- src/js/controller/popover.js | 86 +- src/js/controller/privatekey-upload.js | 325 +- src/js/controller/read-sandbox.js | 347 +- src/js/controller/read.js | 471 ++- src/js/controller/set-passphrase.js | 230 +- src/js/controller/write.js | 898 +++-- src/js/crypto/crypto.js | 197 +- src/js/crypto/pbkdf2-worker.js | 51 +- src/js/crypto/pbkdf2.js | 35 +- src/js/crypto/pgp.js | 752 +++-- src/js/dao/admin-dao.js | 112 +- src/js/dao/devicestorage-dao.js | 178 +- src/js/dao/email-dao.js | 2938 ++++++++--------- src/js/dao/invitation-dao.js | 112 +- src/js/dao/keychain-dao.js | 1444 ++++---- src/js/dao/lawnchair-dao.js | 340 +- src/js/dao/privatekey-dao.js | 370 ++- src/js/dao/publickey-dao.js | 208 +- src/js/dao/rest-dao.js | 228 +- src/js/util/backbutton-handler.js | 98 +- src/js/util/connection-doctor.js | 502 ++- src/js/util/download.js | 104 +- src/js/util/error.js | 54 +- src/js/util/notification.js | 104 +- src/js/util/oauth.js | 160 +- src/js/util/update/update-handler.js | 234 +- 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 | 130 +- src/js/util/update/update-v5.js | 92 +- src/lib/uuid/uuid.js | 245 -- src/package.json | 10 - src/require-config.js | 68 - 57 files changed, 8069 insertions(+), 8598 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 - }; - - /** - * 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}' - }; + 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 +}; - 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; - - var self = {}; - - /** - * Start the application. - */ - self.start = function(options, callback) { - if (self.started) { - return callback(); - } - self.started = true; - self.onError = options.onError; - - // 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(); - } +'use strict'; + +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; + +var self = {}; + +/** + * Start the application. + */ +self.start = function(options, callback) { + if (self.started) { + return callback(); + } + + self.started = true; + self.onError = options.onError; + + // 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(); + } + + function onDeviceReady() { + axe.debug('Starting app.'); + + self.buildModules(); + + // 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); + } +}; - function onDeviceReady() { - axe.debug('Starting app.'); +/** + * 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 + }); + }; - self.buildModules(); + 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(); - // Handle offline and online gracefully - window.addEventListener('online', self.onConnect.bind(self, self.onError)); - window.addEventListener('offline', self.onDisconnect.bind(self)); + emailDao.onError = self.onError; +}; - self._appConfigStore.init('app-config', callback); +/** + * 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) { + if (err) { + callback(err); + return; } - }; - /** - * 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 - }); - }; + // Migrate the databases if necessary + self._updateHandler.update(onUpdate); + }); - 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; - }; + function onUpdate(err) { + if (err) { + callback({ + errMsg: 'Update failed, please reinstall the app.', + err: err + }); + return; + } - /** - * Calls runtime hooks to check if an app update is available. - */ - self.checkForUpdate = function() { - self._updateHandler.checkForUpdate(self.onError); - }; + // account information for the email dao + var account = { + realname: options.realname, + emailAddress: options.emailAddress, + asymKeySize: config.asymKeySize + }; - /** - * 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 - }; - - // init email dao - self._emailDao.init({ - account: account - }, function(err, keypair) { - if (err) { - callback(err); - return; - } +/** + * Event handler that is called when the user agent goes offline. + */ +self.onDisconnect = function() { + self._emailDao.onDisconnect(); +}; - 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); - }); - - 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; - - // 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); - - // connect to clients - self._emailDao.onConnect({ - imapClient: imapClient, - pgpMailer: pgpMailer - }, callback); - } + initClients(credentials); + }); + + 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; + + // 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); + + // 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) : '')); + + 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; + } - function onConnectionError(error) { - axe.debug('Connection error. Attempting reconnect in ' + config.reconnectInterval + ' ms. Error: ' + (error.errMsg || error.message) + (error.stack ? ('\n' + error.stack) : '')); - - setTimeout(function() { - axe.debug('Reconnecting...'); - // re-init client modules on error - self.onConnect(function(err) { - if (err) { - axe.error('Reconnect attempt failed! ' + (err.errMsg || err.message) + (err.stack ? ('\n' + err.stack) : '')); - return; - } - - axe.debug('Reconnect attempt complete.'); - }); - }, config.reconnectInterval); - } - }; + 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 }); + $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' + }); +}); - app.run(function($rootScope) { - // global state... inherited to all child scopes - $rootScope.state = {}; - - // attach global error handler - errorUtil.attachHandler($rootScope); +app.run(function($rootScope) { + // global state... inherited to all child scopes + $rootScope.state = {}; - // attach the back button handler to the root scope - backButtonUtil.attachHandler($rootScope); + // attach global error handler + errorUtil.attachHandler($rootScope); - // attach fastclick - FastClick.attach(document.body); - }); + // attach the back button handler to the root scope + backButtonUtil.attachHandler($rootScope); - // 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); + // attach fastclick + FastClick.attach(document.body); +}); - // manually bootstrap angular due to require.js - angular.element().ready(function() { - angular.bootstrap(document, ['mail']); - }); -}); \ No newline at end of file +// 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,213 +1,211 @@ -define(function(require) { - 'use strict'; - - var axe = require('axe'), - str = require('js/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'; - - /** - * 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) { +'use strict'; + +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'; + +/** + * 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); } - chooseLogin(); + done(); }); 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; + if (self.passwordNeedsDecryption) { + // decrypt password + self._pgp.decrypt(self.password, undefined, function(err, cleartext) { + if (err) { + return callback(err); + } - done(); - }); - return; - } + self.passwordNeedsDecryption = false; + self.password = cleartext; - done(); + done(); + }); + return; } - 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 - } + 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 } - }; - - 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(); + }, + 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); } - // persist the provider - self._appConfigStore.storeList([self.smtp], SMTP_DB_KEY, function(err) { + self._appConfigStore.storeList([self.imap], IMAP_DB_KEY, function(err) { if (err) { return callback(err); } - self._appConfigStore.storeList([self.imap], IMAP_DB_KEY, function(err) { + self._appConfigStore.storeList([self.provider], PROVIDER_DB_KEY, function(err) { if (err) { return callback(err); } - self._appConfigStore.storeList([self.provider], PROVIDER_DB_KEY, function(err) { + self._appConfigStore.storeList([self.emailAddress], EMAIL_ADDR_DB_KEY, function(err) { if (err) { return callback(err); } - self._appConfigStore.storeList([self.emailAddress], EMAIL_ADDR_DB_KEY, function(err) { + self._appConfigStore.storeList([self.username], USERNAME_DB_KEY, function(err) { if (err) { return callback(err); } - self._appConfigStore.storeList([self.username], USERNAME_DB_KEY, function(err) { + self._appConfigStore.storeList([self.realname], REALNAME_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); + } - if (!self.password) { self.credentialsDirty = false; - return callback(); - } + callback(); + }); + return; + } - 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._pgp.encrypt(self.password, undefined, function(err, ciphertext) { + self._appConfigStore.storeList([ciphertext], PASSWD_DB_KEY, function(err) { if (err) { return callback(err); } - self._appConfigStore.storeList([ciphertext], PASSWD_DB_KEY, function(err) { - if (err) { - return callback(err); - } - - self.credentialsDirty = false; - callback(); - }); + self.credentialsDirty = false; + callback(); }); }); }); @@ -215,239 +213,239 @@ define(function(require) { }); }); }); - }; - - /** - * Returns the email address. Loads it from disk, if necessary - */ - Auth.prototype.getEmailAddress = function(callback) { - var self = this; + }); +}; + +/** + * 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 + }); + } - if (self.emailAddress) { - return callback(null, { - emailAddress: self.emailAddress, - realname: self.realname - }); + self._loadCredentials(function(err) { + if (err) { + return callback(err); } - self._loadCredentials(function(err) { - if (err) { - return callback(err); - } - - callback(null, { - emailAddress: self.emailAddress, - realname: self.realname - }); + 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); + }); +}; + +/** + * 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); } - function onToken(err, oauthToken) { + // 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) { 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) { - 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) { + 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(SMTP_DB_KEY, function(err, smtp) { + + loadFromDB(IMAP_DB_KEY, function(err, imap) { if (err) { return callback(err); } - loadFromDB(IMAP_DB_KEY, function(err, imap) { + loadFromDB(USERNAME_DB_KEY, function(err, username) { if (err) { return callback(err); } - loadFromDB(USERNAME_DB_KEY, function(err, username) { + loadFromDB(REALNAME_DB_KEY, function(err, realname) { if (err) { return callback(err); } - loadFromDB(REALNAME_DB_KEY, function(err, realname) { + loadFromDB(EMAIL_ADDR_DB_KEY, function(err, emailAddress) { if (err) { return callback(err); } - - loadFromDB(EMAIL_ADDR_DB_KEY, function(err, emailAddress) { + loadFromDB(PASSWD_DB_KEY, function(err, password) { if (err) { return callback(err); } - loadFromDB(PASSWD_DB_KEY, function(err, password) { + loadFromDB(PROVIDER_DB_KEY, function(err, provider) { 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(); - }); + 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(); }); }); }); }); }); }); + }); - function loadFromDB(key, callback) { - self._appConfigStore.listItems(key, 0, null, function(err, cachedItems) { - callback(err, (!err && cachedItems && cachedItems[0])); - }); - } - }; - - /** - * 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); + function loadFromDB(key, callback) { + self._appConfigStore.listItems(key, 0, null, function(err, cachedItems) { + callback(err, (!err && cachedItems && cachedItems[0])); + }); + } +}; + +/** + * 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; + } - 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; - } + self.storeCredentials(function(err) { + if (err) { + callback(err); + 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, + onConnect(callback); }); + } + }); +}; + +/** + * 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; } - // 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; - } - - onConnect(callback); - }); - } - }); - }; - - /** - * 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(); - }); - }; + callback(); + }); +}; - return Auth; -}); \ No newline at end of file +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; - - /** - * Semaphore-esque flag to avoid 'concurrent' calls to _processOutbox when the timeout fires, but a call is still in process. - * @private */ - this._outboxBusy = false; - }; +/** + * 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; - /** - * 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._keychain = keychain; - /** - * Outbox stops the periodic checking of the local device storage for pending mails. - */ - OutboxBO.prototype.stopChecking = function() { - if (!this._intervalId) { - return; - } - - clearInterval(this._intervalId); - delete this._intervalId; - }; + /** @private */ + this._devicestorage = devicestorage; /** - * 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 - - mail.publicKeysArmored = []; // gather the public keys - mail.uid = mail.id = util.UUID(); // the mail needs a random id & uid for storage in the database - - // do not encrypt mails with a bcc recipient, due to a possible privacy leak - if (mail.bcc.length > 0) { - 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); - 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; +}; + +/** + * 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); +}; + +/** + * Outbox stops the periodic checking of the local device storage for pending mails. + */ +OutboxBO.prototype.stopChecking = function() { + if (!this._intervalId) { + return; + } + + clearInterval(this._intervalId); + delete this._intervalId; +}; + +/** + * 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 + + mail.publicKeysArmored = []; // gather the public keys + mail.uid = mail.id = util.UUID(); // the mail needs a random id & uid for storage in the database + + // do not encrypt mails with a bcc recipient, due to a possible privacy leak + if (mail.bcc.length > 0) { + storeAndForward(mail); + return; + } + + checkRecipients(allReaders); + + // check if there are unregistered recipients + function checkRecipients(recipients) { + var after = _.after(recipients.length, function() { + checkEncrypt(); + }); - // encrypts the body and attachments and persists the mail object - self._emailDao.encrypt({ - mail: mail, - publicKeysArmored: mail.publicKeysArmored - }, function(err) { + // find out if there are unregistered users + recipients.forEach(function(recipient) { + self._keychain.getReceiverPublicKey(recipient.address, function(err, key) { if (err) { callback(err); return; } - storeAndForward(mail); - }); - } - - function storeAndForward(mail) { - // store in outbox - self._devicestorage.storeList([mail], outboxDb, function(err) { - 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); } - callback(); - // don't wait for next round - self._processOutbox(self._onUpdate); + after(); }); - } - }; + }); + } - /** - * 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) { + function checkEncrypt() { + // only encrypt if all recipients have public keys + if (mail.publicKeysArmored.length < allReaders.length) { + storeAndForward(mail); return; } - self._outboxBusy = true; - - // get pending mails from the outbox - self._devicestorage.listItems(outboxDb, 0, null, function(err, pendingMails) { - // error, we're done here + // encrypts the body and attachments and persists the mail object + self._emailDao.encrypt({ + mail: mail, + publicKeysArmored: mail.publicKeysArmored + }, 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); + storeAndForward(mail); + }); + } + + function storeAndForward(mail) { + // store in outbox + self._devicestorage.storeList([mail], outboxDb, function(err) { + if (err) { + callback(err); 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); - }); + 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; + + // also, if a _processOutbox call is still in progress, ignore it. + if (self._outboxBusy) { + return; + } + + 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; + } - // send pending mails if possible - pendingMails.forEach(function(mail) { - send(mail, after); - }); + // 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 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); - } + // 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) { + function onSend(err) { + if (err) { + self._outboxBusy = false; + if (err.code === 42) { + // offline try again later + done(); + } else { self._outboxBusy = false; - if (err.code === 42) { - // offline try again later - done(); - } else { - self._outboxBusy = false; - callback(err); - } - return; + callback(err); } + return; + } - // remove the pending mail from the storage - removeFromStorage(mail, done); + // remove the pending mail from the storage + removeFromStorage(mail, done); - // fire sent notification - if (typeof self.onSent === 'function') { - self.onSent(mail); - } + // 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; - } + // 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(); - }); - } - }; + 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.state.about = { + toggle: function(to) { + $scope.state.lightbox = (to) ? 'about' : undefined; + } + }; - // - // scope variables - // + // + // scope variables + // - $scope.version = cfg.appVersion; - $scope.date = new Date(); + $scope.version = cfg.appVersion; + $scope.date = new Date(); - // - // scope functions - // - }; + // + // scope functions + // +}; - return AboutCtrl; -}); \ No newline at end of file +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'; - - var appController = require('js/app-controller'), - dl = require('js/util/download'), - config = require('js/app-config').config, - pgp, keychain, userId; +'use strict'; + +var appController = require('../app-controller'), + dl = require('../util/download'), + config = require('../app-config').config, + pgp, keychain, userId; + +// +// Controller +// + +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; + } + }; // - // Controller + // scope variables // - var AccountCtrl = function($scope) { - userId = appController._emailDao._account.emailAddress; - keychain = appController._keychain; - pgp = appController._pgp; + 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.state.account = { - toggle: function(to) { - $scope.state.lightbox = (to) ? 'account' : undefined; + $scope.exportKeyFile = function() { + keychain.getUserKeyPair(userId, function(err, keys) { + if (err) { + $scope.onError(err); + return; } - }; - - // - // 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' - }); + + 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' }); - }; + }); }; +}; - return AccountCtrl; -}); \ No newline at end of file +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 - return; - } +var AddAccountCtrl = function($scope, $location, $routeParams) { + if (!appCtrl._auth && !$routeParams.dev) { + $location.path('/'); // init app + return; + } - $scope.step = 1; + $scope.step = 1; - $scope.goTo = function(step) { - $scope.step = step; - }; + $scope.goTo = function(step) { + $scope.step = step; + }; - $scope.createWhiteoutAccount = function() { - if ($scope.form.$invalid) { + $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.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; + $scope.goTo(3); + $scope.$apply(); + }); + }; - if (err) { - $scope.errMsg = err.errMsg || err.message; - $scope.$apply(); - return; - } + $scope.validateUser = function() { + if ($scope.formValidate.$invalid) { + return; + } - $scope.goTo(3); + $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(); - }); - }; - - $scope.validateUser = function() { - if ($scope.formValidate.$invalid) { return; } - $scope.busyValidate = true; - $scope.errMsgValidate = undefined; // reset error msg + // proceed to login + $scope.login(); + }); + }; - // verify user to REST api - appCtrl._adminDao.validateUser({ - emailAddress: $scope.emailAddress, - token: $scope.token.toUpperCase() - }, function(err) { + $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) { - $scope.busyValidate = false; - $scope.errMsgValidate = err.errMsg || err.message; - $scope.$apply(); - return; + return $scope.onError(err); } - - // 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(); + $location.path('/login-set-credentials').search({ + provider: 'gmail' }); - return; - } - - // use normal user/password login - $location.path('/login-set-credentials').search({ - provider: 'gmail' + $scope.$apply(); }); - }; + return; + } - $scope.connectTo = function(provider) { - $location.path('/login-set-credentials').search({ - provider: provider - }); - }; + // 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 +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 - // - - var ContactsCtrl = function($scope) { - keychain = appController._keychain, - pgp = appController._pgp; - - $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'; +// +// Controller +// - // - // scope functions - // +var ContactsCtrl = function($scope) { + keychain = appController._keychain, + pgp = appController._pgp; - $scope.listKeys = function() { - keychain.listLocalPublicKeys(function(err, keys) { - if (err) { - $scope.onError(err); - return; - } + $scope.state.contacts = { + toggle: function(to) { + $scope.state.lightbox = (to) ? 'contacts' : undefined; - keys.forEach(addParams); + $scope.listKeys(); + } + }; - $scope.keys = keys; - $scope.$apply(); + // set default value so that the popover height is correct on init + $scope.fingerprint = 'XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX'; - function addParams(key) { - var params = pgp.getKeyParams(key.publicKey); - _.extend(key, params); - } - }); - }; + // + // scope functions + // - $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.listKeys = function() { + keychain.listLocalPublicKeys(function(err, keys) { + if (err) { + $scope.onError(err); + return; + } - $scope.fingerprint = formatted; - }; + keys.forEach(addParams); - $scope.importKey = function(publicKeyArmored) { - var keyParams, pubkey; + $scope.keys = keys; + $scope.$apply(); - // verifiy public key string - if (publicKeyArmored.indexOf('-----BEGIN PGP PUBLIC KEY BLOCK-----') < 0) { - $scope.onError({ - showBugReporter: false, - message: 'Invalid public key!' - }); - return; + function addParams(key) { + var params = pgp.getKeyParams(key.publicKey); + _.extend(key, params); } + }); + }; - try { - keyParams = pgp.getKeyParams(publicKeyArmored); - } catch (e) { - $scope.onError(new Error('Error reading public key params!')); - return; - } + $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); - pubkey = { - _id: keyParams._id, - userId: keyParams.userId, - userIds: keyParams.userIds, - publicKey: publicKeyArmored, - imported: true // mark manually imported keys - }; + $scope.fingerprint = formatted; + }; - keychain.saveLocalPublicKey(pubkey, function(err) { - if (err) { - $scope.onError(err); - return; - } + $scope.importKey = function(publicKeyArmored) { + var keyParams, pubkey; - // update displayed keys - $scope.listKeys(); + // 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 }; - $scope.removeKey = function(key) { - keychain.removeLocalPublicKey(key._id, function(err) { - if (err) { - $scope.onError(err); - return; - } + keychain.saveLocalPublicKey(pubkey, function(err) { + if (err) { + $scope.onError(err); + return; + } - // update displayed keys - $scope.listKeys(); - }); - }; + // update displayed keys + $scope.listKeys(); + }); }; - // - // Directives - // + $scope.removeKey = function(key) { + keychain.removeLocalPublicKey(key._id, function(err) { + if (err) { + $scope.onError(err); + return; + } - var ngModule = angular.module('contacts', []); + // update displayed keys + $scope.listKeys(); + }); + }; +}; - 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)); - } - }); +// +// Directives +// + +var ngModule = angular.module('contacts', []); - function importKey(file) { - var reader = new FileReader(); - reader.onload = function(e) { - scope.importKey(e.target.result); - }; - reader.readAsText(file); +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)); } - }; - }); + }); - ngModule.directive('keyfileBtn', function() { - return function(scope, elm) { - elm.on('click touchstart', function(e) { - e.preventDefault(); - document.querySelector('#keyfile-input').click(); - }); - }; - }); + 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(); + }); + }; +}); - return ContactsCtrl; -}); \ No newline at end of file +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 - return; - } +var LoginExistingCtrl = function($scope, $location, $routeParams) { + if (!appController._emailDao && !$routeParams.dev) { + $location.path('/'); // init app + return; + } - var emailDao = appController._emailDao; + var emailDao = appController._emailDao; - $scope.buttonEnabled = true; + $scope.buttonEnabled = true; + $scope.incorrect = false; + + $scope.change = function() { $scope.incorrect = false; + }; + + $scope.confirmPassphrase = function() { + if (!$scope.passphrase) { + return; + } - $scope.change = function() { - $scope.incorrect = false; - }; + // disable button once loggin has started + $scope.buttonEnabled = false; + $scope.incorrect = false; + unlockCrypto(); + }; - $scope.confirmPassphrase = function() { - if (!$scope.passphrase) { + function unlockCrypto() { + var userId = emailDao._account.emailAddress; + emailDao._keychain.getUserKeyPair(userId, function(err, keypair) { + if (err) { + handleError(err); 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); + }); + } - emailDao.unlock({ - keypair: keypair, - passphrase: $scope.passphrase - }, onUnlock); - }); + function onUnlock(err) { + if (err) { + $scope.incorrect = true; + $scope.buttonEnabled = true; + $scope.$apply(); + return; } - function onUnlock(err) { + appController._auth.storeCredentials(function(err) { if (err) { - $scope.incorrect = true; - $scope.buttonEnabled = true; - $scope.$apply(); - return; + return $scope.onError(err); } - appController._auth.storeCredentials(function(err) { - if (err) { - return $scope.onError(err); - } - - $location.path('/desktop'); - $scope.$apply(); - }); - } + $location.path('/desktop'); + $scope.$apply(); + }); + } - function handleError(err) { - $scope.incorrect = true; - $scope.buttonEnabled = true; - $scope.onError(err); - } - }; + function handleError(err) { + $scope.incorrect = true; + $scope.buttonEnabled = true; + $scope.onError(err); + } +}; - return LoginExistingCtrl; -}); \ No newline at end of file +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 - return; - } +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.'; + 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; - } + 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'); - }; + // 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; - } + /** + * 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); + // 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; + } - setTimeout(function() { - emailDao.unlock({ - passphrase: undefined // generate key without passphrase - }, function(err) { + appController._auth.storeCredentials(function(err) { if (err) { - $scope.setState(states.IDLE); - $scope.onError(err); - return; + return $scope.onError(err); } - appController._auth.storeCredentials(function(err) { - if (err) { - return $scope.onError(err); - } - - $location.path('/desktop'); - $scope.$apply(); - }); + $location.path('/desktop'); + $scope.$apply(); }); - }, 500); - }; - - /** - * [signUpToNewsletter description] - * @param {Function} callback (optional) - */ - $scope.signUpToNewsletter = function(callback) { - if (!$scope.state.newsletter) { - return; - } + }); + }, 500); + }; - var address = emailDao._account.emailAddress; - var uri = 'https://whiteout.us8.list-manage.com/subscribe/post?u=52ea5a9e1be9e1d194f184158&id=6538e8f09f'; + /** + * [signUpToNewsletter description] + * @param {Function} callback (optional) + */ + $scope.signUpToNewsletter = function(callback) { + if (!$scope.state.newsletter) { + return; + } - var formData = new FormData(); - formData.append('EMAIL', address); - formData.append('b_52ea5a9e1be9e1d194f184158_6538e8f09f', ''); + var address = emailDao._account.emailAddress; + var uri = 'https://whiteout.us8.list-manage.com/subscribe/post?u=52ea5a9e1be9e1d194f184158&id=6538e8f09f'; - var xhr = new XMLHttpRequest(); - xhr.open('post', uri, true); + var formData = new FormData(); + formData.append('EMAIL', address); + formData.append('b_52ea5a9e1be9e1d194f184158_6538e8f09f', ''); - xhr.onload = function() { - if (callback) { - callback(null, xhr); - } - }; + var xhr = new XMLHttpRequest(); + xhr.open('post', uri, true); - xhr.onerror = function(err) { - if (callback) { - callback(err); - } - }; - - xhr.send(formData); + xhr.onload = function() { + if (callback) { + callback(null, xhr); + } }; - $scope.setState = function(state) { - $scope.state.ui = state; + xhr.onerror = function(err) { + if (callback) { + callback(err); + } }; + + xhr.send(formData); + }; + + $scope.setState = function(state) { + $scope.state.ui = state; }; +}; - return LoginInitialCtrl; -}); \ No newline at end of file +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 emailDao = appController._emailDao, - pgp = appController._pgp; +var LoginExistingCtrl = function($scope, $location, $routeParams) { + if (!appController._emailDao && !$routeParams.dev) { + $location.path('/'); // init app + return; + } - $scope.incorrect = false; + var emailDao = appController._emailDao, + pgp = appController._pgp; - $scope.confirmPassphrase = function() { - $scope.incorrect = false; - unlockCrypto(); - }; + $scope.incorrect = false; - 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; - } + $scope.confirmPassphrase = function() { + $scope.incorrect = false; + unlockCrypto(); + }; - keypair = keypair || {}; + 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; + } - // 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; - } - } + keypair = keypair || {}; - // parse keypair params - var privKeyParams, pubKeyParams; + // 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 { - privKeyParams = pgp.getKeyParams($scope.key.privateKeyArmored); - pubKeyParams = pgp.getKeyParams($scope.key.publicKeyArmored); + $scope.key.publicKeyArmored = pgp.extractPublicKey($scope.key.privateKeyArmored); } catch (e) { - $scope.onError(new Error('Error reading key params!')); + $scope.onError(new Error('Error parsing public key from private key!')); return; } + } - // set parsed private key - keypair.privateKey = { - _id: privKeyParams._id, + // 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: privKeyParams.userIds, - encryptedKey: $scope.key.privateKeyArmored + userIds: pubKeyParams.userIds, + publicKey: $scope.key.publicKeyArmored }; + } - 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; } - // 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); - }); + emailDao._keychain.putUserKeyPair(keypair, onUnlock); }); + }); + } + + function onUnlock(err) { + if (err) { + $scope.onError(err); + return; } - function onUnlock(err) { + appController._auth.storeCredentials(function(err) { if (err) { - $scope.onError(err); - return; + return $scope.onError(err); } - appController._auth.storeCredentials(function(err) { - if (err) { - return $scope.onError(err); - } + $location.path('/desktop'); + $scope.$apply(); + }); + } +}; - $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(); - 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 (files.length === 0) { + if (index === -1) { + scope.onError(new Error('Error parsing private PGP key block!')); 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; - }); + keyParts = { + publicKeyArmored: rawKeys.substring(0, index).trim(), + privateKeyArmored: rawKeys.substring(index, rawKeys.length).trim() }; - reader.readAsText(files[0]); - }); - }; - }); - return LoginExistingCtrl; -}); \ No newline at end of file + 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 - return; +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 keychain = appController._keychain, - emailDao = appController._emailDao, - userId = emailDao._account.emailAddress; + var value = evt.clipboardData.getData('text/plain'); + if (!value) { + return; + } - $scope.step = 1; + 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.handlePaste = function(event) { - var evt = event; - if (evt.originalEvent) { - evt = evt.originalEvent; - } + $scope.verifyRecoveryToken = function(callback) { + if (!$scope.recoveryToken) { + $scope.onError(new Error('Please set the recovery token!')); + return; + } - var value = evt.clipboardData.getData('text/plain'); - if (!value) { + keychain.getUserKeyPair(userId, function(err, keypair) { + if (err) { + $scope.onError(err); 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; - } + // remember for storage later + $scope.cachedKeypair = keypair; - keychain.getUserKeyPair(userId, function(err, keypair) { + keychain.downloadPrivateKey({ + userId: userId, + keyId: keypair.publicKey._id, + recoveryToken: $scope.recoveryToken.toUpperCase() + }, function(err, encryptedPrivateKey) { if (err) { $scope.onError(err); return; } - // remember for storage later - $scope.cachedKeypair = keypair; + $scope.encryptedPrivateKey = encryptedPrivateKey; + callback(); + }); + }); + }; - keychain.downloadPrivateKey({ - userId: userId, - keyId: keypair.publicKey._id, - recoveryToken: $scope.recoveryToken.toUpperCase() - }, function(err, encryptedPrivateKey) { - if (err) { - $scope.onError(err); - return; - } + $scope.decryptAndStorePrivateKeyLocally = function() { + var inputCode = '' + $scope.code0 + $scope.code1 + $scope.code2 + $scope.code3 + $scope.code4 + $scope.code5; - $scope.encryptedPrivateKey = encryptedPrivateKey; - callback(); - }); - }); - }; + if (!inputCode) { + $scope.onError(new Error('Please enter the keychain code!')); + return; + } - $scope.decryptAndStorePrivateKeyLocally = function() { - var inputCode = '' + $scope.code0 + $scope.code1 + $scope.code2 + $scope.code3 + $scope.code4 + $scope.code5; + var options = $scope.encryptedPrivateKey; + options.code = inputCode.toUpperCase(); - if (!inputCode) { - $scope.onError(new Error('Please enter the keychain code!')); + keychain.decryptAndStorePrivateKeyLocally(options, function(err, privateKey) { + if (err) { + $scope.onError(err); return; } - var options = $scope.encryptedPrivateKey; - options.code = inputCode.toUpperCase(); + // add private key to cached keypair object + $scope.cachedKeypair.privateKey = privateKey; - keychain.decryptAndStorePrivateKeyLocally(options, function(err, privateKey) { + // try empty passphrase + emailDao.unlock({ + keypair: $scope.cachedKeypair, + passphrase: undefined + }, function(err) { if (err) { - $scope.onError(err); + // go to passphrase login screen + $scope.goTo('/login-existing'); return; } - // add private key to cached keypair object - $scope.cachedKeypair.privateKey = privateKey; - - // try empty passphrase - emailDao.unlock({ - keypair: $scope.cachedKeypair, - passphrase: undefined - }, function(err) { + // passphrase is corrent ... go to main app + appController._auth.storeCredentials(function(err) { if (err) { - // go to passphrase login screen - $scope.goTo('/login-existing'); - return; + return $scope.onError(err); } - // passphrase is corrent ... go to main app - appController._auth.storeCredentials(function(err) { - if (err) { - return $scope.onError(err); - } - - $scope.goTo('/desktop'); - }); + $scope.goTo('/desktop'); }); }); - }; + }); + }; - $scope.goForward = function() { - if ($scope.step === 1) { - $scope.verifyRecoveryToken(function() { - $scope.step++; - $scope.$apply(); - }); - return; - } + $scope.goForward = function() { + if ($scope.step === 1) { + $scope.verifyRecoveryToken(function() { + $scope.step++; + $scope.$apply(); + }); + return; + } - if ($scope.step === 2) { - $scope.decryptAndStorePrivateKeyLocally(); - return; - } - }; + if ($scope.step === 2) { + $scope.decryptAndStorePrivateKeyLocally(); + return; + } + }; - $scope.goTo = function(location) { - $location.path(location); - $scope.$apply(); - }; + $scope.goTo = function(location) { + $location.path(location); + $scope.$apply(); }; +}; - return LoginPrivateKeyDownloadCtrl; -}); \ No newline at end of file +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'; - - 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 SetCredentialsCtrl = function($scope, $location, $routeParams) { - if (!appCtrl._auth && !$routeParams.dev) { - $location.path('/'); // init app - return; +'use strict'; + +var ENCRYPTION_METHOD_NONE = 0; +var ENCRYPTION_METHOD_STARTTLS = 1; +var ENCRYPTION_METHOD_TLS = 2; + +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 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; - - // - // 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; + // 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; + + // 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; - - // 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; + } + + $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(); - }); - }; + $scope.busy = false; + $scope.$apply(); + }); }; +}; - return SetCredentialsCtrl; -}); \ No newline at end of file +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(); - }); + // initiate controller by creating email dao + appController.init({ + emailAddress: info.emailAddress, + realname: info.realname + }, function(err, availableKeys) { + if (err) { + $scope.onError(err); + return; + } - function initializeUser() { - // get OAuth token from chrome - appController._auth.getEmailAddress(function(err, info) { + 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; } - // check if account needs to be selected - if (!info.emailAddress) { - goTo('/add-account'); + if (privateKeySynced) { + // private key is synced, proceed to download + goTo('/login-privatekey-download'); return; } - // initiate controller by creating email dao - appController.init({ - emailAddress: info.emailAddress, - realname: info.realname - }, function(err, availableKeys) { - if (err) { - $scope.onError(err); - return; - } - - redirect(availableKeys); - }); + // no private key, import key file + goTo('/login-new-device'); }); - } - 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; - } - - 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'); - }); - } - } + } else { + // public and private key available, try empty passphrase + appController._emailDao.unlock({ + keypair: availableKeys, + passphrase: undefined + }, function(err) { + if (err) { + goTo('/login-existing'); + return; + } - function goTo(location) { - $scope.$apply(function() { - $location.path(location); + 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 - // - - emailDao = appController._emailDao; - outboxBo = appController._outboxBo; - keychainDao = appController._keychain; - - /** - * Gathers unread notifications to be cancelled later - */ - $scope.pendingNotifications = []; - - // - // scope functions - // +var MailListCtrl = function($scope, $routeParams) { + // + // Init + // - $scope.getBody = function(email) { - emailDao.getBody({ - folder: currentFolder(), - message: email - }, function(err) { - if (err && err.code !== 42) { - $scope.onError(err); - return; - } + emailDao = appController._emailDao; + outboxBo = appController._outboxBo; + keychainDao = appController._keychain; - // display fetched body - $scope.$digest(); + /** + * Gathers unread notifications to be cancelled later + */ + $scope.pendingNotifications = []; - // automatically decrypt if it's the selected email - if (email === currentMessage()) { - emailDao.decryptBody({ - message: email - }, $scope.onError); - } - }); - }; + // + // scope functions + // - /** - * 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; + // display fetched body + $scope.$digest(); - if (!firstSelect) { - // only toggle to read view on 2nd select in mobile mode - $scope.state.read.toggle(true); + // automatically decrypt if it's the selected email + if (email === currentMessage()) { + emailDao.decryptBody({ + message: email + }, $scope.onError); } - firstSelect = false; + }); + }; - keychainDao.refreshKeyForUserId(email.from[0].address, onKeyRefreshed); + /** + * Called when clicking on an email list item + */ + $scope.select = function(email) { + // unselect an item + if (!email) { + $scope.state.mailList.selected = undefined; + return; + } - function onKeyRefreshed(err) { - if (err) { - $scope.onError(err); - } + $scope.state.mailList.selected = email; - emailDao.decryptBody({ - message: email - }, $scope.onError); + if (!firstSelect) { + // only toggle to read view on 2nd select in mobile mode + $scope.state.read.toggle(true); + } + firstSelect = false; - // if the email is unread, please sync the new state. - // otherweise forget about it. - if (!email.unread) { - return; - } + keychainDao.refreshKeyForUserId(email.from[0].address, onKeyRefreshed); - // 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()); - } + 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); + $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; } - }; - /** - * Mark an email as unread or read, respectively - */ - $scope.toggleUnread = function(message) { - updateStatus('Updating unread flag...'); + if (err) { + updateStatus('Error on sync!'); + $scope.onError(err); + return; + } + + updateStatus('Online'); + $scope.$apply(); + }); + }; - message.unread = !message.unread; - emailDao.setFlags({ + /** + * 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 + }; - // share local scope functions with root state - $scope.state.mailList = { - remove: $scope.remove - }; + // + // watch tasks + // - // - // watch tasks - // + /** + * List emails from folder when user changes folder + */ + $scope._stopWatchTask = $scope.$watch('state.nav.currentFolder', function() { + if (!currentFolder()) { + return; + } - /** - * List emails from folder when user changes folder - */ - $scope._stopWatchTask = $scope.$watch('state.nav.currentFolder', function() { - if (!currentFolder()) { - return; - } + // reset searchFilter + $scope.searchText = undefined; - // reset searchFilter - $scope.searchText = undefined; + // in development, display dummy mail objects + if ($routeParams.dev) { + updateStatus('Last update: ', new Date()); + currentFolder().messages = createDummyMails(); + return; + } - // in development, display dummy mail objects - if ($routeParams.dev) { - updateStatus('Last update: ', new Date()); - currentFolder().messages = createDummyMails(); - return; - } + // display and select first + openCurrentFolder(); + }); - // display and select first - openCurrentFolder(); - }); + $scope.watchMessages = $scope.$watchCollection('state.nav.currentFolder.messages', function(messages) { + if (!messages) { + return; + } - $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); - // 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]); + } + }); - // 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; + } - /** - * 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; - 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; + } - 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); + }; - // 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); + } - /** - * 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; + } - if (!searchText) { - // set display buffer to first messages - $scope.displayMessages = currentFolder().messages.slice(0, INIT_DISPLAY_LEN); + // 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('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); - }; + 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; - } + /** + * 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'); + // 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 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; - } - } - + function checkAddresses(header) { + if (!header || !header.length) { 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)) { + for (var i = 0; i < header.length; i++) { + if (contains(header[i].name) || contains(header[i].address)) { return true; } - return false; } - // user native js Array.filter - return messages.filter(matchMetaDataFirst); - }; + return false; + } /** - * Sync current folder when client comes back online + * Filter meta data first and then only look at plaintext and decrypted message bodies */ - $scope.watchOnline = $scope.$watch('account.online', function(isOnline) { - if (isOnline) { - updateStatus('Online'); - openCurrentFolder(); - } else { - updateStatus('Offline mode'); + function matchMetaDataFirst(m) { + // compare subject + if (contains(m.subject)) { + return true; } - }, true); - - // - // Helper Functions - // + // 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; + } - function openCurrentFolder() { - emailDao.openFolder({ - folder: currentFolder() - }, function(error) { - // dont wait until scroll to load visible mail bodies - $scope.loadVisibleBodies(); + // user native js Array.filter + return messages.filter(matchMetaDataFirst); + }; - // don't display error for offline case - if (error && error.code === 42) { - return; - } - $scope.onError(error); - }); + /** + * 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); - function updateStatus(lbl, time) { - $scope.lastUpdateLbl = lbl; - $scope.lastUpdate = (time) ? time : ''; - } + // + // Helper Functions + // - function currentFolder() { - return $scope.state.nav.currentFolder; - } + function openCurrentFolder() { + emailDao.openFolder({ + folder: currentFolder() + }, function(error) { + // dont wait until scroll to load visible mail bodies + $scope.loadVisibleBodies(); - function currentMessage() { - return $scope.state.mailList.selected; - } + // don't display error for offline case + if (error && error.code === 42) { + return; + } + $scope.onError(error); + }); + } - // - // Notification API - // + function updateStatus(lbl, time) { + $scope.lastUpdateLbl = lbl; + $scope.lastUpdate = (time) ? time : ''; + } - (emailDao || {}).onIncomingMessage = function(msgs) { - var note, title, message, unreadMsgs; + function currentFolder() { + return $scope.state.nav.currentFolder; + } - unreadMsgs = msgs.filter(function(msg) { - return msg.unread; - }); + function currentMessage() { + return $scope.state.mailList.selected; + } - if (unreadMsgs.length === 0) { - return; - } + // + // Notification API + // - 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'); - } + (emailDao || {}).onIncomingMessage = function(msgs) { + var note, title, message, unreadMsgs; - 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); - } + unreadMsgs = msgs.filter(function(msg) { + return msg.unread; + }); + + if (unreadMsgs.length === 0) { + return; + } - // mark message as read - $scope.select(_.findWhere(currentFolder().messages, { - uid: unreadMsgs[0].uid - })); + 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); } - }); - $scope.pendingNotifications.push(note); - }; + + // mark message as read + $scope.select(_.findWhere(currentFolder().messages, { + uid: unreadMsgs[0].uid + })); + } + }); + $scope.pendingNotifications.push(note); }; +}; - // - // Directives - // +// +// Directives +// - var ngModule = angular.module('mail-list', []); +var ngModule = angular.module('mail-list', []); - ngModule.directive('woTouch', function($parse) { - return function(scope, elm, attrs) { - var handler = $parse(attrs.woTouch); +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('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 - }); + 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; - } +ngModule.directive('listScroll', function() { + return { + link: function(scope, elm, attrs) { + var model = attrs.listScroll, + listEl = elm[0], + scrollTimeout; - 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; - } + /* + * 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]; + - // load body when scrolling - listEl.onscroll = function() { - if (scrollTimeout) { - // remove timeout so that only scroll end - clearTimeout(scrollTimeout); + 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; } - 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() { + // load body when scrolling + listEl.onscroll = function() { + if (scrollTimeout) { + // remove timeout so that only scroll end + clearTimeout(scrollTimeout); + } + scrollTimeout = setTimeout(function() { scope.loadVisibleBodies(); - }); - } - }; - }); + }, 300); + }; - function byUidDescending(a, b) { - if (a.uid < b.uid) { - return 1; - } else if (b.uid < a.uid) { - return -1; - } else { - return 0; + // 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; + }; - // 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; + 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 MailListCtrl; -}); \ No newline at end of file + 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; + } - $scope.state.nav = { - open: false, - toggle: function(to) { - this.open = to; - } - }; + emailDao = appController._emailDao; + outboxBo = appController._outboxBo; - $scope.openFolder = function(folder) { - $scope.state.nav.currentFolder = folder; - $scope.state.nav.toggle(false); - }; + // + // scope functions + // - $scope.onOutboxUpdate = function(err, count) { - if (err) { - $scope.onError(err); - return; - } + $scope.state.nav = { + open: false, + toggle: function(to) { + this.open = to; + } + }; - // update the outbox mail count - var outbox = _.findWhere($scope.account.folders, { - type: config.outboxMailboxType - }); - outbox.count = count; - $scope.$apply(); + $scope.openFolder = function(folder) { + $scope.state.nav.currentFolder = folder; + $scope.state.nav.toggle(false); + }; - 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]); + $scope.onOutboxUpdate = function(err, count) { + if (err) { + $scope.onError(err); + return; } - // 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(); - } + // update the outbox mail count + var outbox = _.findWhere($scope.account.folders, { + type: config.outboxMailboxType }); + outbox.count = count; + $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; + emailDao.refreshFolder({ + folder: outbox + }, $scope.onError); + }; - // set notificatio handler for sent messages - outboxBo.onSent = sentNotification; - // start checking outbox periodically - outboxBo.startChecking($scope.onOutboxUpdate); + $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 + // - function sentNotification(email) { - notification.create({ - title: 'Message sent', - message: email.subject, - timeout: 2000 - }, function() {}); + // 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; } - - // 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' - }]; + // select inbox if not yet selected + if (!$scope.state.nav.currentFolder) { + $scope.openFolder($scope.account.folders[0]); + $scope.$apply(); } - }; + }); // - // Directives + // helper functions // - 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; - } + function initializeFolders() { + // create dummy folder in dev environment only + if ($routeParams.dev) { + createDummyFolders(); + 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(); - } + // 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(); + } + + }); + }; +}); - return NavigationCtrl; -}); \ No newline at end of file +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'; - - var angular = require('angular'); - - // - // Controller - // - - var PopoverCtrl = function($scope) { - $scope.state.popover = {}; +'use strict'; + +// +// Controller +// + +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'; - - var angular = require('angular'), - appController = require('js/app-controller'), - util = require('js/crypto/util'), - 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) { +'use strict'; + +var appController = require('../app-controller'), + util = require('crypto-lib').util, + 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 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(); + }); + } + }; - // 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) { + 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.handlePaste = function(event) { - var evt = event; - if (evt.originalEvent) { - evt = evt.originalEvent; + $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; } - var value = evt.clipboardData.getData('text/plain'); - if (!value) { + if (privateKeySynced) { + callback(privateKeySynced); 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; - } + callback(); + }); + }; - if (privateKeySynced) { - callback(privateKeySynced); - return; - } + $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); - callback(); - }); - }; + // 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; - $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); + 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; + } - // clear input fields of any previous artifacts - $scope.code0 = $scope.code1 = $scope.code2 = $scope.code3 = $scope.code4 = $scope.code5 = ''; - }; + return true; + }; + + $scope.setDeviceName = function(callback) { + keychain.setDeviceName($scope.deviceName, callback); + }; - $scope.verifyCode = function() { - var inputCode = '' + $scope.code0 + $scope.code1 + $scope.code2 + $scope.code3 + $scope.code4 + $scope.code5; + $scope.encryptAndUploadKey = function(callback) { + var userId = appController._emailDao._account.emailAddress; + var code = $scope.code; - 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; + // register device to keychain service + keychain.registerDevice({ + userId: userId + }, function(err) { + if (err) { $scope.onError(err); - return false; + return; } - return true; - }; + // 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.setDeviceName = function(callback) { - keychain.setDeviceName($scope.deviceName, callback); - }; + $scope.goForward = function() { + if ($scope.step < 2) { + $scope.step++; + return; + } - $scope.encryptAndUploadKey = function(callback) { - var userId = appController._emailDao._account.emailAddress; - var code = $scope.code; + if ($scope.step === 2 && $scope.verifyCode()) { + $scope.step++; + return; + } - // register device to keychain service - keychain.registerDevice({ - userId: userId - }, function(err) { + 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) { - $scope.step++; - return; - } - - if ($scope.step === 2 && $scope.verifyCode()) { + // show spinner $scope.step++; - return; - } + $scope.$apply(); - 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; - - scope.$watch(attr.ngModel, function(val) { - if (val && val.length === maxLen) { - var nextinput = element.next('input'); - if (nextinput.length) { - nextinput[0].focus(); - } +}; + +// +// Directives +// + +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'; - - // 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)); - } +'use strict'; + +// 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)); + } - // 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; + document.body.innerHTML = html; - scaleToFit(); - }; + scaleToFit(); +}; - window.addEventListener('resize', 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; +/** + * 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 parseLines(body) { + var lines = []; + body.split('\n').forEach(parseLine); - function parseLine(line) { - var regex = /^>*/; - var result = regex.exec(line); + 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; + lines.push({ + text: line.replace(regex, '').trim(), + level: (result && result.length > 0) ? result[0].length : 0 + }); } - function buildTextNodes(lines) { - var i, j, root, currentLevel, currentNode, levelDelta; - - root = new Node(); - currentLevel = 0; - currentNode = root; + return lines; + } - // 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--; - } + 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; + // add text to the current node + currentNode.addLine(lines[i].text); } - function Node(parent) { - this.parent = parent; - this.children = []; - } - Node.prototype.addLine = function(lineText) { - var c, l; - - c = this.children; - l = c.length; + return root; + } - // 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; - } + function Node(parent) { + this.parent = parent; + this.children = []; + } + Node.prototype.addLine = function(lineText) { + var c, l; - // append line to last child (add newline between lines) - c[l - 1] += lineText + '\n'; - }; + c = this.children; + l = c.length; - function removeParentReference(node) { - if (!node.children) { - // this is a text leaf ... terminate recursion - return; - } + // 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; + } - // remove parent node to prevent infinite loop in JSON stringify - delete node.parent; + // append line to last child (add newline between lines) + c[l - 1] += lineText + '\n'; + }; - 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]); - } - } + function removeParentReference(node) { + if (!node.children) { + // this is a text leaf ... terminate recursion + return; } - nodes = buildTextNodes(parseLines(textBody.replace(/ >/g, '>'))); - removeParentReference(nodes); + // remove parent node to prevent infinite loop in JSON stringify + delete node.parent; - return nodes; + 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]); + } + } } - /** - * 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] + '
'; + 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] + '
'; } - - for (i = 0; i < node.children.length; i++) { - html += '
' + render(node.children[i]) + '
'; - } - return html; } - function createArchor(url) { - return '
' + url + ''; + for (i = 0; i < node.children.length; i++) { + html += '
' + render(node.children[i]) + '
'; } - function isLineEmpty(line) { - return line.replace(/>/g, '').trim().length === 0; - } + return html; + } - for (var j = 0; j < root.children.length; j++) { - // start by rendering the root nodes children - body += render(root.children[j]); - } + function createArchor(url) { + return '' + url + ''; + } - return '
' + body + '
'; + function isLineEmpty(line) { + return line.replace(/>/g, '').trim().length === 0; } - /** - * Transform scale content to fit iframe width - */ - function scaleToFit() { - var view = document.getElementsByClassName('scale-body').item(0); - if(!view) { - return; - } + for (var j = 0; j < root.children.length; j++) { + // start by rendering the root nodes children + body += render(root.children[j]); + } - var parentWidth = view.parentNode.offsetWidth; - var w = view.offsetWidth; - var scale = ''; + return '
' + body + '
'; +} - if(w > parentWidth) { - scale = parentWidth / w; - scale = 'scale(' + scale + ',' + scale + ')'; - } +/** + * 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 = ''; - view.style['-webkit-transform-origin'] = '0 0'; - view.style.transformOrigin = '0 0'; - view.style['-webkit-transform'] = scale; - view.style.transform = scale; + if (w > parentWidth) { + scale = parentWidth / w; + scale = 'scale(' + scale + ',' + scale + ')'; } -})(); \ No newline at end of file + 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'; - - 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; - - // - // Controller - // - - var ReadCtrl = function($scope) { - - 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.'; - - $scope.state.read = { - open: false, - toggle: function(to) { - this.open = to; - } - }; +'use strict'; - $scope.getKeyId = function(address) { - $scope.keyId = 'Searching...'; - keychain.getReceiverPublicKey(address, function(err, pubkey) { - if (err) { - $scope.onError(err); - return; - } +var appController = require('../app-controller'), + download = require('../util/download'), + str = require('../app-config').string, + emailDao, invitationDao, outbox, pgp, keychain; - if (!pubkey) { - $scope.keyId = 'User has no key. Click to invite.'; - $scope.$apply(); - return; - } +// +// Controller +// - var fpr = pgp.getFingerprint(pubkey.publicKey); - var formatted = fpr.slice(32); +var ReadCtrl = function($scope) { - $scope.keyId = 'PGP key: ' + formatted; - $scope.$apply(); - }); - }; + 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.'; + + $scope.state.read = { + open: false, + toggle: function(to) { + this.open = to; + } + }; - $scope.$watch('state.mailList.selected', function(mail) { - if (!mail) { + $scope.getKeyId = function(address) { + $scope.keyId = 'Searching...'; + keychain.getReceiverPublicKey(address, function(err, pubkey) { + if (err) { + $scope.onError(err); 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); + 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(); }); + }; - function checkPublicKey(user) { - user.secure = undefined; + $scope.$watch('state.mailList.selected', function(mail) { + if (!mail) { + return; + } - if (!keychain) { + // 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; } - keychain.getReceiverPublicKey(user.address, function(err, pubkey) { - if (err) { - $scope.onError(err); - return; - } - - if (pubkey && pubkey.publicKey) { - user.secure = true; - } else { - user.secure = false; - } + if (pubkey && pubkey.publicKey) { + user.secure = true; + } else { + user.secure = false; + } - $scope.$apply(); + $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; } - $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); + }; - 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...'; - $scope.invite = function(user) { - // only invite non-pgp users - if (user.secure) { + var sender = emailDao._account.emailAddress, + recipient = user.address; + + invitationDao.invite({ + recipient: recipient, + sender: sender + }, function(err) { + if (err) { + $scope.onError(err); 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 - }; + var invitationMail = { + from: [{ + address: sender + }], + to: [{ + address: recipient + }], + cc: [], + bcc: [], + subject: str.invitationSubject, + body: str.invitationMessage + }; - // send invitation mail - outbox.put(invitationMail, $scope.onError); - }); - }; + // send invitation mail + outbox.put(invitationMail, $scope.onError); + }); }; +}; - // - // Directives - // +// +// Directives +// - var ngModule = angular.module('read', []); +var ngModule = angular.module('read', []); - ngModule.directive('replySelection', function() { - return function(scope, elm) { - var popover, visible; +ngModule.directive('replySelection', function() { + return function(scope, elm) { + var popover, visible; - popover = angular.element(document.querySelector('.reply-selection')); - visible = false; + popover = angular.element(document.querySelector('.reply-selection')); + visible = false; - elm.on('touchstart click', appear); - elm.parent().parent().on('touchstart click', disappear); - popover.on('touchstart click', disappear); + elm.on('touchstart click', appear); + elm.parent().parent().on('touchstart click', disappear); + popover.on('touchstart click', disappear); - function appear(e) { - e.preventDefault(); - e.stopPropagation(); + function appear(e) { + e.preventDefault(); + e.stopPropagation(); - visible = true; + visible = true; - // set popover position - var top = elm[0].offsetTop; - var left = elm[0].offsetLeft; - var width = elm[0].offsetWidth; - var height = elm[0].offsetHeight; + // set popover position + var top = elm[0].offsetTop; + var left = elm[0].offsetLeft; + var width = elm[0].offsetWidth; + var height = elm[0].offsetHeight; - popover[0].style.transition = 'opacity 0.1s linear'; - popover[0].style.top = (top + height) + 'px'; - popover[0].style.left = (left + width / 2 - popover[0].offsetWidth / 2) + 'px'; - popover[0].style.opacity = '1'; - } + popover[0].style.transition = 'opacity 0.1s linear'; + popover[0].style.top = (top + height) + 'px'; + popover[0].style.left = (left + width / 2 - popover[0].offsetWidth / 2) + 'px'; + popover[0].style.opacity = '1'; + } - function disappear() { - if (!visible) { - return; - } + function disappear() { + if (!visible) { + return; + } - popover[0].style.transition = 'opacity 0.25s linear, top 0.25s step-end, left 0.25s step-end'; - popover[0].style.opacity = '0'; - popover[0].style.top = '-9999px'; - popover[0].style.left = '-9999px'; - visible = false; + popover[0].style.transition = 'opacity 0.25s linear, top 0.25s step-end, left 0.25s step-end'; + popover[0].style.opacity = '0'; + popover[0].style.top = '-9999px'; + popover[0].style.left = '-9999px'; + visible = false; + } + }; +}); + +ngModule.directive('frameLoad', function($timeout, $window) { + return function(scope, elm) { + var iframe = elm[0]; + + 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); } - }; - }); + }); - ngModule.directive('frameLoad', function($timeout, $window) { - return function(scope, elm) { - var iframe = elm[0]; - - scope.$watch('state.read.open', function(open) { - if (open) { - // trigger rendering of iframe - // otherwise scale to fit would not compute correct dimensions on mobile - displayText(scope.state.mailList.selected ? scope.state.mailList.selected.body : undefined); - displayHtml(scope.state.mailList.selected ? scope.state.mailList.selected.html : undefined); - } - }); + $window.addEventListener('resize', scaleToFit); - $window.addEventListener('resize', scaleToFit); + 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(); + }; - 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(); - }; + function displayText(body) { + var mail = scope.state.mailList.selected; + if ((mail && mail.html) || (mail && mail.encrypted && !mail.decrypted)) { + return; + } - 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 + }, '*'); - // send text body for rendering in iframe - iframe.contentWindow.postMessage({ - text: body - }, '*'); + $timeout(scaleToFit, 0); + } - $timeout(scaleToFit, 0); + function displayHtml(html) { + if (!html) { + return; } - 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); + // 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 + }, '*'); + }; } - // 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 - }); + $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 + ')'; } - }; - }); - return ReadCtrl; -}); \ No newline at end of file + 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.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); - } + // + // scope variables + // - if (!passphrase) { - // no rating for empty passphrase - $scope.passphraseMsg = ''; - return; - } + // + // scope functions + // - if (passphrase.length < 8 || SAME.test(passphrase)) { - $scope.passphraseMsg = 'Very weak'; - return; - } + /* + * 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); + } - 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; - } - }; + if (!passphrase) { + // no rating for empty passphrase + $scope.passphraseMsg = ''; + return; + } - $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); - }); - }; + if (passphrase.length < 8 || SAME.test(passphrase)) { + $scope.passphraseMsg = 'Very weak'; + return; + } - function onPassphraseChanged(err, newPrivateKeyArmored) { + 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) { - 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 - }; + pgp.changePassphrase({ + privateKeyArmored: savedKey.encryptedKey, + oldPassphrase: $scope.oldPassphrase, + newPassphrase: $scope.newPassphrase + }, onPassphraseChanged); + }); + }; - keychain.saveLocalPrivateKey(privateKey, onKeyPersisted); + function onPassphraseChanged(err, newPrivateKeyArmored) { + if (err) { + err.showBugReporter = false; + $scope.onError(err); + return; } - function onKeyPersisted(err) { - if (err) { - $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); + } - $scope.state.setPassphrase.toggle(false); - $scope.$apply(); - $scope.onError({ - title: 'Success', - message: 'Passphrase change complete.' - }); + function onKeyPersisted(err) { + if (err) { + $scope.onError(err); + return; } - }; - return SetPassphraseCtrl; -}); \ No newline at end of file + $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; - - // set default value so that the popover height is correct on init - $scope.keyId = 'XXXXXXXX'; - - // - // Init - // - - $scope.state.writer = { - write: function(replyTo, replyAll, forward) { - $scope.state.lightbox = 'write'; - $scope.replyTo = replyTo; - - resetFields(); - - // fill fields depending on replyTo - fillFields(replyTo, replyAll, forward); - - $scope.verify($scope.to[0]); - }, - reportBug: function() { - $scope.state.lightbox = 'write'; - resetFields(); - reportBug(); - $scope.verify($scope.to[0]); - }, - close: function() { - $scope.state.lightbox = undefined; - } - }; - - 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.state.writer = { + write: function(replyTo, replyAll, forward) { + $scope.state.lightbox = 'write'; + $scope.replyTo = replyTo; + + resetFields(); + + // fill fields depending on replyTo + fillFields(replyTo, replyAll, forward); + + $scope.verify($scope.to[0]); + }, + reportBug: function() { + $scope.state.lightbox = 'write'; + resetFields(); + reportBug(); + $scope.verify($scope.to[0]); + }, + close: function() { + $scope.state.lightbox = undefined; } + }; - 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]'; - } - - dump += '[' + date.toISOString() + ']'; + 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; + } + + 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]'; + } - // component is optional - if (component) { - dump += '[' + component + ']'; - } + dump += '[' + date.toISOString() + ']'; - // log may be an error or a string - dump += ' ' + (log || '').toString(); + // component is optional + if (component) { + dump += '[' + component + ']'; + } - // if an error it is, a stack trace it has. print it, we should. - if (log.stack) { - dump += ' . Stack: ' + log.stack; - } + // log may be an error or a string + dump += ' ' + (log || '').toString(); - dump += '\n'; + // if an error it is, a stack trace it has. print it, we should. + if (log.stack) { + dump += ' . Stack: ' + log.stack; } - }; - axe.dump(appender); - $scope.to = [{ - address: str.supportAddress - }]; - $scope.writerTitle = str.bugReportTitle; - $scope.subject = str.bugReportSubject; - $scope.body = str.bugReportBody + dump; + 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; - } + function fillFields(re, replyAll, forward) { + var replyTo, from, sentDate, body; - $scope.writerTitle = (forward) ? 'Forward' : 'Reply'; + if (!re) { + return; + } - replyTo = re.replyTo && re.replyTo[0] && re.replyTo[0].address || re.from[0].address; + $scope.writerTitle = (forward) ? 'Forward' : 'Reply'; - // fill recipient field and references - if (!forward) { - $scope.to.unshift({ - address: replyTo - }); - $scope.to.forEach($scope.verify); + replyTo = re.replyTo && re.replyTo[0] && re.replyTo[0].address || re.from[0].address; - $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; - } + // 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 (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 - }); + 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); - } + // 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 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 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'); + // 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; - } + 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 (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.body) { + body += re.body.trim().split('\n').join('\n> ').replace(/ >/g, '>'); + $scope.body = body; } + } - // - // Editing headers - // + // + // Editing headers + // - /** - * Verify email address and fetch its public key - */ - $scope.verify = function(recipient) { - if (!recipient) { - return; - } + /** + * 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; + // 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; + } - // 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; + } - // 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; } + } - 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(); + }); + } + }; - $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; - /** - * 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; - } - } + 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); - // only allow sending if receviers exist - if (numReceivers < 1) { + function check(recipient) { + // validate address + if (!util.validateEmailAddress(recipient.address)) { return; } - - // bcc automatically disables secure sending - if ($scope.bcc.filter(filterEmptyAddresses).length > 0) { + numReceivers++; + if (!recipient.secure) { 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; - } - }; + // only allow sending if receviers exist + if (numReceivers < 1) { + return; + } - // - // Editing attachments - // + // bcc automatically disables secure sending + if ($scope.bcc.filter(filterEmptyAddresses).length > 0) { + allSecure = false; + } - $scope.remove = function(attachment) { - $scope.attachments.splice($scope.attachments.indexOf(attachment), 1); + 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; + } + }; + + // + // Editing attachments + // + + $scope.remove = function(attachment) { + $scope.attachments.splice($scope.attachments.indexOf(attachment), 1); + }; + + // + // 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: {} }; - // - // 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: {} - }; + if ($scope.inReplyTo) { + email.headers['in-reply-to'] = '<' + $scope.inReplyTo + '>'; + } - 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(); - if ($scope.references && $scope.references.length) { - email.headers.references = $scope.references.map(function(reference) { - return '<' + reference + '>'; - }).join(' '); + // persist the email to disk for later sending + outbox.put(email, function(err) { + if (err) { + $scope.onError(err); + return; } - // close the writer - $scope.state.writer.close(); + // 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; + } - // persist the email to disk for later sending - outbox.put(email, function(err) { - if (err) { + $scope.replyTo.answered = true; + emailDao.setFlags({ + folder: currentFolder(), + message: $scope.replyTo + }, function(err) { + if (err && err.code !== 42) { $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; - } - - $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(); - }); + // offline or no error, let's apply the ui changes + $scope.$apply(); }); + }); - }; + }; - // - // Tag input & Autocomplete - // + // + // Tag input & Autocomplete + // - $scope.tagStyle = function(recipient) { - var classes = ['label']; - if (recipient.secure === false) { - classes.push('label-primary'); - } - return classes; - }; + $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(); + $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; - } + 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(); + $scope.addressBookCache = keys.map(function(key) { + return { + address: key.userId + }; }); - - } 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; + } else { + filter(); } - /* - * Visitor to filter out objects without an address property, i.e. empty addresses - */ - function filterEmptyAddresses(addr) { - return !!addr.address; + // 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; + }; // - // Directives + // Helpers // - 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); - } - }); - } - }; - }); - - 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); - } - }); - } - }; - }); - - 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 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); + } + }); + } + }; +}); + +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); } }); + } + }; +}); + +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(); + }); + } + }; +}); - 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('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)); } - }; - }); - - ngModule.directive('attachmentBtn', function() { - return function(scope, elm) { - elm.on('click touchstart', function(e) { - e.preventDefault(); - document.querySelector('#attachment-input').click(); - }); - }; - }); + }); + + 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(); + }); + }; +}); - return WriteCtrl; -}); \ No newline at end of file +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; - } - - callback(null, ct); - }; +/** + * 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; - /** - * 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 { + ct = aes.encrypt(plaintext, key, iv); + } catch (err) { + callback(err); + return; + } - try { - pt = aes.decrypt(ciphertext, key, iv); - } catch (err) { - callback(err); - return; - } + callback(null, ct); +}; - callback(null, pt); - }; +/** + * 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; - /** - * 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); - } - }); - }; + try { + pt = aes.decrypt(ciphertext, key, iv); + } catch (err) { + callback(err); + return; + } - // - // helper functions - // + callback(null, pt); +}; - 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 - }); - return; - }; - // send data to the worker - worker.postMessage(options.args); - return; +/** + * 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 +// - // no WebWorker support... do synchronous call - var result; - try { - result = options.noWorker(); - } catch (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; + } + // 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; + } + + // 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); +} - return Crypto; -}); \ No newline at end of file +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 forge.util.encode64(key); +}; - return self; -}); \ No newline at end of file +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!')); +/** + * Generate a key pair for the user + */ +PGP.prototype.generateKeys = function(options, callback) { + var userId, passphrase; + + if (!util.emailRegEx.test(options.emailAddress) || !options.keySize) { + callback(new Error('Crypto init failed. Not all options set!')); + 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); + + 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); - - function onGenerated(err, keys) { - if (err) { - callback(new Error('Keygeneration failed!')); - return; - } - - callback(null, { - keyId: keys.key.primaryKey.getKeyId().toHex().toUpperCase(), - privateKeyArmored: keys.privateKeyArmored, - publicKeyArmored: keys.publicKeyArmored - }); - } - }; + 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(); - } +/** + * 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]); - } + // 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!'); - } + if (!this._publicKey) { + throw new Error('No public key set for fingerprint generation!'); + } - // get local fingerpring - return fingerprint(this._publicKey); - }; + // get local fingerpring + return fingerprint(this._publicKey); +}; - /** - * Show a user's key id. - */ - PGP.prototype.getKeyId = function(keyArmored) { - var key, pubKeyId, privKeyId; +/** + * 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(); - } + // 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!'); - } + // 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(); + 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!'); - } + if (!pubKeyId || !privKeyId || pubKeyId !== privKeyId) { + throw new Error('Key IDs do not match!'); + } - return pubKeyId; - }; + 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!'); - } +/** + * 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; + 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() - }); + // 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, - }; + }); + + 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(); - }; +/** + * 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; +/** + * 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; - } + // 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; - } + 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; - } + // 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; - } + // 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; - } + // 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(); - }; + 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; - } +/** + * 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() - }); - }; + 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; +/** + * 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; + // 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.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; - } + 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; - } + // 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; - } + // 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; + // 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; - } + // check if new passphrase really works + if (!privKey.decrypt(newPassphrase)) { + callback(new Error('Decrypting key with new passphrase failed!')); + return; + } - callback(null, newKeyArmored); - }; + callback(null, newKeyArmored); +}; - /** - * Encrypt and sign a pgp message for a list of receivers - */ - PGP.prototype.encrypt = function(plaintext, publicKeysArmored, callback) { - var publicKeys; +/** + * 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; - } + // 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; + // 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); + 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 { - // if no public keys are available encrypt for myself - openpgp.signAndEncryptMessage([this._publicKey], this._privateKey, plaintext, callback); + // 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; + } - /** - * 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; - } + // decrypt and verify pgp message + openpgp.decryptAndVerifyMessage(this._privateKey, publicKeys, message, onDecrypted); - // 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!')); + function onDecrypted(err, decrypted) { + if (err) { + callback(new Error('Error decrypting and verifying 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)); + } +}; - // 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; + } - /** - * 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; + } - // 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!')); + openpgp.verifyClearSignedMessage(publicKeys, message, function(err, result) { + if (err) { + callback(new Error('Error verifying 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)); + }); +}; - 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; - /** - * 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; - } + // 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; + // 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; - } + 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)); - }; + 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; - } +/** + * 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; - } + 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; } - return PGP; -}); \ No newline at end of file + // 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'; - - 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; - - if (!options.emailAddress || !options.password || !options.phone) { - callback(new Error('Incomplete arguments!')); +'use strict'; + +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; + + 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; - }; - - DeviceStorageDAO.prototype.init = function(emailAddress, callback) { - this._localDbDao.init(emailAddress, callback); - }; - - /** - * 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 = []; - - // nothing to store - if (!list || list.length === 0) { - callback(); - return; - } - // validate type - if (!type) { - callback({ - errMsg: 'Type is not set!' - }); - return; - } - - // format items for batch storing in dao - list.forEach(function(i) { - key = createKey(i, type); - - items.push({ - key: key, - object: i - }); + +'use strict'; + +var DeviceStorageDAO = function(localDbDao) { + this._localDbDao = localDbDao; +}; + +DeviceStorageDAO.prototype.init = function(emailAddress, callback) { + this._localDbDao.init(emailAddress, callback); +}; + +/** + * 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 = []; + + // nothing to store + if (!list || list.length === 0) { + callback(); + return; + } + // validate type + if (!type) { + callback({ + errMsg: 'Type is not set!' + }); + return; + } + + // 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); +}; - 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; +/** + * 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 DeviceStorageDAO; -}); \ No newline at end of file + 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'; - - var util = require('js/crypto/util'), - _ = require('underscore'), - config = require('js/app-config').config, - str = require('js/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; - }; - - - // - // - // 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; +'use strict'; + +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; +}; + + +// +// +// 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; + + // 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; - } + function initKeychain() { + // call getUserKeyPair to read/sync keypair with devicestorage/cloud + self._keychain.getUserKeyPair(emailAddress, function(err, storedKeypair) { + if (err) { + callback(err); + return; + } - // init keychain and then crypto module - initKeychain(); + keypair = storedKeypair; + initFolders(); + }); + } - function initKeychain() { - // call getUserKeyPair to read/sync keypair with devicestorage/cloud - self._keychain.getUserKeyPair(emailAddress, function(err, storedKeypair) { - if (err) { - callback(err); - return; - } + 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; + } - keypair = storedKeypair; - initFolders(); - }); + 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; } - 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; - } + handleGenerated(generatedKeypair); + }); - callback(null, keypair); - }); + 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; } - }; - /** - * 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; + // 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; + } - if (options.keypair) { - // import existing key pair into crypto module - handleExistingKeypair(options.keypair); + // 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; } - // 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) { + // 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; } - handleGenerated(generatedKeypair); + // set decrypted privateKey to pgpMailer + self._pgpbuilder._privateKey = self._pgp._privateKey; + callback(); }); + } - 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 - }); - - if (!matchingPrivUserId || !matchingPubUserId || keypair.privateKey.userId !== self._account.emailAddress || keypair.publicKey.userId !== self._account.emailAddress) { - callback(new Error('User IDs dont match!')); + 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; } - // 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; - } - - // 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; - } - - // set decrypted privateKey to pgpMailer - self._pgpbuilder._privateKey = self._pgp._privateKey; - callback(); - }); - }); - } - }; - - /** - * 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; + }); + } +}; + +/** + * 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; + + if (!self._account.online) { + err = new Error('Client is currently offline!'); + err.code = 42; + callback(err); + return; + } - if (!self._account.online) { - err = new Error('Client is currently offline!'); - err.code = 42; - callback(err); - return; - } + if (options.folder.path === config.outboxMailboxPath) { + return; + } - if (options.folder.path === config.outboxMailboxPath) { + 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; } - 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; + 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; } - 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); + }); - 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); + }); - // 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(); + }); - done(); + function done(err) { + self.done(); // stop the spinner + updateUnreadCount(folder); // update the unread count + callback(err); + } +}; + +/** + * 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; + } - function done(err) { - self.done(); // stop the spinner - updateUnreadCount(folder); // update the unread count - callback(err); + // 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; } - }; - /** - * 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; + // if there are verification messages in the synced messages, handle it + var verificationMessages = _.filter(messages, function(message) { + return message.subject === str.verificationSubject; + }); - self.busy(); + // if there are verification messages, continue after we've tried to verify + if (verificationMessages.length > 0) { + var after = _.after(verificationMessages.length, storeHeaders); - if (!self._account.online) { - done({ - errMsg: 'Client is currently offline!', - code: 42 + 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); + after(); + }); }); 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; - } + // no verification messages, just proceed as usual + storeHeaders(); - // 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); - - 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); - after(); - }); - }); + function storeHeaders() { + if (_.isEmpty(messages)) { + // nothing to do, we're done here + done(); return; } - // no verification messages, just proceed as usual - storeHeaders(); - - function storeHeaders() { - if (_.isEmpty(messages)) { - // nothing to do, we're done here - done(); + // persist the encrypted message to the local storage + self._localStoreMessages({ + folder: folder, + emails: messages + }, function(err) { + if (err) { + done(err); 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; - }); + // 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); - } + [].unshift.apply(folder.messages, messages); // add the new messages to the folder + updateUnreadCount(folder); // update the unread count - // 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; + // notify about new messages only for the inbox + if (folder.type === FOLDER_TYPE_INBOX) { + self.onIncomingMessage(messages); } - - 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); - }); - }); + done(); }); } - }; + }); - /** - * 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); + function done(err) { + self.done(); // stop the spinner + callback(err); + } - // delete only locally - if (options.localOnly || options.folder.path === config.outboxMailboxPath) { - deleteLocal(); - return; - } + // 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; + } - deleteImap(); + 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}/; - function deleteImap() { - if (!self._account.online) { - // no action if we're not online - done({ - errMsg: 'Client is currently offline!', - code: 42 - }); + // there's no valid uuid in the message, so forget about it + if (!uuidRegex.test(uuid)) { + localCallback(null, false); return; } - // delete from IMAP - self._imapDeleteMessage({ - folder: folder, - uid: message.uid - }, function(err) { + // there's a valid uuid in the message, so try to verify it + self._keychain.verifyPublicKey(uuid, function(err) { if (err) { - done(err); + localCallback({ + errMsg: 'Verifying your public key failed: ' + err.errMsg + }, true); return; } - deleteLocal(); + // 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); + }); }); - } - - 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); - } - }; - - /** - * 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 + }); + } +}; + +/** + * 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; + } - // 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; - } + deleteImap(); + function deleteImap() { if (!self._account.online) { // no action if we're not online done({ @@ -579,1153 +506,1222 @@ define(function(require) { 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, - 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; - } + // delete from IMAP + self._imapDeleteMessage({ + folder: folder, + uid: message.uid + }, function(err) { + if (err) { + done(err); + 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; - } + deleteLocal(); + }); + } - // 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; - } + function deleteLocal() { + // delete from indexed db + self._localDeleteMessage({ + folder: folder, + uid: message.uid + }, done); + } - // extract the content - extractContent(); - }); - }); - }); + 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); + } +}; + +/** + * 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; + } - 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'); + // 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 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(); - } + if (!self._account.online) { + // no action if we're not online + done({ + errMsg: 'Client is currently offline!', + code: 42 + }); + return; + } - /* - * 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]; - } + markImap(); - if (!message.signed) { - // message is not signed, so we're done here - return setBody(); + 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; } - // 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); - } + markStorage(); + }); + } - done(); + 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; - function done(err) { - self.done(); - message.loadingBody = false; - callback(err, err ? undefined : message); - } - }; - - EmailDAO.prototype._checkSignatures = function(message, callback) { - var self = this; + // store + self._localStoreMessages({ + folder: folder, + emails: [storedMessage] + }, done); + }); + } - self._keychain.getReceiverPublicKey(message.from[0].address, function(err, senderPublicKey) { - if (err) { - return callback(err); - } + 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; + } - // get the receiver's public key to check the message signature - var senderKey = senderPublicKey ? senderPublicKey.publicKey : undefined; + message.loadingBody = true; - 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(); - /** - * Retrieves an attachment matching a body part for a given uid and a folder + /* + * read this before inspecting the method! * - * @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 + * 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. */ - 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); + 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; } - 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; + var localMessage = localMessages[0]; - // 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; - } + // 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; + }); - message.decryptingBody = true; + // do we need to fetch content from the imap server? + var needsFetch = false; + contentParts.forEach(function(part) { + needsFetch = (typeof part.content === 'undefined'); + }); - 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); + if (!needsFetch) { + // if we have all the content we need, + // we can extract the content + message.bodyParts = localMessage.bodyParts; + extractContent(); + 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.'); - } - - // 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(); + // 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; } - // 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(); - } + // piece together the parsed bodyparts and the empty attachments which have not been parsed + message.bodyParts = parsedBodyParts.concat(attachmentParts); + localMessage.bodyParts = parsedBodyParts.concat(attachmentParts); - // 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(); - }); + // persist it to disk + self._localStoreMessages({ + folder: folder, + emails: [localMessage] + }, function(error) { + if (error) { + done(error); 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(); - } + // extract the content + extractContent(); }); }); }); + } - function showError(msg) { - message.body = msg; - message.decrypted = true; // display error msg in body - done(); + 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(); - message.decryptingBody = false; - callback(err, err ? undefined : message); - } - }; + var root = message.bodyParts; - /** - * 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; + 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; } - 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); - } - - // 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); - }); - }); - }; + var body = _.pluck(filterBodyParts(root, MSG_PART_TYPE_TEXT), MSG_PART_ATTR_CONTENT).join('\n'); - /** - * 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 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(); + } - if (!self._account.online) { - callback({ - errMsg: 'Client is currently offline!', - code: 42 - }); - return; + /* + * 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]; } - self.busy(); - // add suffix to plaintext mail - options.email.body += str.signature + config.cloudUrl + '/' + self._account.emailAddress; + if (!message.signed) { + // message is not signed, so we're done here + return setBody(); + } - // 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) { + // check the signatures for signed messages + self._checkSignatures(message, function(err, signaturesValid) { if (err) { - return callback(err); + return done(err); } - // upload the sent message to the sent folder if necessary - var sentFolder = _.findWhere(self._account.folders, { - type: FOLDER_TYPE_SENT - }); + message.signaturesValid = signaturesValid; + setBody(); + }); - if (self.ignoreUploadOnSent || !sentFolder || !rfcText) { - self.done(); - return callback(); + 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); } - self._imapClient.uploadMessage({ - path: sentFolder.path, - message: rfcText - }, function(err) { - self.done(); - callback(err); - }); - }); - }; + done(); + } + } - /** - * 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); - }); + 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); + } - // - // - // Event Handlers - // - // + // 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; + } - /** - * 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; + message.decryptingBody = true; - self._account.loggingIn = 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); + } - self._imapClient = options.imapClient; - self._pgpMailer = options.pgpMailer; + // 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.'); + } - self._imapClient.login(function(err) { - self._account.loggingIn = false; + // 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 (err) { - callback(err); - return; + // 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(); } - // init folders - self._initFoldersFromImap(function(err) { + // 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; + return; + } - // set status to online after setting cache to prevent race condition - self._account.online = true; + // message had a signature in the ciphertext, so we're done here + setBody(); - // set up the imap client to listen for changes in the inbox - var inbox = _.findWhere(self._account.folders, { - type: FOLDER_TYPE_INBOX - }); + 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 (!inbox) { - return callback(); - } + message.decrypted = true; - self._imapClient.listenForChanges({ - path: inbox.path - }, callback); + // we're done here! + done(); + } }); }); - }; + }); - /** - * 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; + function showError(msg) { + message.body = msg; + message.decrypted = true; // display error msg in body + done(); + } - // logout of imap-client - self._imapClient.logout(function() { - // ignore error, because it's not problem if logout fails - if (callback) { - callback(); - } + 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; + } - // 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; + 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); + } - var folder = _.findWhere(self._account.folders, { - path: options.path + // upload the sent message to the sent folder if necessary + var sentFolder = _.findWhere(self._account.folders, { + type: FOLDER_TYPE_SENT }); - if (!folder) { - // ignore updates for an unknown folder - return; + if (self.ignoreUploadOnSent || !sentFolder || !rfcText) { + self.done(); + return callback(); } - 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)); - }); + 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; + } + self.busy(); + + // add suffix to plaintext mail + options.email.body += str.signature + config.cloudUrl + '/' + self._account.emailAddress; + + // 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); } - }; - - - // - // - // 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 + // upload the sent message to the sent folder if necessary + var sentFolder = _.findWhere(self._account.folders, { + type: FOLDER_TYPE_SENT + }); - // fetch list from local cache - self._devicestorage.listItems(FOLDER_DB_TYPE, 0, null, function(err, stored) { - if (err) { - return done(err); - } + if (self.ignoreUploadOnSent || !sentFolder || !rfcText) { + self.done(); + return callback(); + } - self._account.folders = stored[0] || []; - self._initMessagesFromDisk(done); + 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; - function done(err) { - self.done(); // stop the spinner + if (err) { callback(err); + return; } - }; - - /** - * 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) { + // init folders + self._initFoldersFromImap(function(err) { if (err) { - return done(err); + callback(err); + return; } - // 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; + // 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; + } - // 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; + var uids, highestModseq, lastUid; - // check if there is a folder of this type locally available - localFolderWithType = _.findWhere(self._account.folders, { - type: mbxType + uids = _.pluck(folder.messages, MSG_ATTR_UID).sort(function(a, b) { + return a - b; }); + lastUid = uids[uids.length - 1]; - 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; - } + highestModseq = _.pluck(folder.messages, 'modseq').sort(function(a, b) { + return a - b; + }).pop(); - // 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; - } + mailboxCache[folder.path] = { + exists: lastUid, + uidNext: lastUid + 1, + uidlist: uids, + highestModseq: highestModseq + }; + }); + self._imapClient.mailboxCache = mailboxCache; - if (!wellKnownFolders[mbxType] || !wellKnownFolders[mbxType].length) { - // no imap folders of the respective mailbox type, so nothing to do here - return; - } + // set status to online after setting cache to prevent race condition + self._account.online = true; - /** - * 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; + // set up the imap client to listen for changes in the inbox + var inbox = _.findWhere(self._account.folders, { + type: FOLDER_TYPE_INBOX }); - // if folders have not changed, can fill them with messages directly - if (!foldersChanged) { - return self._initMessagesFromDisk(done); + if (!inbox) { + return callback(); } - // 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); - }); + self._imapClient.listenForChanges({ + path: inbox.path + }, callback); }); - - function done(err) { - self.done(); // stop the spinner - callback(err); + }); +}; + +/** + * 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; + } - /** - * 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 (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 (!self._account.folders || self._account.folders.length === 0) { - return callback(); - } + 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 after = _.after(self._account.folders.length, callback); + var message = _.findWhere(folder.messages, { + uid: changedMsg.uid + }); - self._account.folders.forEach(function(folder) { - if (folder.messages) { - // the folder is already initialized - return after(); + if (!message) { + return; } - // sync messages from disk to the folder model - self.refreshFolder({ - folder: folder - }, function(err) { - if (err) { - return callback(err); - } + // 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; - after(); - }); + self.setFlags({ + folder: folder, + message: message, + localOnly: true + }, self.onError.bind(self)); }); - }; + } +}; - EmailDAO.prototype.busy = function() { - this._account.busy++; - }; - EmailDAO.prototype.done = function() { - if (this._account.busy > 0) { - this._account.busy--; - } - }; +// +// +// 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; - // - // - // IMAP API - // - // + self.busy(); // start the spinner - /** - * 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; + // fetch list from local cache + self._devicestorage.listItems(FOLDER_DB_TYPE, 0, null, function(err, stored) { + if (err) { + return done(err); } - options.path = options.folder.path; - this._imapClient.updateFlags(options, callback); - }; + self._account.folders = stored[0] || []; + self._initMessagesFromDisk(done); + }); - /** - * 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; + 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); } - 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); + // initialize the folders to something meaningful if that hasn't already happened + self._account.folders = self._account.folders || []; - return; - } + // 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 + }]; - // move the message to the trash folder - this._imapClient.moveMessage({ - path: options.folder.path, - destination: trash.path, - uid: options.uid - }, callback); - }; + // indicates if we need to persist anything to disk + var foldersChanged = false; - /** - * 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; + // 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; - if (!this._account.online) { - callback({ - errMsg: 'Client is currently offline!', - code: 42 + // check if there is a folder of this type locally available + localFolderWithType = _.findWhere(self._account.folders, { + type: mbxType }); - return; - } - options.path = options.folder.path; - self._imapClient.listMessages(options, callback); - }; + if (localFolderWithType) { + // we have a local folder available, so let's check if this folder still exists on imap - /** - * 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; + imapFolderWithPath = _.findWhere(wellKnownFolders[mbxType], { + path: localFolderWithType.path + }); - if (!self._account.online) { - callback({ - errMsg: 'Client is currently offline!', - code: 42 - }); - return; - } + if (imapFolderWithPath) { + // folder present on imap, no need to update. + return; + } - options.path = options.folder.path; - self._imapClient.getBodyParts(options, function(err) { - if (err) { - callback(err); + // 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; } - // interpret the raw content of the email - self._mailreader.parse(options, callback); + + /** + * 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); + } - // - // - // Local Storage API - // - // + // 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); + }); + }); - /** - * 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); - }; + 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(); + } - /** - * 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); - }; + var after = _.after(self._account.folders.length, 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; + self._account.folders.forEach(function(folder) { + if (folder.messages) { + // the folder is already initialized + return after(); } - var dbType = 'email_' + path + '_' + (uid || id); - this._devicestorage.removeList(dbType, callback); - }; + // 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; + } - // - // - // Helper Functions - // - // + 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 + }); - /** - * 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; + // 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); - folder.count = folder.path === config.outboxMailboxPath ? allMsgs : unreadMsgs; + return; } - /** - * 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); - } + // 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 result; + return; } - /** - * 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 = ''; + 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; + } - var internalReference = _.findWhere(message.attachments, { - id: src - }); + 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; + } - if (internalReference) { - for (var i = 0; i < internalReference.content.byteLength; i++) { - payload += String.fromCharCode(internalReference.content[i]); - } + 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 + }); - try { - localSource = 'data:application/octet-stream;base64,' + btoa(payload); // try to replace the source - } catch (e) {} + if (internalReference) { + for (var i = 0; i < internalReference.content.byteLength; i++) { + payload += String.fromCharCode(internalReference.content[i]); } - return prefix + localSource + suffix; - }); - } + try { + localSource = 'data:application/octet-stream;base64,' + btoa(payload); // try to replace the source + } catch (e) {} + } + + return prefix + localSource + suffix; + }); +} - return EmailDAO; -}); \ No newline at end of file +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'; - - /** - * 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 - // - - InvitationDAO.INVITE_MISSING = 1; - InvitationDAO.INVITE_PENDING = 2; - InvitationDAO.INVITE_SUCCESS = 4; - - // - // 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!' - }); +'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; +}; + +// +// Constants +// + +InvitationDAO.INVITE_MISSING = 1; +InvitationDAO.INVITE_PENDING = 2; +InvitationDAO.INVITE_SUCCESS = 4; + +// +// 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!' + }); + 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,937 +2,935 @@ * 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; - - var DB_PUBLICKEY = 'publickey', - DB_PRIVATEKEY = 'privatekey', - DB_DEVICENAME = 'devicename', - DB_DEVICE_SECRET = 'devicesecret'; - - var KeychainDAO = function(localDbDao, publicKeyDao, privateKeyDao, crypto, pgp) { - this._localDbDao = localDbDao; - this._publicKeyDao = publicKeyDao; - this._privateKeyDao = privateKeyDao; - this._crypto = crypto; - this._pgp = pgp; - }; - - // - // Public key functions - // - - /** - * 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); - }; - - /** - * 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 = []; - - // return empty array if key ids are emtpy - if (ids.length < 1) { - callback(null, pubkeys); - return; - } - after = _.after(ids.length, function() { - callback(null, pubkeys); - }); +'use strict'; - _.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; - } +var util = require('crypto-lib').util, + config = require('../app-config').config; + +var DB_PUBLICKEY = 'publickey', + DB_PRIVATEKEY = 'privatekey', + DB_DEVICENAME = 'devicename', + DB_DEVICE_SECRET = 'devicesecret'; + +var KeychainDAO = function(localDbDao, publicKeyDao, privateKeyDao, crypto, pgp) { + this._localDbDao = localDbDao; + this._publicKeyDao = publicKeyDao; + this._privateKeyDao = privateKeyDao; + this._crypto = crypto; + this._pgp = pgp; +}; + +// +// Public key functions +// + +/** + * 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); +}; - // check if public key with that id has already been fetched - already = null; - already = _.findWhere(pubkeys, { - _id: i._id +/** + * 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 = []; + + // return empty array if key ids are emtpy + if (ids.length < 1) { + callback(null, pubkeys); + return; + } + + after = _.after(ids.length, function() { + callback(null, pubkeys); + }); + + _.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); - } + return; + } - after(); // asynchronously iterate through objects + // 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(); + }); +}; + +/** + * 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; + } + + if (err) { + // there was an error, exit and inform + callback(err); return; } - // no need to refresh manually imported public keys - if (localKey.imported) { + if (cloudKey && cloudKey._id === localKey._id) { + // the key is present on the server, all is well callback(null, localKey); return; } - // check if the key id still exists on the key server - checkKeyExists(localKey); + // 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 - callback(null, localKey); - return; - } + 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) { - // there was an error, exit and inform - callback(err); - return; - } + if (err) { + callback(err); + return; + } - if (cloudKey && cloudKey._id === localKey._id) { - // the key is present on the server, all is well + // 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; } - // the key has changed, update the key - updateKey(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; + } + + // 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; } - 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; - } + // query primary email address + var pubkey = _.findWhere(allPubkeys, { + userId: userId + }); - if (err) { - callback(err); - 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; } + } + } - // 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 (pubkey && pubkey._id) { + // that user's public key is already in local storage + callback(null, pubkey); + 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); - }); - }); - }); + // 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; } - }; - /** - * 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; + if (err) { + callback(err); + return; + } - // search local keyring for public key - self._localDbDao.list(DB_PUBLICKEY, 0, null, function(err, allPubkeys) { + if (!cloudPubkey) { + // public key has been deleted without replacement + callback(); + return; + } + + self.saveLocalPublicKey(cloudPubkey, function(err) { if (err) { callback(err); return; } - // query primary email address - var pubkey = _.findWhere(allPubkeys, { - userId: userId - }); + callback(null, cloudPubkey); + }); + } +}; - // 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; - } - } - } +// +// Device registration functions +// - if (pubkey && pubkey._id) { - // that user's public key is already in local storage - callback(null, pubkey); - return; - } +/** + * 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; + } - // no public key by that user id in storage - // find from cloud by email address - self._publicKeyDao.getByUserId(userId, onKeyReceived); - }); + this._localDbDao.persist(DB_DEVICENAME, deviceName, callback); +}; - function onKeyReceived(err, cloudPubkey) { - if (err && err.code === 42) { - // offline - callback(); - return; - } +/** + * 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; } - if (!cloudPubkey) { - // public key has been deleted without replacement - callback(); - return; - } - - self.saveLocalPublicKey(cloudPubkey, function(err) { - if (err) { - callback(err); - return; - } + callback(null, deviceSecret); + }); + }); +}; - 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!')); +/** + * 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; } - 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) { + 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 (!deviceName) { - callback(new Error('Device name not set!')); + if (!regSessionKey.encryptedRegSessionKey) { + callback(new Error('Invalid format for session key!')); return; } - callback(null, deviceName); + decryptSessionKey(regSessionKey); }); - }; + } - /** - * 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) { + function decryptSessionKey(regSessionKey) { + self.lookupPublicKey(config.serverPrivateKeyId, function(err, serverPubkey) { if (err) { callback(err); return; } - if (storedDevSecret) { - // a device key is already available locally - callback(null, storedDevSecret); + if (!serverPubkey || !serverPubkey.publicKey) { + callback(new Error('Server public key for device registration not found!')); 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; + // 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!')); } - callback(null, deviceSecret); + uploadDeviceSecret(decrypedSessionKey); }); }); - }; - - /** - * 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) { + } + + function uploadDeviceSecret(regSessionKey) { + // read device secret from local storage + self.getDeviceSecret(function(err, deviceSecret) { 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) { + // generate iv + var iv = util.random(config.symIvSize); + // encrypt deviceSecret + self._crypto.encrypt(deviceSecret, regSessionKey, iv, function(err, encryptedDeviceSecret) { if (err) { callback(err); return; } - if (!regSessionKey.encryptedRegSessionKey) { - callback(new Error('Invalid format for session key!')); - return; - } - - decryptSessionKey(regSessionKey); + // upload encryptedDeviceSecret + self._privateKeyDao.uploadDeviceSecret({ + userId: options.userId, + deviceName: devName, + encryptedDeviceSecret: encryptedDeviceSecret, + iv: iv + }, callback); }); - } - - 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; - } +// +// Private key functions +// - // 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!')); - } +/** + * 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; + } - uploadDeviceSecret(decrypedSessionKey); - }); - }); + if (!authSessionKey.encryptedAuthSessionKey || !authSessionKey.encryptedChallenge || !authSessionKey.sessionId) { + callback(new Error('Invalid format for session key!')); + return; } - function uploadDeviceSecret(regSessionKey) { - // read device secret from local storage - self.getDeviceSecret(function(err, deviceSecret) { - if (err) { - callback(err); - return; - } + // remember session id for verification + sessionId = authSessionKey.sessionId; - // generate iv - var iv = util.random(config.symIvSize); - // encrypt deviceSecret - self._crypto.encrypt(deviceSecret, regSessionKey, iv, function(err, encryptedDeviceSecret) { - if (err) { - callback(err); - return; - } + decryptSessionKey(authSessionKey); + }); - // 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) { + function decryptSessionKey(authSessionKey) { + self.lookupPublicKey(config.serverPrivateKeyId, function(err, serverPubkey) { if (err) { callback(err); return; } - if (!authSessionKey.encryptedAuthSessionKey || !authSessionKey.encryptedChallenge || !authSessionKey.sessionId) { - callback(new Error('Invalid format for session key!')); + if (!serverPubkey || !serverPubkey.publicKey) { + callback(new Error('Server public key for authentication not found!')); 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 session key - var ct1 = authSessionKey.encryptedAuthSessionKey; - self._pgp.decrypt(ct1, serverPubkey.publicKey, function(err, decryptedSessionKey, signaturesValid) { + // 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!')); } - // 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); - }); + encryptChallenge(decryptedSessionKey, decryptedChallenge); }); }); - } + }); + } + + function encryptChallenge(sessionKey, challenge) { + // get device secret + self.getDeviceSecret(function(err, deviceSecret) { + if (err) { + callback(err); + return; + } - function encryptChallenge(sessionKey, challenge) { - // get device secret - self.getDeviceSecret(function(err, deviceSecret) { + var iv = util.random(config.symIvSize); + // encrypt the challenge + self._crypto.encrypt(challenge, sessionKey, iv, function(err, encryptedChallenge) { if (err) { callback(err); return; } - var iv = util.random(config.symIvSize); - // encrypt the challenge - self._crypto.encrypt(challenge, sessionKey, iv, function(err, encryptedChallenge) { + // encrypt the device secret + self._crypto.encrypt(deviceSecret, sessionKey, iv, function(err, encryptedDeviceSecret) { 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); - }); + 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; + } - function replyChallenge(response, sessionKey) { - // respond to challenge by uploading the with the session key encrypted challenge - self._privateKeyDao.verifyAuthentication({ - userId: userId, + callback(null, { sessionId: sessionId, - encryptedChallenge: response.encryptedChallenge, - encryptedDeviceSecret: response.encryptedDeviceSecret, - iv: response.iv - }, function(err) { - if (err) { - callback(err); - return; - } - - callback(null, { - sessionId: sessionId, - sessionKey: sessionKey - }); + 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); +/** + * 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; + } - 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) { + 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; } - encryptPrivateKey(key); + var payload = { + _id: privkeyId, + userId: options.userId, + encryptedPrivateKey: ct, + salt: salt, + iv: iv + }; + + uploadPrivateKey(payload); }); - } + }); + } - function encryptPrivateKey(encryptionKey) { - // get private key from local storage - self.getUserKeyPair(options.userId, function(err, keypair) { + 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; } - 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 - }; + // replace the encryptedPrivateKey with the double wrapped ciphertext + payload.encryptedPrivateKey = ct; + // set sessionId + payload.sessionId = authSessionKey.sessionId; - uploadPrivateKey(payload); - }); + // upload the encrypted priavet key + self._privateKeyDao.upload(payload, callback); }); - } + }); + } +}; - function uploadPrivateKey(payload) { - // authenticate to server for upload - self._authenticateToPrivateKeyServer(options.userId, function(err, authSessionKey) { - if (err) { - callback(err); - return; - } +/** + * 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); +}; - // 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; - } +/** + * 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); +}; - // replace the encryptedPrivateKey with the double wrapped ciphertext - payload.encryptedPrivateKey = ct; - // set sessionId - payload.sessionId = authSessionKey.sessionId; +/** + * 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); +}; - // 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!')); +/** + * 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; } - // derive key from the code and the salt using PBKDF2 - self._crypto.deriveKey(code, salt, keySize, function(err, key) { + 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(err); + callback(new Error('Invalid keychain code!')); return; } - decryptAndStore(key); - }); + // 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; + } - function decryptAndStore(derivedKey) { - // decrypt the private key with the derived key - var ct = options.encryptedPrivateKey, - iv = options.iv; + var keyObject = { + _id: options._id, + userId: options.userId, + encryptedKey: privateKeyArmored + }; - self._crypto.decrypt(ct, derivedKey, iv, function(err, privateKeyArmored) { + // store private key locally + self.saveLocalPrivateKey(keyObject, function(err) { if (err) { - callback(new Error('Invalid keychain code!')); + callback(err); return; } - // validate pgp key - var keyParams; - try { - keyParams = self._pgp.getKeyParams(privateKeyArmored); - } catch (e) { - callback(new Error('Error parsing private PGP key!')); - return; - } + callback(null, keyObject); + }); + }); + } +}; - if (keyParams._id !== options._id || keyParams.userId !== options.userId) { - callback(new Error('Private key parameters don\'t match with public key\'s!')); - return; - } +// +// Keypair functions +// - var keyObject = { - _id: options._id, - userId: options.userId, - encryptedKey: privateKeyArmored - }; +/** + * 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; - // store private key locally - self.saveLocalPrivateKey(keyObject, function(err) { - if (err) { - callback(err); - return; - } + // search for user's public key locally + self._localDbDao.list(DB_PUBLICKEY, 0, null, function(err, allPubkeys) { + if (err) { + callback(err); + return; + } - callback(null, keyObject); - }); - }); + 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; } - }; - - // - // 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) { + + // 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; } - var pubkey = _.findWhere(allPubkeys, { - userId: userId - }); + 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; + } - if (pubkey && pubkey._id) { - // that user's public key is already in local storage... - // sync keypair to the cloud - syncKeypair(pubkey._id); + // 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; } - // no public key by that user id in storage - // find from cloud by email address - self._publicKeyDao.getByUserId(userId, function(err, cloudPubkey) { + // persist private key in local storage + self.lookupPrivateKey(keypairId, function(err, savedPrivkey) { + var keys = {}; + 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; + if (savedPubkey && savedPubkey.publicKey) { + keys.publicKey = savedPubkey; } - // 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; + if (savedPrivkey && savedPrivkey.encryptedKey) { + keys.privateKey = savedPrivkey; } - // persist private key in local storage - self.lookupPrivateKey(keypairId, function(err, savedPrivkey) { - var keys = {}; + callback(null, keys); + }); + }); + } +}; - if (err) { - callback(err); - return; - } +/** + * 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; - if (savedPubkey && savedPubkey.publicKey) { - keys.publicKey = savedPubkey; - } + // validate input + if (!keypair || !keypair.publicKey || !keypair.privateKey || !keypair.publicKey.userId || keypair.publicKey.userId !== keypair.privateKey.userId) { + callback({ + errMsg: 'Incorrect input!' + }); + return; + } - if (savedPrivkey && savedPrivkey.encryptedKey) { - keys.privateKey = savedPrivkey; - } + // don't check the user's own public key for deletion in refreshKeyForUserId + keypair.publicKey.imported = true; - 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!' - }); + // store public key locally + self.saveLocalPublicKey(keypair.publicKey, function(err) { + if (err) { + callback(err); 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) { + // persist public key in cloud storage + self._publicKeyDao.put(keypair.publicKey, function(err) { + // validate result 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); - }); + // store private key locally + self.saveLocalPrivateKey(keypair.privateKey, callback); }); - }; + }); +}; - // - // Helper functions - // +// +// Helper functions +// - KeychainDAO.prototype.lookupPublicKey = function(id, callback) { - var self = this; +KeychainDAO.prototype.lookupPublicKey = function(id, callback) { + var self = this; - if (!id) { - callback({ - errMsg: 'ID must be set for public key query!' - }); + 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; } - // lookup in local storage - self._localDbDao.read(DB_PUBLICKEY + '_' + id, function(err, pubkey) { + if (pubkey) { + callback(null, pubkey); + return; + } + + // fetch from cloud storage + self._publicKeyDao.get(id, function(err, cloudPubkey) { if (err) { callback(err); return; } - if (pubkey) { - callback(null, pubkey); - return; - } - - // fetch from cloud storage - self._publicKeyDao.get(id, function(err, cloudPubkey) { + // cache public key in cache + self.saveLocalPublicKey(cloudPubkey, function(err) { if (err) { callback(err); return; } - // cache public key in cache - self.saveLocalPublicKey(cloudPubkey, function(err) { - if (err) { - callback(err); - return; - } - - callback(null, cloudPubkey); - }); + 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(); + }); +}; - callback(); +/** + * Create or update an object + */ +LawnchairDAO.prototype.persist = function(key, object, callback) { + if (!key || !object) { + callback({ + errMsg: 'Key and Object must be set!' }); - }; - - /** - * Create or update an object - */ - LawnchairDAO.prototype.persist = function(key, object, callback) { - if (!key || !object) { + return; + } + + this._db.save({ + key: key, + object: object + }, function(persisted) { + if (persisted.key !== key) { callback({ - errMsg: 'Key and Object must be set!' + errMsg: 'Persisting failed!' }); return; } - this._db.save({ - key: key, - object: object - }, function(persisted) { - if (persisted.key !== key) { - callback({ - errMsg: 'Persisting failed!' - }); - return; - } + callback(); + }); +}; - 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; + } - /** - * Persist a bunch of items at once - */ - LawnchairDAO.prototype.batch = function(list, callback) { - if (!(list instanceof Array)) { + this._db.batch(list, function(res) { + if (!res) { callback({ - errMsg: 'Input must be of type Array!' + errMsg: 'Persisting batch failed!' }); return; } - this._db.batch(list, function(res) { - if (!res) { - callback({ - errMsg: 'Persisting batch failed!' - }); - return; - } + callback(); + }); +}; - callback(); +/** + * Read a single item by its key + */ +LawnchairDAO.prototype.read = function(key, callback) { + if (!key) { + callback({ + errMsg: 'Key must be specified!' }); - }; + return; + } - /** - * 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(); } + }); +}; - 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!' }); - }; - - /** - * 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; - } + return; + } - // get all keys - self._db.keys(function(keys) { + // 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]); - } + // check if key begins with type + keys.forEach(function(key) { + if (key.indexOf(type) === 0) { + matchingKeys.push(key); } + }); - // return if there are no matching keys - if (intervalKeys.length === 0) { - callback(null, list); - return; + // 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]); } + } - // fetch all items from data-store with matching key - self._db.get(intervalKeys, function(intervalList) { - intervalList.forEach(function(item) { - list.push(item.object); - }); + // return if there are no matching keys + if (intervalKeys.length === 0) { + callback(null, list); + return; + } - // return only the interval between offset and num - callback(null, list); + // 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; +/** + * 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); } + }); - // remove all matching keys - after = _.after(matchingKeys.length, callback); - _.each(matchingKeys, function(key) { - self._db.remove(key, after); - }); + 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); - }; +/** + * 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'; - - var PrivateKeyDAO = function(restDao) { - this._restDao = restDao; - }; - - // - // 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; - - if (!options.userId || !options.deviceName) { - callback(new Error('Incomplete arguments!')); +'use strict'; + +var PrivateKeyDAO = function(restDao) { + this._restDao = restDao; +}; + +// +// 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; + + 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); - }; - - // - // 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!')); + 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; } - 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); - }; - - /** - * 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; - } - - 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 + 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); +}; + +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'; - - 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; - - 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); - }); - }; - - /** - * 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; - } - - if (err) { - callback(err); - return; - } - - 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; - - this._restDao.get({ - uri: uri - }, function(err, keys) { - // not found - if (err && err.code === 404) { - callback(); - return; - } - - if (err) { - callback(err); - 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; - } - - 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); - }; - - /** - * 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 +'use strict'; + +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; + + 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); + }); +}; + +/** + * 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; + } + + if (err) { + callback(err); + return; + } + + 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; + + this._restDao.get({ + uri: uri + }, function(err, keys) { + // not found + if (err && err.code === 404) { + callback(); + return; + } + + if (err) { + callback(err); + 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; + } + + 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); +}; + +/** + * 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); +}; + +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'; - - var config = require('js/app-config').config; +'use strict'; + +var config = require('../app-config').config; + +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); +}; + +/** + * 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); +}; + +/** + * DELETE (remove) request + */ +RestDAO.prototype.remove = function(uri, callback) { + this._processRequest({ + method: 'DELETE', + uri: uri + }, callback); +}; + +// +// helper functions +// + +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; + } + + 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; + } + + 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; + + 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; + } - var RestDAO = function(baseUri) { - if (baseUri) { - this._baseUri = baseUri; - } else { - this._baseUri = config.cloudUrl; + callback(null, res, xhr.status); + return; } - }; - - /** - * 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); + callback({ + code: xhr.status, + errMsg: xhr.statusText + }); }; - /** - * PUT (update) request - */ - RestDAO.prototype.put = function(item, uri, callback) { - this._processRequest({ - method: 'PUT', - payload: item, - uri: uri - }, callback); + xhr.onerror = function() { + callback({ + code: 42, + errMsg: 'Error calling ' + options.method + ' on ' + options.uri + }); }; - /** - * DELETE (remove) request - */ - RestDAO.prototype.remove = function(uri, callback) { - this._processRequest({ - method: 'DELETE', - uri: uri - }, callback); - }; - - // - // helper functions - // - - 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; - } - - 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; - } - - 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; - - 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; - } - - callback({ - code: xhr.status, - errMsg: xhr.statusText - }); - }; - - xhr.onerror = function() { - callback({ - code: 42, - errMsg: 'Error calling ' + options.method + ' on ' + options.uri - }); - }; - - xhr.send(options.payload ? JSON.stringify(options.payload) : undefined); - }; + xhr.send(options.payload ? JSON.stringify(options.payload) : undefined); +}; - return RestDAO; -}); \ No newline at end of file +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); - } - }; +/** + * 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'); +function handleBackButton(event) { + axe.debug(DEBUG_TAG, 'back button pressed'); - // this disarms the default behavior which we NEVER want - event.preventDefault(); - event.stopPropagation(); + // 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(); - } + 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(); } +} - return backBtnHandler; -}); \ No newline at end of file +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'; - - var TCPSocket = require('tcp-socket'), - appConfig = require('js/app-config'), - cfg = appConfig.config, - strings = appConfig.string, - ImapClient = require('imap-client'), - SmtpClient = require('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() {}; - - - // - // 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; - - - // - // 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; - - // 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 - } - }); - }; +'use strict'; + +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() {}; + + +// +// 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; + + +// +// 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; + + // 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 + } + }); +}; + +/** + * 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!')); + } - /** - * 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!')); + 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 +// + +/** + * 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); + } }; + socket.onopen = function() { + socket.close(); + }; - // - // Internal API - // + 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; + } + }; - /** - * 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(); + // 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(OFFLINE, strings.connDocOffline)); + callback(createError(GENERIC_ERROR, strings.connDocGenericError.replace('{0}', host).replace('{1}', error.message), error)); } }; - /** - * 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(); - }; + self._imap.login(function() { + loggedIn = true; - 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.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.onerror = function(error) { + if (error) { + errored = true; + callback(createError(AUTH_REJECTED, strings.connDocAuthRejected.replace('{0}', host), error)); + } + }; - self._smtp.onclose = function() { - if (!errored) { - callback(); - } - }; + self._smtp.onidle = function() { + self._smtp.quit(); + }; - self._smtp.connect(); + self._smtp.onclose = function() { + if (!errored) { + callback(); + } }; + self._smtp.connect(); +}; - // - // Helper Functions - // - function createError(code, message, underlyingError) { - var error = new Error(message); - error.code = code; - error.underlyingError = underlyingError; +// +// Helper Functions +// - return error; - } +function createError(code, message, underlyingError) { + var error = new Error(message); + error.code = code; + error.underlyingError = underlyingError; + + return error; +} - return ConnectionDoctor; -}); \ No newline at end of file +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('../app-config').config; + +var self = {}; + +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; + } - var cfg = require('js/app-config').config; + if (!self.hasPermission) { + // don't wait until callback returns + Notification.requestPermission(function(permission) { + if (permission === "granted") { + self.hasPermission = true; + } + }); + } - var self = {}; + var notification = new Notification(options.title, { + body: options.message, + icon: cfg.iconPath + }); + notification.onclick = function() { + window.focus(); + options.onClick(); + }; - if (window.Notification) { - self.hasPermission = Notification.permission === "granted"; + if (options.timeout > 0) { + setTimeout(function() { + notification.close(); + }, options.timeout); } - /** - * 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 - }); - notification.onclick = function() { - window.focus(); - options.onClick(); - }; - - if (options.timeout > 0) { - setTimeout(function() { - notification.close(); - }, options.timeout); - } - - return notification; - }; + return notification; +}; - self.close = function(notification) { - notification.close(); - }; +self.close = function(notification) { + notification.close(); +}; - return self; -}); \ No newline at end of file +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); +}; - /** - * 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 }; - /** - * 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; + } - // 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; + } - 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; } - // 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); - }); + 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; +/** + * 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; - } + 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); + // 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; + } - /** - * Get email address from google api - * @param {String} token The oauth token - */ - OAuth.prototype.queryEmailAddress = function(token, callback) { - if (!token) { + // 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: 'Invalid OAuth token!' + errMsg: 'Error looking up email address on google api!' }); 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); - }); - }; + callback(null, info.email); + }); +}; - return OAuth; -}); \ No newline at end of file +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'; - - 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'); - - /** - * 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(); +'use strict'; + +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; +}; + +/** + * 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; } - scriptOptions = { - appConfigStorage: self._appConfigStorage, - userStorage: self._userStorage, - auth: self._auth - }; - - // add all the necessary database updates to the queue - for (var i = options.currentVersion; i < options.targetVersion; i++) { - queue.push(self._updateScripts[i]); + // parse the database version number + if (items && items.length > 0) { + currentVersion = parseInt(items[0], 10); } - // takes the next update from the queue and executes it - function executeNextUpdate(err) { - if (err) { - callback(err); - return; - } + 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 + }; - if (queue.length < 1) { - // we're done - callback(); - return; - } + // add all the necessary database updates to the queue + for (var i = options.currentVersion; i < options.targetVersion; i++) { + queue.push(self._updateScripts[i]); + } - // process next update - var script = queue.shift(); - script(scriptOptions, executeNextUpdate); + // takes the next update from the queue and executes it + function executeNextUpdate(err) { + if (err) { + callback(err); + return; } - executeNextUpdate(); - }; + if (queue.length < 1) { + // we're done + callback(); + return; + } - /** - * 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(); - } + // 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(); } - }); - }); - 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'; - - var config = require('js/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; - - - var imap = config.gmail.imap, - smtp = config.gmail.smtp; - - // load the email address (if existing) - loadFromDB(EMAIL_ADDR_DB_KEY, function(err, emailAddress) { +'use strict'; + +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; + + + var imap = config.gmail.imap, + smtp = config.gmail.smtp; + + // 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 (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 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); + } - 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); } - // 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'; - - 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'; +'use strict'; + +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; + +/** + * 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) { + 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; + }); - var POST_UPDATE_DB_VERSION = 5; + if (foldersForType.length <= 1) { + return; // nothing to do here + } - /** - * 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 duplicate folders + for (var i = 1; i < foldersForType.length; i++) { + folders.splice(folders.indexOf(foldersForType[i]), 1); + } + }); - // remove the emails - options.userStorage.listItems(FOLDER_DB_TYPE, 0, null, function(err, stored) { + 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 | 164 +- test/unit/add-account-ctrl-test.js | 354 +- test/unit/admin-dao-test.js | 216 +- test/unit/app-controller-test.js | 335 +- test/unit/auth-test.js | 601 ++- test/unit/backbutton-handler-test.js | 103 +- test/unit/connection-doctor-test.js | 671 ++- test/unit/contacts-ctrl-test.js | 292 +- test/unit/crypto-test.js | 73 +- test/unit/devicestorage-dao-test.js | 134 +- test/unit/dialog-ctrl-test.js | 72 +- test/unit/email-dao-test.js | 3875 ++++++++--------- test/unit/index.html | 63 +- test/unit/invitation-dao-test.js | 173 +- test/unit/keychain-dao-test.js | 2206 +++++----- test/unit/lawnchair-dao-test.js | 232 +- test/unit/login-ctrl-test.js | 420 +- test/unit/login-existing-ctrl-test.js | 192 +- test/unit/login-initial-ctrl-test.js | 317 +- test/unit/login-new-device-ctrl-test.js | 318 +- .../login-privatekey-download-ctrl-test.js | 426 +- test/unit/login-set-credentials-ctrl-test.js | 162 +- test/unit/mail-list-ctrl-test.js | 774 ++-- test/unit/main.js | 113 - test/unit/navigation-ctrl-test.js | 156 +- test/unit/oauth-test.js | 323 +- test/unit/outbox-bo-test.js | 511 ++- test/unit/pgp-test.js | 746 ++-- test/unit/privatekey-dao-test.js | 324 +- test/unit/privatekey-upload-ctrl-test.js | 467 +- test/unit/publickey-dao-test.js | 244 +- test/unit/read-ctrl-test.js | 310 +- test/unit/rest-dao-test.js | 350 +- test/unit/set-passphrase-ctrl-test.js | 200 +- test/unit/update-handler-test.js | 719 ++- test/unit/write-ctrl-test.js | 656 ++- 40 files changed, 8677 insertions(+), 8825 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: [] + } + }, + unitTest: { + files: { + 'test/unit/index.js': ['test/unit/*-test.js'] + }, + options: { + external: [] } }, - /* TODO: - tls-worker: {}, - mailreader-worker: {}, - pbkdf2-worker: {}, - unitTest: {}, - unitTest: {}, - integrationTest: {} + /* + 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 - }); + 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 - }); + angular.module('accounttest', []); + mocks.module('accounttest'); + mocks.inject(function($rootScope, $controller) { + scope = $rootScope.$new(); + scope.state = {}; + accountCtrl = $controller(AccountCtrl, { + $scope: scope }); }); + }); - afterEach(function() {}); + 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('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(); + }); + 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(); + 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(); - }); + 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(); - }; + 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(); - }); + 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'; - - 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'); - - 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); - - 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() {}); - - ctrl = $controller(AddAccountCtrl, { - $location: location, - $scope: scope, - $routeParams: {} - }); +'use strict'; + +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; + + 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 = {}; + + 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: {} }); }); + }); - afterEach(function() { - // restore the app controller module - appController._auth = origAuth; - - location.path.restore(); - location.search.restore(); - if (scope.$apply.restore) { - scope.$apply.restore(); - } - }); + afterEach(function() { + // restore the app controller module + appController._auth = origAuth; - describe('createWhiteoutAccount', function() { - it('should return early for invalid form', function() { - scope.form.$invalid = true; - scope.createWhiteoutAccount(); - expect(adminStub.createUser.called).to.be.false; - }); + location.path.restore(); + location.search.restore(); + if (scope.$apply.restore) { + scope.$apply.restore(); + } + }); - 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; - }); + 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 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; - }); + 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; }); - 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 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; + }); + }); - it('should fail to error creating user', function(done) { - scope.formValidate.$invalid = false; - scope.token = 'asfd'; - adminStub.validateUser.yieldsAsync(new Error('asdf')); + describe('validateUser', function() { + it('should return early for invalid form', function() { + scope.formValidate.$invalid = true; + scope.validateUser(); + expect(adminStub.validateUser.called).to.be.false; + }); - scope.$apply = function() { - expect(scope.busyValidate).to.be.false; - expect(scope.errMsgValidate).to.equal('asdf'); - expect(adminStub.validateUser.calledOnce).to.be.true; - done(); - }; + it('should fail to error creating user', function(done) { + scope.formValidate.$invalid = false; + scope.token = 'asfd'; + adminStub.validateUser.yieldsAsync(new Error('asdf')); - scope.validateUser(); - expect(scope.busyValidate).to.be.true; - }); + scope.$apply = function() { + expect(scope.busyValidate).to.be.false; + expect(scope.errMsgValidate).to.equal('asdf'); + expect(adminStub.validateUser.calledOnce).to.be.true; + done(); + }; - it('should work', function(done) { - scope.formValidate.$invalid = false; - scope.token = 'asfd'; - adminStub.validateUser.yieldsAsync(); + scope.validateUser(); + expect(scope.busyValidate).to.be.true; + }); - scope.login = function() { - expect(scope.busyValidate).to.be.true; - expect(scope.errMsgValidate).to.be.undefined; - expect(adminStub.validateUser.calledOnce).to.be.true; - done(); - }; + it('should work', function(done) { + scope.formValidate.$invalid = false; + scope.token = 'asfd'; + adminStub.validateUser.yieldsAsync(); - scope.validateUser(); + 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(); + 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; - }); + 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; - } - }; + describe('connectToGoogle', function() { + it('should forward to login', function() { + authStub._oauth = { + isSupported: function() { + return true; + } + }; - authStub.getOAuthToken.yields(); + authStub.getOAuthToken.yields(); - scope.connectToGoogle(); + 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; - }); + 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; - } - }; + it('should not use oauth for gmail', function() { + authStub._oauth = { + isSupported: function() { + return false; + } + }; - scope.connectToGoogle(); + 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; - }); + 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; - } - }; + it('should not forward to login when oauth fails', function(done) { + authStub._oauth = { + isSupported: function() { + return true; + } + }; - authStub.getOAuthToken.yields(new Error()); + 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; + scope.onError = function(err) { + expect(err).to.exist; + expect(location.path.called).to.be.false; + expect(location.search.called).to.be.false; - done(); - }; + done(); + }; - scope.connectToGoogle(); - }); + scope.connectToGoogle(); }); + }); - describe('connectTo', function() { - it('should forward to login', function() { - var provider = 'wmail'; - scope.connectTo(provider); + 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; - }); + 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() {}); + + describe('createUser', function() { + it('should fail due to incomplete args', function(done) { + var opt = { + emailAddress: emailAddress + }; - afterEach(function() {}); + adminDao.createUser(opt, function(err) { + expect(err).to.exist; + done(); + }); + }); - describe('createUser', 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.createUser(opt, function(err) { - expect(err).to.exist; - done(); - }); + restDaoStub.post.withArgs(opt, '/user').yields({ + code: 409 }); - 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(); - }); + 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' - }; + 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()); + 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(); - }); + 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' - }; + it('should work', function(done) { + var opt = { + emailAddress: emailAddress, + password: password, + phone: '12345' + }; - restDaoStub.post.withArgs(opt, '/user').yields(); + 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.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 - }; + 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(); - }); + 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' - }; + 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()); + 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(); - }); + 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' + }; - it('should work with no error object', function(done) { - var opt = { - emailAddress: emailAddress, - token: 'H45Z6D' - }; + restDaoStub.post.withArgs(opt, '/user/validate').yields(); - 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(); + }); + }); - 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 }); - 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.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'; - - 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; - - describe('App Controller unit tests', function() { - var emailDaoStub, outboxStub, updateHandlerStub, appConfigStoreStub, devicestorageStub, isOnlineStub, authStub; +'use strict'; + +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; + + 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'); + }); - 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(); + }); - 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('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('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('onDisconnect', function() { - it('should work', function() { - controller.onDisconnect(); + 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('logout', function() { - it('should work', function(done) { - authStub.logout.yields(); - emailDaoStub.onDisconnect.yields(new Error()); + describe('onConnect', function() { + beforeEach(function() { + controller._emailDao._account = {}; + }); - controller.onError = function(err) { - expect(err).to.exist; - expect(authStub.logout.calledOnce).to.be.true; - expect(emailDaoStub.onDisconnect.calledOnce).to.be.true; - done(); - }; + it('should not connect if offline', function(done) { + isOnlineStub.returns(false); - controller.logout(); + controller.onConnect(function(err) { + expect(err).to.not.exist; + done(); }); }); - describe('onConnect', function() { - beforeEach(function() { - controller._emailDao._account = {}; - }); - - it('should not connect if offline', function(done) { - isOnlineStub.returns(false); + 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(); - }); + 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; + 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.not.exist; - done(); - }); + controller.onConnect(function(err) { + expect(err).to.exist; + expect(authStub.getCredentials.calledOnce).to.be.true; + 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(); - 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(); - }); + 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; + describe('init', function() { + var onConnectStub, emailAddress; - beforeEach(function() { - emailAddress = 'alice@bob.com'; + beforeEach(function() { + emailAddress = 'alice@bob.com'; - // onConnect - onConnectStub = sinon.stub(controller, 'onConnect'); - }); + // onConnect + onConnectStub = sinon.stub(controller, 'onConnect'); + }); - afterEach(function() { - onConnectStub.restore(); - }); + afterEach(function() { + onConnectStub.restore(); + }); - it('should fail due to error in storage initialization', function(done) { - devicestorageStub.init.withArgs(undefined).yields({}); + 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(); - }); + 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 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 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(); - }); + 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(); }); }); }); 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'; - - 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; - - 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; - - // 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' - }; +'use strict'; + +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; + + // 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' + }; + + 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); - 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; + 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); - done(); - }); + 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('#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('#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; - - 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(); - }); + 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; + + 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(); }); }); + }); - describe('#getOAuthToken', function() { - it('should refresh token with known email address', function(done) { - auth.emailAddress = emailAddress; - auth.oauthToken = 'oldToken'; + 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); + 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); + 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; + expect(oauthStub.refreshToken.calledOnce).to.be.true; - done(); - }); + done(); }); + }); - it('should fetch token with known email address', function(done) { - auth.emailAddress = emailAddress; - oauthStub.getOAuthToken.withArgs(emailAddress).yieldsAsync(null, oauthToken); + 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); + 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.getOAuthToken.calledOnce).to.be.true; - done(); - }); + 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); + 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); + 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; + expect(oauthStub.getOAuthToken.calledOnce).to.be.true; + expect(oauthStub.queryEmailAddress.calledOnce).to.be.true; - done(); - }); + done(); }); + }); - it('should fail when email address fetch fails', function(done) { - oauthStub.getOAuthToken.yieldsAsync(null, oauthToken); - oauthStub.queryEmailAddress.yieldsAsync(new Error()); + 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; + 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; + expect(oauthStub.getOAuthToken.calledOnce).to.be.true; + expect(oauthStub.queryEmailAddress.calledOnce).to.be.true; - done(); - }); + done(); }); + }); - it('should fail when oauth fetch fails', function(done) { - oauthStub.getOAuthToken.yieldsAsync(new Error()); + 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; + 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; + 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]); - - 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.passwordNeedsDecryption).to.be.true; - - expect(storageStub.listItems.callCount).to.equal(7); + 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); - done(); - }); + expect(auth.passwordNeedsDecryption).to.be.true; + + expect(storageStub.listItems.callCount).to.equal(7); + + done(); }); + }); - it('should fail', function(done) { - storageStub.listItems.yieldsAsync(new Error()); + 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; + 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; + expect(storageStub.listItems.calledOnce).to.be.true; - done(); - }); + done(); }); }); + }); - describe('#handleCertificateUpdate', function() { - var storeCredentialsStub; - var dummyCert = 'cert'; + describe('#handleCertificateUpdate', function() { + var storeCredentialsStub; + var dummyCert = 'cert'; - function onConnectDummy() {} + function onConnectDummy() {} - beforeEach(function() { - storeCredentialsStub = sinon.stub(auth, 'storeCredentials'); - }); + beforeEach(function() { + storeCredentialsStub = sinon.stub(auth, 'storeCredentials'); + }); - it('should work for Trust on first use', function(done) { - auth.imap = {}; - storeCredentialsStub.yields(); + it('should work for Trust on first use', function(done) { + auth.imap = {}; + storeCredentialsStub.yields(); - function callback(err) { - expect(err).to.not.exist; - expect(storeCredentialsStub.callCount).to.equal(1); - done(); - } - auth.handleCertificateUpdate('imap', onConnectDummy, callback, dummyCert); - }); + 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 stored cert', function() { + auth.imap = { + ca: dummyCert + }; + storeCredentialsStub.yields(); - it('should work for stored cert', function() { - auth.imap = { - ca: dummyCert - }; - storeCredentialsStub.yields(); + auth.handleCertificateUpdate('imap', onConnectDummy, onConnectDummy, dummyCert); + expect(storeCredentialsStub.callCount).to.equal(0); + }); - auth.handleCertificateUpdate('imap', onConnectDummy, onConnectDummy, dummyCert); + it('should work for pinned cert', function(done) { + auth.imap = { + ca: 'other', + pinned: true + }; + storeCredentialsStub.yields(); + + 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 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(); - } - } + function onConnect(callback) { + callback(); + } - function onConnect(callback) { - callback(); - } - - auth.handleCertificateUpdate('imap', onConnect, callback, dummyCert); - }); + auth.handleCertificateUpdate('imap', onConnect, callback, dummyCert); }); + }); - describe('#logout', function() { - it('should fail to to error in calling db clear', function(done) { - storageStub.clear.yields(new Error()); + describe('#logout', function() { + it('should fail to to error in calling db clear', function(done) { + storageStub.clear.yields(new Error()); - auth.logout(function(err) { - expect(err).to.exist; - done(); - }); + auth.logout(function(err) { + expect(err).to.exist; + done(); }); + }); - it('should work', function(done) { - storageStub.clear.yields(); + 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'; - - 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; +'use strict'; + +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; + + beforeEach(function() { + // + // Stubs + // + + // 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; + }); - describe('Connection Doctor', function() { - var doctor; - var socketStub, imapStub, smtpStub, credentials; + afterEach(function() { + TCPSocket.open.restore(); + }); - beforeEach(function() { - // - // Stubs - // - - // there is no socket shim for for this use case, use dummy object - socketStub = { - close: function() { - this.onclose(); + 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); } - }; - - 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; + done(); + }); }); + }); - afterEach(function() { - TCPSocket.open.restore(); - }); + 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; - 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(); - }); + done(); }); + + socketStub.oncert(); + socketStub.onopen(); }); - describe('#_checkReachable', function() { - it('should be able to reach the host w/o cert', function(done) { - credentials.imap.ca = undefined; + 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.oncert(); - socketStub.onopen(); + 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(); }); - 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'; - } - }); + socketStub.onopen(); + }); - 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; + 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(); + }); - done(); - }); + socketStub.oncert(); + socketStub.onerror(); + socketStub.onclose(); + }); - socketStub.onopen(); + 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(); }); - 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(); + socketStub.onerror({ + data: new Error() }); + 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; + 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 - done(); - }); + 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; - socketStub.onerror({ - data: new Error() - }); - socketStub.onclose(); + done(); }); + }); + }); - 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 + 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._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; + 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(); - }); + 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 logout', function(done) { + imapStub.login.yieldsAsync(); + imapStub.listWellKnownFolders.yieldsAsync(null, { + Inbox: [{}] + }); - 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.calledOnce).to.be.true; - done(); - }); + done(); }); - it('should fail w/ generic error on logout', function(done) { - imapStub.login.yieldsAsync(); - imapStub.listWellKnownFolders.yieldsAsync(null, { - Inbox: [{}] - }); + setTimeout(function() { + // this error is thrown while we're waiting for the logout + imapStub.onError(new Error()); + }, 50); + }); - 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: [] }); - 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; + 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(); - }); + done(); }); + }); - it('should fail w/ generic error on listing folders fails', function(done) { - imapStub.login.yieldsAsync(); - imapStub.listWellKnownFolders.yieldsAsync(new Error()); + 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(); - }); - }); + 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; - 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; - - 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; - 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(); }); - describe('#check', function() { - beforeEach(function() { - sinon.stub(doctor, '_checkOnline'); - sinon.stub(doctor, '_checkReachable'); - sinon.stub(doctor, '_checkImap'); - sinon.stub(doctor, '_checkSmtp'); + 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(); }); - 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(); - }); + 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(); }); + }); - 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 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 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', 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.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(); - }); + 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(); }); + }); - 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 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.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.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 reachability', function(done) { - doctor._checkOnline.yieldsAsync(); - doctor._checkReachable.withArgs(credentials.imap).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.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.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 offline', function(done) { - doctor._checkOnline.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.called).to.be.false; - 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 w/o config', function(done) { - doctor.credentials = doctor._imap = doctor._smtp = undefined; + 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; + 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'; - - 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'); - - 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); - - angular.module('contactstest', []); - mocks.module('contactstest'); - mocks.inject(function($rootScope, $controller) { - scope = $rootScope.$new(); - scope.state = {}; - contactsCtrl = $controller(ContactsCtrl, { - $scope: scope - }); +'use strict'; + +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; + + 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; - }); + 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('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); + 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.onError = function(err) { + expect(err).to.equal(42); + done(); + }; - scope.listKeys(); - }); + 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(); + 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' - }; + describe('getFingerprint', function() { + it('should work', function() { + var key = { + fingerprint: 'YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY' + }; - scope.getFingerprint(key); + scope.getFingerprint(key); - expect(scope.fingerprint).to.equal('YYYY YYYY YYYY YYYY YYYY ... YYYY YYYY YYYY YYYY YYYY'); - }); + 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-----'; - 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); + pgpMock.getKeyParams.returns({ + _id: '12345', + userId: 'max@example.com', + userIds: [] }); - it('should fail due to invalid armored key', function(done) { - var keyArmored = '-----BEGIN PGP PRIVATE KEY BLOCK-----'; + keychainMock.saveLocalPublicKey.withArgs({ + _id: '12345', + userId: 'max@example.com', + userIds: [], + publicKey: '-----BEGIN PGP PUBLIC KEY BLOCK-----', + imported: true + }).yields(); - scope.onError = function(err) { - expect(err).to.exist; - done(); - }; + scope.listKeys = function() { + done(); + }; - scope.importKey(keyArmored); - }); + scope.importKey(keyArmored); + }); - it('should fail due to error in pgp.getKeyParams', function(done) { - var keyArmored = '-----BEGIN PGP PUBLIC KEY BLOCK-----'; + it('should fail due to invalid armored key', function(done) { + var keyArmored = '-----BEGIN PGP PRIVATE KEY BLOCK-----'; - pgpMock.getKeyParams.throws(new Error('WAT')); + scope.onError = function(err) { + expect(err).to.exist; + done(); + }; - scope.onError = function(err) { - expect(err).to.exist; - done(); - }; + scope.importKey(keyArmored); + }); - scope.importKey(keyArmored); - }); + it('should fail due to error in pgp.getKeyParams', function(done) { + var keyArmored = '-----BEGIN PGP PUBLIC KEY BLOCK-----'; - it('should fail due to error in keychain.saveLocalPublicKey', function(done) { - var keyArmored = '-----BEGIN PGP PUBLIC KEY BLOCK-----'; + pgpMock.getKeyParams.throws(new Error('WAT')); - pgpMock.getKeyParams.returns({ - _id: '12345', - userId: 'max@example.com' - }); + scope.onError = function(err) { + expect(err).to.exist; + done(); + }; - keychainMock.saveLocalPublicKey.yields(42); + scope.importKey(keyArmored); + }); - scope.onError = function(err) { - expect(err).to.equal(42); - done(); - }; + it('should fail due to error in keychain.saveLocalPublicKey', function(done) { + var keyArmored = '-----BEGIN PGP PUBLIC KEY BLOCK-----'; - scope.importKey(keyArmored); + 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' - }; + describe('removeKey', function() { + it('should work', function(done) { + var key = { + _id: '12345' + }; - keychainMock.removeLocalPublicKey.withArgs('12345').yields(); + keychainMock.removeLocalPublicKey.withArgs('12345').yields(); - scope.listKeys = function() { - done(); - }; + scope.listKeys = function() { + done(); + }; - scope.removeKey(key); - }); + scope.removeKey(key); + }); - it('should fail due to error in keychain.removeLocalPublicKey', function(done) { - var key = { - _id: '12345' - }; + it('should fail due to error in keychain.removeLocalPublicKey', function(done) { + var key = { + _id: '12345' + }; - keychainMock.removeLocalPublicKey.withArgs('12345').yields(42); + keychainMock.removeLocalPublicKey.withArgs('12345').yields(42); - scope.onError = function(err) { - expect(err).to.equal(42); - done(); - }; + scope.onError = function(err) { + expect(err).to.equal(42); + done(); + }; - scope.removeKey(key); - }); + 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) { - expect(err).to.not.exist; - expect(ciphertext).to.exist; + 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(decrypted).to.equal(plaintext); + crypto.decrypt(ciphertext, key, iv, function(err, decrypted) { + expect(err).to.not.exist; + expect(decrypted).to.equal(plaintext); - done(); - }); + done(); }); }); }); + }); - describe("PBKDF2 (Async/Worker)", function() { - it('should work', function(done) { - var salt = util.random(keySize); + 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); + crypto.deriveKey(password, salt, keySize, function(err, key) { + expect(err).to.not.exist; + expect(util.base642Str(key).length * 8).to.equal(keySize); - done(); - }); + 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() {}); + afterEach(function() {}); - describe('init', function() { - it('should work', function(done) { - lawnchairDaoStub.init.yields(); + 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.init(testUser, function(err) { + expect(err).to.not.exist; + expect(lawnchairDaoStub.init.calledOnce).to.be.true; + done(); }); }); + }); - describe('store list', function() { - it('should fail', function(done) { - var list = [{}]; + describe('store list', function() { + it('should fail', function(done) { + var list = [{}]; - storageDao.storeList(list, '', function(err) { - expect(err).to.exist; - done(); - }); + storageDao.storeList(list, '', function(err) { + expect(err).to.exist; + done(); }); + }); - it('should work with empty list', function(done) { - var list = []; + it('should work with empty list', function(done) { + var list = []; - storageDao.storeList(list, 'email', function(err) { - expect(err).to.not.exist; - done(); - }); + storageDao.storeList(list, 'email', function(err) { + expect(err).to.not.exist; + done(); }); + }); - it('should work', function(done) { - lawnchairDaoStub.batch.yields(); + it('should work', function(done) { + lawnchairDaoStub.batch.yields(); - var list = [{ - foo: 'bar' - }]; + 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; + expect(lawnchairDaoStub.batch.calledOnce).to.be.true; + done(); }); }); + }); - describe('remove list', function() { - it('should work', function(done) { - lawnchairDaoStub.removeList.yields(); + describe('remove list', function() { + it('should work', function(done) { + lawnchairDaoStub.removeList.yields(); - storageDao.removeList('email', function(err) { - expect(err).to.not.exist; - expect(lawnchairDaoStub.removeList.calledOnce).to.be.true; - done(); - }); + storageDao.removeList('email', function(err) { + expect(err).to.not.exist; + expect(lawnchairDaoStub.removeList.calledOnce).to.be.true; + done(); }); }); + }); - describe('list items', function() { - it('should work', function(done) { - lawnchairDaoStub.list.yields(); + describe('list items', function() { + it('should work', function(done) { + lawnchairDaoStub.list.yields(); - storageDao.listItems('email', 0, null, function(err) { - expect(err).to.not.exist; - expect(lawnchairDaoStub.list.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(); + 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(); - }); + 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 - }); + 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() {}); + 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('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); - }); + 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; - - beforeEach(function() { - // - // test data - // - emailAddress = 'asdf@asdf.com'; - passphrase = 'asdf'; - asymKeySize = 2048; - - inboxFolder = { - name: 'Inbox', - type: 'Inbox', - path: 'INBOX', - messages: [] - }; - - sentFolder = { - name: 'Sent', - type: 'Sent', - path: 'SENT', - messages: [] - }; - - draftsFolder = { - name: 'Drafts', - type: 'Drafts', - path: 'DRAFTS', - messages: [] - }; - - outboxFolder = { - name: 'Outbox', - type: 'Outbox', - path: 'OUTBOX', - messages: [] - }; - - trashFolder = { - name: 'Trash', - type: 'Trash', - path: 'TRASH', - messages: [] - }; - - folders = [inboxFolder, outboxFolder, trashFolder, sentFolder]; - - account = { - emailAddress: emailAddress, - asymKeySize: asymKeySize, - folders: folders, - online: true - }; - - 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 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); - }); + // + emailAddress = 'asdf@asdf.com'; + passphrase = 'asdf'; + asymKeySize = 2048; + + inboxFolder = { + name: 'Inbox', + type: 'Inbox', + path: 'INBOX', + messages: [] + }; + + sentFolder = { + name: 'Sent', + type: 'Sent', + path: 'SENT', + messages: [] + }; + + draftsFolder = { + name: 'Drafts', + type: 'Drafts', + path: 'DRAFTS', + messages: [] + }; + + outboxFolder = { + name: 'Outbox', + type: 'Outbox', + path: 'OUTBOX', + messages: [] + }; + + trashFolder = { + name: 'Trash', + type: 'Trash', + path: 'TRASH', + messages: [] + }; + + folders = [inboxFolder, outboxFolder, trashFolder, sentFolder]; + + account = { + emailAddress: emailAddress, + asymKeySize: asymKeySize, + folders: folders, + online: true + }; + + 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 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); + }); - 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(); - }); + done(); }); + }); - it('should fail when keychain errors', function(done) { - keychainStub.getUserKeyPair.yieldsAsync({}); + 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; + 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 - }] - }); + describe('#unlock', function() { + it('should unlock', function(done) { + pgpStub.getKeyParams.returns({ + _id: mockKeyPair.publicKey._id, + userId: emailAddress, + userIds: [{ + emailAddress: emailAddress + }] + }); - pgpStub.importKeys.withArgs({ - passphrase: passphrase, - privateKeyArmored: mockKeyPair.privateKey.encryptedKey, - publicKeyArmored: mockKeyPair.publicKey.publicKey - }).yieldsAsync(); - pgpStub._privateKey = { - foo: 'bar' - }; - - dao.unlock({ - passphrase: passphrase, - keypair: mockKeyPair - }, function(err) { - expect(err).to.not.exist; - - expect(pgpStub.importKeys.calledOnce).to.be.true; - expect(dao._pgpbuilder._privateKey).to.equal(pgpStub._privateKey); - - done(); - }); + pgpStub.importKeys.withArgs({ + passphrase: passphrase, + privateKeyArmored: mockKeyPair.privateKey.encryptedKey, + publicKeyArmored: mockKeyPair.publicKey.publicKey + }).yieldsAsync(); + pgpStub._privateKey = { + foo: 'bar' + }; + + dao.unlock({ + passphrase: passphrase, + keypair: mockKeyPair + }, function(err) { + expect(err).to.not.exist; + + expect(pgpStub.importKeys.calledOnce).to.be.true; + expect(dao._pgpbuilder._privateKey).to.equal(pgpStub._privateKey); + + done(); }); + }); - it('should generate a keypair and unlock', function(done) { - var keypair = { - keyId: 123, - publicKeyArmored: mockKeyPair.publicKey.publicKey, - privateKeyArmored: mockKeyPair.privateKey.encryptedKey - }; - - 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 generate a keypair and unlock', function(done) { + var keypair = { + keyId: 123, + publicKeyArmored: mockKeyPair.publicKey.publicKey, + privateKeyArmored: mockKeyPair.privateKey.encryptedKey + }; + + 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({}); + 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; + 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; + expect(pgpStub.generateKeys.calledOnce).to.be.true; + expect(pgpStub.importKeys.calledOnce).to.be.true; + expect(keychainStub.putUserKeyPair.calledOnce).to.be.true; - done(); - }); + done(); }); + }); - it('should fail when import fails', function(done) { - var keypair = { - keyId: 123, - publicKeyArmored: 'qwerty', - privateKeyArmored: 'asdfgh' - }; + 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({}); + pgpStub.generateKeys.withArgs().yieldsAsync(null, keypair); + pgpStub.importKeys.withArgs().yieldsAsync({}); - dao.unlock({ - passphrase: passphrase - }, function(err) { - expect(err).to.exist; + 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(pgpStub.generateKeys.calledOnce).to.be.true; + expect(pgpStub.importKeys.calledOnce).to.be.true; - done(); - }); + done(); }); + }); - it('should fail when generation fails', function(done) { - pgpStub.generateKeys.yieldsAsync({}); + it('should fail when generation fails', function(done) { + pgpStub.generateKeys.yieldsAsync({}); - dao.unlock({ - passphrase: passphrase - }, function(err) { - expect(err).to.exist; + dao.unlock({ + passphrase: passphrase + }, function(err) { + expect(err).to.exist; - expect(pgpStub.generateKeys.calledOnce).to.be.true; + 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(); + 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(); - }); + 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 - }); - - expect(imapClientStub.selectMailbox.called).to.be.false; + it('should not open the virtual outbox folder in imap', function() { + dao.openFolder({ + folder: outboxFolder }); - it('should not do anything in offline mode', function(done) { - account.online = false; + expect(imapClientStub.selectMailbox.called).to.be.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; + describe('#refreshFolder', function() { + var localListStub, mail; - beforeEach(function() { - localListStub = sinon.stub(dao, '_localListMessages'); - mail = { - uid: 123, - unread: true - }; - }); + 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]); + 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); + dao.refreshFolder({ + folder: inboxFolder + }, function(err) { + expect(err).to.not.exist; + expect(inboxFolder.count).to.equal(1); + expect(inboxFolder.messages).to.contain(mail); - done(); - }); + done(); }); + }); - it('should not add messages from disk', function(done) { - inboxFolder.messages = [mail]; - localListStub.withArgs({ - folder: inboxFolder - }).yieldsAsync(null, [mail]); + 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); + dao.refreshFolder({ + folder: inboxFolder + }, function(err) { + expect(err).to.not.exist; + expect(inboxFolder.count).to.equal(1); + expect(inboxFolder.messages).to.contain(mail); - done(); - }); + done(); }); + }); - it('should remove messages from memory', function(done) { - inboxFolder.messages = [mail]; - localListStub.withArgs({ - folder: inboxFolder - }).yieldsAsync(null, []); + 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; + 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; - - beforeEach(function() { - imapListStub = sinon.stub(dao, '_imapListMessages'); - imapDeleteStub = sinon.stub(dao, '_imapDeleteMessage'); - imapGetStub = sinon.stub(dao, '_getBodyParts'); - localStoreStub = sinon.stub(dao, '_localStoreMessages'); - - 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; - }; - }); - - it('should fetch message downstream', function(done) { - imapListStub.withArgs(opts).yieldsAsync(null, [message]); - - 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(localStoreStub.calledOnce).to.be.true; - expect(imapListStub.calledOnce).to.be.true; - - done(); - }); - }); - - it('should not notify for other folders', function(done) { - opts.folder = sentFolder; + describe('#fetchMessages', function() { + var imapListStub, imapGetStub, imapDeleteStub, localStoreStub; + var opts, message, validUuid, corruptedUuid, verificationSubject; + var notified; + + beforeEach(function() { + imapListStub = sinon.stub(dao, '_imapListMessages'); + imapDeleteStub = sinon.stub(dao, '_imapDeleteMessage'); + imapGetStub = sinon.stub(dao, '_getBodyParts'); + localStoreStub = sinon.stub(dao, '_localStoreMessages'); + + 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; + }; + }); - imapListStub.withArgs(opts).yieldsAsync(null, [message]); + it('should fetch message downstream', function(done) { + imapListStub.withArgs(opts).yieldsAsync(null, [message]); - localStoreStub.withArgs({ - folder: sentFolder, - emails: [message] - }).yieldsAsync(); + localStoreStub.withArgs({ + folder: inboxFolder, + emails: [message] + }).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.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; - done(); - }); + done(); }); + }); - it('should verify verification mails', function(done) { - message.subject = verificationSubject; - - imapListStub.withArgs(opts).yieldsAsync(null, [message]); + it('should not notify for other folders', function(done) { + opts.folder = sentFolder; - imapGetStub.withArgs({ - folder: inboxFolder, - uid: message.uid, - bodyParts: message.bodyParts - }).yieldsAsync(null, [{ - type: 'text', - content: '' + cfg.cloudUrl + cfg.verificationUrl + validUuid - }]); + imapListStub.withArgs(opts).yieldsAsync(null, [message]); - 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(); - }); - }); + localStoreStub.withArgs({ + folder: sentFolder, + emails: [message] + }).yieldsAsync(); - it('should not verify invalid verification mails', function(done) { - message.subject = verificationSubject; + 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; - imapListStub.withArgs(opts).yieldsAsync(null, [message]); + done(); + }); + }); - imapGetStub.withArgs({ - folder: inboxFolder, - uid: message.uid, - bodyParts: message.bodyParts - }).yieldsAsync(null, [{ - type: 'text', - content: '' + cfg.cloudUrl + cfg.verificationUrl + corruptedUuid - }]); + 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(); + }); + }); - 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 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; + it('should display verification mail when verification failed', function(done) { + message.subject = verificationSubject; - imapListStub.withArgs(opts).yieldsAsync(null, [message]); + 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 - }]); + 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({}); + keychainStub.verifyPublicKey.withArgs(validUuid).yieldsAsync({}); - localStoreStub.withArgs({ - folder: inboxFolder, - emails: [message] - }).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(); - }); + 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]; - }); + 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 - }; + it('should delete from imap, local, memory', function(done) { + var deleteOpts = { + folder: inboxFolder, + uid: message.uid + }; - imapDeleteStub.withArgs(deleteOpts).yieldsAsync(); - localDeleteStub.withArgs(deleteOpts).yieldsAsync(); + 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); + 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(); - }); + done(); }); + }); - it('should delete from local, memory', function(done) { - var deleteOpts = { - folder: inboxFolder, - uid: message.uid - }; + it('should delete from local, memory', function(done) { + var deleteOpts = { + folder: inboxFolder, + uid: message.uid + }; - localDeleteStub.withArgs(deleteOpts).yieldsAsync(); + 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); + 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(); - }); + done(); }); + }); - it('should delete from outbox from local, memory', function(done) { - var deleteOpts = { - folder: outboxFolder, - uid: message.uid - }; + it('should delete from outbox from local, memory', function(done) { + var deleteOpts = { + folder: outboxFolder, + uid: message.uid + }; - localDeleteStub.withArgs(deleteOpts).yieldsAsync(); + 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); + 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(); - }); + done(); }); + }); - it('should fail at delete from local', function(done) { - var deleteOpts = { - folder: inboxFolder, - uid: message.uid - }; + it('should fail at delete from local', function(done) { + var deleteOpts = { + folder: inboxFolder, + uid: message.uid + }; - imapDeleteStub.withArgs(deleteOpts).yieldsAsync(); - localDeleteStub.withArgs(deleteOpts).yieldsAsync({}); + 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); + 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(); - }); + done(); }); + }); - it('should fail at delete from imap', function(done) { - var deleteOpts = { - folder: inboxFolder, - uid: message.uid - }; + it('should fail at delete from imap', function(done) { + var deleteOpts = { + folder: inboxFolder, + uid: message.uid + }; - imapDeleteStub.withArgs(deleteOpts).yieldsAsync({}); + 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); + 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(); - }); + 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); + 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; - - 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(); - }); + 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]); + 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(); + 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; + 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(); - }); + 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 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(); + 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; + 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(); - }); + 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(); - }); + }); + 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'); - }); + describe('#getBody', function() { + var localListStub, localStoreStub, imapGetStub, uid; - it('should not do anything if the message already has content', function() { - var message = { - body: 'bender is great!' - }; + beforeEach(function() { + uid = 12345, + localListStub = sinon.stub(dao, '_localListMessages'); + localStoreStub = sinon.stub(dao, '_localStoreMessages'); + imapGetStub = sinon.stub(dao, '_getBodyParts'); + }); - 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; - - 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; - }); + // should do nothing + }); - it('should read a pgp/mime from the device', function(done) { - var message, ct, pt; + it('should read an unencrypted body from the device', function(done) { + var message, body; - pt = 'bender is great!'; - ct = '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----'; - message = { - uid: uid, - encrypted: true - }; + body = 'bender is great! bender is great!'; + message = { + uid: uid + }; - localListStub.withArgs({ - folder: inboxFolder, - uid: uid - }).yieldsAsync(null, [{ - bodyParts: [{ - type: 'text', - content: pt - }, { - type: 'encrypted', - content: ct - }] - }]); + 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(ct); - expect(msg.encrypted).to.be.true; - expect(message.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(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; + 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 - }] - }]); + 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; + 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(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; + expect(localListStub.calledOnce).to.be.true; - done(); - }); - expect(message.loadingBody).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, [{ + 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: body - }]); - - dao.getBody({ - message: message, - folder: inboxFolder - }, function(err, msg) { - expect(err).to.not.exist; + 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; + }); - expect(msg).to.equal(message); - expect(msg.body).to.equal(body); - expect(msg.loadingBody).to.be.false; + it('should read a pgp/inline from the device', function(done) { + var message, ct, pt; - expect(localListStub.calledOnce).to.be.true; - expect(imapGetStub.calledOnce).to.be.true; - expect(localStoreStub.calledOnce).to.be.true; + ct = '-----BEGIN PGP MESSAGE-----\nasdasdasd\n-----END PGP MESSAGE-----'; + pt = 'bla bla yadda yadda'; + message = { + uid: uid + }; - 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, [{ + localListStub.yieldsAsync(null, [{ + bodyParts: [{ type: 'text', content: pt }, { - type: 'encrypted', + type: 'text', content: ct - }]); - + }, { + type: 'text', + content: pt + }] + }]); - 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(msg.loadingBody).to.be.false; + 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; - 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('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/inline from the device', function(done) { + var message, pt, expected; - localListStub.yieldsAsync(null, [message]); - localStoreStub.yieldsAsync({}); - imapGetStub.yieldsAsync(null, [{ + 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: 'bender is great! bender is great!' - }]); + 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; + }); - 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; + 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; + }); - expect(message.loadingBody).to.be.false; + it('should stream from imap and set encrypted body', function(done) { + var message, ct, pt; - done(); - }); + 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' - }] - }; + 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({}); + 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; + 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; + expect(message.loadingBody).to.be.false; - done(); - }); + 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 = {}; + describe('#getAttachment', function() { + var imapGetStub, uid; - imapGetStub.withArgs({ - folder: inboxFolder, - uid: uid, - bodyParts: [attmt] - }).yieldsAsync(null, [{ - content: 'CONTENT!!!' - }]); + beforeEach(function() { + uid = 123456; + imapGetStub = sinon.stub(dao, '_getBodyParts'); + }); - 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 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 = {}; + it('should error during fetch', function(done) { + var attmt = {}; - imapGetStub.yieldsAsync({}); + 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; + 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(); - }); + done(); }); }); + }); - describe('#decryptBody', function() { - it('should do nothing when the message is not encrypted', function(done) { - var message = { - encrypted: false, - decrypted: true, - body: 'asd' - }; + 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); + }); - 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' + }; - 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); + }); - dao.decryptBody({ - message: message - }, done); - }); + it('should do nothing when the message has no body', function(done) { + var message = { + encrypted: true, + decrypted: false, + body: '' + }; - it('should do nothing when the message has no body', function(done) { - var message = { - encrypted: true, - decrypted: false, - body: '' - }; + dao.decryptBody({ + message: message + }, done); + }); - 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('should do nothing when the message is decrypting', function(done) { - var message = { - encrypted: true, - decrypted: false, - body: 'asd', - decryptingBody: true - }; + 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 + }] + }; - dao.decryptBody({ - message: message - }, done); - }); + 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', function(done) { - var message, ct, pt, parsed; + 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 + }] + }; - 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, [{ + 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: 'text', - content: parsed - }] - }]); + 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; + + done(); + }); + + expect(message.decryptingBody).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.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(); - }); + it('decrypt a pgp/inline message', function(done) { + var message, ct, pt; + + 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 + }] + }; - expect(message.decryptingBody).to.be.true; - }); + keychainStub.getReceiverPublicKey.withArgs(message.from[0].address).yieldsAsync(null, mockKeyPair.publicKey); + pgpStub.decrypt.withArgs(ct, mockKeyPair.publicKey.publicKey).yieldsAsync(null, pt, true); - it('decrypt a pgp/mime message with inner signature', function(done) { - var message, ct, pt, parsed, signed, signedMimeTree, signature; + dao.decryptBody({ + message: message + }, function(error, msg) { + expect(error).to.not.exist; - 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: 'text', - content: signed - }], - signedMessage: signedMimeTree, - signature: signature - }] - }]); + 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; - 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; - - 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 - }] - }; - - keychainStub.getReceiverPublicKey.withArgs(message.from[0].address).yieldsAsync(null, mockKeyPair.publicKey); - pgpStub.decrypt.withArgs(ct, mockKeyPair.publicKey.publicKey).yieldsAsync(null, pt, 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(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; - - done(); - }); + expect(message.decryptingBody).to.be.true; + }); - expect(message.decryptingBody).to.be.true; + 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-----' + }] + }; + + keychainStub.getReceiverPublicKey.yieldsAsync(null, mockKeyPair.publicKey); + pgpStub.decrypt.yieldsAsync(new Error('fail.')); + + 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; + + 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-----' - }] - }; - - keychainStub.getReceiverPublicKey.yieldsAsync(null, mockKeyPair.publicKey); - pgpStub.decrypt.yieldsAsync(new Error('fail.')); - - 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; - - done(); - }); - }); + 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 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(); - }); + 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(); }); }); + }); - describe('#sendEncrypted', function() { - var publicKeys = ["PUBLIC KEY"], - dummyMail = { - publicKeysArmored: publicKeys - }; + describe('#sendEncrypted', function() { + var publicKeys = ["PUBLIC KEY"], + dummyMail = { + publicKeysArmored: publicKeys + }; - it('should send encrypted and upload to sent', function(done) { - var msg = 'wow. such message. much rfc2822.'; + it('should send encrypted and upload to sent', function(done) { + var msg = 'wow. such message. much rfc2822.'; - imapClientStub.uploadMessage.withArgs({ - path: sentFolder.path, - message: msg - }).yields(); + imapClientStub.uploadMessage.withArgs({ + path: sentFolder.path, + message: msg + }).yields(); - pgpMailerStub.send.withArgs({ - encrypt: true, - mail: dummyMail, - smtpclient: undefined, - publicKeysArmored: publicKeys - }).yieldsAsync(null, msg); + 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; + 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; + expect(pgpMailerStub.send.calledOnce).to.be.true; + expect(imapClientStub.uploadMessage.calledOnce).to.be.true; - done(); - }); + done(); }); + }); - it('should send encrypted and not upload to sent', function(done) { - var msg = 'wow. such message. much rfc2822.'; + it('should send encrypted and not upload to sent', function(done) { + var msg = 'wow. such message. much rfc2822.'; - dao.ignoreUploadOnSent = true; + dao.ignoreUploadOnSent = true; - pgpMailerStub.send.withArgs({ - encrypt: true, - mail: dummyMail, - smtpclient: undefined, - publicKeysArmored: publicKeys - }).yieldsAsync(null, msg); + 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; + 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; + expect(pgpMailerStub.send.calledOnce).to.be.true; + expect(imapClientStub.uploadMessage.called).to.be.false; - done(); - }); + done(); }); + }); - it('should not send when pgpmailer fails', function(done) { - pgpMailerStub.send.yieldsAsync({}); + it('should not send when pgpmailer fails', function(done) { + pgpMailerStub.send.yieldsAsync({}); - dao.sendEncrypted({ - email: dummyMail - }, function(err) { - expect(err).to.exist; + dao.sendEncrypted({ + email: dummyMail + }, function(err) { + expect(err).to.exist; - expect(pgpMailerStub.send.calledOnce).to.be.true; - expect(imapClientStub.uploadMessage.called).to.be.false; + expect(pgpMailerStub.send.calledOnce).to.be.true; + expect(imapClientStub.uploadMessage.called).to.be.false; - done(); - }); + done(); }); + }); - it('should not send in offline mode', function(done) { - account.online = false; + 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(); - }); + 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.'; + describe('#sendPlaintext', function() { + var dummyMail = {}; - pgpMailerStub.send.withArgs({ - smtpclient: undefined, - mail: dummyMail - }).yieldsAsync(null, msg); + it('should send in the plain and upload to sent', function(done) { + var msg = 'wow. such message. much rfc2822.'; - 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(); - }); - }); + pgpMailerStub.send.withArgs({ + smtpclient: undefined, + mail: dummyMail + }).yieldsAsync(null, msg); - it('should send in the plain and not upload to sent', function(done) { - var msg = 'wow. such message. much rfc2822.'; + imapClientStub.uploadMessage.withArgs({ + path: sentFolder.path, + message: msg + }).yields(); - dao.ignoreUploadOnSent = true; + 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(); + }); + }); - pgpMailerStub.send.withArgs({ - smtpclient: undefined, - mail: dummyMail - }).yieldsAsync(null, msg); + it('should send in the plain and not upload to sent', function(done) { + var msg = 'wow. such message. much rfc2822.'; - 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(); - }); - }); + dao.ignoreUploadOnSent = true; - it('should not send due to error', function(done) { - pgpMailerStub.send.yieldsAsync({}); + pgpMailerStub.send.withArgs({ + smtpclient: undefined, + mail: dummyMail + }).yieldsAsync(null, msg); - 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(); - }); + 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 in offline mode', function(done) { - account.online = false; + it('should not send due to error', function(done) { + pgpMailerStub.send.yieldsAsync({}); - dao.sendPlaintext({}, function(err) { - expect(err).to.exist; - expect(pgpMailerStub.send.called).to.be.false; - expect(imapClientStub.uploadMessage.called).to.be.false; - done(); - }); + 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(); }); }); - describe('#encrypt', function() { - it('should encrypt', function(done) { - pgpBuilderStub.encrypt.yieldsAsync(); + it('should not send in offline mode', function(done) { + account.online = false; - dao.encrypt({}, function() { - expect(pgpBuilderStub.encrypt.calledOnce).to.be.true; - done(); - }); + dao.sendPlaintext({}, function(err) { + expect(err).to.exist; + expect(pgpMailerStub.send.called).to.be.false; + expect(imapClientStub.uploadMessage.called).to.be.false; + 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('#encrypt', function() { + it('should encrypt', function(done) { + pgpBuilderStub.encrypt.yieldsAsync(); + + dao.encrypt({}, function() { + expect(pgpBuilderStub.encrypt.calledOnce).to.be.true; + done(); }); }); + }); + }); - describe('#onDisconnect', function() { - it('should discard imapClient and pgpMailer', function(done) { - imapClientStub.logout.yieldsAsync(); + describe('event handlers', function() { - dao.onDisconnect(done); + describe('#onConnect', function() { + var initFoldersStub; - expect(dao._account.online).to.be.false; - expect(dao._imapClient).to.not.exist; - expect(dao._pgpMailer).to.not.exist; + 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('#_onSyncUpdate', function() { - var fetchMessagesStub, deleteMessagesStub, setFlagsStub, msgs; + describe('#onDisconnect', function() { + it('should discard imapClient and pgpMailer', function(done) { + imapClientStub.logout.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.onDisconnect(done); + + expect(dao._account.online).to.be.false; + expect(dao._imapClient).to.not.exist; + expect(dao._pgpMailer).to.not.exist; + }); + }); - it('should get new message', function(done) { - fetchMessagesStub.withArgs({ - folder: inboxFolder, - firstUid: 1, - lastUid: 3 - }).yieldsAsync(); + 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'); + }); - dao.onError = function(err) { - expect(err).to.not.exist; - expect(fetchMessagesStub.calledOnce).to.be.true; - done(); - }; + it('should get new message', function(done) { + fetchMessagesStub.withArgs({ + folder: inboxFolder, + firstUid: 1, + lastUid: 3 + }).yieldsAsync(); - dao._onSyncUpdate({ - type: 'new', - path: inboxFolder.path, - list: [1, 3] - }); + 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(); + 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.onError = function(err) { + expect(err).to.not.exist; + expect(deleteMessagesStub.calledOnce).to.be.true; + done(); + }; - dao._onSyncUpdate({ - type: 'deleted', - path: inboxFolder.path, - list: [5] - }); + 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(); + 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.onError = function(err) { + expect(err).to.not.exist; + expect(setFlagsStub.calledOnce).to.be.true; + done(); + }; - dao._onSyncUpdate({ - type: 'messages', - path: inboxFolder.path, - list: msgs - }); + 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(); - }); - }); + describe('internal API', function() { + describe('#_checkSignatures', function() { + it('should check signatures in clearsigned message', function(done) { + var message = { + from: [{ + address: 'asdasdasd' + }], + clearSignedMessage: 'trallalalalala' + }; - 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(); - }); - }); + keychainStub.getReceiverPublicKey.withArgs(message.from[0].address).yieldsAsync(null, mockKeyPair.publicKey); + pgpStub.verifyClearSignedMessage.withArgs(message.clearSignedMessage, mockKeyPair.publicKey.publicKey).yieldsAsync(null, true); - 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(); - }); + 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 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(); - }); + 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(); }); }); - describe('#_initFoldersFromDisk', function() { - beforeEach(function() { - sinon.stub(dao, 'refreshFolder'); + 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 initialize from disk if offline and not refresh folder', function(done) { - devicestorageStub.listItems.withArgs('folders').yieldsAsync(null, [ - [inboxFolder] - ]); - dao.refreshFolder.withArgs({ - folder: inboxFolder - }).yieldsAsync(); + it('should error while fetching public key', function(done) { + var message = { + from: [{ + address: 'asdasdasd' + }], + signedMessage: 'trallalalalala', + signature: 'ugauga' + }; - dao._initFoldersFromDisk(function(err) { - expect(err).to.not.exist; - expect(devicestorageStub.listItems.calledOnce).to.be.true; - expect(dao.refreshFolder.called).to.be.false; - done(); - }); - }); + keychainStub.getReceiverPublicKey.yieldsAsync(new Error()); - 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(); - }); + 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'); + }); - describe('#_initFoldersFromImap', 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 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 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(); }); + }); + }); - it('should update folders from imap', function(done) { - account.folders = [inboxFolder, outboxFolder, trashFolder, { - name: 'foo', - type: 'Sent', - path: 'bar', - }]; + describe('#_initFoldersFromImap', function() { + beforeEach(function() { + sinon.stub(dao, 'refreshFolder'); + }); - 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(); - }); + 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(); }); }); - describe('#_imapMark', function() { - it('should flag a mail', function(done) { - imapClientStub.updateFlags.withArgs({ + 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, - 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(); - }); - }); - }); + 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 + }]); - describe('#_imapDeleteMessage', function() { - var uid = 1337; + return true; + }), 'folders').yieldsAsync(); - it('should fail when disconnected', function(done) { - dao._account.online = false; + dao.refreshFolder.yieldsAsync(); - dao._imapDeleteMessage({}, function(err) { - expect(err.code).to.equal(42); - done(); - }); + 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 move to trash', function(done) { - imapClientStub.moveMessage.withArgs({ - path: inboxFolder.path, - uid: uid, - destination: trashFolder.path - }).yieldsAsync(); - - dao._imapDeleteMessage({ - folder: inboxFolder, - uid: uid - }, 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(); }); + }); + }); - it('should purge message', function(done) { - imapClientStub.deleteMessage.withArgs({ - path: trashFolder.path, - uid: uid - }).yieldsAsync(); + describe('#_imapDeleteMessage', function() { + var uid = 1337; - dao._imapDeleteMessage({ - folder: trashFolder, - uid: uid - }, done); + it('should fail when disconnected', function(done) { + dao._account.online = false; + + dao._imapDeleteMessage({}, function(err) { + expect(err.code).to.equal(42); + done(); }); }); - describe('#_imapListMessages', function() { - var firstUid = 1337, - lastUid = 1339; + 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(); - it('should list messages', function(done) { - imapClientStub.listMessages.withArgs({ - folder: inboxFolder, - path: inboxFolder.path, - firstUid: firstUid, - lastUid: lastUid - }).yieldsAsync(null, []); + dao._imapDeleteMessage({ + folder: trashFolder, + uid: uid + }, done); + }); + }); - dao._imapListMessages({ - folder: inboxFolder, - firstUid: firstUid, - lastUid: lastUid - }, function(err, msgs) { - expect(err).to.not.exist; - expect(msgs).to.exist; + describe('#_imapListMessages', function() { + var firstUid = 1337, + lastUid = 1339; - expect(imapClientStub.listMessages.calledOnce).to.be.true; + it('should list messages', function(done) { + imapClientStub.listMessages.withArgs({ + folder: inboxFolder, + path: inboxFolder.path, + firstUid: firstUid, + lastUid: lastUid + }).yieldsAsync(null, []); - done(); - }); + 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({}); + 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; + 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(); - }); + done(); }); + }); - it('should fail when disconnected', function(done) { - dao._account.online = false; + it('should fail when disconnected', function(done) { + dao._account.online = false; - dao._imapListMessages({}, function(err) { - expect(err.code).to.equal(42); - done(); - }); + 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(); - }); + 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; +