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

Hello there' + Math.random() + '

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

Hello there' + Math.random() + '

'; + this.encrypted = true; + this.decrypted = true; + }; + + var dummies = [], + i = 100; + while (i--) { + // every second/third/fourth dummy mail with unread/attachments/answered + dummies.push(new Email((i % 2 === 0), (i % 3 === 0), (i % 5 === 0))); + } + + return dummies; +} + +exports = MailListCtrl; \ No newline at end of file diff --git a/src/js/controller/navigation.js b/src/js/controller/navigation.js index 70bd878..ccccaa2 100644 --- a/src/js/controller/navigation.js +++ b/src/js/controller/navigation.js @@ -1,210 +1,206 @@ -define(function(require) { - 'use strict'; +'use strict'; - var angular = require('angular'), - appController = require('js/app-controller'), - config = require('js/app-config').config, - notification = require('js/util/notification'), - backBtnHandler = require('js/util/backbutton-handler'), - _ = require('underscore'), - emailDao, outboxBo; +var appController = require('../app-controller'), + config = require('../app-config').config, + notification = require('../util/notification'), + backBtnHandler = require('../util/backbutton-handler'), + emailDao, outboxBo; + +// +// Controller +// + +var NavigationCtrl = function($scope, $routeParams, $location) { + if (!appController._emailDao && !$routeParams.dev) { + $location.path('/'); // init app + return; + } + + emailDao = appController._emailDao; + outboxBo = appController._outboxBo; // - // Controller + // scope functions // - var NavigationCtrl = function($scope, $routeParams, $location) { - if (!appController._emailDao && !$routeParams.dev) { - $location.path('/'); // init app - return; - } - - emailDao = appController._emailDao; - outboxBo = appController._outboxBo; - - // - // scope functions - // - - $scope.state.nav = { - open: false, - toggle: function(to) { - this.open = to; - } - }; - - $scope.openFolder = function(folder) { - $scope.state.nav.currentFolder = folder; - $scope.state.nav.toggle(false); - }; - - $scope.onOutboxUpdate = function(err, count) { - if (err) { - $scope.onError(err); - return; - } - - // update the outbox mail count - var outbox = _.findWhere($scope.account.folders, { - type: config.outboxMailboxType - }); - outbox.count = count; - $scope.$apply(); - - emailDao.refreshFolder({ - folder: outbox - }, $scope.onError); - }; - - $scope.logout = function() { - $scope.onError({ - title: 'Logout', - message: 'Are you sure you want to logout?', - callback: function(confirm) { - if (confirm) { - appController.logout(); - } - }, - sync: true - }); - }; - - // - // Start - // - - // handle back button - backBtnHandler.start(); - // init folders - initializeFolders(); - - // select inbox as the current folder on init - if ($scope.account.folders && $scope.account.folders.length > 0) { - $scope.openFolder($scope.account.folders[0]); - } - // connect imap/smtp clients on first startup - appController.onConnect(function(err) { - if (err) { - $scope.onError(err); - return; - } - - // select inbox if not yet selected - if (!$scope.state.nav.currentFolder) { - $scope.openFolder($scope.account.folders[0]); - $scope.$apply(); - } - }); - - // - // helper functions - // - - function initializeFolders() { - // create dummy folder in dev environment only - if ($routeParams.dev) { - createDummyFolders(); - return; - } - - // get pointer to account/folder/message tree on root scope - $scope.$root.account = emailDao._account; - - // set notificatio handler for sent messages - outboxBo.onSent = sentNotification; - // start checking outbox periodically - outboxBo.startChecking($scope.onOutboxUpdate); - - } - - function sentNotification(email) { - notification.create({ - title: 'Message sent', - message: email.subject, - timeout: 2000 - }, function() {}); - } - - - // attach dummy folders for development - function createDummyFolders() { - $scope.$root.account = {}; - $scope.account.folders = [{ - type: 'Inbox', - count: 2, - path: 'INBOX' - }, { - type: 'Sent', - count: 0, - path: 'SENT' - }, { - type: config.outboxMailboxType, - count: 0, - path: config.outboxMailboxPath - }, { - type: 'Drafts', - count: 0, - path: 'DRAFTS' - }, { - type: 'Trash', - count: 0, - path: 'TRASH' - }]; + $scope.state.nav = { + open: false, + toggle: function(to) { + this.open = to; } }; + $scope.openFolder = function(folder) { + $scope.state.nav.currentFolder = folder; + $scope.state.nav.toggle(false); + }; + + $scope.onOutboxUpdate = function(err, count) { + if (err) { + $scope.onError(err); + return; + } + + // update the outbox mail count + var outbox = _.findWhere($scope.account.folders, { + type: config.outboxMailboxType + }); + outbox.count = count; + $scope.$apply(); + + emailDao.refreshFolder({ + folder: outbox + }, $scope.onError); + }; + + $scope.logout = function() { + $scope.onError({ + title: 'Logout', + message: 'Are you sure you want to logout?', + callback: function(confirm) { + if (confirm) { + appController.logout(); + } + }, + sync: true + }); + }; + // - // Directives + // Start // - var ngModule = angular.module('navigation', []); - ngModule.directive('keyShortcuts', function($timeout) { - return function(scope, elm) { - elm.bind('keydown', function(e) { - // global state is not yet set, ignore keybaord shortcuts - if (!scope.state) { - return; - } + // handle back button + backBtnHandler.start(); + // init folders + initializeFolders(); - var modifier = e.ctrlKey || e.metaKey; + // select inbox as the current folder on init + if ($scope.account.folders && $scope.account.folders.length > 0) { + $scope.openFolder($scope.account.folders[0]); + } + // connect imap/smtp clients on first startup + appController.onConnect(function(err) { + if (err) { + $scope.onError(err); + return; + } - if (modifier && e.keyCode === 78 && scope.state.lightbox !== 'write') { - // n -> new mail - e.preventDefault(); - scope.state.writer.write(); - scope.$apply(); - - } else if (modifier && e.keyCode === 70 && scope.state.lightbox !== 'write') { - // f -> find - e.preventDefault(); - scope.state.mailList.searching = true; - $timeout(function() { - scope.state.mailList.searching = false; - }, 200); - scope.$apply(); - - } else if (modifier && e.keyCode === 82 && scope.state.lightbox !== 'write' && scope.state.mailList.selected) { - // r -> reply - e.preventDefault(); - scope.state.writer.write(scope.state.mailList.selected); - scope.$apply(); - - } else if (e.keyCode === 27 && scope.state.lightbox !== undefined) { - // escape -> close current lightbox - e.preventDefault(); - scope.state.lightbox = undefined; - scope.$apply(); - - } else if (e.keyCode === 27 && scope.state.nav.open) { - // escape -> close nav view - e.preventDefault(); - scope.state.nav.toggle(false); - scope.$apply(); - } - - }); - }; + // select inbox if not yet selected + if (!$scope.state.nav.currentFolder) { + $scope.openFolder($scope.account.folders[0]); + $scope.$apply(); + } }); - return NavigationCtrl; -}); \ No newline at end of file + // + // helper functions + // + + function initializeFolders() { + // create dummy folder in dev environment only + if ($routeParams.dev) { + createDummyFolders(); + return; + } + + // get pointer to account/folder/message tree on root scope + $scope.$root.account = emailDao._account; + + // set notificatio handler for sent messages + outboxBo.onSent = sentNotification; + // start checking outbox periodically + outboxBo.startChecking($scope.onOutboxUpdate); + + } + + function sentNotification(email) { + notification.create({ + title: 'Message sent', + message: email.subject, + timeout: 2000 + }, function() {}); + } + + + // attach dummy folders for development + function createDummyFolders() { + $scope.$root.account = {}; + $scope.account.folders = [{ + type: 'Inbox', + count: 2, + path: 'INBOX' + }, { + type: 'Sent', + count: 0, + path: 'SENT' + }, { + type: config.outboxMailboxType, + count: 0, + path: config.outboxMailboxPath + }, { + type: 'Drafts', + count: 0, + path: 'DRAFTS' + }, { + type: 'Trash', + count: 0, + path: 'TRASH' + }]; + } +}; + +// +// Directives +// + +var ngModule = angular.module('navigation', []); +ngModule.directive('keyShortcuts', function($timeout) { + return function(scope, elm) { + elm.bind('keydown', function(e) { + // global state is not yet set, ignore keybaord shortcuts + if (!scope.state) { + return; + } + + var modifier = e.ctrlKey || e.metaKey; + + if (modifier && e.keyCode === 78 && scope.state.lightbox !== 'write') { + // n -> new mail + e.preventDefault(); + scope.state.writer.write(); + scope.$apply(); + + } else if (modifier && e.keyCode === 70 && scope.state.lightbox !== 'write') { + // f -> find + e.preventDefault(); + scope.state.mailList.searching = true; + $timeout(function() { + scope.state.mailList.searching = false; + }, 200); + scope.$apply(); + + } else if (modifier && e.keyCode === 82 && scope.state.lightbox !== 'write' && scope.state.mailList.selected) { + // r -> reply + e.preventDefault(); + scope.state.writer.write(scope.state.mailList.selected); + scope.$apply(); + + } else if (e.keyCode === 27 && scope.state.lightbox !== undefined) { + // escape -> close current lightbox + e.preventDefault(); + scope.state.lightbox = undefined; + scope.$apply(); + + } else if (e.keyCode === 27 && scope.state.nav.open) { + // escape -> close nav view + e.preventDefault(); + scope.state.nav.toggle(false); + scope.$apply(); + } + + }); + }; +}); + +exports = NavigationCtrl; \ No newline at end of file diff --git a/src/js/controller/popover.js b/src/js/controller/popover.js index 32148eb..ed6654a 100644 --- a/src/js/controller/popover.js +++ b/src/js/controller/popover.js @@ -1,47 +1,43 @@ -define(function(require) { - 'use strict'; +'use strict'; - var angular = require('angular'); +// +// Controller +// - // - // Controller - // +var PopoverCtrl = function($scope) { + $scope.state.popover = {}; +}; - var PopoverCtrl = function($scope) { - $scope.state.popover = {}; +// +// Directives +// + +var ngModule = angular.module('popover', []); +ngModule.directive('popover', function() { + return function(scope, elm, attrs) { + var selector = attrs.popover; + var popover = angular.element(document.querySelector(selector)); + + elm.on('mouseover', function() { + // set popover position + var top = elm[0].offsetTop; + var left = elm[0].offsetLeft; + var width = elm[0].offsetWidth; + var height = elm[0].offsetHeight; + + popover[0].style.transition = 'opacity 0.3s linear'; + popover[0].style.top = (top + height / 2 - popover[0].offsetHeight / 2) + 'px'; + popover[0].style.left = (left + width) + 'px'; + popover[0].style.opacity = '1'; + }); + + elm.on('mouseout', function() { + popover[0].style.transition = 'opacity 0.3s linear, top 0.3s step-end, left 0.3s step-end'; + popover[0].style.opacity = '0'; + popover[0].style.top = '-9999px'; + popover[0].style.left = '-9999px'; + }); }; +}); - // - // Directives - // - - var ngModule = angular.module('popover', []); - ngModule.directive('popover', function() { - return function(scope, elm, attrs) { - var selector = attrs.popover; - var popover = angular.element(document.querySelector(selector)); - - elm.on('mouseover', function() { - // set popover position - var top = elm[0].offsetTop; - var left = elm[0].offsetLeft; - var width = elm[0].offsetWidth; - var height = elm[0].offsetHeight; - - popover[0].style.transition = 'opacity 0.3s linear'; - popover[0].style.top = (top + height / 2 - popover[0].offsetHeight / 2) + 'px'; - popover[0].style.left = (left + width) + 'px'; - popover[0].style.opacity = '1'; - }); - - elm.on('mouseout', function() { - popover[0].style.transition = 'opacity 0.3s linear, top 0.3s step-end, left 0.3s step-end'; - popover[0].style.opacity = '0'; - popover[0].style.top = '-9999px'; - popover[0].style.left = '-9999px'; - }); - }; - }); - - return PopoverCtrl; -}); \ No newline at end of file +exports = PopoverCtrl; \ No newline at end of file diff --git a/src/js/controller/privatekey-upload.js b/src/js/controller/privatekey-upload.js index 4ebe585..7c48dad 100644 --- a/src/js/controller/privatekey-upload.js +++ b/src/js/controller/privatekey-upload.js @@ -1,204 +1,201 @@ -define(function(require) { - 'use strict'; +'use strict'; - var angular = require('angular'), - appController = require('js/app-controller'), - util = require('js/crypto/util'), - keychain, pgp; +var appController = require('../app-controller'), + util = require('crypto-lib').util, + keychain, pgp; - var PrivateKeyUploadCtrl = function($scope) { - keychain = appController._keychain; - pgp = keychain._pgp; +var PrivateKeyUploadCtrl = function($scope) { + keychain = appController._keychain; + pgp = keychain._pgp; - $scope.state.privateKeyUpload = { - toggle: function(to) { - // open lightbox - $scope.state.lightbox = (to) ? 'privatekey-upload' : undefined; - if (!to) { - return; - } - - // show syncing status - $scope.step = 4; - // check if key is already synced - $scope.checkServerForKey(function(privateKeySynced) { - if (privateKeySynced) { - // close lightbox - $scope.state.lightbox = undefined; - // show message - $scope.onError({ - title: 'Info', - message: 'Your PGP key has already been synced.' - }); - return; - } - - // show sync ui if key is not synced - $scope.displayUploadUi(); - }); - } - }; - - $scope.handlePaste = function(event) { - var evt = event; - if (evt.originalEvent) { - evt = evt.originalEvent; - } - - var value = evt.clipboardData.getData('text/plain'); - if (!value) { + $scope.state.privateKeyUpload = { + toggle: function(to) { + // open lightbox + $scope.state.lightbox = (to) ? 'privatekey-upload' : undefined; + if (!to) { return; } - value = value.replace(/-/g, ''); - $scope.code0 = value.slice(0, 4); - $scope.code1 = value.slice(4, 8); - $scope.code2 = value.slice(8, 12); - $scope.code3 = value.slice(12, 16); - $scope.code4 = value.slice(16, 20); - $scope.code5 = value.slice(20, 24); - }; - - $scope.checkServerForKey = function(callback) { - var keyParams = pgp.getKeyParams(); - keychain.hasPrivateKey({ - userId: keyParams.userId, - keyId: keyParams._id - }, function(err, privateKeySynced) { - if (err) { - $scope.onError(err); - return; - } - + // show syncing status + $scope.step = 4; + // check if key is already synced + $scope.checkServerForKey(function(privateKeySynced) { if (privateKeySynced) { - callback(privateKeySynced); + // close lightbox + $scope.state.lightbox = undefined; + // show message + $scope.onError({ + title: 'Info', + message: 'Your PGP key has already been synced.' + }); return; } - callback(); + // show sync ui if key is not synced + $scope.displayUploadUi(); }); - }; + } + }; - $scope.displayUploadUi = function() { - // go to step 1 - $scope.step = 1; - // generate new code for the user - $scope.code = util.randomString(24); - $scope.displayedCode = $scope.code.slice(0, 4) + '-' + $scope.code.slice(4, 8) + '-' + $scope.code.slice(8, 12) + '-' + $scope.code.slice(12, 16) + '-' + $scope.code.slice(16, 20) + '-' + $scope.code.slice(20, 24); + $scope.handlePaste = function(event) { + var evt = event; + if (evt.originalEvent) { + evt = evt.originalEvent; + } - // clear input fields of any previous artifacts - $scope.code0 = $scope.code1 = $scope.code2 = $scope.code3 = $scope.code4 = $scope.code5 = ''; - }; + var value = evt.clipboardData.getData('text/plain'); + if (!value) { + return; + } - $scope.verifyCode = function() { - var inputCode = '' + $scope.code0 + $scope.code1 + $scope.code2 + $scope.code3 + $scope.code4 + $scope.code5; + value = value.replace(/-/g, ''); + $scope.code0 = value.slice(0, 4); + $scope.code1 = value.slice(4, 8); + $scope.code2 = value.slice(8, 12); + $scope.code3 = value.slice(12, 16); + $scope.code4 = value.slice(16, 20); + $scope.code5 = value.slice(20, 24); + }; - if (inputCode.toUpperCase() !== $scope.code) { - var err = new Error('The code does not match. Please go back and check the generated code.'); - err.sync = true; + $scope.checkServerForKey = function(callback) { + var keyParams = pgp.getKeyParams(); + keychain.hasPrivateKey({ + userId: keyParams.userId, + keyId: keyParams._id + }, function(err, privateKeySynced) { + if (err) { $scope.onError(err); - return false; + return; } - return true; - }; + if (privateKeySynced) { + callback(privateKeySynced); + return; + } - $scope.setDeviceName = function(callback) { - keychain.setDeviceName($scope.deviceName, callback); - }; + callback(); + }); + }; - $scope.encryptAndUploadKey = function(callback) { - var userId = appController._emailDao._account.emailAddress; - var code = $scope.code; + $scope.displayUploadUi = function() { + // go to step 1 + $scope.step = 1; + // generate new code for the user + $scope.code = util.randomString(24); + $scope.displayedCode = $scope.code.slice(0, 4) + '-' + $scope.code.slice(4, 8) + '-' + $scope.code.slice(8, 12) + '-' + $scope.code.slice(12, 16) + '-' + $scope.code.slice(16, 20) + '-' + $scope.code.slice(20, 24); - // register device to keychain service - keychain.registerDevice({ - userId: userId - }, function(err) { + // clear input fields of any previous artifacts + $scope.code0 = $scope.code1 = $scope.code2 = $scope.code3 = $scope.code4 = $scope.code5 = ''; + }; + + $scope.verifyCode = function() { + var inputCode = '' + $scope.code0 + $scope.code1 + $scope.code2 + $scope.code3 + $scope.code4 + $scope.code5; + + if (inputCode.toUpperCase() !== $scope.code) { + var err = new Error('The code does not match. Please go back and check the generated code.'); + err.sync = true; + $scope.onError(err); + return false; + } + + return true; + }; + + $scope.setDeviceName = function(callback) { + keychain.setDeviceName($scope.deviceName, callback); + }; + + $scope.encryptAndUploadKey = function(callback) { + var userId = appController._emailDao._account.emailAddress; + var code = $scope.code; + + // register device to keychain service + keychain.registerDevice({ + userId: userId + }, function(err) { + if (err) { + $scope.onError(err); + return; + } + + // encrypt private PGP key using code and upload + keychain.uploadPrivateKey({ + userId: userId, + code: code + }, callback); + }); + }; + + $scope.goBack = function() { + if ($scope.step > 1) { + $scope.step--; + } + }; + + $scope.goForward = function() { + if ($scope.step < 2) { + $scope.step++; + return; + } + + if ($scope.step === 2 && $scope.verifyCode()) { + $scope.step++; + return; + } + + if ($scope.step === 3) { + // set device name to local storage + $scope.setDeviceName(function(err) { if (err) { $scope.onError(err); return; } - // encrypt private PGP key using code and upload - keychain.uploadPrivateKey({ - userId: userId, - code: code - }, callback); - }); - }; - - $scope.goBack = function() { - if ($scope.step > 1) { - $scope.step--; - } - }; - - $scope.goForward = function() { - if ($scope.step < 2) { + // show spinner $scope.step++; - return; - } + $scope.$apply(); - if ($scope.step === 2 && $scope.verifyCode()) { - $scope.step++; - return; - } - - if ($scope.step === 3) { - // set device name to local storage - $scope.setDeviceName(function(err) { + // init key sync + $scope.encryptAndUploadKey(function(err) { if (err) { $scope.onError(err); return; } - // show spinner - $scope.step++; - $scope.$apply(); - - // init key sync - $scope.encryptAndUploadKey(function(err) { - if (err) { - $scope.onError(err); - return; - } - - // close sync dialog - $scope.state.privateKeyUpload.toggle(false); - // show success message - $scope.onError({ - title: 'Success', - message: 'Whiteout Keychain setup successful!' - }); + // close sync dialog + $scope.state.privateKeyUpload.toggle(false); + // show success message + $scope.onError({ + title: 'Success', + message: 'Whiteout Keychain setup successful!' }); }); - } - }; - + }); + } }; - // - // Directives - // +}; - var ngModule = angular.module('privatekey-upload', []); - ngModule.directive('focusNext', function() { - return { - link: function(scope, element, attr) { - var maxLen = element[0].maxLength; +// +// Directives +// - scope.$watch(attr.ngModel, function(val) { - if (val && val.length === maxLen) { - var nextinput = element.next('input'); - if (nextinput.length) { - nextinput[0].focus(); - } +var ngModule = angular.module('privatekey-upload', []); +ngModule.directive('focusNext', function() { + return { + link: function(scope, element, attr) { + var maxLen = element[0].maxLength; + + scope.$watch(attr.ngModel, function(val) { + if (val && val.length === maxLen) { + var nextinput = element.next('input'); + if (nextinput.length) { + nextinput[0].focus(); } - }); - } - }; - }); + } + }); + } + }; +}); - return PrivateKeyUploadCtrl; -}); \ No newline at end of file +exports = PrivateKeyUploadCtrl; \ No newline at end of file diff --git a/src/js/controller/read-sandbox.js b/src/js/controller/read-sandbox.js index 7a778e1..ca38807 100644 --- a/src/js/controller/read-sandbox.js +++ b/src/js/controller/read-sandbox.js @@ -1,218 +1,215 @@ -(function() { - 'use strict'; +'use strict'; - // set listener for event from main window - window.onmessage = function(e) { - var html = ''; +// set listener for event from main window +window.onmessage = function(e) { + var html = ''; - if (e.data.html) { - // display html mail body - html = '
' + e.data.html + '
'; - } else if (e.data.text) { - // diplay text mail body by with colored conversation nodes - html = renderNodes(parseConversation(e.data.text)); - } + if (e.data.html) { + // display html mail body + html = '
' + e.data.html + '
'; + } else if (e.data.text) { + // diplay text mail body by with colored conversation nodes + html = renderNodes(parseConversation(e.data.text)); + } - // sanitize HTML content: https://github.com/cure53/DOMPurify - html = window.DOMPurify.sanitize(html); - // make links open in a new window - html = html.replace(/]+\b)src=['"][^'">]+['"]/ig, function(match, prefix) { - return prefix; + // remove sources where necessary + if (e.data.removeImages) { + html = html.replace(/(]+\b)src=['"][^'">]+['"]/ig, function(match, prefix) { + return prefix; + }); + } + + document.body.innerHTML = html; + + scaleToFit(); +}; + +window.addEventListener('resize', scaleToFit); + +/** + * Parse email body and generate conversation nodes + * @param {Object} email The email object + * @return {Node} The root node of the conversion + */ +function parseConversation(textBody) { + var nodes; + + function parseLines(body) { + var lines = []; + body.split('\n').forEach(parseLine); + + function parseLine(line) { + var regex = /^>*/; + var result = regex.exec(line); + + lines.push({ + text: line.replace(regex, '').trim(), + level: (result && result.length > 0) ? result[0].length : 0 }); } - document.body.innerHTML = html; + return lines; + } - scaleToFit(); + function buildTextNodes(lines) { + var i, j, root, currentLevel, currentNode, levelDelta; + + root = new Node(); + currentLevel = 0; + currentNode = root; + + // iterate over text lines + for (i = 0; i < lines.length; i++) { + levelDelta = lines[i].level - currentLevel; + + if (levelDelta === 0) { + // we are at the desired node ... no traversal required + } else if (levelDelta > 0) { + // traverse to child node(s) + for (j = 0; j < levelDelta; j++) { + var newChild = new Node(currentNode); + // create new child node + currentNode.children.push(newChild); + // go to last child node + currentNode = newChild; + // increase current level by one + currentLevel++; + } + } else { + // traverse to parent(s) + for (j = levelDelta; j < 0; j++) { + currentNode = currentNode.parent; + currentLevel--; + } + } + + // add text to the current node + currentNode.addLine(lines[i].text); + } + + return root; + } + + function Node(parent) { + this.parent = parent; + this.children = []; + } + Node.prototype.addLine = function(lineText) { + var c, l; + + c = this.children; + l = c.length; + + // append text node to children if last child is not a text node + if (l < 1 || typeof c[l - 1] !== 'string') { + c[l] = ''; + l = c.length; + } + + // append line to last child (add newline between lines) + c[l - 1] += lineText + '\n'; }; - window.addEventListener('resize', scaleToFit); - - /** - * Parse email body and generate conversation nodes - * @param {Object} email The email object - * @return {Node} The root node of the conversion - */ - function parseConversation(textBody) { - var nodes; - - function parseLines(body) { - var lines = []; - body.split('\n').forEach(parseLine); - - function parseLine(line) { - var regex = /^>*/; - var result = regex.exec(line); - - lines.push({ - text: line.replace(regex, '').trim(), - level: (result && result.length > 0) ? result[0].length : 0 - }); - } - - return lines; - } - - function buildTextNodes(lines) { - var i, j, root, currentLevel, currentNode, levelDelta; - - root = new Node(); - currentLevel = 0; - currentNode = root; - - // iterate over text lines - for (i = 0; i < lines.length; i++) { - levelDelta = lines[i].level - currentLevel; - - if (levelDelta === 0) { - // we are at the desired node ... no traversal required - } else if (levelDelta > 0) { - // traverse to child node(s) - for (j = 0; j < levelDelta; j++) { - var newChild = new Node(currentNode); - // create new child node - currentNode.children.push(newChild); - // go to last child node - currentNode = newChild; - // increase current level by one - currentLevel++; - } - } else { - // traverse to parent(s) - for (j = levelDelta; j < 0; j++) { - currentNode = currentNode.parent; - currentLevel--; - } - } - - // add text to the current node - currentNode.addLine(lines[i].text); - } - - return root; - } - - function Node(parent) { - this.parent = parent; - this.children = []; - } - Node.prototype.addLine = function(lineText) { - var c, l; - - c = this.children; - l = c.length; - - // append text node to children if last child is not a text node - if (l < 1 || typeof c[l - 1] !== 'string') { - c[l] = ''; - l = c.length; - } - - // append line to last child (add newline between lines) - c[l - 1] += lineText + '\n'; - }; - - function removeParentReference(node) { - if (!node.children) { - // this is a text leaf ... terminate recursion - return; - } - - // remove parent node to prevent infinite loop in JSON stringify - delete node.parent; - - for (var i = 0; i < node.children.length; i++) { - if (typeof node.children[i] === 'string') { - // remove trailing newline in string - node.children[i] = node.children[i].replace(/\n$/, ''); - } else { - // I used recursion ... - removeParentReference(node.children[i]); - } - } - } - - nodes = buildTextNodes(parseLines(textBody.replace(/ >/g, '>'))); - removeParentReference(nodes); - - return nodes; - } - - /** - * Render the conversation nodes as markup. This is not injected directly into the DOM, but rather send to a sandboxed iframe to be rendered - * @param {Node} root The conversation root node - * @return {Strin} The conversation as markup - */ - function renderNodes(root) { - var body = ''; - - function render(node) { - var i, html = ''; - if (!node.children) { - // this is a text leaf - var lines = node.split('\n'); - for (i = 0; i < lines.length; i++) { - // replace all urls with anchors - lines[i] = lines[i].replace(/(https?:\/\/[^\s]+)/g, createArchor); - // wrap line into an element for easier styling - html += '
' + lines[i] + '
'; - } - return html; - } - - for (i = 0; i < node.children.length; i++) { - html += '
' + render(node.children[i]) + '
'; - } - - return html; - } - - function createArchor(url) { - return '
' + url + ''; - } - - function isLineEmpty(line) { - return line.replace(/>/g, '').trim().length === 0; - } - - for (var j = 0; j < root.children.length; j++) { - // start by rendering the root nodes children - body += render(root.children[j]); - } - - return '
' + body + '
'; - } - - /** - * Transform scale content to fit iframe width - */ - function scaleToFit() { - var view = document.getElementsByClassName('scale-body').item(0); - if(!view) { + function removeParentReference(node) { + if (!node.children) { + // this is a text leaf ... terminate recursion return; } - var parentWidth = view.parentNode.offsetWidth; - var w = view.offsetWidth; - var scale = ''; + // remove parent node to prevent infinite loop in JSON stringify + delete node.parent; - if(w > parentWidth) { - scale = parentWidth / w; - scale = 'scale(' + scale + ',' + scale + ')'; + for (var i = 0; i < node.children.length; i++) { + if (typeof node.children[i] === 'string') { + // remove trailing newline in string + node.children[i] = node.children[i].replace(/\n$/, ''); + } else { + // I used recursion ... + removeParentReference(node.children[i]); + } } - - view.style['-webkit-transform-origin'] = '0 0'; - view.style.transformOrigin = '0 0'; - view.style['-webkit-transform'] = scale; - view.style.transform = scale; } -})(); \ No newline at end of file + nodes = buildTextNodes(parseLines(textBody.replace(/ >/g, '>'))); + removeParentReference(nodes); + + return nodes; +} + +/** + * Render the conversation nodes as markup. This is not injected directly into the DOM, but rather send to a sandboxed iframe to be rendered + * @param {Node} root The conversation root node + * @return {Strin} The conversation as markup + */ +function renderNodes(root) { + var body = ''; + + function render(node) { + var i, html = ''; + if (!node.children) { + // this is a text leaf + var lines = node.split('\n'); + for (i = 0; i < lines.length; i++) { + // replace all urls with anchors + lines[i] = lines[i].replace(/(https?:\/\/[^\s]+)/g, createArchor); + // wrap line into an element for easier styling + html += '
' + lines[i] + '
'; + } + return html; + } + + for (i = 0; i < node.children.length; i++) { + html += '
' + render(node.children[i]) + '
'; + } + + return html; + } + + function createArchor(url) { + return '' + url + ''; + } + + function isLineEmpty(line) { + return line.replace(/>/g, '').trim().length === 0; + } + + for (var j = 0; j < root.children.length; j++) { + // start by rendering the root nodes children + body += render(root.children[j]); + } + + return '
' + body + '
'; +} + +/** + * Transform scale content to fit iframe width + */ +function scaleToFit() { + var view = document.getElementsByClassName('scale-body').item(0); + if (!view) { + return; + } + + var parentWidth = view.parentNode.offsetWidth; + var w = view.offsetWidth; + var scale = ''; + + if (w > parentWidth) { + scale = parentWidth / w; + scale = 'scale(' + scale + ',' + scale + ')'; + } + + view.style['-webkit-transform-origin'] = '0 0'; + view.style.transformOrigin = '0 0'; + view.style['-webkit-transform'] = scale; + view.style.transform = scale; +} \ No newline at end of file diff --git a/src/js/controller/read.js b/src/js/controller/read.js index dc3ea9c..acbfcb8 100644 --- a/src/js/controller/read.js +++ b/src/js/controller/read.js @@ -1,286 +1,283 @@ -define(function(require) { - 'use strict'; +'use strict'; - var appController = require('js/app-controller'), - download = require('js/util/download'), - angular = require('angular'), - str = require('js/app-config').string, - emailDao, invitationDao, outbox, pgp, keychain; +var appController = require('../app-controller'), + download = require('../util/download'), + str = require('../app-config').string, + emailDao, invitationDao, outbox, pgp, keychain; - // - // Controller - // +// +// Controller +// - var ReadCtrl = function($scope) { +var ReadCtrl = function($scope) { - emailDao = appController._emailDao; - invitationDao = appController._invitationDao; - outbox = appController._outboxBo; - pgp = appController._pgp; - keychain = appController._keychain; + emailDao = appController._emailDao; + invitationDao = appController._invitationDao; + outbox = appController._outboxBo; + pgp = appController._pgp; + keychain = appController._keychain; - // set default value so that the popover height is correct on init - $scope.keyId = 'No key found.'; + // set default value so that the popover height is correct on init + $scope.keyId = 'No key found.'; - $scope.state.read = { - open: false, - toggle: function(to) { - this.open = to; - } - }; - - $scope.getKeyId = function(address) { - $scope.keyId = 'Searching...'; - keychain.getReceiverPublicKey(address, function(err, pubkey) { - if (err) { - $scope.onError(err); - return; - } - - if (!pubkey) { - $scope.keyId = 'User has no key. Click to invite.'; - $scope.$apply(); - return; - } - - var fpr = pgp.getFingerprint(pubkey.publicKey); - var formatted = fpr.slice(32); - - $scope.keyId = 'PGP key: ' + formatted; - $scope.$apply(); - }); - }; - - $scope.$watch('state.mailList.selected', function(mail) { - if (!mail) { - return; - } - - // display sender security status - mail.from.forEach(checkPublicKey); - // display recipient security status - mail.to.forEach(checkPublicKey); - // display recipient security status - Array.isArray(mail.cc) && mail.cc.forEach(checkPublicKey); - }); - - function checkPublicKey(user) { - user.secure = undefined; - - if (!keychain) { - return; - } - - keychain.getReceiverPublicKey(user.address, function(err, pubkey) { - if (err) { - $scope.onError(err); - return; - } - - if (pubkey && pubkey.publicKey) { - user.secure = true; - } else { - user.secure = false; - } - - $scope.$apply(); - }); + $scope.state.read = { + open: false, + toggle: function(to) { + this.open = to; } - - $scope.download = function(attachment) { - // download file to disk if content is available - if (attachment.content) { - download.createDownload({ - content: attachment.content, - filename: attachment.filename, - contentType: attachment.mimeType - }); - return; - } - - var folder = $scope.state.nav.currentFolder; - var email = $scope.state.mailList.selected; - emailDao.getAttachment({ - folder: folder, - uid: email.uid, - attachment: attachment - }, $scope.onError); - }; - - $scope.invite = function(user) { - // only invite non-pgp users - if (user.secure) { - return; - } - - $scope.keyId = 'Sending invitation...'; - - var sender = emailDao._account.emailAddress, - recipient = user.address; - - invitationDao.invite({ - recipient: recipient, - sender: sender - }, function(err) { - if (err) { - $scope.onError(err); - return; - } - - var invitationMail = { - from: [{ - address: sender - }], - to: [{ - address: recipient - }], - cc: [], - bcc: [], - subject: str.invitationSubject, - body: str.invitationMessage - }; - - // send invitation mail - outbox.put(invitationMail, $scope.onError); - }); - }; }; - // - // Directives - // - - var ngModule = angular.module('read', []); - - ngModule.directive('replySelection', function() { - return function(scope, elm) { - var popover, visible; - - popover = angular.element(document.querySelector('.reply-selection')); - visible = false; - - elm.on('touchstart click', appear); - elm.parent().parent().on('touchstart click', disappear); - popover.on('touchstart click', disappear); - - function appear(e) { - e.preventDefault(); - e.stopPropagation(); - - visible = true; - - // set popover position - var top = elm[0].offsetTop; - var left = elm[0].offsetLeft; - var width = elm[0].offsetWidth; - var height = elm[0].offsetHeight; - - popover[0].style.transition = 'opacity 0.1s linear'; - popover[0].style.top = (top + height) + 'px'; - popover[0].style.left = (left + width / 2 - popover[0].offsetWidth / 2) + 'px'; - popover[0].style.opacity = '1'; + $scope.getKeyId = function(address) { + $scope.keyId = 'Searching...'; + keychain.getReceiverPublicKey(address, function(err, pubkey) { + if (err) { + $scope.onError(err); + return; } - function disappear() { - if (!visible) { - return; - } - - popover[0].style.transition = 'opacity 0.25s linear, top 0.25s step-end, left 0.25s step-end'; - popover[0].style.opacity = '0'; - popover[0].style.top = '-9999px'; - popover[0].style.left = '-9999px'; - visible = false; + if (!pubkey) { + $scope.keyId = 'User has no key. Click to invite.'; + $scope.$apply(); + return; } - }; + + var fpr = pgp.getFingerprint(pubkey.publicKey); + var formatted = fpr.slice(32); + + $scope.keyId = 'PGP key: ' + formatted; + $scope.$apply(); + }); + }; + + $scope.$watch('state.mailList.selected', function(mail) { + if (!mail) { + return; + } + + // display sender security status + mail.from.forEach(checkPublicKey); + // display recipient security status + mail.to.forEach(checkPublicKey); + // display recipient security status + Array.isArray(mail.cc) && mail.cc.forEach(checkPublicKey); }); - ngModule.directive('frameLoad', function($timeout, $window) { - return function(scope, elm) { - var iframe = elm[0]; + function checkPublicKey(user) { + user.secure = undefined; - scope.$watch('state.read.open', function(open) { - if (open) { - // trigger rendering of iframe - // otherwise scale to fit would not compute correct dimensions on mobile - displayText(scope.state.mailList.selected ? scope.state.mailList.selected.body : undefined); - displayHtml(scope.state.mailList.selected ? scope.state.mailList.selected.html : undefined); - } + if (!keychain) { + return; + } + + keychain.getReceiverPublicKey(user.address, function(err, pubkey) { + if (err) { + $scope.onError(err); + return; + } + + if (pubkey && pubkey.publicKey) { + user.secure = true; + } else { + user.secure = false; + } + + $scope.$apply(); + }); + } + + $scope.download = function(attachment) { + // download file to disk if content is available + if (attachment.content) { + download.createDownload({ + content: attachment.content, + filename: attachment.filename, + contentType: attachment.mimeType }); + return; + } - $window.addEventListener('resize', scaleToFit); + var folder = $scope.state.nav.currentFolder; + var email = $scope.state.mailList.selected; + emailDao.getAttachment({ + folder: folder, + uid: email.uid, + attachment: attachment + }, $scope.onError); + }; - iframe.onload = function() { - // set listeners - scope.$watch('state.mailList.selected.body', displayText); - scope.$watch('state.mailList.selected.html', displayHtml); - // display initial message body - scope.$apply(); + $scope.invite = function(user) { + // only invite non-pgp users + if (user.secure) { + return; + } + + $scope.keyId = 'Sending invitation...'; + + var sender = emailDao._account.emailAddress, + recipient = user.address; + + invitationDao.invite({ + recipient: recipient, + sender: sender + }, function(err) { + if (err) { + $scope.onError(err); + return; + } + + var invitationMail = { + from: [{ + address: sender + }], + to: [{ + address: recipient + }], + cc: [], + bcc: [], + subject: str.invitationSubject, + body: str.invitationMessage }; - function displayText(body) { - var mail = scope.state.mailList.selected; - if ((mail && mail.html) || (mail && mail.encrypted && !mail.decrypted)) { - return; - } + // send invitation mail + outbox.put(invitationMail, $scope.onError); + }); + }; +}; - // send text body for rendering in iframe - iframe.contentWindow.postMessage({ - text: body - }, '*'); +// +// Directives +// - $timeout(scaleToFit, 0); +var ngModule = angular.module('read', []); + +ngModule.directive('replySelection', function() { + return function(scope, elm) { + var popover, visible; + + popover = angular.element(document.querySelector('.reply-selection')); + visible = false; + + elm.on('touchstart click', appear); + elm.parent().parent().on('touchstart click', disappear); + popover.on('touchstart click', disappear); + + function appear(e) { + e.preventDefault(); + e.stopPropagation(); + + visible = true; + + // set popover position + var top = elm[0].offsetTop; + var left = elm[0].offsetLeft; + var width = elm[0].offsetWidth; + var height = elm[0].offsetHeight; + + popover[0].style.transition = 'opacity 0.1s linear'; + popover[0].style.top = (top + height) + 'px'; + popover[0].style.left = (left + width / 2 - popover[0].offsetWidth / 2) + 'px'; + popover[0].style.opacity = '1'; + } + + function disappear() { + if (!visible) { + return; } - function displayHtml(html) { - if (!html) { - return; - } + popover[0].style.transition = 'opacity 0.25s linear, top 0.25s step-end, left 0.25s step-end'; + popover[0].style.opacity = '0'; + popover[0].style.top = '-9999px'; + popover[0].style.left = '-9999px'; + visible = false; + } + }; +}); - // if there are image tags in the html? - var hasImages = /]+\bsrc=['"][^'">]+['"]/ig.test(html); - scope.showImageButton = hasImages; +ngModule.directive('frameLoad', function($timeout, $window) { + return function(scope, elm) { + var iframe = elm[0]; - iframe.contentWindow.postMessage({ - html: html, - removeImages: hasImages // avoids doing unnecessary work on the html - }, '*'); - - // only add a scope function to reload the html if there are images - if (hasImages) { - // reload WITH images - scope.displayImages = function() { - scope.showImageButton = false; - iframe.contentWindow.postMessage({ - html: html, - removeImages: false - }, '*'); - }; - } - - $timeout(scaleToFit, 0); + scope.$watch('state.read.open', function(open) { + if (open) { + // trigger rendering of iframe + // otherwise scale to fit would not compute correct dimensions on mobile + displayText(scope.state.mailList.selected ? scope.state.mailList.selected.body : undefined); + displayHtml(scope.state.mailList.selected ? scope.state.mailList.selected.html : undefined); } + }); - // transform scale iframe (necessary on iOS) to fit container width - function scaleToFit() { - var parentWidth = elm.parent().width(); - var w = elm.width(); - var scale = ''; + $window.addEventListener('resize', scaleToFit); - if (w > parentWidth) { - scale = parentWidth / w; - scale = 'scale(' + scale + ',' + scale + ')'; - } - - elm.css({ - '-webkit-transform-origin': '0 0', - 'transform-origin': '0 0', - '-webkit-transform': scale, - 'transform': scale - }); - } + iframe.onload = function() { + // set listeners + scope.$watch('state.mailList.selected.body', displayText); + scope.$watch('state.mailList.selected.html', displayHtml); + // display initial message body + scope.$apply(); }; - }); - return ReadCtrl; -}); \ No newline at end of file + function displayText(body) { + var mail = scope.state.mailList.selected; + if ((mail && mail.html) || (mail && mail.encrypted && !mail.decrypted)) { + return; + } + + // send text body for rendering in iframe + iframe.contentWindow.postMessage({ + text: body + }, '*'); + + $timeout(scaleToFit, 0); + } + + function displayHtml(html) { + if (!html) { + return; + } + + // if there are image tags in the html? + var hasImages = /]+\bsrc=['"][^'">]+['"]/ig.test(html); + scope.showImageButton = hasImages; + + iframe.contentWindow.postMessage({ + html: html, + removeImages: hasImages // avoids doing unnecessary work on the html + }, '*'); + + // only add a scope function to reload the html if there are images + if (hasImages) { + // reload WITH images + scope.displayImages = function() { + scope.showImageButton = false; + iframe.contentWindow.postMessage({ + html: html, + removeImages: false + }, '*'); + }; + } + + $timeout(scaleToFit, 0); + } + + // transform scale iframe (necessary on iOS) to fit container width + function scaleToFit() { + var parentWidth = elm.parent().width(); + var w = elm.width(); + var scale = ''; + + if (w > parentWidth) { + scale = parentWidth / w; + scale = 'scale(' + scale + ',' + scale + ')'; + } + + elm.css({ + '-webkit-transform-origin': '0 0', + 'transform-origin': '0 0', + '-webkit-transform': scale, + 'transform': scale + }); + } + }; +}); + +exports = ReadCtrl; \ No newline at end of file diff --git a/src/js/controller/set-passphrase.js b/src/js/controller/set-passphrase.js index 726c78b..9dceaf4 100644 --- a/src/js/controller/set-passphrase.js +++ b/src/js/controller/set-passphrase.js @@ -1,138 +1,136 @@ -define(function(require) { - 'use strict'; +'use strict'; - var appController = require('js/app-controller'), - pgp, keychain; +var appController = require('../app-controller'), + pgp, keychain; - // - // Controller - // +// +// Controller +// - var SetPassphraseCtrl = function($scope) { - keychain = appController._keychain; - pgp = appController._pgp; +var SetPassphraseCtrl = function($scope) { + keychain = appController._keychain; + pgp = appController._pgp; - $scope.state.setPassphrase = { - toggle: function(to) { - $scope.state.lightbox = (to) ? 'set-passphrase' : undefined; + $scope.state.setPassphrase = { + toggle: function(to) { + $scope.state.lightbox = (to) ? 'set-passphrase' : undefined; - $scope.newPassphrase = undefined; - $scope.oldPassphrase = undefined; - $scope.confirmation = undefined; - $scope.passphraseMsg = undefined; - } - }; - - // - // scope variables - // - - // - // scope functions - // - - /* - * Taken from jQuery validate.password plug-in 1.0 - * http://bassistance.de/jquery-plugins/jquery-plugin-validate.password/ - * - * Copyright (c) 2009 Jörn Zaefferer - * - * Licensed under the MIT - * http://www.opensource.org/licenses/mit-license.php - */ - $scope.checkPassphraseQuality = function() { - var passphrase = $scope.newPassphrase; - $scope.passphraseRating = 0; - - var LOWER = /[a-z]/, - UPPER = /[A-Z]/, - DIGIT = /[0-9]/, - DIGITS = /[0-9].*[0-9]/, - SPECIAL = /[^a-zA-Z0-9]/, - SAME = /^(.)\1+$/; - - function uncapitalize(str) { - return str.substring(0, 1).toLowerCase() + str.substring(1); - } - - if (!passphrase) { - // no rating for empty passphrase - $scope.passphraseMsg = ''; - return; - } - - if (passphrase.length < 8 || SAME.test(passphrase)) { - $scope.passphraseMsg = 'Very weak'; - return; - } - - var lower = LOWER.test(passphrase), - upper = UPPER.test(uncapitalize(passphrase)), - digit = DIGIT.test(passphrase), - digits = DIGITS.test(passphrase), - special = SPECIAL.test(passphrase); - - if (lower && upper && digit || lower && digits || upper && digits || special) { - $scope.passphraseMsg = 'Strong'; - $scope.passphraseRating = 3; - } else if (lower && upper || lower && digit || upper && digit) { - $scope.passphraseMsg = 'Good'; - $scope.passphraseRating = 2; - } else { - $scope.passphraseMsg = 'Weak'; - $scope.passphraseRating = 1; - } - }; - - $scope.setPassphrase = function() { - var keyId = pgp.getKeyParams()._id; - keychain.lookupPrivateKey(keyId, function(err, savedKey) { - if (err) { - $scope.onError(err); - return; - } - - pgp.changePassphrase({ - privateKeyArmored: savedKey.encryptedKey, - oldPassphrase: $scope.oldPassphrase, - newPassphrase: $scope.newPassphrase - }, onPassphraseChanged); - }); - }; - - function onPassphraseChanged(err, newPrivateKeyArmored) { - if (err) { - err.showBugReporter = false; - $scope.onError(err); - return; - } - - // persist new armored key - var keyParams = pgp.getKeyParams(newPrivateKeyArmored); - var privateKey = { - _id: keyParams._id, - userId: keyParams.userId, - userIds: keyParams.userIds, - encryptedKey: newPrivateKeyArmored - }; - - keychain.saveLocalPrivateKey(privateKey, onKeyPersisted); - } - - function onKeyPersisted(err) { - if (err) { - $scope.onError(err); - return; - } - - $scope.state.setPassphrase.toggle(false); - $scope.$apply(); - $scope.onError({ - title: 'Success', - message: 'Passphrase change complete.' - }); + $scope.newPassphrase = undefined; + $scope.oldPassphrase = undefined; + $scope.confirmation = undefined; + $scope.passphraseMsg = undefined; } }; - return SetPassphraseCtrl; -}); \ No newline at end of file + // + // scope variables + // + + // + // scope functions + // + + /* + * Taken from jQuery validate.password plug-in 1.0 + * http://bassistance.de/jquery-plugins/jquery-plugin-validate.password/ + * + * Copyright (c) 2009 Jörn Zaefferer + * + * Licensed under the MIT + * http://www.opensource.org/licenses/mit-license.php + */ + $scope.checkPassphraseQuality = function() { + var passphrase = $scope.newPassphrase; + $scope.passphraseRating = 0; + + var LOWER = /[a-z]/, + UPPER = /[A-Z]/, + DIGIT = /[0-9]/, + DIGITS = /[0-9].*[0-9]/, + SPECIAL = /[^a-zA-Z0-9]/, + SAME = /^(.)\1+$/; + + function uncapitalize(str) { + return str.substring(0, 1).toLowerCase() + str.substring(1); + } + + if (!passphrase) { + // no rating for empty passphrase + $scope.passphraseMsg = ''; + return; + } + + if (passphrase.length < 8 || SAME.test(passphrase)) { + $scope.passphraseMsg = 'Very weak'; + return; + } + + var lower = LOWER.test(passphrase), + upper = UPPER.test(uncapitalize(passphrase)), + digit = DIGIT.test(passphrase), + digits = DIGITS.test(passphrase), + special = SPECIAL.test(passphrase); + + if (lower && upper && digit || lower && digits || upper && digits || special) { + $scope.passphraseMsg = 'Strong'; + $scope.passphraseRating = 3; + } else if (lower && upper || lower && digit || upper && digit) { + $scope.passphraseMsg = 'Good'; + $scope.passphraseRating = 2; + } else { + $scope.passphraseMsg = 'Weak'; + $scope.passphraseRating = 1; + } + }; + + $scope.setPassphrase = function() { + var keyId = pgp.getKeyParams()._id; + keychain.lookupPrivateKey(keyId, function(err, savedKey) { + if (err) { + $scope.onError(err); + return; + } + + pgp.changePassphrase({ + privateKeyArmored: savedKey.encryptedKey, + oldPassphrase: $scope.oldPassphrase, + newPassphrase: $scope.newPassphrase + }, onPassphraseChanged); + }); + }; + + function onPassphraseChanged(err, newPrivateKeyArmored) { + if (err) { + err.showBugReporter = false; + $scope.onError(err); + return; + } + + // persist new armored key + var keyParams = pgp.getKeyParams(newPrivateKeyArmored); + var privateKey = { + _id: keyParams._id, + userId: keyParams.userId, + userIds: keyParams.userIds, + encryptedKey: newPrivateKeyArmored + }; + + keychain.saveLocalPrivateKey(privateKey, onKeyPersisted); + } + + function onKeyPersisted(err) { + if (err) { + $scope.onError(err); + return; + } + + $scope.state.setPassphrase.toggle(false); + $scope.$apply(); + $scope.onError({ + title: 'Success', + message: 'Passphrase change complete.' + }); + } +}; + +exports = SetPassphraseCtrl; \ No newline at end of file diff --git a/src/js/controller/write.js b/src/js/controller/write.js index 41d0c12..81f901d 100644 --- a/src/js/controller/write.js +++ b/src/js/controller/write.js @@ -1,534 +1,530 @@ -define(function(require) { - 'use strict'; +'use strict'; - var angular = require('angular'), - _ = require('underscore'), - appController = require('js/app-controller'), - axe = require('axe'), - util = require('js/crypto/util'), - str = require('js/app-config').string, - pgp, emailDao, outbox, keychainDao, auth; +var appController = require('../app-controller'), + axe = require('axe-logger'), + util = require('crypto-lib').util, + str = require('../app-config').string, + pgp, emailDao, outbox, keychainDao, auth; + +// +// Controller +// + +var WriteCtrl = function($scope, $filter, $q) { + pgp = appController._pgp; + auth = appController._auth; + emailDao = appController._emailDao; + outbox = appController._outboxBo; + keychainDao = appController._keychain; + + // set default value so that the popover height is correct on init + $scope.keyId = 'XXXXXXXX'; // - // Controller + // Init // - var WriteCtrl = function($scope, $filter, $q) { - pgp = appController._pgp; - auth = appController._auth; - emailDao = appController._emailDao; - outbox = appController._outboxBo; - keychainDao = appController._keychain; + $scope.state.writer = { + write: function(replyTo, replyAll, forward) { + $scope.state.lightbox = 'write'; + $scope.replyTo = replyTo; - // set default value so that the popover height is correct on init - $scope.keyId = 'XXXXXXXX'; + resetFields(); - // - // Init - // + // fill fields depending on replyTo + fillFields(replyTo, replyAll, forward); - $scope.state.writer = { - write: function(replyTo, replyAll, forward) { - $scope.state.lightbox = 'write'; - $scope.replyTo = replyTo; + $scope.verify($scope.to[0]); + }, + reportBug: function() { + $scope.state.lightbox = 'write'; + resetFields(); + reportBug(); + $scope.verify($scope.to[0]); + }, + close: function() { + $scope.state.lightbox = undefined; + } + }; - resetFields(); + function resetFields() { + $scope.writerTitle = 'New email'; + $scope.to = []; + $scope.showCC = false; + $scope.cc = []; + $scope.showBCC = false; + $scope.bcc = []; + $scope.subject = ''; + $scope.body = ''; + $scope.attachments = []; + $scope.addressBookCache = undefined; + } - // fill fields depending on replyTo - fillFields(replyTo, replyAll, forward); + function reportBug() { + var dump = ''; + var appender = { + log: function(level, date, component, log) { + // add a tag for the log level + if (level === axe.DEBUG) { + dump += '[DEBUG]'; + } else if (level === axe.INFO) { + dump += '[INFO]'; + } else if (level === axe.WARN) { + dump += '[WARN]'; + } else if (level === axe.ERROR) { + dump += '[ERROR]'; + } - $scope.verify($scope.to[0]); - }, - reportBug: function() { - $scope.state.lightbox = 'write'; - resetFields(); - reportBug(); - $scope.verify($scope.to[0]); - }, - close: function() { - $scope.state.lightbox = undefined; + dump += '[' + date.toISOString() + ']'; + + // component is optional + if (component) { + dump += '[' + component + ']'; + } + + // log may be an error or a string + dump += ' ' + (log || '').toString(); + + // if an error it is, a stack trace it has. print it, we should. + if (log.stack) { + dump += ' . Stack: ' + log.stack; + } + + dump += '\n'; } }; + axe.dump(appender); - function resetFields() { - $scope.writerTitle = 'New email'; - $scope.to = []; - $scope.showCC = false; - $scope.cc = []; - $scope.showBCC = false; - $scope.bcc = []; - $scope.subject = ''; - $scope.body = ''; - $scope.attachments = []; - $scope.addressBookCache = undefined; + $scope.to = [{ + address: str.supportAddress + }]; + $scope.writerTitle = str.bugReportTitle; + $scope.subject = str.bugReportSubject; + $scope.body = str.bugReportBody + dump; + + } + + function fillFields(re, replyAll, forward) { + var replyTo, from, sentDate, body; + + if (!re) { + return; } - function reportBug() { - var dump = ''; - var appender = { - log: function(level, date, component, log) { - // add a tag for the log level - if (level === axe.DEBUG) { - dump += '[DEBUG]'; - } else if (level === axe.INFO) { - dump += '[INFO]'; - } else if (level === axe.WARN) { - dump += '[WARN]'; - } else if (level === axe.ERROR) { - dump += '[ERROR]'; - } + $scope.writerTitle = (forward) ? 'Forward' : 'Reply'; - dump += '[' + date.toISOString() + ']'; + replyTo = re.replyTo && re.replyTo[0] && re.replyTo[0].address || re.from[0].address; - // component is optional - if (component) { - dump += '[' + component + ']'; - } + // fill recipient field and references + if (!forward) { + $scope.to.unshift({ + address: replyTo + }); + $scope.to.forEach($scope.verify); - // log may be an error or a string - dump += ' ' + (log || '').toString(); - - // if an error it is, a stack trace it has. print it, we should. - if (log.stack) { - dump += ' . Stack: ' + log.stack; - } - - dump += '\n'; - } - }; - axe.dump(appender); - - $scope.to = [{ - address: str.supportAddress - }]; - $scope.writerTitle = str.bugReportTitle; - $scope.subject = str.bugReportSubject; - $scope.body = str.bugReportBody + dump; - - } - - function fillFields(re, replyAll, forward) { - var replyTo, from, sentDate, body; - - if (!re) { - return; + $scope.references = (re.references || []); + if (re.id && $scope.references.indexOf(re.id) < 0) { + // references might not exist yet, so use the double concat + $scope.references = $scope.references.concat(re.id); } - - $scope.writerTitle = (forward) ? 'Forward' : 'Reply'; - - replyTo = re.replyTo && re.replyTo[0] && re.replyTo[0].address || re.from[0].address; - - // fill recipient field and references - if (!forward) { - $scope.to.unshift({ - address: replyTo - }); - $scope.to.forEach($scope.verify); - - $scope.references = (re.references || []); - if (re.id && $scope.references.indexOf(re.id) < 0) { - // references might not exist yet, so use the double concat - $scope.references = $scope.references.concat(re.id); - } - if (re.id) { - $scope.inReplyTo = re.id; - } - } - if (replyAll) { - re.to.concat(re.cc).forEach(function(recipient) { - var me = emailDao._account.emailAddress; - if (recipient.address === me && replyTo !== me) { - // don't reply to yourself - return; - } - $scope.cc.unshift({ - address: recipient.address - }); - }); - - // filter duplicates - $scope.cc = _.uniq($scope.cc, function(recipient) { - return recipient.address; - }); - $scope.showCC = true; - $scope.cc.forEach($scope.verify); - } - - // fill attachments and references on forward - if (forward) { - // create a new array, otherwise removing an attachment will also - // remove it from the original in the mail list as a side effect - $scope.attachments = [].concat(re.attachments); - if (re.id) { - $scope.references = [re.id]; - } - } - - // fill subject - if (forward) { - $scope.subject = 'Fwd: ' + re.subject; - } else { - $scope.subject = 'Re: ' + ((re.subject) ? re.subject.replace('Re: ', '') : ''); - } - - // fill text body - from = re.from[0].name || replyTo; - sentDate = $filter('date')(re.sentDate, 'EEEE, MMM d, yyyy h:mm a'); - - function createString(array) { - var str = ''; - array.forEach(function(to) { - str += (str) ? ', ' : ''; - str += ((to.name) ? to.name : to.address) + ' <' + to.address + '>'; - }); - return str; - } - - if (forward) { - body = '\n\n' + - '---------- Forwarded message ----------\n' + - 'From: ' + re.from[0].name + ' <' + re.from[0].address + '>\n' + - 'Date: ' + sentDate + '\n' + - 'Subject: ' + re.subject + '\n' + - 'To: ' + createString(re.to) + '\n' + - ((re.cc && re.cc.length > 0) ? 'Cc: ' + createString(re.cc) + '\n' : '') + - '\n\n'; - - } else { - body = '\n\n' + sentDate + ' ' + from + ' wrote:\n> '; - } - - if (re.body) { - body += re.body.trim().split('\n').join('\n> ').replace(/ >/g, '>'); - $scope.body = body; + if (re.id) { + $scope.inReplyTo = re.id; } } - - // - // Editing headers - // - - /** - * Verify email address and fetch its public key - */ - $scope.verify = function(recipient) { - if (!recipient) { - return; - } - - // set display to insecure while fetching keys - recipient.key = undefined; - recipient.secure = false; - $scope.checkSendStatus(); - - // verify email address - if (!util.validateEmailAddress(recipient.address)) { - recipient.secure = undefined; - $scope.checkSendStatus(); - return; - } - - // keychainDao is undefined in local dev environment - if (keychainDao) { - // check if to address is contained in known public keys - // when we write an email, we always need to work with the latest keys available - keychainDao.refreshKeyForUserId(recipient.address, function(err, key) { - if (err) { - $scope.onError(err); - return; - } - - if (key) { - // compare again since model could have changed during the roundtrip - var matchingUserId = _.findWhere(key.userIds, { - emailAddress: recipient.address - }); - // compare either primary userId or (if available) multiple IDs - if (key.userId === recipient.address || matchingUserId) { - recipient.key = key; - recipient.secure = true; - } - } - - $scope.checkSendStatus(); - $scope.$digest(); - }); - } - }; - - /** - * Check if it is ok to send an email depending on the invitation state of the addresses - */ - $scope.checkSendStatus = function() { - $scope.okToSend = false; - $scope.sendBtnText = undefined; - $scope.sendBtnSecure = undefined; - - var allSecure = true; - var numReceivers = 0; - - // count number of receivers and check security - $scope.to.forEach(check); - $scope.cc.forEach(check); - $scope.bcc.forEach(check); - - function check(recipient) { - // validate address - if (!util.validateEmailAddress(recipient.address)) { + if (replyAll) { + re.to.concat(re.cc).forEach(function(recipient) { + var me = emailDao._account.emailAddress; + if (recipient.address === me && replyTo !== me) { + // don't reply to yourself return; } - numReceivers++; - if (!recipient.secure) { - allSecure = false; - } + $scope.cc.unshift({ + address: recipient.address + }); + }); + + // filter duplicates + $scope.cc = _.uniq($scope.cc, function(recipient) { + return recipient.address; + }); + $scope.showCC = true; + $scope.cc.forEach($scope.verify); + } + + // fill attachments and references on forward + if (forward) { + // create a new array, otherwise removing an attachment will also + // remove it from the original in the mail list as a side effect + $scope.attachments = [].concat(re.attachments); + if (re.id) { + $scope.references = [re.id]; } + } - // only allow sending if receviers exist - if (numReceivers < 1) { - return; - } + // fill subject + if (forward) { + $scope.subject = 'Fwd: ' + re.subject; + } else { + $scope.subject = 'Re: ' + ((re.subject) ? re.subject.replace('Re: ', '') : ''); + } - // bcc automatically disables secure sending - if ($scope.bcc.filter(filterEmptyAddresses).length > 0) { - allSecure = false; - } + // fill text body + from = re.from[0].name || replyTo; + sentDate = $filter('date')(re.sentDate, 'EEEE, MMM d, yyyy h:mm a'); - if (allSecure) { - // send encrypted if all secure - $scope.okToSend = true; - $scope.sendBtnText = str.sendBtnSecure; - $scope.sendBtnSecure = true; - } else { - // send plaintext - $scope.okToSend = true; - $scope.sendBtnText = str.sendBtnClear; - $scope.sendBtnSecure = false; - } - }; + function createString(array) { + var str = ''; + array.forEach(function(to) { + str += (str) ? ', ' : ''; + str += ((to.name) ? to.name : to.address) + ' <' + to.address + '>'; + }); + return str; + } - // - // Editing attachments - // + if (forward) { + body = '\n\n' + + '---------- Forwarded message ----------\n' + + 'From: ' + re.from[0].name + ' <' + re.from[0].address + '>\n' + + 'Date: ' + sentDate + '\n' + + 'Subject: ' + re.subject + '\n' + + 'To: ' + createString(re.to) + '\n' + + ((re.cc && re.cc.length > 0) ? 'Cc: ' + createString(re.cc) + '\n' : '') + + '\n\n'; - $scope.remove = function(attachment) { - $scope.attachments.splice($scope.attachments.indexOf(attachment), 1); - }; + } else { + body = '\n\n' + sentDate + ' ' + from + ' wrote:\n> '; + } - // - // Editing email body - // + if (re.body) { + body += re.body.trim().split('\n').join('\n> ').replace(/ >/g, '>'); + $scope.body = body; + } + } - $scope.sendToOutbox = function() { - var email; + // + // Editing headers + // - // build email model for smtp-client - email = { - from: [{ - name: emailDao._account.realname, - address: emailDao._account.emailAddress - }], - to: $scope.to.filter(filterEmptyAddresses), - cc: $scope.cc.filter(filterEmptyAddresses), - bcc: $scope.bcc.filter(filterEmptyAddresses), - subject: $scope.subject.trim() ? $scope.subject.trim() : str.fallbackSubject, // Subject line, or the fallback subject, if nothing valid was entered - body: $scope.body.trim(), // use parsed plaintext body - attachments: $scope.attachments, - sentDate: new Date(), - headers: {} - }; + /** + * Verify email address and fetch its public key + */ + $scope.verify = function(recipient) { + if (!recipient) { + return; + } - if ($scope.inReplyTo) { - email.headers['in-reply-to'] = '<' + $scope.inReplyTo + '>'; - } + // set display to insecure while fetching keys + recipient.key = undefined; + recipient.secure = false; + $scope.checkSendStatus(); - if ($scope.references && $scope.references.length) { - email.headers.references = $scope.references.map(function(reference) { - return '<' + reference + '>'; - }).join(' '); - } + // verify email address + if (!util.validateEmailAddress(recipient.address)) { + recipient.secure = undefined; + $scope.checkSendStatus(); + return; + } - // close the writer - $scope.state.writer.close(); - - // persist the email to disk for later sending - outbox.put(email, function(err) { + // keychainDao is undefined in local dev environment + if (keychainDao) { + // check if to address is contained in known public keys + // when we write an email, we always need to work with the latest keys available + keychainDao.refreshKeyForUserId(recipient.address, function(err, key) { if (err) { $scope.onError(err); return; } - // if we need to synchronize replyTo.answered = true to imap, - // let's do that. otherwise, we're done - if (!$scope.replyTo || $scope.replyTo.answered) { - return; + if (key) { + // compare again since model could have changed during the roundtrip + var matchingUserId = _.findWhere(key.userIds, { + emailAddress: recipient.address + }); + // compare either primary userId or (if available) multiple IDs + if (key.userId === recipient.address || matchingUserId) { + recipient.key = key; + recipient.secure = true; + } } - $scope.replyTo.answered = true; - emailDao.setFlags({ - folder: currentFolder(), - message: $scope.replyTo - }, function(err) { - if (err && err.code !== 42) { - $scope.onError(err); - return; - } - - // offline or no error, let's apply the ui changes - $scope.$apply(); - }); + $scope.checkSendStatus(); + $scope.$digest(); }); - - }; - - // - // Tag input & Autocomplete - // - - $scope.tagStyle = function(recipient) { - var classes = ['label']; - if (recipient.secure === false) { - classes.push('label-primary'); - } - return classes; - }; - - $scope.lookupAddressBook = function(query) { - var deferred = $q.defer(); - - if (!$scope.addressBookCache) { - // populate address book cache - keychainDao.listLocalPublicKeys(function(err, keys) { - if (err) { - $scope.onError(err); - return; - } - - $scope.addressBookCache = keys.map(function(key) { - return { - address: key.userId - }; - }); - filter(); - }); - - } else { - filter(); - } - - // query address book cache - function filter() { - var addresses = $scope.addressBookCache.filter(function(i) { - return i.address.indexOf(query) !== -1; - }); - deferred.resolve(addresses); - } - - return deferred.promise; - }; - - // - // Helpers - // - - function currentFolder() { - return $scope.state.nav.currentFolder; - } - - /* - * Visitor to filter out objects without an address property, i.e. empty addresses - */ - function filterEmptyAddresses(addr) { - return !!addr.address; } }; + /** + * Check if it is ok to send an email depending on the invitation state of the addresses + */ + $scope.checkSendStatus = function() { + $scope.okToSend = false; + $scope.sendBtnText = undefined; + $scope.sendBtnSecure = undefined; + + var allSecure = true; + var numReceivers = 0; + + // count number of receivers and check security + $scope.to.forEach(check); + $scope.cc.forEach(check); + $scope.bcc.forEach(check); + + function check(recipient) { + // validate address + if (!util.validateEmailAddress(recipient.address)) { + return; + } + numReceivers++; + if (!recipient.secure) { + allSecure = false; + } + } + + // only allow sending if receviers exist + if (numReceivers < 1) { + return; + } + + // bcc automatically disables secure sending + if ($scope.bcc.filter(filterEmptyAddresses).length > 0) { + allSecure = false; + } + + if (allSecure) { + // send encrypted if all secure + $scope.okToSend = true; + $scope.sendBtnText = str.sendBtnSecure; + $scope.sendBtnSecure = true; + } else { + // send plaintext + $scope.okToSend = true; + $scope.sendBtnText = str.sendBtnClear; + $scope.sendBtnSecure = false; + } + }; // - // Directives + // Editing attachments // - var ngModule = angular.module('write', []); + $scope.remove = function(attachment) { + $scope.attachments.splice($scope.attachments.indexOf(attachment), 1); + }; - ngModule.directive('focusMe', function($timeout, $parse) { - return { - //scope: true, // optionally create a child scope - link: function(scope, element, attrs) { - var model = $parse(attrs.focusMe); - scope.$watch(model, function(value) { - if (value === true) { - $timeout(function() { - var el = element[0]; - el.focus(); - // set cursor to start of textarea - if (el.type === 'textarea') { - el.selectionStart = 0; - el.selectionEnd = 0; - } - }, 100); - } - }); - } + // + // Editing email body + // + + $scope.sendToOutbox = function() { + var email; + + // build email model for smtp-client + email = { + from: [{ + name: emailDao._account.realname, + address: emailDao._account.emailAddress + }], + to: $scope.to.filter(filterEmptyAddresses), + cc: $scope.cc.filter(filterEmptyAddresses), + bcc: $scope.bcc.filter(filterEmptyAddresses), + subject: $scope.subject.trim() ? $scope.subject.trim() : str.fallbackSubject, // Subject line, or the fallback subject, if nothing valid was entered + body: $scope.body.trim(), // use parsed plaintext body + attachments: $scope.attachments, + sentDate: new Date(), + headers: {} }; - }); - ngModule.directive('focusInput', function($timeout, $parse) { - return { - //scope: true, // optionally create a child scope - link: function(scope, element, attrs) { - var model = $parse(attrs.focusInput); - scope.$watch(model, function(value) { - if (value === true) { - $timeout(function() { - element.find('input').first().focus(); - }, 100); - } - }); + if ($scope.inReplyTo) { + email.headers['in-reply-to'] = '<' + $scope.inReplyTo + '>'; + } + + if ($scope.references && $scope.references.length) { + email.headers.references = $scope.references.map(function(reference) { + return '<' + reference + '>'; + }).join(' '); + } + + // close the writer + $scope.state.writer.close(); + + // persist the email to disk for later sending + outbox.put(email, function(err) { + if (err) { + $scope.onError(err); + return; } - }; - }); - ngModule.directive('focusInputOnClick', function() { - return { - //scope: true, // optionally create a child scope - link: function(scope, element) { - element.on('click', function() { - element.find('input').first().focus(); - }); + // if we need to synchronize replyTo.answered = true to imap, + // let's do that. otherwise, we're done + if (!$scope.replyTo || $scope.replyTo.answered) { + return; } - }; - }); - ngModule.directive('attachmentInput', function() { - return function(scope, elm) { - elm.on('change', function(e) { - for (var i = 0; i < e.target.files.length; i++) { - addAttachment(e.target.files.item(i)); + $scope.replyTo.answered = true; + emailDao.setFlags({ + folder: currentFolder(), + message: $scope.replyTo + }, function(err) { + if (err && err.code !== 42) { + $scope.onError(err); + return; + } + + // offline or no error, let's apply the ui changes + $scope.$apply(); + }); + }); + + }; + + // + // Tag input & Autocomplete + // + + $scope.tagStyle = function(recipient) { + var classes = ['label']; + if (recipient.secure === false) { + classes.push('label-primary'); + } + return classes; + }; + + $scope.lookupAddressBook = function(query) { + var deferred = $q.defer(); + + if (!$scope.addressBookCache) { + // populate address book cache + keychainDao.listLocalPublicKeys(function(err, keys) { + if (err) { + $scope.onError(err); + return; + } + + $scope.addressBookCache = keys.map(function(key) { + return { + address: key.userId + }; + }); + filter(); + }); + + } else { + filter(); + } + + // query address book cache + function filter() { + var addresses = $scope.addressBookCache.filter(function(i) { + return i.address.indexOf(query) !== -1; + }); + deferred.resolve(addresses); + } + + return deferred.promise; + }; + + // + // Helpers + // + + function currentFolder() { + return $scope.state.nav.currentFolder; + } + + /* + * Visitor to filter out objects without an address property, i.e. empty addresses + */ + function filterEmptyAddresses(addr) { + return !!addr.address; + } +}; + + +// +// Directives +// + +var ngModule = angular.module('write', []); + +ngModule.directive('focusMe', function($timeout, $parse) { + return { + //scope: true, // optionally create a child scope + link: function(scope, element, attrs) { + var model = $parse(attrs.focusMe); + scope.$watch(model, function(value) { + if (value === true) { + $timeout(function() { + var el = element[0]; + el.focus(); + // set cursor to start of textarea + if (el.type === 'textarea') { + el.selectionStart = 0; + el.selectionEnd = 0; + } + }, 100); } }); + } + }; +}); - function addAttachment(file) { - var reader = new FileReader(); - reader.onload = function(e) { - scope.attachments.push({ - filename: file.name, - mimeType: file.type, - content: new Uint8Array(e.target.result) - }); - scope.$digest(); - }; - reader.readAsArrayBuffer(file); - } - }; - }); - - ngModule.directive('attachmentBtn', function() { - return function(scope, elm) { - elm.on('click touchstart', function(e) { - e.preventDefault(); - document.querySelector('#attachment-input').click(); +ngModule.directive('focusInput', function($timeout, $parse) { + return { + //scope: true, // optionally create a child scope + link: function(scope, element, attrs) { + var model = $parse(attrs.focusInput); + scope.$watch(model, function(value) { + if (value === true) { + $timeout(function() { + element.find('input').first().focus(); + }, 100); + } }); - }; - }); + } + }; +}); - return WriteCtrl; -}); \ No newline at end of file +ngModule.directive('focusInputOnClick', function() { + return { + //scope: true, // optionally create a child scope + link: function(scope, element) { + element.on('click', function() { + element.find('input').first().focus(); + }); + } + }; +}); + +ngModule.directive('attachmentInput', function() { + return function(scope, elm) { + elm.on('change', function(e) { + for (var i = 0; i < e.target.files.length; i++) { + addAttachment(e.target.files.item(i)); + } + }); + + function addAttachment(file) { + var reader = new FileReader(); + reader.onload = function(e) { + scope.attachments.push({ + filename: file.name, + mimeType: file.type, + content: new Uint8Array(e.target.result) + }); + scope.$digest(); + }; + reader.readAsArrayBuffer(file); + } + }; +}); + +ngModule.directive('attachmentBtn', function() { + return function(scope, elm) { + elm.on('click touchstart', function(e) { + e.preventDefault(); + document.querySelector('#attachment-input').click(); + }); + }; +}); + +exports = WriteCtrl; \ No newline at end of file diff --git a/src/js/crypto/crypto.js b/src/js/crypto/crypto.js index 7b09f53..a2d964e 100644 --- a/src/js/crypto/crypto.js +++ b/src/js/crypto/crypto.js @@ -2,122 +2,121 @@ * High level crypto api that invokes native crypto (if available) and * gracefully degrades to JS crypto (if unavailable) */ -define(function(require) { - 'use strict'; - var aes = require('js/crypto/aes-gcm'), - pbkdf2 = require('js/crypto/pbkdf2'), - config = require('js/app-config').config, - axe = require('axe'); +'use strict'; - var PBKDF2_WORKER = '/crypto/pbkdf2-worker.js'; +var aes = require('crypto-lib').aes, + pbkdf2 = require('./pbkdf2'), + config = require('../app-config').config, + axe = require('axe-logger'); - var Crypto = function() {}; +var PBKDF2_WORKER = '/crypto/pbkdf2-worker.js'; - /** - * Encrypt plaintext using AES-GCM. - * @param {String} plaintext The input string in UTF-16 - * @param {String} key The base64 encoded key - * @param {String} iv The base64 encoded IV - * @param {Function} callback(error, ciphertext) - * @return {String} The base64 encoded ciphertext - */ - Crypto.prototype.encrypt = function(plaintext, key, iv, callback) { - var ct; +var Crypto = function() {}; - try { - ct = aes.encrypt(plaintext, key, iv); - } catch (err) { - callback(err); - return; +/** + * Encrypt plaintext using AES-GCM. + * @param {String} plaintext The input string in UTF-16 + * @param {String} key The base64 encoded key + * @param {String} iv The base64 encoded IV + * @param {Function} callback(error, ciphertext) + * @return {String} The base64 encoded ciphertext + */ +Crypto.prototype.encrypt = function(plaintext, key, iv, callback) { + var ct; + + try { + ct = aes.encrypt(plaintext, key, iv); + } catch (err) { + callback(err); + return; + } + + callback(null, ct); +}; + +/** + * Decrypt ciphertext suing AES-GCM + * @param {String} ciphertext The base64 encoded ciphertext + * @param {String} key The base64 encoded key + * @param {String} iv The base64 encoded IV + * @param {Function} callback(error, plaintext) + * @return {String} The decrypted plaintext in UTF-16 + */ +Crypto.prototype.decrypt = function(ciphertext, key, iv, callback) { + var pt; + + try { + pt = aes.decrypt(ciphertext, key, iv); + } catch (err) { + callback(err); + return; + } + + callback(null, pt); +}; + +/** + * Do PBKDF2 key derivation in a WebWorker thread + */ +Crypto.prototype.deriveKey = function(password, salt, keySize, callback) { + startWorker({ + script: PBKDF2_WORKER, + args: { + password: password, + salt: salt, + keySize: keySize + }, + callback: callback, + noWorker: function() { + return pbkdf2.getKey(password, salt, keySize); } + }); +}; - callback(null, ct); - }; +// +// helper functions +// - /** - * Decrypt ciphertext suing AES-GCM - * @param {String} ciphertext The base64 encoded ciphertext - * @param {String} key The base64 encoded key - * @param {String} iv The base64 encoded IV - * @param {Function} callback(error, plaintext) - * @return {String} The decrypted plaintext in UTF-16 - */ - Crypto.prototype.decrypt = function(ciphertext, key, iv, callback) { - var pt; - - try { - pt = aes.decrypt(ciphertext, key, iv); - } catch (err) { - callback(err); - return; - } - - callback(null, pt); - }; - - /** - * Do PBKDF2 key derivation in a WebWorker thread - */ - Crypto.prototype.deriveKey = function(password, salt, keySize, callback) { - startWorker({ - script: PBKDF2_WORKER, - args: { - password: password, - salt: salt, - keySize: keySize - }, - callback: callback, - noWorker: function() { - return pbkdf2.getKey(password, salt, keySize); - } - }); - }; - - // - // helper functions - // - - function startWorker(options) { - // check for WebWorker support - if (window.Worker) { - // init webworker thread - var worker = new Worker(config.workerPath + options.script); - worker.onmessage = function(e) { - if (e.data.err) { - options.callback(e.data.err); - return; - } - // return result from the worker - options.callback(null, e.data); - }; - worker.onerror = function(e) { - // show error message in logger - axe.error('Error handling web worker: Line ' + e.lineno + ' in ' + e.filename + ': ' + e.message); - // return error - options.callback({ - errMsg: (e.message) ? e.message : e - }); +function startWorker(options) { + // check for WebWorker support + if (window.Worker) { + // init webworker thread + var worker = new Worker(config.workerPath + options.script); + worker.onmessage = function(e) { + if (e.data.err) { + options.callback(e.data.err); return; - }; - // send data to the worker - worker.postMessage(options.args); - return; - } - - // no WebWorker support... do synchronous call - var result; - try { - result = options.noWorker(); - } catch (e) { + } + // return result from the worker + options.callback(null, e.data); + }; + worker.onerror = function(e) { + // show error message in logger + axe.error('Error handling web worker: Line ' + e.lineno + ' in ' + e.filename + ': ' + e.message); // return error options.callback({ errMsg: (e.message) ? e.message : e }); return; - } - options.callback(null, result); + }; + // send data to the worker + worker.postMessage(options.args); + return; } - return Crypto; -}); \ No newline at end of file + // no WebWorker support... do synchronous call + var result; + try { + result = options.noWorker(); + } catch (e) { + // return error + options.callback({ + errMsg: (e.message) ? e.message : e + }); + return; + } + options.callback(null, result); +} + +exports = Crypto; \ No newline at end of file diff --git a/src/js/crypto/pbkdf2-worker.js b/src/js/crypto/pbkdf2-worker.js index fd71a41..665ddf0 100644 --- a/src/js/crypto/pbkdf2-worker.js +++ b/src/js/crypto/pbkdf2-worker.js @@ -1,38 +1,23 @@ -(function() { - 'use strict'; +'use strict'; - // import web worker dependencies - importScripts('../../lib/require.js'); +var pbkdf2 = require('./pbkdf2'); - /** - * In the web worker thread context, 'this' and 'self' can be used as a global - * variable namespace similar to the 'window' object in the main thread - */ - self.onmessage = function(e) { - // fetch dependencies via require.js - require(['../../require-config'], function() { - require.config({ - baseUrl: '../../lib' - }); +/** + * In the web worker thread context, 'this' and 'self' can be used as a global + * variable namespace similar to the 'window' object in the main thread + */ +self.onmessage = function(e) { + var i = e.data, + key = null; - require(['js/crypto/pbkdf2'], function(pbkdf2) { + if (i.password && i.salt && i.keySize) { + // start deriving key + key = pbkdf2.getKey(i.password, i.salt, i.keySize); - var i = e.data, - key = null; + } else { + throw 'Not all arguments for web worker crypto are defined!'; + } - if (i.password && i.salt && i.keySize) { - // start deriving key - key = pbkdf2.getKey(i.password, i.salt, i.keySize); - - } else { - throw 'Not all arguments for web worker crypto are defined!'; - } - - // pass output back to main thread - self.postMessage(key); - - }); - }); - }; - -}()); \ No newline at end of file + // pass output back to main thread + self.postMessage(key); +}; \ No newline at end of file diff --git a/src/js/crypto/pbkdf2.js b/src/js/crypto/pbkdf2.js index 3a0b12a..e2d31f4 100644 --- a/src/js/crypto/pbkdf2.js +++ b/src/js/crypto/pbkdf2.js @@ -1,25 +1,24 @@ /** * A Wrapper for Forge's PBKDF2 function */ -define(['forge'], function(forge) { - 'use strict'; - var self = {}; +'use strict'; - /** - * PBKDF2-HMAC-SHA256 key derivation with a random salt and 10000 iterations - * @param {String} password The password in UTF8 - * @param {String} salt The base64 encoded salt - * @param {String} keySize The key size in bits - * @return {String} The base64 encoded key - */ - self.getKey = function(password, salt, keySize) { - var saltUtf8 = forge.util.decode64(salt); - var md = forge.md.sha256.create(); - var key = forge.pkcs5.pbkdf2(password, saltUtf8, 10000, keySize / 8, md); +var self = {}; - return forge.util.encode64(key); - }; +/** + * PBKDF2-HMAC-SHA256 key derivation with a random salt and 10000 iterations + * @param {String} password The password in UTF8 + * @param {String} salt The base64 encoded salt + * @param {String} keySize The key size in bits + * @return {String} The base64 encoded key + */ +self.getKey = function(password, salt, keySize) { + var saltUtf8 = forge.util.decode64(salt); + var md = forge.md.sha256.create(); + var key = forge.pkcs5.pbkdf2(password, saltUtf8, 10000, keySize / 8, md); - return self; -}); \ No newline at end of file + return forge.util.encode64(key); +}; + +exports = self; \ No newline at end of file diff --git a/src/js/crypto/pgp.js b/src/js/crypto/pgp.js index 2a50b80..63a84b7 100644 --- a/src/js/crypto/pgp.js +++ b/src/js/crypto/pgp.js @@ -1,452 +1,450 @@ /** * High level crypto api that handles all calls to OpenPGP.js */ -define(function(require) { - 'use strict'; - var openpgp = require('openpgp'), - util = require('openpgp').util, - config = require('js/app-config').config; +'use strict'; - var PGP = function() { - openpgp.config.prefer_hash_algorithm = openpgp.enums.hash.sha256; - openpgp.initWorker(config.workerPath + '/../lib/openpgp/openpgp.worker.js'); - }; +var util = openpgp.util, + config = require('../app-config').config; - /** - * Generate a key pair for the user - */ - PGP.prototype.generateKeys = function(options, callback) { - var userId, passphrase; +var PGP = function() { + openpgp.config.prefer_hash_algorithm = openpgp.enums.hash.sha256; + openpgp.initWorker(config.workerPath + '/../lib/openpgp/openpgp.worker.js'); +}; - if (!util.emailRegEx.test(options.emailAddress) || !options.keySize) { - callback(new Error('Crypto init failed. Not all options set!')); - return; - } +/** + * Generate a key pair for the user + */ +PGP.prototype.generateKeys = function(options, callback) { + var userId, passphrase; - // generate keypair - userId = 'Whiteout User <' + options.emailAddress + '>'; - passphrase = (options.passphrase) ? options.passphrase : undefined; - openpgp.generateKeyPair({ - keyType: 1, // (keytype 1=RSA) - numBits: options.keySize, - userId: userId, - passphrase: passphrase - }, onGenerated); + if (!util.emailRegEx.test(options.emailAddress) || !options.keySize) { + callback(new Error('Crypto init failed. Not all options set!')); + return; + } - function onGenerated(err, keys) { - if (err) { - callback(new Error('Keygeneration failed!')); - return; - } + // generate keypair + userId = 'Whiteout User <' + options.emailAddress + '>'; + passphrase = (options.passphrase) ? options.passphrase : undefined; + openpgp.generateKeyPair({ + keyType: 1, // (keytype 1=RSA) + numBits: options.keySize, + userId: userId, + passphrase: passphrase + }, onGenerated); - callback(null, { - keyId: keys.key.primaryKey.getKeyId().toHex().toUpperCase(), - privateKeyArmored: keys.privateKeyArmored, - publicKeyArmored: keys.publicKeyArmored - }); - } - }; - - /** - * Show a user's fingerprint - */ - PGP.prototype.getFingerprint = function(keyArmored) { - function fingerprint(key) { - return key.primaryKey.getFingerprint().toUpperCase(); - } - - // process armored key input - if (keyArmored) { - return fingerprint(openpgp.key.readArmored(keyArmored).keys[0]); - } - - if (!this._publicKey) { - throw new Error('No public key set for fingerprint generation!'); - } - - // get local fingerpring - return fingerprint(this._publicKey); - }; - - /** - * Show a user's key id. - */ - PGP.prototype.getKeyId = function(keyArmored) { - var key, pubKeyId, privKeyId; - - // process armored key input - if (keyArmored) { - key = openpgp.key.readArmored(keyArmored).keys[0]; - return key.primaryKey.getKeyId().toHex().toUpperCase(); - } - - // check already imported keys - if (!this._privateKey || !this._publicKey) { - throw new Error('Cannot read key IDs... keys not set!'); - } - - pubKeyId = this._publicKey.primaryKey.getKeyId().toHex().toUpperCase(); - privKeyId = this._privateKey.primaryKey.getKeyId().toHex().toUpperCase(); - - if (!pubKeyId || !privKeyId || pubKeyId !== privKeyId) { - throw new Error('Key IDs do not match!'); - } - - return pubKeyId; - }; - - /** - * Read all relevant params of an armored key. - */ - PGP.prototype.getKeyParams = function(keyArmored) { - var key, packet, userIds; - - // process armored key input - if (keyArmored) { - key = openpgp.key.readArmored(keyArmored).keys[0]; - } else if (this._publicKey) { - key = this._publicKey; - } else { - throw new Error('Cannot read key params... keys not set!'); - } - - packet = key.primaryKey; - - // read user names and email addresses - userIds = []; - key.getUserIds().forEach(function(userId) { - userIds.push({ - name: userId.split('<')[0].trim(), - emailAddress: userId.split('<')[1].split('>')[0].trim() - }); - }); - - return { - _id: packet.getKeyId().toHex().toUpperCase(), - userId: userIds[0].emailAddress, // the primary (first) email address of the key - userIds: userIds, // a dictonary of all the key's name/address pairs - fingerprint: packet.getFingerprint().toUpperCase(), - algorithm: packet.algorithm, - bitSize: packet.getBitSize(), - created: packet.created, - }; - }; - - /** - * Extract a public key from a private key - * @param {String} privateKeyArmored The private PGP key block - * @return {String} The publick PGP key block - */ - PGP.prototype.extractPublicKey = function(privateKeyArmored) { - var privkey = openpgp.key.readArmored(privateKeyArmored).keys[0]; - var pubkey = privkey.toPublic(); - return pubkey.armor(); - }; - - /** - * Import the user's key pair - */ - PGP.prototype.importKeys = function(options, callback) { - var pubKeyId, privKeyId, self = this; - - // check options - if (!options.privateKeyArmored || !options.publicKeyArmored) { - callback(new Error('Importing keys failed. Not all options set!')); - return; - } - - function resetKeys() { - self._publicKey = undefined; - self._privateKey = undefined; - } - - // read armored keys - try { - this._publicKey = openpgp.key.readArmored(options.publicKeyArmored).keys[0]; - this._privateKey = openpgp.key.readArmored(options.privateKeyArmored).keys[0]; - } catch (e) { - resetKeys(); - callback(new Error('Importing keys failed. Parsing error!')); - return; - } - - // decrypt private key with passphrase - if (!this._privateKey.decrypt(options.passphrase)) { - resetKeys(); - callback(new Error('Incorrect passphrase!')); - return; - } - - // check if keys have the same id - pubKeyId = this._publicKey.primaryKey.getKeyId().toHex(); - privKeyId = this._privateKey.primaryKey.getKeyId().toHex(); - if (!pubKeyId || !privKeyId || pubKeyId !== privKeyId) { - resetKeys(); - callback(new Error('Key IDs dont match!')); - return; - } - - callback(); - }; - - /** - * Export the user's key pair - */ - PGP.prototype.exportKeys = function(callback) { - if (!this._publicKey || !this._privateKey) { - callback(new Error('Could not export keys!')); + function onGenerated(err, keys) { + if (err) { + callback(new Error('Keygeneration failed!')); return; } callback(null, { - keyId: this._publicKey.primaryKey.getKeyId().toHex().toUpperCase(), - privateKeyArmored: this._privateKey.armor(), - publicKeyArmored: this._publicKey.armor() + keyId: keys.key.primaryKey.getKeyId().toHex().toUpperCase(), + privateKeyArmored: keys.privateKeyArmored, + publicKeyArmored: keys.publicKeyArmored }); - }; + } +}; - /** - * Change the passphrase of an ascii armored private key. - */ - PGP.prototype.changePassphrase = function(options, callback) { - var privKey, packets, newPassphrase, newKeyArmored; - - // set undefined instead of empty string as passphrase - newPassphrase = (options.newPassphrase) ? options.newPassphrase : undefined; - - if (!options.privateKeyArmored) { - callback(new Error('Private key must be specified to change passphrase!')); - return; - } - - if (options.oldPassphrase === newPassphrase || - (!options.oldPassphrase && !newPassphrase)) { - callback(new Error('New and old passphrase are the same!')); - return; - } - - // read armored key - try { - privKey = openpgp.key.readArmored(options.privateKeyArmored).keys[0]; - } catch (e) { - callback(new Error('Importing key failed. Parsing error!')); - return; - } - - // decrypt private key with passphrase - if (!privKey.decrypt(options.oldPassphrase)) { - callback(new Error('Old passphrase incorrect!')); - return; - } - - // encrypt key with new passphrase - try { - packets = privKey.getAllKeyPackets(); - for (var i = 0; i < packets.length; i++) { - packets[i].encrypt(newPassphrase); - } - newKeyArmored = privKey.armor(); - } catch (e) { - callback(new Error('Setting new passphrase failed!')); - return; - } - - // check if new passphrase really works - if (!privKey.decrypt(newPassphrase)) { - callback(new Error('Decrypting key with new passphrase failed!')); - return; - } - - callback(null, newKeyArmored); - }; - - /** - * Encrypt and sign a pgp message for a list of receivers - */ - PGP.prototype.encrypt = function(plaintext, publicKeysArmored, callback) { - var publicKeys; - - // check keys - if (!this._privateKey) { - callback(new Error('Error encrypting. Keys must be set!')); - return; - } - - // parse armored public keys - try { - if (publicKeysArmored && publicKeysArmored.length) { - publicKeys = []; - publicKeysArmored.forEach(function(pubkeyArmored) { - publicKeys = publicKeys.concat(openpgp.key.readArmored(pubkeyArmored).keys); - }); - } - } catch (err) { - callback(new Error('Error encrypting plaintext!')); - return; - } - - if (publicKeys) { - // encrypt and sign the plaintext - openpgp.signAndEncryptMessage(publicKeys, this._privateKey, plaintext, callback); - } else { - // if no public keys are available encrypt for myself - openpgp.signAndEncryptMessage([this._publicKey], this._privateKey, plaintext, callback); - } - }; - - /** - * Decrypts a ciphertext - * @param {String} ciphertext The encrypted PGP message block - * @param {String} publicKeyArmored The public key used to sign the message - * @param {Function} callback(error, plaintext, signaturesValid) signaturesValid is undefined in case there are no signature, null in case there are signatures but the wrong public key or no key was used to verify, true if the signature was successfully verified, or false if the signataure verification failed. - */ - PGP.prototype.decrypt = function(ciphertext, publicKeyArmored, callback) { - var publicKeys, message; - - // check keys - if (!this._privateKey) { - callback(new Error('Error decrypting. Keys must be set!')); - return; - } - - // read keys and ciphertext message - try { - if (publicKeyArmored) { - // parse public keys if available ... - publicKeys = openpgp.key.readArmored(publicKeyArmored).keys; - } else { - // use own public key to know if signatures are available - publicKeys = [this._publicKey]; - } - message = openpgp.message.readArmored(ciphertext); - } catch (err) { - callback(new Error('Error parsing encrypted PGP message!')); - return; - } - - // decrypt and verify pgp message - openpgp.decryptAndVerifyMessage(this._privateKey, publicKeys, message, onDecrypted); - - function onDecrypted(err, decrypted) { - if (err) { - callback(new Error('Error decrypting and verifying PGP message!')); - return; - } - - // return decrypted plaintext - callback(null, decrypted.text, checkSignatureValidity(decrypted.signatures)); - } - }; - - /** - * Verifies a clearsigned message - * @param {String} clearSignedText The clearsigned text, usually from a signed pgp/inline message - * @param {String} publicKeyArmored The public key used to signed the message - * @param {Function} callback(error, signaturesValid) signaturesValid is undefined in case there are no signature, null in case there are signatures but the wrong public key or no key was used to verify, true if the signature was successfully verified, or false if the signataure verification failed. - */ - PGP.prototype.verifyClearSignedMessage = function(clearSignedText, publicKeyArmored, callback) { - var publicKeys, - message; - - // check keys - if (!this._privateKey) { - callback(new Error('Error verifying signed PGP message. Keys must be set!')); - return; - } - - // read keys and ciphertext message - try { - if (publicKeyArmored) { - // parse public keys if available ... - publicKeys = openpgp.key.readArmored(publicKeyArmored).keys; - } else { - // use own public key to know if signatures are available - publicKeys = [this._publicKey]; - } - message = openpgp.cleartext.readArmored(clearSignedText); - } catch (err) { - callback(new Error('Error verifying signed PGP message!')); - return; - } - - openpgp.verifyClearSignedMessage(publicKeys, message, function(err, result) { - if (err) { - callback(new Error('Error verifying PGP message!')); - return; - } - - callback(null, checkSignatureValidity(result.signatures)); - }); - }; - - /** - * Verifies a message with a detached signature - * @param {String} message The signed text, usually from a signed pgp/mime message - * @param {String} pgpSignature The detached signature, usually from a signed pgp/mime message - * @param {String} publicKeyArmored The public key used to signed the message - * @param {Function} callback(error, signaturesValid) signaturesValid is undefined in case there are no signature, null in case there are signatures but the wrong public key or no key was used to verify, true if the signature was successfully verified, or false if the signataure verification failed. - */ - PGP.prototype.verifySignedMessage = function(message, pgpSignature, publicKeyArmored, callback) { - var publicKeys; - - // check keys - if (!this._privateKey) { - callback(new Error('Error verifying signed PGP message. Keys must be set!')); - return; - } - - // read keys and ciphertext message - try { - if (publicKeyArmored) { - // parse public keys if available ... - publicKeys = openpgp.key.readArmored(publicKeyArmored).keys; - } else { - // use own public key to know if signatures are available - publicKeys = [this._publicKey]; - } - } catch (err) { - callback(new Error('Error verifying signed PGP message!')); - return; - } - - var signatures; - try { - var msg = openpgp.message.readSignedContent(message, pgpSignature); - signatures = msg.verify(publicKeys); - } catch (err) { - callback(new Error('Error verifying signed PGP message!')); - return; - } - - callback(null, checkSignatureValidity(signatures)); - }; - - /** - * Checks signature validity - * @param {Object} decrypted OpenPGP.js Signature array - * @return {undefined|null|true|false} - * If signatures array is empty (the message was not signed), returns undefined - * If you're using the wrong public key, returns null. - * If signatures are invalid, returns false. - * If everything is in order, returns true - */ - function checkSignatureValidity(signatures) { - if (!signatures.length) { - // signatures array is empty (the message was not signed) - return; - } - - for (var i = 0; i < signatures.length; i++) { - if (signatures[i].valid !== true) { // null | false - // you're using the wrong public key or signatures are invalid - return signatures[i].valid; - } - } - - // everything is in order - return true; +/** + * Show a user's fingerprint + */ +PGP.prototype.getFingerprint = function(keyArmored) { + function fingerprint(key) { + return key.primaryKey.getFingerprint().toUpperCase(); } - return PGP; -}); \ No newline at end of file + // process armored key input + if (keyArmored) { + return fingerprint(openpgp.key.readArmored(keyArmored).keys[0]); + } + + if (!this._publicKey) { + throw new Error('No public key set for fingerprint generation!'); + } + + // get local fingerpring + return fingerprint(this._publicKey); +}; + +/** + * Show a user's key id. + */ +PGP.prototype.getKeyId = function(keyArmored) { + var key, pubKeyId, privKeyId; + + // process armored key input + if (keyArmored) { + key = openpgp.key.readArmored(keyArmored).keys[0]; + return key.primaryKey.getKeyId().toHex().toUpperCase(); + } + + // check already imported keys + if (!this._privateKey || !this._publicKey) { + throw new Error('Cannot read key IDs... keys not set!'); + } + + pubKeyId = this._publicKey.primaryKey.getKeyId().toHex().toUpperCase(); + privKeyId = this._privateKey.primaryKey.getKeyId().toHex().toUpperCase(); + + if (!pubKeyId || !privKeyId || pubKeyId !== privKeyId) { + throw new Error('Key IDs do not match!'); + } + + return pubKeyId; +}; + +/** + * Read all relevant params of an armored key. + */ +PGP.prototype.getKeyParams = function(keyArmored) { + var key, packet, userIds; + + // process armored key input + if (keyArmored) { + key = openpgp.key.readArmored(keyArmored).keys[0]; + } else if (this._publicKey) { + key = this._publicKey; + } else { + throw new Error('Cannot read key params... keys not set!'); + } + + packet = key.primaryKey; + + // read user names and email addresses + userIds = []; + key.getUserIds().forEach(function(userId) { + userIds.push({ + name: userId.split('<')[0].trim(), + emailAddress: userId.split('<')[1].split('>')[0].trim() + }); + }); + + return { + _id: packet.getKeyId().toHex().toUpperCase(), + userId: userIds[0].emailAddress, // the primary (first) email address of the key + userIds: userIds, // a dictonary of all the key's name/address pairs + fingerprint: packet.getFingerprint().toUpperCase(), + algorithm: packet.algorithm, + bitSize: packet.getBitSize(), + created: packet.created, + }; +}; + +/** + * Extract a public key from a private key + * @param {String} privateKeyArmored The private PGP key block + * @return {String} The publick PGP key block + */ +PGP.prototype.extractPublicKey = function(privateKeyArmored) { + var privkey = openpgp.key.readArmored(privateKeyArmored).keys[0]; + var pubkey = privkey.toPublic(); + return pubkey.armor(); +}; + +/** + * Import the user's key pair + */ +PGP.prototype.importKeys = function(options, callback) { + var pubKeyId, privKeyId, self = this; + + // check options + if (!options.privateKeyArmored || !options.publicKeyArmored) { + callback(new Error('Importing keys failed. Not all options set!')); + return; + } + + function resetKeys() { + self._publicKey = undefined; + self._privateKey = undefined; + } + + // read armored keys + try { + this._publicKey = openpgp.key.readArmored(options.publicKeyArmored).keys[0]; + this._privateKey = openpgp.key.readArmored(options.privateKeyArmored).keys[0]; + } catch (e) { + resetKeys(); + callback(new Error('Importing keys failed. Parsing error!')); + return; + } + + // decrypt private key with passphrase + if (!this._privateKey.decrypt(options.passphrase)) { + resetKeys(); + callback(new Error('Incorrect passphrase!')); + return; + } + + // check if keys have the same id + pubKeyId = this._publicKey.primaryKey.getKeyId().toHex(); + privKeyId = this._privateKey.primaryKey.getKeyId().toHex(); + if (!pubKeyId || !privKeyId || pubKeyId !== privKeyId) { + resetKeys(); + callback(new Error('Key IDs dont match!')); + return; + } + + callback(); +}; + +/** + * Export the user's key pair + */ +PGP.prototype.exportKeys = function(callback) { + if (!this._publicKey || !this._privateKey) { + callback(new Error('Could not export keys!')); + return; + } + + callback(null, { + keyId: this._publicKey.primaryKey.getKeyId().toHex().toUpperCase(), + privateKeyArmored: this._privateKey.armor(), + publicKeyArmored: this._publicKey.armor() + }); +}; + +/** + * Change the passphrase of an ascii armored private key. + */ +PGP.prototype.changePassphrase = function(options, callback) { + var privKey, packets, newPassphrase, newKeyArmored; + + // set undefined instead of empty string as passphrase + newPassphrase = (options.newPassphrase) ? options.newPassphrase : undefined; + + if (!options.privateKeyArmored) { + callback(new Error('Private key must be specified to change passphrase!')); + return; + } + + if (options.oldPassphrase === newPassphrase || + (!options.oldPassphrase && !newPassphrase)) { + callback(new Error('New and old passphrase are the same!')); + return; + } + + // read armored key + try { + privKey = openpgp.key.readArmored(options.privateKeyArmored).keys[0]; + } catch (e) { + callback(new Error('Importing key failed. Parsing error!')); + return; + } + + // decrypt private key with passphrase + if (!privKey.decrypt(options.oldPassphrase)) { + callback(new Error('Old passphrase incorrect!')); + return; + } + + // encrypt key with new passphrase + try { + packets = privKey.getAllKeyPackets(); + for (var i = 0; i < packets.length; i++) { + packets[i].encrypt(newPassphrase); + } + newKeyArmored = privKey.armor(); + } catch (e) { + callback(new Error('Setting new passphrase failed!')); + return; + } + + // check if new passphrase really works + if (!privKey.decrypt(newPassphrase)) { + callback(new Error('Decrypting key with new passphrase failed!')); + return; + } + + callback(null, newKeyArmored); +}; + +/** + * Encrypt and sign a pgp message for a list of receivers + */ +PGP.prototype.encrypt = function(plaintext, publicKeysArmored, callback) { + var publicKeys; + + // check keys + if (!this._privateKey) { + callback(new Error('Error encrypting. Keys must be set!')); + return; + } + + // parse armored public keys + try { + if (publicKeysArmored && publicKeysArmored.length) { + publicKeys = []; + publicKeysArmored.forEach(function(pubkeyArmored) { + publicKeys = publicKeys.concat(openpgp.key.readArmored(pubkeyArmored).keys); + }); + } + } catch (err) { + callback(new Error('Error encrypting plaintext!')); + return; + } + + if (publicKeys) { + // encrypt and sign the plaintext + openpgp.signAndEncryptMessage(publicKeys, this._privateKey, plaintext, callback); + } else { + // if no public keys are available encrypt for myself + openpgp.signAndEncryptMessage([this._publicKey], this._privateKey, plaintext, callback); + } +}; + +/** + * Decrypts a ciphertext + * @param {String} ciphertext The encrypted PGP message block + * @param {String} publicKeyArmored The public key used to sign the message + * @param {Function} callback(error, plaintext, signaturesValid) signaturesValid is undefined in case there are no signature, null in case there are signatures but the wrong public key or no key was used to verify, true if the signature was successfully verified, or false if the signataure verification failed. + */ +PGP.prototype.decrypt = function(ciphertext, publicKeyArmored, callback) { + var publicKeys, message; + + // check keys + if (!this._privateKey) { + callback(new Error('Error decrypting. Keys must be set!')); + return; + } + + // read keys and ciphertext message + try { + if (publicKeyArmored) { + // parse public keys if available ... + publicKeys = openpgp.key.readArmored(publicKeyArmored).keys; + } else { + // use own public key to know if signatures are available + publicKeys = [this._publicKey]; + } + message = openpgp.message.readArmored(ciphertext); + } catch (err) { + callback(new Error('Error parsing encrypted PGP message!')); + return; + } + + // decrypt and verify pgp message + openpgp.decryptAndVerifyMessage(this._privateKey, publicKeys, message, onDecrypted); + + function onDecrypted(err, decrypted) { + if (err) { + callback(new Error('Error decrypting and verifying PGP message!')); + return; + } + + // return decrypted plaintext + callback(null, decrypted.text, checkSignatureValidity(decrypted.signatures)); + } +}; + +/** + * Verifies a clearsigned message + * @param {String} clearSignedText The clearsigned text, usually from a signed pgp/inline message + * @param {String} publicKeyArmored The public key used to signed the message + * @param {Function} callback(error, signaturesValid) signaturesValid is undefined in case there are no signature, null in case there are signatures but the wrong public key or no key was used to verify, true if the signature was successfully verified, or false if the signataure verification failed. + */ +PGP.prototype.verifyClearSignedMessage = function(clearSignedText, publicKeyArmored, callback) { + var publicKeys, + message; + + // check keys + if (!this._privateKey) { + callback(new Error('Error verifying signed PGP message. Keys must be set!')); + return; + } + + // read keys and ciphertext message + try { + if (publicKeyArmored) { + // parse public keys if available ... + publicKeys = openpgp.key.readArmored(publicKeyArmored).keys; + } else { + // use own public key to know if signatures are available + publicKeys = [this._publicKey]; + } + message = openpgp.cleartext.readArmored(clearSignedText); + } catch (err) { + callback(new Error('Error verifying signed PGP message!')); + return; + } + + openpgp.verifyClearSignedMessage(publicKeys, message, function(err, result) { + if (err) { + callback(new Error('Error verifying PGP message!')); + return; + } + + callback(null, checkSignatureValidity(result.signatures)); + }); +}; + +/** + * Verifies a message with a detached signature + * @param {String} message The signed text, usually from a signed pgp/mime message + * @param {String} pgpSignature The detached signature, usually from a signed pgp/mime message + * @param {String} publicKeyArmored The public key used to signed the message + * @param {Function} callback(error, signaturesValid) signaturesValid is undefined in case there are no signature, null in case there are signatures but the wrong public key or no key was used to verify, true if the signature was successfully verified, or false if the signataure verification failed. + */ +PGP.prototype.verifySignedMessage = function(message, pgpSignature, publicKeyArmored, callback) { + var publicKeys; + + // check keys + if (!this._privateKey) { + callback(new Error('Error verifying signed PGP message. Keys must be set!')); + return; + } + + // read keys and ciphertext message + try { + if (publicKeyArmored) { + // parse public keys if available ... + publicKeys = openpgp.key.readArmored(publicKeyArmored).keys; + } else { + // use own public key to know if signatures are available + publicKeys = [this._publicKey]; + } + } catch (err) { + callback(new Error('Error verifying signed PGP message!')); + return; + } + + var signatures; + try { + var msg = openpgp.message.readSignedContent(message, pgpSignature); + signatures = msg.verify(publicKeys); + } catch (err) { + callback(new Error('Error verifying signed PGP message!')); + return; + } + + callback(null, checkSignatureValidity(signatures)); +}; + +/** + * Checks signature validity + * @param {Object} decrypted OpenPGP.js Signature array + * @return {undefined|null|true|false} + * If signatures array is empty (the message was not signed), returns undefined + * If you're using the wrong public key, returns null. + * If signatures are invalid, returns false. + * If everything is in order, returns true + */ +function checkSignatureValidity(signatures) { + if (!signatures.length) { + // signatures array is empty (the message was not signed) + return; + } + + for (var i = 0; i < signatures.length; i++) { + if (signatures[i].valid !== true) { // null | false + // you're using the wrong public key or signatures are invalid + return signatures[i].valid; + } + } + + // everything is in order + return true; +} + +exports = PGP; \ No newline at end of file diff --git a/src/js/dao/admin-dao.js b/src/js/dao/admin-dao.js index dfd7d4f..0a60e9e 100644 --- a/src/js/dao/admin-dao.js +++ b/src/js/dao/admin-dao.js @@ -1,63 +1,61 @@ -define(function() { - 'use strict'; +'use strict'; - var AdminDAO = function(restDao) { - this._restDao = restDao; - }; +var AdminDAO = function(restDao) { + this._restDao = restDao; +}; - /** - * Create a new email account. - * @param {String} options.emailAddress The desired email address - * @param {String} options.password The password to be used for the account. - * @param {String} options.phone The user's mobile phone number (required for verification and password reset). - * @param {Function} callback(error) - */ - AdminDAO.prototype.createUser = function(options, callback) { - var uri; +/** + * Create a new email account. + * @param {String} options.emailAddress The desired email address + * @param {String} options.password The password to be used for the account. + * @param {String} options.phone The user's mobile phone number (required for verification and password reset). + * @param {Function} callback(error) + */ +AdminDAO.prototype.createUser = function(options, callback) { + var uri; - if (!options.emailAddress || !options.password || !options.phone) { - callback(new Error('Incomplete arguments!')); + if (!options.emailAddress || !options.password || !options.phone) { + callback(new Error('Incomplete arguments!')); + return; + } + + uri = '/user'; + this._restDao.post(options, uri, function(err) { + if (err && err.code === 409) { + callback(new Error('User name is already taken!')); + return; + } else if (err) { + callback(new Error('Error creating new user!')); return; } - uri = '/user'; - this._restDao.post(options, uri, function(err) { - if (err && err.code === 409) { - callback(new Error('User name is already taken!')); - return; - } else if (err) { - callback(new Error('Error creating new user!')); - return; - } + callback(); + }); +}; +/** + * Verify a user's phone number by confirming a token to the server. + * @param {String} options.emailAddress The desired email address + * @param {String} options.token The validation token. + * @param {Function} callback(error) + */ +AdminDAO.prototype.validateUser = function(options, callback) { + var uri; + + if (!options.emailAddress || !options.token) { + callback(new Error('Incomplete arguments!')); + return; + } + + uri = '/user/validate'; + this._restDao.post(options, uri, function(err) { + if (!err || (err && err.code === 202)) { + // success callback(); - }); - }; - - /** - * Verify a user's phone number by confirming a token to the server. - * @param {String} options.emailAddress The desired email address - * @param {String} options.token The validation token. - * @param {Function} callback(error) - */ - AdminDAO.prototype.validateUser = function(options, callback) { - var uri; - - if (!options.emailAddress || !options.token) { - callback(new Error('Incomplete arguments!')); - return; + } else { + callback(new Error('Validation failed!')); } + }); +}; - uri = '/user/validate'; - this._restDao.post(options, uri, function(err) { - if (!err || (err && err.code === 202)) { - // success - callback(); - } else { - callback(new Error('Validation failed!')); - } - }); - }; - - return AdminDAO; -}); \ No newline at end of file +exports = AdminDAO; \ No newline at end of file diff --git a/src/js/dao/devicestorage-dao.js b/src/js/dao/devicestorage-dao.js index 9522ead..0b2b9a0 100644 --- a/src/js/dao/devicestorage-dao.js +++ b/src/js/dao/devicestorage-dao.js @@ -1,97 +1,93 @@ /** - * High level storage api that handles all persistence on the device. If - * SQLcipher/SQLite is available, all data is securely persisted there, - * through transparent encryption. If not, the crypto API is - * used to encrypt data on the fly before persisting via a JSON store. + * High level storage api that handles all persistence on the device. */ -define(function() { - 'use strict'; - var DeviceStorageDAO = function(localDbDao) { - this._localDbDao = localDbDao; - }; +'use strict'; - DeviceStorageDAO.prototype.init = function(emailAddress, callback) { - this._localDbDao.init(emailAddress, callback); - }; +var DeviceStorageDAO = function(localDbDao) { + this._localDbDao = localDbDao; +}; - /** - * Stores a list of encrypted items in the object store - * @param list [Array] The list of items to be persisted - * @param type [String] The type of item to be persisted e.g. 'email' - */ - DeviceStorageDAO.prototype.storeList = function(list, type, callback) { - var key, items = []; +DeviceStorageDAO.prototype.init = function(emailAddress, callback) { + this._localDbDao.init(emailAddress, callback); +}; - // nothing to store - if (!list || list.length === 0) { - callback(); - return; - } - // validate type - if (!type) { - callback({ - errMsg: 'Type is not set!' - }); - return; - } +/** + * Stores a list of encrypted items in the object store + * @param list [Array] The list of items to be persisted + * @param type [String] The type of item to be persisted e.g. 'email' + */ +DeviceStorageDAO.prototype.storeList = function(list, type, callback) { + var key, items = []; - // format items for batch storing in dao - list.forEach(function(i) { - key = createKey(i, type); - - items.push({ - key: key, - object: i - }); + // nothing to store + if (!list || list.length === 0) { + callback(); + return; + } + // validate type + if (!type) { + callback({ + errMsg: 'Type is not set!' }); - - this._localDbDao.batch(items, callback); - }; - - /** - * Deletes items of a certain type from storage - */ - DeviceStorageDAO.prototype.removeList = function(type, callback) { - this._localDbDao.removeList(type, callback); - }; - - /** - * List stored items of a given type - * @param type [String] The type of item e.g. 'email' - * @param offset [Number] The offset of items to fetch (0 is the last stored item) - * @param num [Number] The number of items to fetch (null means fetch all) - */ - DeviceStorageDAO.prototype.listItems = function(type, offset, num, callback) { - // fetch all items of a certain type from the data-store - this._localDbDao.list(type, offset, num, callback); - }; - - /** - * Clear the whole device data-store - */ - DeviceStorageDAO.prototype.clear = function(callback) { - this._localDbDao.clear(callback); - }; - - // - // helper functions - // - - function createKey(i, type) { - var key; - - // put uid in key if available... for easy querying - if (i.uid) { - key = type + '_' + i.uid; - } else if (i.id) { - key = type + '_' + i.id; - } else { - key = type; - } - - return key; + return; } - return DeviceStorageDAO; -}); \ No newline at end of file + // format items for batch storing in dao + list.forEach(function(i) { + key = createKey(i, type); + + items.push({ + key: key, + object: i + }); + }); + + this._localDbDao.batch(items, callback); +}; + +/** + * Deletes items of a certain type from storage + */ +DeviceStorageDAO.prototype.removeList = function(type, callback) { + this._localDbDao.removeList(type, callback); +}; + +/** + * List stored items of a given type + * @param type [String] The type of item e.g. 'email' + * @param offset [Number] The offset of items to fetch (0 is the last stored item) + * @param num [Number] The number of items to fetch (null means fetch all) + */ +DeviceStorageDAO.prototype.listItems = function(type, offset, num, callback) { + // fetch all items of a certain type from the data-store + this._localDbDao.list(type, offset, num, callback); +}; + +/** + * Clear the whole device data-store + */ +DeviceStorageDAO.prototype.clear = function(callback) { + this._localDbDao.clear(callback); +}; + +// +// helper functions +// + +function createKey(i, type) { + var key; + + // put uid in key if available... for easy querying + if (i.uid) { + key = type + '_' + i.uid; + } else if (i.id) { + key = type + '_' + i.id; + } else { + key = type; + } + + return key; +} + +exports = DeviceStorageDAO; \ No newline at end of file diff --git a/src/js/dao/email-dao.js b/src/js/dao/email-dao.js index 3207882..c7ef1f9 100644 --- a/src/js/dao/email-dao.js +++ b/src/js/dao/email-dao.js @@ -1,190 +1,224 @@ -define(function(require) { - 'use strict'; +'use strict'; - var util = require('js/crypto/util'), - _ = require('underscore'), - config = require('js/app-config').config, - str = require('js/app-config').string; +var util = require('crypto-lib').util, + config = require('../app-config').config, + str = require('../app-config').string; + +// +// +// Constants +// +// + +var FOLDER_DB_TYPE = 'folders'; + +var SYNC_TYPE_NEW = 'new'; +var SYNC_TYPE_DELETED = 'deleted'; +var SYNC_TYPE_MSGS = 'messages'; + +var FOLDER_TYPE_INBOX = 'Inbox'; +var FOLDER_TYPE_SENT = 'Sent'; +var FOLDER_TYPE_DRAFTS = 'Drafts'; +var FOLDER_TYPE_TRASH = 'Trash'; + +var MSG_ATTR_UID = 'uid'; +var MSG_PART_ATTR_CONTENT = 'content'; +var MSG_PART_TYPE_ATTACHMENT = 'attachment'; +var MSG_PART_TYPE_ENCRYPTED = 'encrypted'; +var MSG_PART_TYPE_SIGNED = 'signed'; +var MSG_PART_TYPE_TEXT = 'text'; +var MSG_PART_TYPE_HTML = 'html'; + +// +// +// Email Dao +// +// + +/** + * High-level data access object that orchestrates everything around the handling of encrypted mails: + * PGP de-/encryption, receiving via IMAP, sending via SMTP, MIME parsing, local db persistence + * + * @param {Object} keychain The keychain DAO handles keys transparently + * @param {Object} pgp Orchestrates decryption + * @param {Object} devicestorage Handles persistence to the local indexed db + * @param {Object} pgpbuilder Generates and encrypts MIME and SMTP messages + * @param {Object} mailreader Parses MIME messages received from IMAP + */ +var EmailDAO = function(keychain, pgp, devicestorage, pgpbuilder, mailreader) { + this._keychain = keychain; + this._pgp = pgp; + this._devicestorage = devicestorage; + this._pgpbuilder = pgpbuilder; + this._mailreader = mailreader; +}; - // - // - // Constants - // - // - - var FOLDER_DB_TYPE = 'folders'; - - var SYNC_TYPE_NEW = 'new'; - var SYNC_TYPE_DELETED = 'deleted'; - var SYNC_TYPE_MSGS = 'messages'; - - var FOLDER_TYPE_INBOX = 'Inbox'; - var FOLDER_TYPE_SENT = 'Sent'; - var FOLDER_TYPE_DRAFTS = 'Drafts'; - var FOLDER_TYPE_TRASH = 'Trash'; - - var MSG_ATTR_UID = 'uid'; - var MSG_PART_ATTR_CONTENT = 'content'; - var MSG_PART_TYPE_ATTACHMENT = 'attachment'; - var MSG_PART_TYPE_ENCRYPTED = 'encrypted'; - var MSG_PART_TYPE_SIGNED = 'signed'; - var MSG_PART_TYPE_TEXT = 'text'; - var MSG_PART_TYPE_HTML = 'html'; - - // - // - // Email Dao - // - // - - /** - * High-level data access object that orchestrates everything around the handling of encrypted mails: - * PGP de-/encryption, receiving via IMAP, sending via SMTP, MIME parsing, local db persistence - * - * @param {Object} keychain The keychain DAO handles keys transparently - * @param {Object} pgp Orchestrates decryption - * @param {Object} devicestorage Handles persistence to the local indexed db - * @param {Object} pgpbuilder Generates and encrypts MIME and SMTP messages - * @param {Object} mailreader Parses MIME messages received from IMAP - */ - var EmailDAO = function(keychain, pgp, devicestorage, pgpbuilder, mailreader) { - this._keychain = keychain; - this._pgp = pgp; - this._devicestorage = devicestorage; - this._pgpbuilder = pgpbuilder; - this._mailreader = mailreader; - }; +// +// +// Public API +// +// - // - // - // Public API - // - // +/** + * Initializes the email dao: + * - validates the email address + * - retrieves the user's key pair (if available) + * - initializes _account.folders with the content from memory + * + * @param {Object} options.account The account + * @param {String} options.account.emailAddress The user's id + * @param {Function} callback(error, keypair) Invoked with the keypair or error information when the email dao is initialized + */ +EmailDAO.prototype.init = function(options, callback) { + var self = this, + keypair; + self._account = options.account; + self._account.busy = 0; // triggers the spinner + self._account.online = false; + self._account.loggingIn = false; - /** - * Initializes the email dao: - * - validates the email address - * - retrieves the user's key pair (if available) - * - initializes _account.folders with the content from memory - * - * @param {Object} options.account The account - * @param {String} options.account.emailAddress The user's id - * @param {Function} callback(error, keypair) Invoked with the keypair or error information when the email dao is initialized - */ - EmailDAO.prototype.init = function(options, callback) { - var self = this, - keypair; + // validate email address + var emailAddress = self._account.emailAddress; + if (!util.validateEmailAddress(emailAddress)) { + callback({ + errMsg: 'The user email address must be specified!' + }); + return; + } - self._account = options.account; - self._account.busy = 0; // triggers the spinner - self._account.online = false; - self._account.loggingIn = false; + // init keychain and then crypto module + initKeychain(); - // validate email address - var emailAddress = self._account.emailAddress; - if (!util.validateEmailAddress(emailAddress)) { - callback({ - errMsg: 'The user email address must be specified!' - }); - return; - } - - // init keychain and then crypto module - initKeychain(); - - function initKeychain() { - // call getUserKeyPair to read/sync keypair with devicestorage/cloud - self._keychain.getUserKeyPair(emailAddress, function(err, storedKeypair) { - if (err) { - callback(err); - return; - } - - keypair = storedKeypair; - initFolders(); - }); - } - - function initFolders() { - // try init folders from memory, since imap client not initiated yet - self._initFoldersFromDisk(function(err) { - // dont handle offline case this time - if (err && err.code !== 42) { - callback(err); - return; - } - - callback(null, keypair); - }); - } - }; - - /** - * Unlocks the keychain by either decrypting an existing private key or generating a new keypair - * @param {String} options.passphrase The passphrase to decrypt the private key - * @param {Function} callback(error) Invoked when the the keychain is unlocked or when an error occurred buring unlocking - */ - EmailDAO.prototype.unlock = function(options, callback) { - var self = this; - - if (options.keypair) { - // import existing key pair into crypto module - handleExistingKeypair(options.keypair); - return; - } - - // no keypair for is stored for the user... generate a new one - self._pgp.generateKeys({ - emailAddress: self._account.emailAddress, - keySize: self._account.asymKeySize, - passphrase: options.passphrase - }, function(err, generatedKeypair) { + function initKeychain() { + // call getUserKeyPair to read/sync keypair with devicestorage/cloud + self._keychain.getUserKeyPair(emailAddress, function(err, storedKeypair) { if (err) { callback(err); return; } - handleGenerated(generatedKeypair); + keypair = storedKeypair; + initFolders(); + }); + } + + function initFolders() { + // try init folders from memory, since imap client not initiated yet + self._initFoldersFromDisk(function(err) { + // dont handle offline case this time + if (err && err.code !== 42) { + callback(err); + return; + } + + callback(null, keypair); + }); + } +}; + +/** + * Unlocks the keychain by either decrypting an existing private key or generating a new keypair + * @param {String} options.passphrase The passphrase to decrypt the private key + * @param {Function} callback(error) Invoked when the the keychain is unlocked or when an error occurred buring unlocking + */ +EmailDAO.prototype.unlock = function(options, callback) { + var self = this; + + if (options.keypair) { + // import existing key pair into crypto module + handleExistingKeypair(options.keypair); + return; + } + + // no keypair for is stored for the user... generate a new one + self._pgp.generateKeys({ + emailAddress: self._account.emailAddress, + keySize: self._account.asymKeySize, + passphrase: options.passphrase + }, function(err, generatedKeypair) { + if (err) { + callback(err); + return; + } + + handleGenerated(generatedKeypair); + }); + + function handleExistingKeypair(keypair) { + var privKeyParams, pubKeyParams; + try { + privKeyParams = self._pgp.getKeyParams(keypair.privateKey.encryptedKey); + pubKeyParams = self._pgp.getKeyParams(keypair.publicKey.publicKey); + } catch (e) { + callback(new Error('Error reading key params!')); + return; + } + + // check if key IDs match + if (!keypair.privateKey._id || keypair.privateKey._id !== keypair.publicKey._id || keypair.privateKey._id !== privKeyParams._id || keypair.publicKey._id !== pubKeyParams._id) { + callback(new Error('Key IDs dont match!')); + return; + } + + // check that key userIds contain email address of user account + var matchingPrivUserId = _.findWhere(privKeyParams.userIds, { + emailAddress: self._account.emailAddress + }); + var matchingPubUserId = _.findWhere(pubKeyParams.userIds, { + emailAddress: self._account.emailAddress }); - function handleExistingKeypair(keypair) { - var privKeyParams, pubKeyParams; - try { - privKeyParams = self._pgp.getKeyParams(keypair.privateKey.encryptedKey); - pubKeyParams = self._pgp.getKeyParams(keypair.publicKey.publicKey); - } catch (e) { - callback(new Error('Error reading key params!')); + if (!matchingPrivUserId || !matchingPubUserId || keypair.privateKey.userId !== self._account.emailAddress || keypair.publicKey.userId !== self._account.emailAddress) { + callback(new Error('User IDs dont match!')); + return; + } + + // import existing key pair into crypto module + self._pgp.importKeys({ + passphrase: options.passphrase, + privateKeyArmored: keypair.privateKey.encryptedKey, + publicKeyArmored: keypair.publicKey.publicKey + }, function(err) { + if (err) { + callback(err); return; } - // check if key IDs match - if (!keypair.privateKey._id || keypair.privateKey._id !== keypair.publicKey._id || keypair.privateKey._id !== privKeyParams._id || keypair.publicKey._id !== pubKeyParams._id) { - callback(new Error('Key IDs dont match!')); + // set decrypted privateKey to pgpMailer + self._pgpbuilder._privateKey = self._pgp._privateKey; + callback(); + }); + } + + function handleGenerated(generatedKeypair) { + // import the new key pair into crypto module + self._pgp.importKeys({ + passphrase: options.passphrase, + privateKeyArmored: generatedKeypair.privateKeyArmored, + publicKeyArmored: generatedKeypair.publicKeyArmored + }, function(err) { + if (err) { + callback(err); return; } - // check that key userIds contain email address of user account - var matchingPrivUserId = _.findWhere(privKeyParams.userIds, { - emailAddress: self._account.emailAddress - }); - var matchingPubUserId = _.findWhere(pubKeyParams.userIds, { - emailAddress: self._account.emailAddress - }); - - if (!matchingPrivUserId || !matchingPubUserId || keypair.privateKey.userId !== self._account.emailAddress || keypair.publicKey.userId !== self._account.emailAddress) { - callback(new Error('User IDs dont match!')); - return; - } - - // import existing key pair into crypto module - self._pgp.importKeys({ - passphrase: options.passphrase, - privateKeyArmored: keypair.privateKey.encryptedKey, - publicKeyArmored: keypair.publicKey.publicKey - }, function(err) { + // persist newly generated keypair + var newKeypair = { + publicKey: { + _id: generatedKeypair.keyId, + userId: self._account.emailAddress, + publicKey: generatedKeypair.publicKeyArmored + }, + privateKey: { + _id: generatedKeypair.keyId, + userId: self._account.emailAddress, + encryptedKey: generatedKeypair.privateKeyArmored + } + }; + self._keychain.putUserKeyPair(newKeypair, function(err) { if (err) { callback(err); return; @@ -194,382 +228,275 @@ define(function(require) { self._pgpbuilder._privateKey = self._pgp._privateKey; callback(); }); - } + }); + } +}; - function handleGenerated(generatedKeypair) { - // import the new key pair into crypto module - self._pgp.importKeys({ - passphrase: options.passphrase, - privateKeyArmored: generatedKeypair.privateKeyArmored, - publicKeyArmored: generatedKeypair.publicKeyArmored - }, function(err) { - if (err) { - callback(err); - return; - } +/** + * Opens a folder in IMAP so that we can receive updates for it. + * Please note that this is a no-op if you try to open the outbox, since it is not an IMAP folder + * but a virtual folder that only exists on disk. + * + * @param {Object} options.folder The folder to be opened + * @param {Function} callback(error) Invoked when the folder has been opened + */ +EmailDAO.prototype.openFolder = function(options, callback) { + var self = this, + err; - // persist newly generated keypair - var newKeypair = { - publicKey: { - _id: generatedKeypair.keyId, - userId: self._account.emailAddress, - publicKey: generatedKeypair.publicKeyArmored - }, - privateKey: { - _id: generatedKeypair.keyId, - userId: self._account.emailAddress, - encryptedKey: generatedKeypair.privateKeyArmored - } - }; - self._keychain.putUserKeyPair(newKeypair, function(err) { - if (err) { - callback(err); - return; - } + if (!self._account.online) { + err = new Error('Client is currently offline!'); + err.code = 42; + callback(err); + return; + } - // set decrypted privateKey to pgpMailer - self._pgpbuilder._privateKey = self._pgp._privateKey; - callback(); - }); - }); - } - }; + if (options.folder.path === config.outboxMailboxPath) { + return; + } - /** - * Opens a folder in IMAP so that we can receive updates for it. - * Please note that this is a no-op if you try to open the outbox, since it is not an IMAP folder - * but a virtual folder that only exists on disk. - * - * @param {Object} options.folder The folder to be opened - * @param {Function} callback(error) Invoked when the folder has been opened - */ - EmailDAO.prototype.openFolder = function(options, callback) { - var self = this, - err; + this._imapClient.selectMailbox({ + path: options.folder.path + }, callback); +}; - if (!self._account.online) { - err = new Error('Client is currently offline!'); - err.code = 42; - callback(err); +/** + * Synchronizes a folder's contents from disk to memory, i.e. if + * a message has disappeared from the disk, this method will remove it from folder.messages, and + * it adds any messages from disk to memory the are not yet in folder.messages + * + * @param {Object} options.folder The folder to synchronize + * @param {Function} callback [description] + */ +EmailDAO.prototype.refreshFolder = function(options, callback) { + var self = this, + folder = options.folder; + + self.busy(); + folder.messages = folder.messages || []; + self._localListMessages({ + folder: folder + }, function(err, storedMessages) { + if (err) { + done(err); return; } - if (options.folder.path === config.outboxMailboxPath) { - return; - } + var storedUids = _.pluck(storedMessages, MSG_ATTR_UID), + memoryUids = _.pluck(folder.messages, MSG_ATTR_UID), + newUids = _.difference(storedUids, memoryUids), // uids of messages that are not yet in memory + removedUids = _.difference(memoryUids, storedUids); // uids of messages that are no longer stored on the disk - this._imapClient.selectMailbox({ - path: options.folder.path - }, callback); - }; - - /** - * Synchronizes a folder's contents from disk to memory, i.e. if - * a message has disappeared from the disk, this method will remove it from folder.messages, and - * it adds any messages from disk to memory the are not yet in folder.messages - * - * @param {Object} options.folder The folder to synchronize - * @param {Function} callback [description] - */ - EmailDAO.prototype.refreshFolder = function(options, callback) { - var self = this, - folder = options.folder; - - self.busy(); - folder.messages = folder.messages || []; - self._localListMessages({ - folder: folder - }, function(err, storedMessages) { - if (err) { - done(err); - return; + // which messages are new on the disk that are not yet in memory? + _.filter(storedMessages, function(msg) { + return _.contains(newUids, msg.uid); + }).forEach(function(newMessage) { + // remove the body parts to not load unnecessary data to memory + // however, don't do that for the outbox. load the full message there. + if (folder.path !== config.outboxMailboxPath) { + delete newMessage.bodyParts; } - var storedUids = _.pluck(storedMessages, MSG_ATTR_UID), - memoryUids = _.pluck(folder.messages, MSG_ATTR_UID), - newUids = _.difference(storedUids, memoryUids), // uids of messages that are not yet in memory - removedUids = _.difference(memoryUids, storedUids); // uids of messages that are no longer stored on the disk - - // which messages are new on the disk that are not yet in memory? - _.filter(storedMessages, function(msg) { - return _.contains(newUids, msg.uid); - }).forEach(function(newMessage) { - // remove the body parts to not load unnecessary data to memory - // however, don't do that for the outbox. load the full message there. - if (folder.path !== config.outboxMailboxPath) { - delete newMessage.bodyParts; - } - - folder.messages.push(newMessage); - }); - - // which messages are no longer on disk, i.e. have been removed/sent/... - _.filter(folder.messages, function(msg) { - return _.contains(removedUids, msg.uid); - }).forEach(function(removedMessage) { - // remove the message - var index = folder.messages.indexOf(removedMessage); - folder.messages.splice(index, 1); - }); - - done(); + folder.messages.push(newMessage); }); - function done(err) { - self.done(); // stop the spinner - updateUnreadCount(folder); // update the unread count - callback(err); - } - }; + // which messages are no longer on disk, i.e. have been removed/sent/... + _.filter(folder.messages, function(msg) { + return _.contains(removedUids, msg.uid); + }).forEach(function(removedMessage) { + // remove the message + var index = folder.messages.indexOf(removedMessage); + folder.messages.splice(index, 1); + }); - /** - * Fetches a message's headers from IMAP. - * - * NB! If we fetch a message whose subject line correspond's to that of a verification message, - * we try to verify that, and if that worked, we delete the verified message from IMAP. - * - * @param {Object} options.folder The folder for which to fetch the message - * @param {Function} callback(error) Invoked when the message is persisted and added to folder.messages - */ - EmailDAO.prototype.fetchMessages = function(options, callback) { - var self = this, - folder = options.folder; + done(); + }); - self.busy(); + function done(err) { + self.done(); // stop the spinner + updateUnreadCount(folder); // update the unread count + callback(err); + } +}; - if (!self._account.online) { - done({ - errMsg: 'Client is currently offline!', - code: 42 - }); +/** + * Fetches a message's headers from IMAP. + * + * NB! If we fetch a message whose subject line correspond's to that of a verification message, + * we try to verify that, and if that worked, we delete the verified message from IMAP. + * + * @param {Object} options.folder The folder for which to fetch the message + * @param {Function} callback(error) Invoked when the message is persisted and added to folder.messages + */ +EmailDAO.prototype.fetchMessages = function(options, callback) { + var self = this, + folder = options.folder; + + self.busy(); + + if (!self._account.online) { + done({ + errMsg: 'Client is currently offline!', + code: 42 + }); + return; + } + + // list the messages starting from the lowest new uid to the highest new uid + self._imapListMessages(options, function(err, messages) { + if (err) { + done(err); return; } - // list the messages starting from the lowest new uid to the highest new uid - self._imapListMessages(options, function(err, messages) { - if (err) { - done(err); - return; - } + // if there are verification messages in the synced messages, handle it + var verificationMessages = _.filter(messages, function(message) { + return message.subject === str.verificationSubject; + }); - // if there are verification messages in the synced messages, handle it - var verificationMessages = _.filter(messages, function(message) { - return message.subject === str.verificationSubject; - }); + // if there are verification messages, continue after we've tried to verify + if (verificationMessages.length > 0) { + var after = _.after(verificationMessages.length, storeHeaders); - // if there are verification messages, continue after we've tried to verify - if (verificationMessages.length > 0) { - var after = _.after(verificationMessages.length, storeHeaders); - - verificationMessages.forEach(function(verificationMessage) { - handleVerification(verificationMessage, function(err, isValid) { - // if it was NOT a valid verification mail, do nothing - // if an error occurred and the mail was a valid verification mail, - // keep the mail in the list so the user can see it and verify manually - if (!isValid || err) { - after(); - return; - } - - // if verification worked, we remove the mail from the list. - messages.splice(messages.indexOf(verificationMessage), 1); + verificationMessages.forEach(function(verificationMessage) { + handleVerification(verificationMessage, function(err, isValid) { + // if it was NOT a valid verification mail, do nothing + // if an error occurred and the mail was a valid verification mail, + // keep the mail in the list so the user can see it and verify manually + if (!isValid || err) { after(); - }); - }); - return; - } - - // no verification messages, just proceed as usual - storeHeaders(); - - function storeHeaders() { - if (_.isEmpty(messages)) { - // nothing to do, we're done here - done(); - return; - } - - // persist the encrypted message to the local storage - self._localStoreMessages({ - folder: folder, - emails: messages - }, function(err) { - if (err) { - done(err); return; } - // this enables us to already show the attachment clip in the message list ui - messages.forEach(function(message) { - message.attachments = message.bodyParts.filter(function(bodyPart) { - return bodyPart.type === MSG_PART_TYPE_ATTACHMENT; - }); - }); - - [].unshift.apply(folder.messages, messages); // add the new messages to the folder - updateUnreadCount(folder); // update the unread count - - // notify about new messages only for the inbox - if (folder.type === FOLDER_TYPE_INBOX) { - self.onIncomingMessage(messages); - } - done(); - }); - } - }); - - function done(err) { - self.done(); // stop the spinner - callback(err); - } - - // Handles verification of public keys, deletion of messages with verified keys - function handleVerification(message, localCallback) { - self._getBodyParts({ - folder: folder, - uid: message.uid, - bodyParts: message.bodyParts - }, function(error, parsedBodyParts) { - // we could not stream the text to determine if the verification was valid or not - // so handle it as if it were valid - if (error) { - localCallback(error, true); - return; - } - - var body = _.pluck(filterBodyParts(parsedBodyParts, MSG_PART_TYPE_TEXT), MSG_PART_ATTR_CONTENT).join('\n'), - verificationUrlPrefix = config.cloudUrl + config.verificationUrl, - uuid = body.split(verificationUrlPrefix).pop().substr(0, config.verificationUuidLength), - uuidRegex = /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/; - - // there's no valid uuid in the message, so forget about it - if (!uuidRegex.test(uuid)) { - localCallback(null, false); - return; - } - - // there's a valid uuid in the message, so try to verify it - self._keychain.verifyPublicKey(uuid, function(err) { - if (err) { - localCallback({ - errMsg: 'Verifying your public key failed: ' + err.errMsg - }, true); - return; - } - - // public key has been verified, delete the message - self._imapDeleteMessage({ - folder: folder, - uid: message.uid - }, function() { - // if we could successfully not delete the message or not doesn't matter. - // just don't show it in whiteout and keep quiet about it - localCallback(null, true); - }); + // if verification worked, we remove the mail from the list. + messages.splice(messages.indexOf(verificationMessage), 1); + after(); }); }); - } - }; - - /** - * Delete a message from IMAP, disk and folder.messages. - * - * Please note that this deletes from disk only if you delete from the outbox, - * since it is not an IMAP folder but a virtual folder that only exists on disk. - * - * @param {Object} options.folder The folder from which to delete the messages - * @param {Object} options.message The message that should be deleted - * @param {Boolean} options.localOnly Indicated if the message should not be removed from IMAP - * @param {Function} callback(error) Invoked when the message was delete, or an error occurred - */ - EmailDAO.prototype.deleteMessage = function(options, callback) { - var self = this, - folder = options.folder, - message = options.message; - - self.busy(); - - folder.messages.splice(folder.messages.indexOf(message), 1); - - // delete only locally - if (options.localOnly || options.folder.path === config.outboxMailboxPath) { - deleteLocal(); return; } - deleteImap(); + // no verification messages, just proceed as usual + storeHeaders(); - function deleteImap() { - if (!self._account.online) { - // no action if we're not online - done({ - errMsg: 'Client is currently offline!', - code: 42 - }); + function storeHeaders() { + if (_.isEmpty(messages)) { + // nothing to do, we're done here + done(); return; } - // delete from IMAP - self._imapDeleteMessage({ + // persist the encrypted message to the local storage + self._localStoreMessages({ folder: folder, - uid: message.uid + emails: messages }, function(err) { if (err) { done(err); return; } - deleteLocal(); + // this enables us to already show the attachment clip in the message list ui + messages.forEach(function(message) { + message.attachments = message.bodyParts.filter(function(bodyPart) { + return bodyPart.type === MSG_PART_TYPE_ATTACHMENT; + }); + }); + + [].unshift.apply(folder.messages, messages); // add the new messages to the folder + updateUnreadCount(folder); // update the unread count + + // notify about new messages only for the inbox + if (folder.type === FOLDER_TYPE_INBOX) { + self.onIncomingMessage(messages); + } + done(); }); } + }); - function deleteLocal() { - // delete from indexed db - self._localDeleteMessage({ - folder: folder, - uid: message.uid - }, done); - } + function done(err) { + self.done(); // stop the spinner + callback(err); + } - function done(err) { - self.done(); // stop the spinner - if (err) { - folder.messages.unshift(message); // re-add the message to the folder in case of an error + // Handles verification of public keys, deletion of messages with verified keys + function handleVerification(message, localCallback) { + self._getBodyParts({ + folder: folder, + uid: message.uid, + bodyParts: message.bodyParts + }, function(error, parsedBodyParts) { + // we could not stream the text to determine if the verification was valid or not + // so handle it as if it were valid + if (error) { + localCallback(error, true); + return; } - updateUnreadCount(folder); // update the unread count, if necessary - callback(err); - } - }; - /** - * Updates a message's 'unread' and 'answered' flags - * - * Please note if you set flags on disk only if you delete from the outbox, - * since it is not an IMAP folder but a virtual folder that only exists on disk. - * - * @param {[type]} options [description] - * @param {Function} callback [description] - */ - EmailDAO.prototype.setFlags = function(options, callback) { - var self = this, - folder = options.folder, - message = options.message; + var body = _.pluck(filterBodyParts(parsedBodyParts, MSG_PART_TYPE_TEXT), MSG_PART_ATTR_CONTENT).join('\n'), + verificationUrlPrefix = config.cloudUrl + config.verificationUrl, + uuid = body.split(verificationUrlPrefix).pop().substr(0, config.verificationUuidLength), + uuidRegex = /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/; - self.busy(); // start the spinner + // there's no valid uuid in the message, so forget about it + if (!uuidRegex.test(uuid)) { + localCallback(null, false); + return; + } - // no-op if the message if not present anymore (for whatever reason) - if (folder.messages.indexOf(message) < 0) { - self.done(); // stop the spinner - return; - } + // there's a valid uuid in the message, so try to verify it + self._keychain.verifyPublicKey(uuid, function(err) { + if (err) { + localCallback({ + errMsg: 'Verifying your public key failed: ' + err.errMsg + }, true); + return; + } - // don't do a roundtrip to IMAP, - // especially if you want to mark outbox messages - if (options.localOnly || options.folder.path === config.outboxMailboxPath) { - markStorage(); - return; - } + // public key has been verified, delete the message + self._imapDeleteMessage({ + folder: folder, + uid: message.uid + }, function() { + // if we could successfully not delete the message or not doesn't matter. + // just don't show it in whiteout and keep quiet about it + localCallback(null, true); + }); + }); + }); + } +}; +/** + * Delete a message from IMAP, disk and folder.messages. + * + * Please note that this deletes from disk only if you delete from the outbox, + * since it is not an IMAP folder but a virtual folder that only exists on disk. + * + * @param {Object} options.folder The folder from which to delete the messages + * @param {Object} options.message The message that should be deleted + * @param {Boolean} options.localOnly Indicated if the message should not be removed from IMAP + * @param {Function} callback(error) Invoked when the message was delete, or an error occurred + */ +EmailDAO.prototype.deleteMessage = function(options, callback) { + var self = this, + folder = options.folder, + message = options.message; + + self.busy(); + + folder.messages.splice(folder.messages.indexOf(message), 1); + + // delete only locally + if (options.localOnly || options.folder.path === config.outboxMailboxPath) { + deleteLocal(); + return; + } + + deleteImap(); + + function deleteImap() { if (!self._account.online) { // no action if we're not online done({ @@ -579,1153 +506,1222 @@ define(function(require) { return; } - markImap(); + // delete from IMAP + self._imapDeleteMessage({ + folder: folder, + uid: message.uid + }, function(err) { + if (err) { + done(err); + return; + } - function markImap() { - // mark a message unread/answered on IMAP - self._imapMark({ - folder: folder, - uid: options.message.uid, - unread: options.message.unread, - answered: options.message.answered - }, function(err) { - if (err) { - done(err); - return; - } + deleteLocal(); + }); + } - markStorage(); - }); + function deleteLocal() { + // delete from indexed db + self._localDeleteMessage({ + folder: folder, + uid: message.uid + }, done); + } + + function done(err) { + self.done(); // stop the spinner + if (err) { + folder.messages.unshift(message); // re-add the message to the folder in case of an error } + updateUnreadCount(folder); // update the unread count, if necessary + callback(err); + } +}; - function markStorage() { - // angular pollutes that data transfer objects with helper properties (e.g. $$hashKey), - // which we do not want to persist to disk. in order to avoid that, we load the pristine - // message from disk, change the flags and re-persist it to disk - self._localListMessages({ +/** + * Updates a message's 'unread' and 'answered' flags + * + * Please note if you set flags on disk only if you delete from the outbox, + * since it is not an IMAP folder but a virtual folder that only exists on disk. + * + * @param {[type]} options [description] + * @param {Function} callback [description] + */ +EmailDAO.prototype.setFlags = function(options, callback) { + var self = this, + folder = options.folder, + message = options.message; + + self.busy(); // start the spinner + + // no-op if the message if not present anymore (for whatever reason) + if (folder.messages.indexOf(message) < 0) { + self.done(); // stop the spinner + return; + } + + // don't do a roundtrip to IMAP, + // especially if you want to mark outbox messages + if (options.localOnly || options.folder.path === config.outboxMailboxPath) { + markStorage(); + return; + } + + if (!self._account.online) { + // no action if we're not online + done({ + errMsg: 'Client is currently offline!', + code: 42 + }); + return; + } + + markImap(); + + function markImap() { + // mark a message unread/answered on IMAP + self._imapMark({ + folder: folder, + uid: options.message.uid, + unread: options.message.unread, + answered: options.message.answered + }, function(err) { + if (err) { + done(err); + return; + } + + markStorage(); + }); + } + + function markStorage() { + // angular pollutes that data transfer objects with helper properties (e.g. $$hashKey), + // which we do not want to persist to disk. in order to avoid that, we load the pristine + // message from disk, change the flags and re-persist it to disk + self._localListMessages({ + folder: folder, + uid: options.message.uid, + }, function(err, storedMessages) { + if (err) { + done(err); + return; + } + + // set the flags + var storedMessage = storedMessages[0]; + storedMessage.unread = options.message.unread; + storedMessage.answered = options.message.answered; + storedMessage.modseq = options.message.modseq || storedMessage.modseq; + + // store + self._localStoreMessages({ folder: folder, - uid: options.message.uid, - }, function(err, storedMessages) { + emails: [storedMessage] + }, done); + }); + } + + function done(err) { + self.done(); // stop the spinner + updateUnreadCount(folder); // update the unread count + callback(err); + } +}; + +/** + * Streams message content + * @param {Object} options.message The message for which to retrieve the body + * @param {Object} options.folder The IMAP folder + * @param {Function} callback(error, message) Invoked when the message is streamed, or provides information if an error occurred + */ +EmailDAO.prototype.getBody = function(options, callback) { + var self = this, + message = options.message, + folder = options.folder; + + // the message either already has a body or is fetching it right now, so no need to become active here + if (message.loadingBody || typeof message.body !== 'undefined') { + return; + } + + message.loadingBody = true; + + self.busy(); + + /* + * read this before inspecting the method! + * + * you will wonder about the round trip to the disk where we load the persisted object. there are two reasons for this behavior: + * 1) if you work with a message that was loaded from the disk, we strip the message.bodyParts array, + * because it is not really necessary to keep everything in memory + * 2) the message in memory is polluted by angular. angular tracks ordering of a list by adding a property + * to the model. this property is auto generated and must not be persisted. + */ + + retrieveContent(); + + function retrieveContent() { + // load the local message from memory + self._localListMessages({ + folder: folder, + uid: message.uid + }, function(err, localMessages) { + if (err || localMessages.length === 0) { + done(err); + return; + } + + var localMessage = localMessages[0]; + + // treat attachment and non-attachment body parts separately: + // we need to fetch the content for non-attachment body parts (encrypted, signed, text, html, resources referenced from the html) + // but we spare the effort and fetch attachment content later upon explicit user request. + var contentParts = localMessage.bodyParts.filter(function(bodyPart) { + return bodyPart.type !== MSG_PART_TYPE_ATTACHMENT || (bodyPart.type === MSG_PART_TYPE_ATTACHMENT && bodyPart.id); + }); + var attachmentParts = localMessage.bodyParts.filter(function(bodyPart) { + return bodyPart.type === MSG_PART_TYPE_ATTACHMENT && !bodyPart.id; + }); + + // do we need to fetch content from the imap server? + var needsFetch = false; + contentParts.forEach(function(part) { + needsFetch = (typeof part.content === 'undefined'); + }); + + if (!needsFetch) { + // if we have all the content we need, + // we can extract the content + message.bodyParts = localMessage.bodyParts; + extractContent(); + return; + } + + // get the raw content from the imap server + self._getBodyParts({ + folder: folder, + uid: localMessage.uid, + bodyParts: contentParts + }, function(err, parsedBodyParts) { if (err) { done(err); return; } - // set the flags - var storedMessage = storedMessages[0]; - storedMessage.unread = options.message.unread; - storedMessage.answered = options.message.answered; - storedMessage.modseq = options.message.modseq || storedMessage.modseq; + // piece together the parsed bodyparts and the empty attachments which have not been parsed + message.bodyParts = parsedBodyParts.concat(attachmentParts); + localMessage.bodyParts = parsedBodyParts.concat(attachmentParts); - // store + // persist it to disk self._localStoreMessages({ folder: folder, - emails: [storedMessage] - }, done); + emails: [localMessage] + }, function(error) { + if (error) { + done(error); + return; + } + + // extract the content + extractContent(); + }); }); + }); + } + + function extractContent() { + if (message.encrypted) { + // show the encrypted message + message.body = filterBodyParts(message.bodyParts, MSG_PART_TYPE_ENCRYPTED)[0].content; + return done(); } - function done(err) { - self.done(); // stop the spinner - updateUnreadCount(folder); // update the unread count - callback(err); - } - }; + var root = message.bodyParts; - /** - * Streams message content - * @param {Object} options.message The message for which to retrieve the body - * @param {Object} options.folder The IMAP folder - * @param {Function} callback(error, message) Invoked when the message is streamed, or provides information if an error occurred - */ - EmailDAO.prototype.getBody = function(options, callback) { - var self = this, - message = options.message, - folder = options.folder; - - // the message either already has a body or is fetching it right now, so no need to become active here - if (message.loadingBody || typeof message.body !== 'undefined') { - return; + if (message.signed) { + // PGP/MIME signed + var signedRoot = filterBodyParts(message.bodyParts, MSG_PART_TYPE_SIGNED)[0]; // in case of a signed message, you only want to show the signed content and ignore the rest + message.signedMessage = signedRoot.signedMessage; + message.signature = signedRoot.signature; + root = signedRoot.content; } - message.loadingBody = true; - - self.busy(); + var body = _.pluck(filterBodyParts(root, MSG_PART_TYPE_TEXT), MSG_PART_ATTR_CONTENT).join('\n'); /* - * read this before inspecting the method! - * - * you will wonder about the round trip to the disk where we load the persisted object. there are two reasons for this behavior: - * 1) if you work with a message that was loaded from the disk, we strip the message.bodyParts array, - * because it is not really necessary to keep everything in memory - * 2) the message in memory is polluted by angular. angular tracks ordering of a list by adding a property - * to the model. this property is auto generated and must not be persisted. + * if the message is plain text and contains pgp/inline, we are only interested in the encrypted + * content, the rest (corporate mail footer, attachments, etc.) is discarded. + * "-----BEGIN/END (...)-----" must be at the start/end of a line, + * the regex must not match a pgp block in a plain text reply or forward of a pgp/inline message, + * the encryption will break for replies/forward, because "> " corrupts the PGP block with non-radix-64 characters, */ + var pgpInlineMatch = /^-{5}BEGIN PGP MESSAGE-{5}[\s\S]*-{5}END PGP MESSAGE-{5}$/im.exec(body); + if (pgpInlineMatch) { + message.body = pgpInlineMatch[0]; // show the plain text content + message.encrypted = true; // signal the ui that we're handling encrypted content - retrieveContent(); - - function retrieveContent() { - // load the local message from memory - self._localListMessages({ - folder: folder, - uid: message.uid - }, function(err, localMessages) { - if (err || localMessages.length === 0) { - done(err); - return; - } - - var localMessage = localMessages[0]; - - // treat attachment and non-attachment body parts separately: - // we need to fetch the content for non-attachment body parts (encrypted, signed, text, html, resources referenced from the html) - // but we spare the effort and fetch attachment content later upon explicit user request. - var contentParts = localMessage.bodyParts.filter(function(bodyPart) { - return bodyPart.type !== MSG_PART_TYPE_ATTACHMENT || (bodyPart.type === MSG_PART_TYPE_ATTACHMENT && bodyPart.id); - }); - var attachmentParts = localMessage.bodyParts.filter(function(bodyPart) { - return bodyPart.type === MSG_PART_TYPE_ATTACHMENT && !bodyPart.id; - }); - - // do we need to fetch content from the imap server? - var needsFetch = false; - contentParts.forEach(function(part) { - needsFetch = (typeof part.content === 'undefined'); - }); - - if (!needsFetch) { - // if we have all the content we need, - // we can extract the content - message.bodyParts = localMessage.bodyParts; - extractContent(); - return; - } - - // get the raw content from the imap server - self._getBodyParts({ - folder: folder, - uid: localMessage.uid, - bodyParts: contentParts - }, function(err, parsedBodyParts) { - if (err) { - done(err); - return; - } - - // piece together the parsed bodyparts and the empty attachments which have not been parsed - message.bodyParts = parsedBodyParts.concat(attachmentParts); - localMessage.bodyParts = parsedBodyParts.concat(attachmentParts); - - // persist it to disk - self._localStoreMessages({ - folder: folder, - emails: [localMessage] - }, function(error) { - if (error) { - done(error); - return; - } - - // extract the content - extractContent(); - }); - }); - }); + // replace the bodyParts info with an artificial bodyPart of type "encrypted" + message.bodyParts = [{ + type: MSG_PART_TYPE_ENCRYPTED, + content: pgpInlineMatch[0], + _isPgpInline: true // used internally to avoid trying to parse non-MIME text with the mailreader + }]; + return done(); } - function extractContent() { - if (message.encrypted) { - // show the encrypted message - message.body = filterBodyParts(message.bodyParts, MSG_PART_TYPE_ENCRYPTED)[0].content; - return done(); - } - - var root = message.bodyParts; - - if (message.signed) { - // PGP/MIME signed - var signedRoot = filterBodyParts(message.bodyParts, MSG_PART_TYPE_SIGNED)[0]; // in case of a signed message, you only want to show the signed content and ignore the rest - message.signedMessage = signedRoot.signedMessage; - message.signature = signedRoot.signature; - root = signedRoot.content; - } - - var body = _.pluck(filterBodyParts(root, MSG_PART_TYPE_TEXT), MSG_PART_ATTR_CONTENT).join('\n'); - - /* - * if the message is plain text and contains pgp/inline, we are only interested in the encrypted - * content, the rest (corporate mail footer, attachments, etc.) is discarded. - * "-----BEGIN/END (...)-----" must be at the start/end of a line, - * the regex must not match a pgp block in a plain text reply or forward of a pgp/inline message, - * the encryption will break for replies/forward, because "> " corrupts the PGP block with non-radix-64 characters, - */ - var pgpInlineMatch = /^-{5}BEGIN PGP MESSAGE-{5}[\s\S]*-{5}END PGP MESSAGE-{5}$/im.exec(body); - if (pgpInlineMatch) { - message.body = pgpInlineMatch[0]; // show the plain text content - message.encrypted = true; // signal the ui that we're handling encrypted content - - // replace the bodyParts info with an artificial bodyPart of type "encrypted" - message.bodyParts = [{ - type: MSG_PART_TYPE_ENCRYPTED, - content: pgpInlineMatch[0], - _isPgpInline: true // used internally to avoid trying to parse non-MIME text with the mailreader - }]; - return done(); - } - - /* - * any content before/after the PGP block will be discarded, - * "-----BEGIN/END (...)-----" must be at the start/end of a line, - * after the hash (and possibly other) arbitrary headers, the signed payload begins, - * the text is followed by a final \n and then the pgp signature begins - * untrusted attachments and html is ignored - */ - var clearSignedMatch = /^-{5}BEGIN PGP SIGNED MESSAGE-{5}\nHash:[ ][^\n]+\n(?:[A-Za-z]+:[ ][^\n]+\n)*\n([\s\S]*)\n-{5}BEGIN PGP SIGNATURE-{5}[\S\s]*-{5}END PGP SIGNATURE-{5}$/im.exec(body); - if (clearSignedMatch) { - // PGP/INLINE signed - message.signed = true; - message.clearSignedMessage = clearSignedMatch[0]; - body = clearSignedMatch[1]; - } - - if (!message.signed) { - // message is not signed, so we're done here - return setBody(); - } - - // check the signatures for signed messages - self._checkSignatures(message, function(err, signaturesValid) { - if (err) { - return done(err); - } - - message.signaturesValid = signaturesValid; - setBody(); - }); - - function setBody() { - message.body = body; - if (!message.clearSignedMessage) { - message.attachments = filterBodyParts(root, MSG_PART_TYPE_ATTACHMENT); - message.html = _.pluck(filterBodyParts(root, MSG_PART_TYPE_HTML), MSG_PART_ATTR_CONTENT).join('\n'); - inlineExternalImages(message); - } - - done(); - } + /* + * any content before/after the PGP block will be discarded, + * "-----BEGIN/END (...)-----" must be at the start/end of a line, + * after the hash (and possibly other) arbitrary headers, the signed payload begins, + * the text is followed by a final \n and then the pgp signature begins + * untrusted attachments and html is ignored + */ + var clearSignedMatch = /^-{5}BEGIN PGP SIGNED MESSAGE-{5}\nHash:[ ][^\n]+\n(?:[A-Za-z]+:[ ][^\n]+\n)*\n([\s\S]*)\n-{5}BEGIN PGP SIGNATURE-{5}[\S\s]*-{5}END PGP SIGNATURE-{5}$/im.exec(body); + if (clearSignedMatch) { + // PGP/INLINE signed + message.signed = true; + message.clearSignedMessage = clearSignedMatch[0]; + body = clearSignedMatch[1]; } - - function done(err) { - self.done(); - message.loadingBody = false; - callback(err, err ? undefined : message); - } - }; - - EmailDAO.prototype._checkSignatures = function(message, callback) { - var self = this; - - self._keychain.getReceiverPublicKey(message.from[0].address, function(err, senderPublicKey) { - if (err) { - return callback(err); - } - - // get the receiver's public key to check the message signature - var senderKey = senderPublicKey ? senderPublicKey.publicKey : undefined; - - if (message.clearSignedMessage) { - self._pgp.verifyClearSignedMessage(message.clearSignedMessage, senderKey, callback); - } else if (message.signedMessage && message.signature) { - self._pgp.verifySignedMessage(message.signedMessage, message.signature, senderKey, callback); - } else { - callback(null, undefined); - } - }); - }; - - /** - * Retrieves an attachment matching a body part for a given uid and a folder - * - * @param {Object} options.folder The folder where to find the attachment - * @param {Number} options.uid The uid for the message the attachment body part belongs to - * @param {Object} options.attachment The attachment body part to fetch and parse from IMAP - * @param {Function} callback(error, attachment) Invoked when the attachment body part was retrieved and parsed, or an error occurred - */ - EmailDAO.prototype.getAttachment = function(options, callback) { - var self = this, - attachment = options.attachment; - - self.busy(); - attachment.busy = true; - self._getBodyParts({ - folder: options.folder, - uid: options.uid, - bodyParts: [attachment] - }, function(err, parsedBodyParts) { - attachment.busy = false; - if (err) { - callback(err); - return; - } - self.done(); - // add the content to the original object - attachment.content = parsedBodyParts[0].content; - callback(err, err ? undefined : attachment); - }); - }; - - /** - * Decrypts a message and replaces sets the decrypted plaintext as the message's body, html, or attachment, respectively. - * The first encrypted body part's ciphertext (in the content property) will be decrypted. - * - * @param {Object} options.message The message - * @param {Function} callback(error, message) - */ - EmailDAO.prototype.decryptBody = function(options, callback) { - var self = this, - message = options.message; - - // the message is decrypting has no body, is not encrypted or has already been decrypted - if (!message.bodyParts || message.decryptingBody || !message.body || !message.encrypted || message.decrypted) { - callback(null, message); - return; + if (!message.signed) { + // message is not signed, so we're done here + return setBody(); } - message.decryptingBody = true; - - self.busy(); - // get the sender's public key for signature checking - self._keychain.getReceiverPublicKey(message.from[0].address, function(err, senderPublicKey) { + // check the signatures for signed messages + self._checkSignatures(message, function(err, signaturesValid) { if (err) { return done(err); } - // get the receiver's public key to check the message signature - var encryptedNode = filterBodyParts(message.bodyParts, MSG_PART_TYPE_ENCRYPTED)[0]; - var senderKey = senderPublicKey ? senderPublicKey.publicKey : undefined; - self._pgp.decrypt(encryptedNode.content, senderKey, function(err, decrypted, signaturesValid) { - if (err || !decrypted) { - return showError(err.message || 'An error occurred during the decryption.'); - } - - // if the decryption worked and signatures are present, everything's fine. - // no error is thrown if signatures are not present - message.signed = typeof signaturesValid !== 'undefined'; - message.signaturesValid = signaturesValid; - - // if the encrypted node contains pgp/inline, we must not parse it - // with the mailreader as it is not well-formed MIME - if (encryptedNode._isPgpInline) { - message.body = decrypted; - message.decrypted = true; - return done(); - } - - // the mailparser works on the .raw property - encryptedNode.raw = decrypted; - - // parse the decrypted raw content in the mailparser - self._mailreader.parse({ - bodyParts: [encryptedNode] - }, function(err, root) { - if (err) { - return showError(err.errMsg || err.message); - } - - if (!message.signed) { - // message had no signature in the ciphertext, so there's a little extra effort to be done here - // is there a signed MIME node? - var signedRoot = filterBodyParts(root, MSG_PART_TYPE_SIGNED)[0]; - if (!signedRoot) { - // no signed MIME node, obviously an unsigned PGP/MIME message - return setBody(); - } - - // if there is something signed in here, we're only interested in the signed content - message.signedMessage = signedRoot.signedMessage; - message.signature = signedRoot.signature; - root = signedRoot.content; - - // check the signatures for encrypted messages - self._checkSignatures(message, function(err, signaturesValid) { - if (err) { - return done(err); - } - - message.signed = typeof signaturesValid !== 'undefined'; - message.signaturesValid = signaturesValid; - setBody(); - }); - return; - } - - // message had a signature in the ciphertext, so we're done here - setBody(); - - function setBody() { - // we have successfully interpreted the descrypted message, - // so let's update the views on the message parts - message.body = _.pluck(filterBodyParts(root, MSG_PART_TYPE_TEXT), MSG_PART_ATTR_CONTENT).join('\n'); - message.html = _.pluck(filterBodyParts(root, MSG_PART_TYPE_HTML), MSG_PART_ATTR_CONTENT).join('\n'); - message.attachments = _.reject(filterBodyParts(root, MSG_PART_TYPE_ATTACHMENT), function(attmt) { - // remove the pgp-signature from the attachments - return attmt.mimeType === "application/pgp-signature"; - }); - inlineExternalImages(message); - - message.decrypted = true; - - // we're done here! - done(); - } - }); - }); + message.signaturesValid = signaturesValid; + setBody(); }); - function showError(msg) { - message.body = msg; - message.decrypted = true; // display error msg in body + function setBody() { + message.body = body; + if (!message.clearSignedMessage) { + message.attachments = filterBodyParts(root, MSG_PART_TYPE_ATTACHMENT); + message.html = _.pluck(filterBodyParts(root, MSG_PART_TYPE_HTML), MSG_PART_ATTR_CONTENT).join('\n'); + inlineExternalImages(message); + } + done(); } + } - function done(err) { - self.done(); - message.decryptingBody = false; - callback(err, err ? undefined : message); - } - }; - /** - * Encrypted (if necessary) and sends a message with a predefined clear text greeting. - * - * @param {Object} options.email The message to be sent - * @param {Function} callback(error) Invoked when the message was sent, or an error occurred - */ - EmailDAO.prototype.sendEncrypted = function(options, callback) { - var self = this; + function done(err) { + self.done(); + message.loadingBody = false; + callback(err, err ? undefined : message); + } +}; - if (!self._account.online) { - callback({ - errMsg: 'Client is currently offline!', - code: 42 - }); - return; +EmailDAO.prototype._checkSignatures = function(message, callback) { + var self = this; + + self._keychain.getReceiverPublicKey(message.from[0].address, function(err, senderPublicKey) { + if (err) { + return callback(err); } - self.busy(); - // mime encode, sign, encrypt and send email via smtp - self._pgpMailer.send({ - encrypt: true, - smtpclient: options.smtpclient, // filled solely in the integration test, undefined in normal usage - mail: options.email, - publicKeysArmored: options.email.publicKeysArmored - }, function(err, rfcText) { - if (err) { - return callback(err); - } + // get the receiver's public key to check the message signature + var senderKey = senderPublicKey ? senderPublicKey.publicKey : undefined; - // upload the sent message to the sent folder if necessary - var sentFolder = _.findWhere(self._account.folders, { - type: FOLDER_TYPE_SENT - }); - - if (self.ignoreUploadOnSent || !sentFolder || !rfcText) { - self.done(); - return callback(); - } - - self._imapClient.uploadMessage({ - path: sentFolder.path, - message: rfcText - }, function(err) { - self.done(); - callback(err); - }); - }); - }; - - /** - * Sends a signed message in the plain - * - * @param {Object} options.email The message to be sent - * @param {Function} callback(error) Invoked when the message was sent, or an error occurred - */ - EmailDAO.prototype.sendPlaintext = function(options, callback) { - var self = this; - - if (!self._account.online) { - callback({ - errMsg: 'Client is currently offline!', - code: 42 - }); - return; + if (message.clearSignedMessage) { + self._pgp.verifyClearSignedMessage(message.clearSignedMessage, senderKey, callback); + } else if (message.signedMessage && message.signature) { + self._pgp.verifySignedMessage(message.signedMessage, message.signature, senderKey, callback); + } else { + callback(null, undefined); } - self.busy(); + }); +}; - // add suffix to plaintext mail - options.email.body += str.signature + config.cloudUrl + '/' + self._account.emailAddress; +/** + * Retrieves an attachment matching a body part for a given uid and a folder + * + * @param {Object} options.folder The folder where to find the attachment + * @param {Number} options.uid The uid for the message the attachment body part belongs to + * @param {Object} options.attachment The attachment body part to fetch and parse from IMAP + * @param {Function} callback(error, attachment) Invoked when the attachment body part was retrieved and parsed, or an error occurred + */ +EmailDAO.prototype.getAttachment = function(options, callback) { + var self = this, + attachment = options.attachment; - // mime encode, sign and send email via smtp - self._pgpMailer.send({ - smtpclient: options.smtpclient, // filled solely in the integration test, undefined in normal usage - mail: options.email - }, function(err, rfcText) { - if (err) { - return callback(err); - } - - // upload the sent message to the sent folder if necessary - var sentFolder = _.findWhere(self._account.folders, { - type: FOLDER_TYPE_SENT - }); - - if (self.ignoreUploadOnSent || !sentFolder || !rfcText) { - self.done(); - return callback(); - } - - self._imapClient.uploadMessage({ - path: sentFolder.path, - message: rfcText - }, function(err) { - self.done(); - callback(err); - }); - }); - }; - - /** - * Signs and encrypts a message - * - * @param {Object} options.email The message to be encrypted - * @param {Function} callback(error, message) Invoked when the message was encrypted, or an error occurred - */ - EmailDAO.prototype.encrypt = function(options, callback) { - var self = this; - - self.busy(); - self._pgpbuilder.encrypt(options, function(err) { - self.done(); + self.busy(); + attachment.busy = true; + self._getBodyParts({ + folder: options.folder, + uid: options.uid, + bodyParts: [attachment] + }, function(err, parsedBodyParts) { + attachment.busy = false; + if (err) { callback(err); - }); + return; + } + self.done(); + // add the content to the original object + attachment.content = parsedBodyParts[0].content; + callback(err, err ? undefined : attachment); + }); +}; - }; +/** + * Decrypts a message and replaces sets the decrypted plaintext as the message's body, html, or attachment, respectively. + * The first encrypted body part's ciphertext (in the content property) will be decrypted. + * + * @param {Object} options.message The message + * @param {Function} callback(error, message) + */ +EmailDAO.prototype.decryptBody = function(options, callback) { + var self = this, + message = options.message; + // the message is decrypting has no body, is not encrypted or has already been decrypted + if (!message.bodyParts || message.decryptingBody || !message.body || !message.encrypted || message.decrypted) { + callback(null, message); + return; + } - // - // - // Event Handlers - // - // + message.decryptingBody = true; + self.busy(); + // get the sender's public key for signature checking + self._keychain.getReceiverPublicKey(message.from[0].address, function(err, senderPublicKey) { + if (err) { + return done(err); + } - /** - * This handler should be invoked when navigator.onLine === true. It will try to connect a - * given instance of the imap client. If the connection attempt was successful, it will - * update the locally available folders with the newly received IMAP folder listing. - * - * @param {Object} options.imapClient The IMAP client used to receive messages - * @param {Object} options.pgpMailer The SMTP client used to send messages - * @param {Function} callback [description] - */ - EmailDAO.prototype.onConnect = function(options, callback) { - var self = this; - - self._account.loggingIn = true; - - self._imapClient = options.imapClient; - self._pgpMailer = options.pgpMailer; - - self._imapClient.login(function(err) { - self._account.loggingIn = false; - - if (err) { - callback(err); - return; + // get the receiver's public key to check the message signature + var encryptedNode = filterBodyParts(message.bodyParts, MSG_PART_TYPE_ENCRYPTED)[0]; + var senderKey = senderPublicKey ? senderPublicKey.publicKey : undefined; + self._pgp.decrypt(encryptedNode.content, senderKey, function(err, decrypted, signaturesValid) { + if (err || !decrypted) { + return showError(err.message || 'An error occurred during the decryption.'); } - // init folders - self._initFoldersFromImap(function(err) { + // if the decryption worked and signatures are present, everything's fine. + // no error is thrown if signatures are not present + message.signed = typeof signaturesValid !== 'undefined'; + message.signaturesValid = signaturesValid; + + // if the encrypted node contains pgp/inline, we must not parse it + // with the mailreader as it is not well-formed MIME + if (encryptedNode._isPgpInline) { + message.body = decrypted; + message.decrypted = true; + return done(); + } + + // the mailparser works on the .raw property + encryptedNode.raw = decrypted; + + // parse the decrypted raw content in the mailparser + self._mailreader.parse({ + bodyParts: [encryptedNode] + }, function(err, root) { if (err) { - callback(err); - return; + return showError(err.errMsg || err.message); } - // attach sync update handler - self._imapClient.onSyncUpdate = self._onSyncUpdate.bind(self); - - // fill the imap mailboxCache with information we have locally available: - // - highest locally available moseq - // - list of locally available uids - // - highest locally available uid - // - next expected uid - var mailboxCache = {}; - self._account.folders.forEach(function(folder) { - if (folder.messages.length === 0) { - return; + if (!message.signed) { + // message had no signature in the ciphertext, so there's a little extra effort to be done here + // is there a signed MIME node? + var signedRoot = filterBodyParts(root, MSG_PART_TYPE_SIGNED)[0]; + if (!signedRoot) { + // no signed MIME node, obviously an unsigned PGP/MIME message + return setBody(); } - var uids, highestModseq, lastUid; + // if there is something signed in here, we're only interested in the signed content + message.signedMessage = signedRoot.signedMessage; + message.signature = signedRoot.signature; + root = signedRoot.content; - uids = _.pluck(folder.messages, MSG_ATTR_UID).sort(function(a, b) { - return a - b; + // check the signatures for encrypted messages + self._checkSignatures(message, function(err, signaturesValid) { + if (err) { + return done(err); + } + + message.signed = typeof signaturesValid !== 'undefined'; + message.signaturesValid = signaturesValid; + setBody(); }); - lastUid = uids[uids.length - 1]; - - highestModseq = _.pluck(folder.messages, 'modseq').sort(function(a, b) { - return a - b; - }).pop(); - - mailboxCache[folder.path] = { - exists: lastUid, - uidNext: lastUid + 1, - uidlist: uids, - highestModseq: highestModseq - }; - }); - self._imapClient.mailboxCache = mailboxCache; - - // set status to online after setting cache to prevent race condition - self._account.online = true; - - // set up the imap client to listen for changes in the inbox - var inbox = _.findWhere(self._account.folders, { - type: FOLDER_TYPE_INBOX - }); - - if (!inbox) { - return callback(); - } - - self._imapClient.listenForChanges({ - path: inbox.path - }, callback); - }); - }); - }; - - /** - * This handler should be invoked when navigator.onLine === false. - * It will discard the imap client and pgp mailer - */ - EmailDAO.prototype.onDisconnect = function(callback) { - var self = this; - - // logout of imap-client - self._imapClient.logout(function() { - // ignore error, because it's not problem if logout fails - if (callback) { - callback(); - } - }); - - // discard clients - self._account.online = false; - self._imapClient = undefined; - self._pgpMailer = undefined; - }; - - /** - * The are updates in the IMAP folder of the following type - * - 'new': a list of uids that are newly available - * - 'deleted': a list of uids that were deleted from IMAP available - * - 'messages': a list of messages (uid + flags) that where changes are available - * - * @param {String} options.type The type of the update - * @param {String} options.path The mailbox for which updates are available - * @param {Array} options.list Array containing update information. Number (uid) or mail with Object (uid and flags), respectively - */ - EmailDAO.prototype._onSyncUpdate = function(options) { - var self = this; - - var folder = _.findWhere(self._account.folders, { - path: options.path - }); - - if (!folder) { - // ignore updates for an unknown folder - return; - } - - if (options.type === SYNC_TYPE_NEW) { - // new messages available on imap, fetch from imap and store to disk and memory - self.fetchMessages({ - folder: folder, - firstUid: Math.min.apply(null, options.list), - lastUid: Math.max.apply(null, options.list) - }, self.onError.bind(self)); - } else if (options.type === SYNC_TYPE_DELETED) { - // messages have been deleted, remove from local storage and memory - options.list.forEach(function(uid) { - var message = _.findWhere(folder.messages, { - uid: uid - }); - - if (!message) { return; } - self.deleteMessage({ - folder: folder, - message: message, - localOnly: true - }, self.onError.bind(self)); - }); - } else if (options.type === SYNC_TYPE_MSGS) { - // NB! several possible reasons why this could be called. - // if a message in the array has uid value and flag array, it had a possible flag update - options.list.forEach(function(changedMsg) { - if (!changedMsg.uid || !changedMsg.flags) { - return; - } + // message had a signature in the ciphertext, so we're done here + setBody(); - var message = _.findWhere(folder.messages, { - uid: changedMsg.uid - }); - - if (!message) { - return; - } - - // update unread, answered, modseq to the latest info - message.answered = changedMsg.flags.indexOf('\\Answered') > -1; - message.unread = changedMsg.flags.indexOf('\\Seen') === -1; - message.modseq = changedMsg.modseq; - - self.setFlags({ - folder: folder, - message: message, - localOnly: true - }, self.onError.bind(self)); - }); - } - }; - - - // - // - // Internal API - // - // - - - /** - * Updates the folder information from memory, and adds/removes folders in account.folders. - * The locally available messages are loaded from memory - * - * @param {Function} callback Invoked when the folders are up to date - */ - EmailDAO.prototype._initFoldersFromDisk = function(callback) { - var self = this; - - self.busy(); // start the spinner - - // fetch list from local cache - self._devicestorage.listItems(FOLDER_DB_TYPE, 0, null, function(err, stored) { - if (err) { - return done(err); - } - - self._account.folders = stored[0] || []; - self._initMessagesFromDisk(done); - }); - - function done(err) { - self.done(); // stop the spinner - callback(err); - } - }; - - /** - * Updates the folder information from imap (if we're online). Adds/removes folders in account.folders, - * if we added/removed folder in IMAP. If we have an uninitialized folder that lacks folder.messages, - * all the locally available messages are loaded from memory. - * - * @param {Function} callback Invoked when the folders are up to date - */ - EmailDAO.prototype._initFoldersFromImap = function(callback) { - var self = this; - - self.busy(); // start the spinner - - // fetch list from imap server - self._imapClient.listWellKnownFolders(function(err, wellKnownFolders) { - if (err) { - return done(err); - } - - // initialize the folders to something meaningful if that hasn't already happened - self._account.folders = self._account.folders || []; - - // smuggle the outbox into the well known folders, which is obv not present on imap... - wellKnownFolders[config.outboxMailboxType] = [{ - name: config.outboxMailboxName, - type: config.outboxMailboxType, - path: config.outboxMailboxPath - }]; - - // indicates if we need to persist anything to disk - var foldersChanged = false; - - // the folders listed in the navigation pane - [FOLDER_TYPE_INBOX, FOLDER_TYPE_SENT, config.outboxMailboxType, FOLDER_TYPE_DRAFTS, FOLDER_TYPE_TRASH].forEach(function(mbxType) { - var localFolderWithType, imapFolderWithPath; - - // check if there is a folder of this type locally available - localFolderWithType = _.findWhere(self._account.folders, { - type: mbxType - }); - - if (localFolderWithType) { - // we have a local folder available, so let's check if this folder still exists on imap - - imapFolderWithPath = _.findWhere(wellKnownFolders[mbxType], { - path: localFolderWithType.path + function setBody() { + // we have successfully interpreted the descrypted message, + // so let's update the views on the message parts + message.body = _.pluck(filterBodyParts(root, MSG_PART_TYPE_TEXT), MSG_PART_ATTR_CONTENT).join('\n'); + message.html = _.pluck(filterBodyParts(root, MSG_PART_TYPE_HTML), MSG_PART_ATTR_CONTENT).join('\n'); + message.attachments = _.reject(filterBodyParts(root, MSG_PART_TYPE_ATTACHMENT), function(attmt) { + // remove the pgp-signature from the attachments + return attmt.mimeType === "application/pgp-signature"; }); + inlineExternalImages(message); - if (imapFolderWithPath) { - // folder present on imap, no need to update. - return; - } + message.decrypted = true; - // folder not present on imap, so remove the folder and see if there are any updates for this folder type - self._account.folders.splice(self._account.folders.indexOf(localFolderWithType), 1); - foldersChanged = true; + // we're done here! + done(); } - - if (!wellKnownFolders[mbxType] || !wellKnownFolders[mbxType].length) { - // no imap folders of the respective mailbox type, so nothing to do here - return; - } - - /** - * we have no local folder of the type, so do something intelligent, - * i.e. take the first folder of the respective type - */ - self._account.folders.push(wellKnownFolders[mbxType][0]); - foldersChanged = true; - }); - - // if folders have not changed, can fill them with messages directly - if (!foldersChanged) { - return self._initMessagesFromDisk(done); - } - - // persist encrypted list in device storage - // note: the folders in the ui also include the messages array, so let's create a clean array here - var folders = self._account.folders.map(function(folder) { - return { - name: folder.name, - path: folder.path, - type: folder.type - }; - }); - self._devicestorage.storeList([folders], FOLDER_DB_TYPE, function(err) { - if (err) { - return done(err); - } - - self._initMessagesFromDisk(done); }); }); + }); - function done(err) { - self.done(); // stop the spinner - callback(err); + function showError(msg) { + message.body = msg; + message.decrypted = true; // display error msg in body + done(); + } + + function done(err) { + self.done(); + message.decryptingBody = false; + callback(err, err ? undefined : message); + } +}; + +/** + * Encrypted (if necessary) and sends a message with a predefined clear text greeting. + * + * @param {Object} options.email The message to be sent + * @param {Function} callback(error) Invoked when the message was sent, or an error occurred + */ +EmailDAO.prototype.sendEncrypted = function(options, callback) { + var self = this; + + if (!self._account.online) { + callback({ + errMsg: 'Client is currently offline!', + code: 42 + }); + return; + } + + self.busy(); + // mime encode, sign, encrypt and send email via smtp + self._pgpMailer.send({ + encrypt: true, + smtpclient: options.smtpclient, // filled solely in the integration test, undefined in normal usage + mail: options.email, + publicKeysArmored: options.email.publicKeysArmored + }, function(err, rfcText) { + if (err) { + return callback(err); } - }; - /** - * Fill uninitialized folders with the locally available messages. - * - * @param {Function} callback Invoked when the folders are filled with messages - */ - EmailDAO.prototype._initMessagesFromDisk = function(callback) { - var self = this; + // upload the sent message to the sent folder if necessary + var sentFolder = _.findWhere(self._account.folders, { + type: FOLDER_TYPE_SENT + }); - if (!self._account.folders || self._account.folders.length === 0) { + if (self.ignoreUploadOnSent || !sentFolder || !rfcText) { + self.done(); return callback(); } - var after = _.after(self._account.folders.length, callback); - - self._account.folders.forEach(function(folder) { - if (folder.messages) { - // the folder is already initialized - return after(); - } - - // sync messages from disk to the folder model - self.refreshFolder({ - folder: folder - }, function(err) { - if (err) { - return callback(err); - } - - after(); - }); + self._imapClient.uploadMessage({ + path: sentFolder.path, + message: rfcText + }, function(err) { + self.done(); + callback(err); }); - }; + }); +}; - EmailDAO.prototype.busy = function() { - this._account.busy++; - }; +/** + * Sends a signed message in the plain + * + * @param {Object} options.email The message to be sent + * @param {Function} callback(error) Invoked when the message was sent, or an error occurred + */ +EmailDAO.prototype.sendPlaintext = function(options, callback) { + var self = this; - EmailDAO.prototype.done = function() { - if (this._account.busy > 0) { - this._account.busy--; - } - }; + if (!self._account.online) { + callback({ + errMsg: 'Client is currently offline!', + code: 42 + }); + return; + } + self.busy(); + // add suffix to plaintext mail + options.email.body += str.signature + config.cloudUrl + '/' + self._account.emailAddress; - - // - // - // IMAP API - // - // - - /** - * Mark messages as un-/read or un-/answered on IMAP - * - * @param {Object} options.folder The folder where to find the message - * @param {Number} options.uid The uid for which to change the flags - * @param {Number} options.unread Un-/Read flag - * @param {Number} options.answered Un-/Answered flag - */ - EmailDAO.prototype._imapMark = function(options, callback) { - if (!this._account.online) { - callback({ - errMsg: 'Client is currently offline!', - code: 42 - }); - return; + // mime encode, sign and send email via smtp + self._pgpMailer.send({ + smtpclient: options.smtpclient, // filled solely in the integration test, undefined in normal usage + mail: options.email + }, function(err, rfcText) { + if (err) { + return callback(err); } - options.path = options.folder.path; - this._imapClient.updateFlags(options, callback); - }; - - /** - * If we're in the trash folder or no trash folder is available, this deletes a message from IMAP. - * Otherwise, it moves a message to the trash folder. - * - * @param {Object} options.folder The folder where to find the message - * @param {Number} options.uid The uid of the message - * @param {Function} callback(error) Callback with an error object in case something went wrong. - */ - EmailDAO.prototype._imapDeleteMessage = function(options, callback) { - if (!this._account.online) { - callback({ - errMsg: 'Client is currently offline!', - code: 42 - }); - return; - } - - var trash = _.findWhere(this._account.folders, { - type: FOLDER_TYPE_TRASH + // upload the sent message to the sent folder if necessary + var sentFolder = _.findWhere(self._account.folders, { + type: FOLDER_TYPE_SENT }); - // there's no known trash folder to move the mail to or we're in the trash folder, so we can purge the message - if (!trash || options.folder === trash) { - this._imapClient.deleteMessage({ - path: options.folder.path, - uid: options.uid - }, callback); + if (self.ignoreUploadOnSent || !sentFolder || !rfcText) { + self.done(); + return callback(); + } + self._imapClient.uploadMessage({ + path: sentFolder.path, + message: rfcText + }, function(err) { + self.done(); + callback(err); + }); + }); +}; + +/** + * Signs and encrypts a message + * + * @param {Object} options.email The message to be encrypted + * @param {Function} callback(error, message) Invoked when the message was encrypted, or an error occurred + */ +EmailDAO.prototype.encrypt = function(options, callback) { + var self = this; + + self.busy(); + self._pgpbuilder.encrypt(options, function(err) { + self.done(); + callback(err); + }); + +}; + + +// +// +// Event Handlers +// +// + + +/** + * This handler should be invoked when navigator.onLine === true. It will try to connect a + * given instance of the imap client. If the connection attempt was successful, it will + * update the locally available folders with the newly received IMAP folder listing. + * + * @param {Object} options.imapClient The IMAP client used to receive messages + * @param {Object} options.pgpMailer The SMTP client used to send messages + * @param {Function} callback [description] + */ +EmailDAO.prototype.onConnect = function(options, callback) { + var self = this; + + self._account.loggingIn = true; + + self._imapClient = options.imapClient; + self._pgpMailer = options.pgpMailer; + + self._imapClient.login(function(err) { + self._account.loggingIn = false; + + if (err) { + callback(err); return; } - // move the message to the trash folder - this._imapClient.moveMessage({ - path: options.folder.path, - destination: trash.path, - uid: options.uid - }, callback); - }; - - /** - * Get list messsage headers without the body - * - * @param {String} options.folder The folder - * @param {Number} options.firstUid The lower bound of the uid (inclusive) - * @param {Number} options.lastUid The upper bound of the uid range (inclusive) - * @param {Function} callback (error, messages) The callback when the imap client is done fetching message metadata - */ - EmailDAO.prototype._imapListMessages = function(options, callback) { - var self = this; - - if (!this._account.online) { - callback({ - errMsg: 'Client is currently offline!', - code: 42 - }); - return; - } - - options.path = options.folder.path; - self._imapClient.listMessages(options, callback); - }; - - /** - * Stream an email messsage's body - * @param {String} options.folder The folder - * @param {String} options.uid the message's uid - * @param {Object} options.bodyParts The message, as retrieved by _imapListMessages - * @param {Function} callback (error, message) The callback when the imap client is done streaming message text content - */ - EmailDAO.prototype._getBodyParts = function(options, callback) { - var self = this; - - if (!self._account.online) { - callback({ - errMsg: 'Client is currently offline!', - code: 42 - }); - return; - } - - options.path = options.folder.path; - self._imapClient.getBodyParts(options, function(err) { + // init folders + self._initFoldersFromImap(function(err) { if (err) { callback(err); return; } - // interpret the raw content of the email - self._mailreader.parse(options, callback); - }); - }; + // attach sync update handler + self._imapClient.onSyncUpdate = self._onSyncUpdate.bind(self); - // - // - // Local Storage API - // - // - - - /** - * List the locally available items form the indexed db stored under "email_[FOLDER PATH]_[MESSAGE UID]" (if a message was provided), - * or "email_[FOLDER PATH]", respectively - * - * @param {Object} options.folder The folder for which to list the content - * @param {Object} options.uid A specific uid to look up locally in the folder - * @param {Function} callback(error, list) Invoked with the results of the query, or further information, if an error occurred - */ - EmailDAO.prototype._localListMessages = function(options, callback) { - var dbType = 'email_' + options.folder.path + (options.uid ? '_' + options.uid : ''); - this._devicestorage.listItems(dbType, 0, null, callback); - }; - - /** - * Stores a bunch of messages to the indexed db. The messages are stored under "email_[FOLDER PATH]_[MESSAGE UID]" - * - * @param {Object} options.folder The folder for which to list the content - * @param {Array} options.messages The messages to store - * @param {Function} callback(error, list) Invoked with the results of the query, or further information, if an error occurred - */ - EmailDAO.prototype._localStoreMessages = function(options, callback) { - var dbType = 'email_' + options.folder.path; - this._devicestorage.storeList(options.emails, dbType, callback); - }; - - /** - * Stores a bunch of messages to the indexed db. The messages are stored under "email_[FOLDER PATH]_[MESSAGE UID]" - * - * @param {Object} options.folder The folder for which to list the content - * @param {Array} options.messages The messages to store - * @param {Function} callback(error, list) Invoked with the results of the query, or further information, if an error occurred - */ - EmailDAO.prototype._localDeleteMessage = function(options, callback) { - var path = options.folder.path, - uid = options.uid, - id = options.id; - - if (!path || !(uid || id)) { - callback({ - errMsg: 'Invalid options!' - }); - return; - } - - var dbType = 'email_' + path + '_' + (uid || id); - this._devicestorage.removeList(dbType, callback); - }; - - - // - // - // Helper Functions - // - // - - - /** - * Updates a folder's unread count: - * - For the outbox, that's the total number of messages, - * - For every other folder, it's the number of unread messages - */ - function updateUnreadCount(folder) { - var allMsgs = folder.messages.length, - unreadMsgs = _.filter(folder.messages, function(msg) { - return msg.unread; - }).length; - - folder.count = folder.path === config.outboxMailboxPath ? allMsgs : unreadMsgs; - } - - /** - * Helper function that recursively traverses the body parts tree. Looks for bodyParts that match the provided type and aggregates them - * - * @param {Array} bodyParts The bodyParts array - * @param {String} type The type to look up - * @param {undefined} result Leave undefined, only used for recursion - */ - function filterBodyParts(bodyParts, type, result) { - result = result || []; - bodyParts.forEach(function(part) { - if (part.type === type) { - result.push(part); - } else if (Array.isArray(part.content)) { - filterBodyParts(part.content, type, result); - } - }); - return result; - } - - /** - * Helper function that looks through the HTML content for and - * inlines the images linked internally. Manipulates message.html as a side-effect. - * If no attachment matching the internal reference is found, or constructing a data - * uri fails, just remove the source. - * - * @param {Object} message DTO - */ - function inlineExternalImages(message) { - message.html = message.html.replace(/(]+\bsrc=['"])cid:([^'">]+)(['"])/ig, function(match, prefix, src, suffix) { - var localSource = '', - payload = ''; - - var internalReference = _.findWhere(message.attachments, { - id: src - }); - - if (internalReference) { - for (var i = 0; i < internalReference.content.byteLength; i++) { - payload += String.fromCharCode(internalReference.content[i]); + // fill the imap mailboxCache with information we have locally available: + // - highest locally available moseq + // - list of locally available uids + // - highest locally available uid + // - next expected uid + var mailboxCache = {}; + self._account.folders.forEach(function(folder) { + if (folder.messages.length === 0) { + return; } - try { - localSource = 'data:application/octet-stream;base64,' + btoa(payload); // try to replace the source - } catch (e) {} + var uids, highestModseq, lastUid; + + uids = _.pluck(folder.messages, MSG_ATTR_UID).sort(function(a, b) { + return a - b; + }); + lastUid = uids[uids.length - 1]; + + highestModseq = _.pluck(folder.messages, 'modseq').sort(function(a, b) { + return a - b; + }).pop(); + + mailboxCache[folder.path] = { + exists: lastUid, + uidNext: lastUid + 1, + uidlist: uids, + highestModseq: highestModseq + }; + }); + self._imapClient.mailboxCache = mailboxCache; + + // set status to online after setting cache to prevent race condition + self._account.online = true; + + // set up the imap client to listen for changes in the inbox + var inbox = _.findWhere(self._account.folders, { + type: FOLDER_TYPE_INBOX + }); + + if (!inbox) { + return callback(); } - return prefix + localSource + suffix; + self._imapClient.listenForChanges({ + path: inbox.path + }, callback); }); + }); +}; + +/** + * This handler should be invoked when navigator.onLine === false. + * It will discard the imap client and pgp mailer + */ +EmailDAO.prototype.onDisconnect = function(callback) { + var self = this; + + // logout of imap-client + self._imapClient.logout(function() { + // ignore error, because it's not problem if logout fails + if (callback) { + callback(); + } + }); + + // discard clients + self._account.online = false; + self._imapClient = undefined; + self._pgpMailer = undefined; +}; + +/** + * The are updates in the IMAP folder of the following type + * - 'new': a list of uids that are newly available + * - 'deleted': a list of uids that were deleted from IMAP available + * - 'messages': a list of messages (uid + flags) that where changes are available + * + * @param {String} options.type The type of the update + * @param {String} options.path The mailbox for which updates are available + * @param {Array} options.list Array containing update information. Number (uid) or mail with Object (uid and flags), respectively + */ +EmailDAO.prototype._onSyncUpdate = function(options) { + var self = this; + + var folder = _.findWhere(self._account.folders, { + path: options.path + }); + + if (!folder) { + // ignore updates for an unknown folder + return; } - return EmailDAO; -}); \ No newline at end of file + if (options.type === SYNC_TYPE_NEW) { + // new messages available on imap, fetch from imap and store to disk and memory + self.fetchMessages({ + folder: folder, + firstUid: Math.min.apply(null, options.list), + lastUid: Math.max.apply(null, options.list) + }, self.onError.bind(self)); + } else if (options.type === SYNC_TYPE_DELETED) { + // messages have been deleted, remove from local storage and memory + options.list.forEach(function(uid) { + var message = _.findWhere(folder.messages, { + uid: uid + }); + + if (!message) { + return; + } + + self.deleteMessage({ + folder: folder, + message: message, + localOnly: true + }, self.onError.bind(self)); + }); + } else if (options.type === SYNC_TYPE_MSGS) { + // NB! several possible reasons why this could be called. + // if a message in the array has uid value and flag array, it had a possible flag update + options.list.forEach(function(changedMsg) { + if (!changedMsg.uid || !changedMsg.flags) { + return; + } + + var message = _.findWhere(folder.messages, { + uid: changedMsg.uid + }); + + if (!message) { + return; + } + + // update unread, answered, modseq to the latest info + message.answered = changedMsg.flags.indexOf('\\Answered') > -1; + message.unread = changedMsg.flags.indexOf('\\Seen') === -1; + message.modseq = changedMsg.modseq; + + self.setFlags({ + folder: folder, + message: message, + localOnly: true + }, self.onError.bind(self)); + }); + } +}; + + +// +// +// Internal API +// +// + + +/** + * Updates the folder information from memory, and adds/removes folders in account.folders. + * The locally available messages are loaded from memory + * + * @param {Function} callback Invoked when the folders are up to date + */ +EmailDAO.prototype._initFoldersFromDisk = function(callback) { + var self = this; + + self.busy(); // start the spinner + + // fetch list from local cache + self._devicestorage.listItems(FOLDER_DB_TYPE, 0, null, function(err, stored) { + if (err) { + return done(err); + } + + self._account.folders = stored[0] || []; + self._initMessagesFromDisk(done); + }); + + function done(err) { + self.done(); // stop the spinner + callback(err); + } +}; + +/** + * Updates the folder information from imap (if we're online). Adds/removes folders in account.folders, + * if we added/removed folder in IMAP. If we have an uninitialized folder that lacks folder.messages, + * all the locally available messages are loaded from memory. + * + * @param {Function} callback Invoked when the folders are up to date + */ +EmailDAO.prototype._initFoldersFromImap = function(callback) { + var self = this; + + self.busy(); // start the spinner + + // fetch list from imap server + self._imapClient.listWellKnownFolders(function(err, wellKnownFolders) { + if (err) { + return done(err); + } + + // initialize the folders to something meaningful if that hasn't already happened + self._account.folders = self._account.folders || []; + + // smuggle the outbox into the well known folders, which is obv not present on imap... + wellKnownFolders[config.outboxMailboxType] = [{ + name: config.outboxMailboxName, + type: config.outboxMailboxType, + path: config.outboxMailboxPath + }]; + + // indicates if we need to persist anything to disk + var foldersChanged = false; + + // the folders listed in the navigation pane + [FOLDER_TYPE_INBOX, FOLDER_TYPE_SENT, config.outboxMailboxType, FOLDER_TYPE_DRAFTS, FOLDER_TYPE_TRASH].forEach(function(mbxType) { + var localFolderWithType, imapFolderWithPath; + + // check if there is a folder of this type locally available + localFolderWithType = _.findWhere(self._account.folders, { + type: mbxType + }); + + if (localFolderWithType) { + // we have a local folder available, so let's check if this folder still exists on imap + + imapFolderWithPath = _.findWhere(wellKnownFolders[mbxType], { + path: localFolderWithType.path + }); + + if (imapFolderWithPath) { + // folder present on imap, no need to update. + return; + } + + // folder not present on imap, so remove the folder and see if there are any updates for this folder type + self._account.folders.splice(self._account.folders.indexOf(localFolderWithType), 1); + foldersChanged = true; + } + + if (!wellKnownFolders[mbxType] || !wellKnownFolders[mbxType].length) { + // no imap folders of the respective mailbox type, so nothing to do here + return; + } + + /** + * we have no local folder of the type, so do something intelligent, + * i.e. take the first folder of the respective type + */ + self._account.folders.push(wellKnownFolders[mbxType][0]); + foldersChanged = true; + }); + + // if folders have not changed, can fill them with messages directly + if (!foldersChanged) { + return self._initMessagesFromDisk(done); + } + + // persist encrypted list in device storage + // note: the folders in the ui also include the messages array, so let's create a clean array here + var folders = self._account.folders.map(function(folder) { + return { + name: folder.name, + path: folder.path, + type: folder.type + }; + }); + self._devicestorage.storeList([folders], FOLDER_DB_TYPE, function(err) { + if (err) { + return done(err); + } + + self._initMessagesFromDisk(done); + }); + }); + + function done(err) { + self.done(); // stop the spinner + callback(err); + } +}; + +/** + * Fill uninitialized folders with the locally available messages. + * + * @param {Function} callback Invoked when the folders are filled with messages + */ +EmailDAO.prototype._initMessagesFromDisk = function(callback) { + var self = this; + + if (!self._account.folders || self._account.folders.length === 0) { + return callback(); + } + + var after = _.after(self._account.folders.length, callback); + + self._account.folders.forEach(function(folder) { + if (folder.messages) { + // the folder is already initialized + return after(); + } + + // sync messages from disk to the folder model + self.refreshFolder({ + folder: folder + }, function(err) { + if (err) { + return callback(err); + } + + after(); + }); + }); +}; + +EmailDAO.prototype.busy = function() { + this._account.busy++; +}; + +EmailDAO.prototype.done = function() { + if (this._account.busy > 0) { + this._account.busy--; + } +}; + + + +// +// +// IMAP API +// +// + +/** + * Mark messages as un-/read or un-/answered on IMAP + * + * @param {Object} options.folder The folder where to find the message + * @param {Number} options.uid The uid for which to change the flags + * @param {Number} options.unread Un-/Read flag + * @param {Number} options.answered Un-/Answered flag + */ +EmailDAO.prototype._imapMark = function(options, callback) { + if (!this._account.online) { + callback({ + errMsg: 'Client is currently offline!', + code: 42 + }); + return; + } + + options.path = options.folder.path; + this._imapClient.updateFlags(options, callback); +}; + +/** + * If we're in the trash folder or no trash folder is available, this deletes a message from IMAP. + * Otherwise, it moves a message to the trash folder. + * + * @param {Object} options.folder The folder where to find the message + * @param {Number} options.uid The uid of the message + * @param {Function} callback(error) Callback with an error object in case something went wrong. + */ +EmailDAO.prototype._imapDeleteMessage = function(options, callback) { + if (!this._account.online) { + callback({ + errMsg: 'Client is currently offline!', + code: 42 + }); + return; + } + + var trash = _.findWhere(this._account.folders, { + type: FOLDER_TYPE_TRASH + }); + + // there's no known trash folder to move the mail to or we're in the trash folder, so we can purge the message + if (!trash || options.folder === trash) { + this._imapClient.deleteMessage({ + path: options.folder.path, + uid: options.uid + }, callback); + + return; + } + + // move the message to the trash folder + this._imapClient.moveMessage({ + path: options.folder.path, + destination: trash.path, + uid: options.uid + }, callback); +}; + +/** + * Get list messsage headers without the body + * + * @param {String} options.folder The folder + * @param {Number} options.firstUid The lower bound of the uid (inclusive) + * @param {Number} options.lastUid The upper bound of the uid range (inclusive) + * @param {Function} callback (error, messages) The callback when the imap client is done fetching message metadata + */ +EmailDAO.prototype._imapListMessages = function(options, callback) { + var self = this; + + if (!this._account.online) { + callback({ + errMsg: 'Client is currently offline!', + code: 42 + }); + return; + } + + options.path = options.folder.path; + self._imapClient.listMessages(options, callback); +}; + +/** + * Stream an email messsage's body + * @param {String} options.folder The folder + * @param {String} options.uid the message's uid + * @param {Object} options.bodyParts The message, as retrieved by _imapListMessages + * @param {Function} callback (error, message) The callback when the imap client is done streaming message text content + */ +EmailDAO.prototype._getBodyParts = function(options, callback) { + var self = this; + + if (!self._account.online) { + callback({ + errMsg: 'Client is currently offline!', + code: 42 + }); + return; + } + + options.path = options.folder.path; + self._imapClient.getBodyParts(options, function(err) { + if (err) { + callback(err); + return; + } + // interpret the raw content of the email + self._mailreader.parse(options, callback); + }); +}; + + +// +// +// Local Storage API +// +// + + +/** + * List the locally available items form the indexed db stored under "email_[FOLDER PATH]_[MESSAGE UID]" (if a message was provided), + * or "email_[FOLDER PATH]", respectively + * + * @param {Object} options.folder The folder for which to list the content + * @param {Object} options.uid A specific uid to look up locally in the folder + * @param {Function} callback(error, list) Invoked with the results of the query, or further information, if an error occurred + */ +EmailDAO.prototype._localListMessages = function(options, callback) { + var dbType = 'email_' + options.folder.path + (options.uid ? '_' + options.uid : ''); + this._devicestorage.listItems(dbType, 0, null, callback); +}; + +/** + * Stores a bunch of messages to the indexed db. The messages are stored under "email_[FOLDER PATH]_[MESSAGE UID]" + * + * @param {Object} options.folder The folder for which to list the content + * @param {Array} options.messages The messages to store + * @param {Function} callback(error, list) Invoked with the results of the query, or further information, if an error occurred + */ +EmailDAO.prototype._localStoreMessages = function(options, callback) { + var dbType = 'email_' + options.folder.path; + this._devicestorage.storeList(options.emails, dbType, callback); +}; + +/** + * Stores a bunch of messages to the indexed db. The messages are stored under "email_[FOLDER PATH]_[MESSAGE UID]" + * + * @param {Object} options.folder The folder for which to list the content + * @param {Array} options.messages The messages to store + * @param {Function} callback(error, list) Invoked with the results of the query, or further information, if an error occurred + */ +EmailDAO.prototype._localDeleteMessage = function(options, callback) { + var path = options.folder.path, + uid = options.uid, + id = options.id; + + if (!path || !(uid || id)) { + callback({ + errMsg: 'Invalid options!' + }); + return; + } + + var dbType = 'email_' + path + '_' + (uid || id); + this._devicestorage.removeList(dbType, callback); +}; + + +// +// +// Helper Functions +// +// + + +/** + * Updates a folder's unread count: + * - For the outbox, that's the total number of messages, + * - For every other folder, it's the number of unread messages + */ +function updateUnreadCount(folder) { + var allMsgs = folder.messages.length, + unreadMsgs = _.filter(folder.messages, function(msg) { + return msg.unread; + }).length; + + folder.count = folder.path === config.outboxMailboxPath ? allMsgs : unreadMsgs; +} + +/** + * Helper function that recursively traverses the body parts tree. Looks for bodyParts that match the provided type and aggregates them + * + * @param {Array} bodyParts The bodyParts array + * @param {String} type The type to look up + * @param {undefined} result Leave undefined, only used for recursion + */ +function filterBodyParts(bodyParts, type, result) { + result = result || []; + bodyParts.forEach(function(part) { + if (part.type === type) { + result.push(part); + } else if (Array.isArray(part.content)) { + filterBodyParts(part.content, type, result); + } + }); + return result; +} + +/** + * Helper function that looks through the HTML content for and + * inlines the images linked internally. Manipulates message.html as a side-effect. + * If no attachment matching the internal reference is found, or constructing a data + * uri fails, just remove the source. + * + * @param {Object} message DTO + */ +function inlineExternalImages(message) { + message.html = message.html.replace(/(]+\bsrc=['"])cid:([^'">]+)(['"])/ig, function(match, prefix, src, suffix) { + var localSource = '', + payload = ''; + + var internalReference = _.findWhere(message.attachments, { + id: src + }); + + if (internalReference) { + for (var i = 0; i < internalReference.content.byteLength; i++) { + payload += String.fromCharCode(internalReference.content[i]); + } + + try { + localSource = 'data:application/octet-stream;base64,' + btoa(payload); // try to replace the source + } catch (e) {} + } + + return prefix + localSource + suffix; + }); +} + +exports = EmailDAO; \ No newline at end of file diff --git a/src/js/dao/invitation-dao.js b/src/js/dao/invitation-dao.js index f7013de..3243ae1 100644 --- a/src/js/dao/invitation-dao.js +++ b/src/js/dao/invitation-dao.js @@ -1,62 +1,60 @@ -define(function() { - 'use strict'; +'use strict'; - /** - * The InvitationDAO is a high level Data Access Object that access the invitation service REST endpoint. - * @param {Object} restDao The REST Data Access Object abstraction - */ - var InvitationDAO = function(restDao) { - this._restDao = restDao; - }; +/** + * The InvitationDAO is a high level Data Access Object that access the invitation service REST endpoint. + * @param {Object} restDao The REST Data Access Object abstraction + */ +var InvitationDAO = function(restDao) { + this._restDao = restDao; +}; - // - // Constants - // +// +// Constants +// - InvitationDAO.INVITE_MISSING = 1; - InvitationDAO.INVITE_PENDING = 2; - InvitationDAO.INVITE_SUCCESS = 4; +InvitationDAO.INVITE_MISSING = 1; +InvitationDAO.INVITE_PENDING = 2; +InvitationDAO.INVITE_SUCCESS = 4; - // - // API - // +// +// API +// - /** - * Notes an invite for the recipient by the sender in the invitation web service - * @param {String} options.recipient User ID of the recipient - * @param {String} options.sender User ID of the sender - * @param {Function} callback(error, status) Returns information if the invitation worked (INVITE_SUCCESS), if an invitation is already pendin (INVITE_PENDING), or information if an error occurred. - */ - InvitationDAO.prototype.invite = function(options, callback) { - if (typeof options !== 'object' || typeof options.recipient !== 'string' || typeof options.recipient !== 'string') { - callback({ - errMsg: 'erroneous usage of api: incorrect parameters!' - }); +/** + * Notes an invite for the recipient by the sender in the invitation web service + * @param {String} options.recipient User ID of the recipient + * @param {String} options.sender User ID of the sender + * @param {Function} callback(error, status) Returns information if the invitation worked (INVITE_SUCCESS), if an invitation is already pendin (INVITE_PENDING), or information if an error occurred. + */ +InvitationDAO.prototype.invite = function(options, callback) { + if (typeof options !== 'object' || typeof options.recipient !== 'string' || typeof options.recipient !== 'string') { + callback({ + errMsg: 'erroneous usage of api: incorrect parameters!' + }); + return; + } + + var uri = '/invitation/recipient/' + options.recipient + '/sender/' + options.sender; + this._restDao.put({}, uri, completed); + + function completed(error, res, status) { + if (error) { + callback(error); return; } - var uri = '/invitation/recipient/' + options.recipient + '/sender/' + options.sender; - this._restDao.put({}, uri, completed); - - function completed(error, res, status) { - if (error) { - callback(error); - return; - } - - if (status === 201) { - callback(null, InvitationDAO.INVITE_SUCCESS); - return; - } else if (status === 304) { - callback(null, InvitationDAO.INVITE_PENDING); - return; - } - - callback({ - errMsg: 'unexpected invitation state' - }); + if (status === 201) { + callback(null, InvitationDAO.INVITE_SUCCESS); + return; + } else if (status === 304) { + callback(null, InvitationDAO.INVITE_PENDING); + return; } - }; - return InvitationDAO; -}); \ No newline at end of file + callback({ + errMsg: 'unexpected invitation state' + }); + } +}; + +exports = InvitationDAO; \ No newline at end of file diff --git a/src/js/dao/keychain-dao.js b/src/js/dao/keychain-dao.js index 7b070c1..f847c63 100644 --- a/src/js/dao/keychain-dao.js +++ b/src/js/dao/keychain-dao.js @@ -2,241 +2,896 @@ * A high-level Data-Access Api for handling Keypair synchronization * between the cloud service and the device's local storage */ -define(function(require) { - 'use strict'; - var _ = require('underscore'), - util = require('js/crypto/util'), - config = require('js/app-config').config; +'use strict'; - var DB_PUBLICKEY = 'publickey', - DB_PRIVATEKEY = 'privatekey', - DB_DEVICENAME = 'devicename', - DB_DEVICE_SECRET = 'devicesecret'; +var util = require('crypto-lib').util, + config = require('../app-config').config; - var KeychainDAO = function(localDbDao, publicKeyDao, privateKeyDao, crypto, pgp) { - this._localDbDao = localDbDao; - this._publicKeyDao = publicKeyDao; - this._privateKeyDao = privateKeyDao; - this._crypto = crypto; - this._pgp = pgp; - }; +var DB_PUBLICKEY = 'publickey', + DB_PRIVATEKEY = 'privatekey', + DB_DEVICENAME = 'devicename', + DB_DEVICE_SECRET = 'devicesecret'; - // - // Public key functions - // +var KeychainDAO = function(localDbDao, publicKeyDao, privateKeyDao, crypto, pgp) { + this._localDbDao = localDbDao; + this._publicKeyDao = publicKeyDao; + this._privateKeyDao = privateKeyDao; + this._crypto = crypto; + this._pgp = pgp; +}; - /** - * Verifies the public key of a user o nthe public key store - * @param {String} uuid The uuid to verify the key - * @param {Function} callback(error) Callback with an optional error object when the verification is done. If the was an error, the error object contains the information for it. - */ - KeychainDAO.prototype.verifyPublicKey = function(uuid, callback) { - this._publicKeyDao.verify(uuid, callback); - }; +// +// Public key functions +// - /** - * Get an array of public keys by looking in local storage and - * fetching missing keys from the cloud service. - * @param ids [Array] the key ids as [{_id, userId}] - * @return [PublicKeyCollection] The requiested public keys - */ - KeychainDAO.prototype.getPublicKeys = function(ids, callback) { - var self = this, - after, already, pubkeys = []; +/** + * Verifies the public key of a user o nthe public key store + * @param {String} uuid The uuid to verify the key + * @param {Function} callback(error) Callback with an optional error object when the verification is done. If the was an error, the error object contains the information for it. + */ +KeychainDAO.prototype.verifyPublicKey = function(uuid, callback) { + this._publicKeyDao.verify(uuid, callback); +}; - // return empty array if key ids are emtpy - if (ids.length < 1) { - callback(null, pubkeys); - return; - } +/** + * Get an array of public keys by looking in local storage and + * fetching missing keys from the cloud service. + * @param ids [Array] the key ids as [{_id, userId}] + * @return [PublicKeyCollection] The requiested public keys + */ +KeychainDAO.prototype.getPublicKeys = function(ids, callback) { + var self = this, + after, already, pubkeys = []; - after = _.after(ids.length, function() { - callback(null, pubkeys); - }); + // return empty array if key ids are emtpy + if (ids.length < 1) { + callback(null, pubkeys); + return; + } - _.each(ids, function(i) { - // lookup locally and in storage - self.lookupPublicKey(i._id, function(err, pubkey) { - if (err || !pubkey) { - callback({ - errMsg: 'Error looking up public key!', - err: err - }); - return; - } + after = _.after(ids.length, function() { + callback(null, pubkeys); + }); - // check if public key with that id has already been fetched - already = null; - already = _.findWhere(pubkeys, { - _id: i._id + _.each(ids, function(i) { + // lookup locally and in storage + self.lookupPublicKey(i._id, function(err, pubkey) { + if (err || !pubkey) { + callback({ + errMsg: 'Error looking up public key!', + err: err }); - if (!already) { - pubkeys.push(pubkey); - } - - after(); // asynchronously iterate through objects - }); - }); - }; - - /** - * Checks for public key updates of a given user id - * @param {String} userId The user id (email address) for which to check the key - * @param {Function} callback(error, key) Invoked when the key has been updated or an error occurred - */ - KeychainDAO.prototype.refreshKeyForUserId = function(userId, callback) { - var self = this; - - // get the public key corresponding to the userId - self.getReceiverPublicKey(userId, function(err, localKey) { - if (!localKey || !localKey._id) { - // there is no key available, no need to refresh - callback(); return; } - // no need to refresh manually imported public keys - if (localKey.imported) { + // check if public key with that id has already been fetched + already = null; + already = _.findWhere(pubkeys, { + _id: i._id + }); + if (!already) { + pubkeys.push(pubkey); + } + + after(); // asynchronously iterate through objects + }); + }); +}; + +/** + * Checks for public key updates of a given user id + * @param {String} userId The user id (email address) for which to check the key + * @param {Function} callback(error, key) Invoked when the key has been updated or an error occurred + */ +KeychainDAO.prototype.refreshKeyForUserId = function(userId, callback) { + var self = this; + + // get the public key corresponding to the userId + self.getReceiverPublicKey(userId, function(err, localKey) { + if (!localKey || !localKey._id) { + // there is no key available, no need to refresh + callback(); + return; + } + + // no need to refresh manually imported public keys + if (localKey.imported) { + callback(null, localKey); + return; + } + + // check if the key id still exists on the key server + checkKeyExists(localKey); + }); + + // checks if the user's key has been revoked by looking up the key id + function checkKeyExists(localKey) { + self._publicKeyDao.get(localKey._id, function(err, cloudKey) { + if (err && err.code === 42) { + // we're offline, we're done checking the key callback(null, localKey); return; } - // check if the key id still exists on the key server - checkKeyExists(localKey); + if (err) { + // there was an error, exit and inform + callback(err); + return; + } + + if (cloudKey && cloudKey._id === localKey._id) { + // the key is present on the server, all is well + callback(null, localKey); + return; + } + + // the key has changed, update the key + updateKey(localKey); }); + } - // checks if the user's key has been revoked by looking up the key id - function checkKeyExists(localKey) { - self._publicKeyDao.get(localKey._id, function(err, cloudKey) { - if (err && err.code === 42) { - // we're offline, we're done checking the key + function updateKey(localKey) { + // look for an updated key for the user id + self._publicKeyDao.getByUserId(userId, function(err, newKey) { + // offline? + if (err && err.code === 42) { + callback(null, localKey); + return; + } + + if (err) { + callback(err); + return; + } + + // the public key has changed, we need to ask for permission to update the key + self.requestPermissionForKeyUpdate({ + userId: userId, + newKey: newKey + }, function(granted) { + if (!granted) { + // permission was not given to update the key, so don't overwrite the old one! callback(null, localKey); return; } - if (err) { - // there was an error, exit and inform - callback(err); - return; - } - - if (cloudKey && cloudKey._id === localKey._id) { - // the key is present on the server, all is well - callback(null, localKey); - return; - } - - // the key has changed, update the key - updateKey(localKey); - }); - } - - function updateKey(localKey) { - // look for an updated key for the user id - self._publicKeyDao.getByUserId(userId, function(err, newKey) { - // offline? - if (err && err.code === 42) { - callback(null, localKey); - return; - } - - if (err) { - callback(err); - return; - } - - // the public key has changed, we need to ask for permission to update the key - self.requestPermissionForKeyUpdate({ - userId: userId, - newKey: newKey - }, function(granted) { - if (!granted) { - // permission was not given to update the key, so don't overwrite the old one! - callback(null, localKey); + // permission to update the key was given, so delete the old one and persist the new one + self.removeLocalPublicKey(localKey._id, function(err) { + if (err || !newKey) { + // error or no new key to save + callback(err); return; } - // permission to update the key was given, so delete the old one and persist the new one - self.removeLocalPublicKey(localKey._id, function(err) { - if (err || !newKey) { - // error or no new key to save - callback(err); - return; - } - - // persist the new key and return it - self.saveLocalPublicKey(newKey, function(err) { - callback(err, err ? undefined : newKey); - }); + // persist the new key and return it + self.saveLocalPublicKey(newKey, function(err) { + callback(err, err ? undefined : newKey); }); }); - }); + + }); + } +}; + +/** + * Look up a reveiver's public key by user id + * @param userId [String] the receiver's email address + */ +KeychainDAO.prototype.getReceiverPublicKey = function(userId, callback) { + var self = this; + + // search local keyring for public key + self._localDbDao.list(DB_PUBLICKEY, 0, null, function(err, allPubkeys) { + if (err) { + callback(err); + return; } - }; - /** - * Look up a reveiver's public key by user id - * @param userId [String] the receiver's email address - */ - KeychainDAO.prototype.getReceiverPublicKey = function(userId, callback) { - var self = this; - - // search local keyring for public key - self._localDbDao.list(DB_PUBLICKEY, 0, null, function(err, allPubkeys) { - if (err) { - callback(err); - return; - } - - // query primary email address - var pubkey = _.findWhere(allPubkeys, { - userId: userId - }); - - // query mutliple userIds (for imported public keys) - if (!pubkey) { - for (var i = 0, match; i < allPubkeys.length; i++) { - match = _.findWhere(allPubkeys[i].userIds, { - emailAddress: userId - }); - if (match) { - pubkey = allPubkeys[i]; - break; - } - } - } - - if (pubkey && pubkey._id) { - // that user's public key is already in local storage - callback(null, pubkey); - return; - } - - // no public key by that user id in storage - // find from cloud by email address - self._publicKeyDao.getByUserId(userId, onKeyReceived); + // query primary email address + var pubkey = _.findWhere(allPubkeys, { + userId: userId }); - function onKeyReceived(err, cloudPubkey) { - if (err && err.code === 42) { - // offline - callback(); - return; + // query mutliple userIds (for imported public keys) + if (!pubkey) { + for (var i = 0, match; i < allPubkeys.length; i++) { + match = _.findWhere(allPubkeys[i].userIds, { + emailAddress: userId + }); + if (match) { + pubkey = allPubkeys[i]; + break; + } } + } + if (pubkey && pubkey._id) { + // that user's public key is already in local storage + callback(null, pubkey); + return; + } + + // no public key by that user id in storage + // find from cloud by email address + self._publicKeyDao.getByUserId(userId, onKeyReceived); + }); + + function onKeyReceived(err, cloudPubkey) { + if (err && err.code === 42) { + // offline + callback(); + return; + } + + if (err) { + callback(err); + return; + } + + if (!cloudPubkey) { + // public key has been deleted without replacement + callback(); + return; + } + + self.saveLocalPublicKey(cloudPubkey, function(err) { if (err) { callback(err); return; } - if (!cloudPubkey) { - // public key has been deleted without replacement - callback(); + callback(null, cloudPubkey); + }); + } +}; + +// +// Device registration functions +// + +/** + * Set the device's memorable name e.g 'iPhone Work' + * @param {String} deviceName The device name + * @param {Function} callback(error) + */ +KeychainDAO.prototype.setDeviceName = function(deviceName, callback) { + if (!deviceName) { + callback(new Error('Please set a device name!')); + return; + } + + this._localDbDao.persist(DB_DEVICENAME, deviceName, callback); +}; + +/** + * Get the device' memorable name from local storage. Throws an error if not set + * @param {Function} callback(error, deviceName) + * @return {String} The device name + */ +KeychainDAO.prototype.getDeviceName = function(callback) { + // check if deviceName is already persisted in storage + this._localDbDao.read(DB_DEVICENAME, function(err, deviceName) { + if (err) { + callback(err); + return; + } + + if (!deviceName) { + callback(new Error('Device name not set!')); + return; + } + + callback(null, deviceName); + }); +}; + +/** + * Geneate a device specific key and secret to authenticate to the private key service. + * @param {Function} callback(error, deviceSecret:[base64 encoded string]) + */ +KeychainDAO.prototype.getDeviceSecret = function(callback) { + var self = this; + + // generate random deviceSecret or get from storage + self._localDbDao.read(DB_DEVICE_SECRET, function(err, storedDevSecret) { + if (err) { + callback(err); + return; + } + + if (storedDevSecret) { + // a device key is already available locally + callback(null, storedDevSecret); + return; + } + + // generate random deviceSecret + var deviceSecret = util.random(config.symKeySize); + // persist deviceSecret to local storage (in plaintext) + self._localDbDao.persist(DB_DEVICE_SECRET, deviceSecret, function(err) { + if (err) { + callback(err); return; } + callback(null, deviceSecret); + }); + }); +}; + +/** + * Register the device on the private key server. This will give the device access to upload an encrypted private key. + * @param {String} options.userId The user's email address + * @param {Function} callback(error) + */ +KeychainDAO.prototype.registerDevice = function(options, callback) { + var self = this, + devName; + + // check if deviceName is already persisted in storage + self.getDeviceName(function(err, deviceName) { + if (err) { + callback(err); + return; + } + + requestDeviceRegistration(deviceName); + }); + + function requestDeviceRegistration(deviceName) { + devName = deviceName; + + // request device registration session key + self._privateKeyDao.requestDeviceRegistration({ + userId: options.userId, + deviceName: deviceName + }, function(err, regSessionKey) { + if (err) { + callback(err); + return; + } + + if (!regSessionKey.encryptedRegSessionKey) { + callback(new Error('Invalid format for session key!')); + return; + } + + decryptSessionKey(regSessionKey); + }); + } + + function decryptSessionKey(regSessionKey) { + self.lookupPublicKey(config.serverPrivateKeyId, function(err, serverPubkey) { + if (err) { + callback(err); + return; + } + + if (!serverPubkey || !serverPubkey.publicKey) { + callback(new Error('Server public key for device registration not found!')); + return; + } + + // decrypt the session key + var ct = regSessionKey.encryptedRegSessionKey; + self._pgp.decrypt(ct, serverPubkey.publicKey, function(err, decrypedSessionKey, signaturesValid) { + if (err || !signaturesValid) { + return callback(err || new Error('Verifying PGP signature failed!')); + } + + uploadDeviceSecret(decrypedSessionKey); + }); + }); + } + + function uploadDeviceSecret(regSessionKey) { + // read device secret from local storage + self.getDeviceSecret(function(err, deviceSecret) { + if (err) { + callback(err); + return; + } + + // generate iv + var iv = util.random(config.symIvSize); + // encrypt deviceSecret + self._crypto.encrypt(deviceSecret, regSessionKey, iv, function(err, encryptedDeviceSecret) { + if (err) { + callback(err); + return; + } + + // upload encryptedDeviceSecret + self._privateKeyDao.uploadDeviceSecret({ + userId: options.userId, + deviceName: devName, + encryptedDeviceSecret: encryptedDeviceSecret, + iv: iv + }, callback); + }); + }); + } +}; + +// +// Private key functions +// + +/** + * Authenticate to the private key server (required before private PGP key upload). + * @param {String} userId The user's email address + * @param {Function} callback(error, authSessionKey) + * @return {Object} {sessionId:String, sessionKey:[base64 encoded]} + */ +KeychainDAO.prototype._authenticateToPrivateKeyServer = function(userId, callback) { + var self = this, + sessionId; + + // request auth session key required for upload + self._privateKeyDao.requestAuthSessionKey({ + userId: userId + }, function(err, authSessionKey) { + if (err) { + callback(err); + return; + } + + if (!authSessionKey.encryptedAuthSessionKey || !authSessionKey.encryptedChallenge || !authSessionKey.sessionId) { + callback(new Error('Invalid format for session key!')); + return; + } + + // remember session id for verification + sessionId = authSessionKey.sessionId; + + decryptSessionKey(authSessionKey); + }); + + function decryptSessionKey(authSessionKey) { + self.lookupPublicKey(config.serverPrivateKeyId, function(err, serverPubkey) { + if (err) { + callback(err); + return; + } + + if (!serverPubkey || !serverPubkey.publicKey) { + callback(new Error('Server public key for authentication not found!')); + return; + } + + // decrypt the session key + var ct1 = authSessionKey.encryptedAuthSessionKey; + self._pgp.decrypt(ct1, serverPubkey.publicKey, function(err, decryptedSessionKey, signaturesValid) { + if (err || !signaturesValid) { + return callback(err || new Error('Verifying PGP signature failed!')); + } + + // decrypt the challenge + var ct2 = authSessionKey.encryptedChallenge; + self._pgp.decrypt(ct2, serverPubkey.publicKey, function(err, decryptedChallenge, signaturesValid) { + if (err || !signaturesValid) { + return callback(err || new Error('Verifying PGP signature failed!')); + } + + encryptChallenge(decryptedSessionKey, decryptedChallenge); + }); + }); + }); + } + + function encryptChallenge(sessionKey, challenge) { + // get device secret + self.getDeviceSecret(function(err, deviceSecret) { + if (err) { + callback(err); + return; + } + + var iv = util.random(config.symIvSize); + // encrypt the challenge + self._crypto.encrypt(challenge, sessionKey, iv, function(err, encryptedChallenge) { + if (err) { + callback(err); + return; + } + + // encrypt the device secret + self._crypto.encrypt(deviceSecret, sessionKey, iv, function(err, encryptedDeviceSecret) { + if (err) { + callback(err); + return; + } + + replyChallenge({ + encryptedChallenge: encryptedChallenge, + encryptedDeviceSecret: encryptedDeviceSecret, + iv: iv + }, sessionKey); + }); + }); + }); + } + + function replyChallenge(response, sessionKey) { + // respond to challenge by uploading the with the session key encrypted challenge + self._privateKeyDao.verifyAuthentication({ + userId: userId, + sessionId: sessionId, + encryptedChallenge: response.encryptedChallenge, + encryptedDeviceSecret: response.encryptedDeviceSecret, + iv: response.iv + }, function(err) { + if (err) { + callback(err); + return; + } + + callback(null, { + sessionId: sessionId, + sessionKey: sessionKey + }); + }); + } +}; + +/** + * Encrypt and upload the private PGP key to the server. + * @param {String} options.userId The user's email address + * @param {String} options.code The randomly generated or self selected code used to derive the key for the encryption of the private PGP key + * @param {Function} callback(error) + */ +KeychainDAO.prototype.uploadPrivateKey = function(options, callback) { + var self = this, + keySize = config.symKeySize, + salt; + + if (!options.userId || !options.code) { + callback(new Error('Incomplete arguments!')); + return; + } + + deriveKey(options.code); + + function deriveKey(code) { + // generate random salt + salt = util.random(keySize); + // derive key from the code using PBKDF2 + self._crypto.deriveKey(code, salt, keySize, function(err, key) { + if (err) { + callback(err); + return; + } + + encryptPrivateKey(key); + }); + } + + function encryptPrivateKey(encryptionKey) { + // get private key from local storage + self.getUserKeyPair(options.userId, function(err, keypair) { + if (err) { + callback(err); + return; + } + + var privkeyId = keypair.privateKey._id, + pgpBlock = keypair.privateKey.encryptedKey; + + // encrypt the private key with the derived key + var iv = util.random(config.symIvSize); + self._crypto.encrypt(pgpBlock, encryptionKey, iv, function(err, ct) { + if (err) { + callback(err); + return; + } + + var payload = { + _id: privkeyId, + userId: options.userId, + encryptedPrivateKey: ct, + salt: salt, + iv: iv + }; + + uploadPrivateKey(payload); + }); + }); + } + + function uploadPrivateKey(payload) { + // authenticate to server for upload + self._authenticateToPrivateKeyServer(options.userId, function(err, authSessionKey) { + if (err) { + callback(err); + return; + } + + // encrypt encryptedPrivateKey again using authSessionKey + var pt = payload.encryptedPrivateKey, + iv = payload.iv, + key = authSessionKey.sessionKey; + self._crypto.encrypt(pt, key, iv, function(err, ct) { + if (err) { + callback(err); + return; + } + + // replace the encryptedPrivateKey with the double wrapped ciphertext + payload.encryptedPrivateKey = ct; + // set sessionId + payload.sessionId = authSessionKey.sessionId; + + // upload the encrypted priavet key + self._privateKeyDao.upload(payload, callback); + }); + }); + } +}; + +/** + * Request downloading the user's encrypted private key. This will initiate the server to send the recovery token via email/sms to the user. + * @param {String} options.userId The user's email address + * @param {String} options.keyId The private PGP key id + * @param {Function} callback(error) + */ +KeychainDAO.prototype.requestPrivateKeyDownload = function(options, callback) { + this._privateKeyDao.requestDownload(options, callback); +}; + +/** + * Query if an encrypted private PGP key exists on the server without initializing the recovery procedure + * @param {String} options.userId The user's email address + * @param {String} options.keyId The private PGP key id + * @param {Function} callback(error) + */ +KeychainDAO.prototype.hasPrivateKey = function(options, callback) { + this._privateKeyDao.hasPrivateKey(options, callback); +}; + +/** + * Download the encrypted private PGP key from the server using the recovery token. + * @param {String} options.userId The user's email address + * @param {String} options.keyId The user's email address + * @param {String} options.recoveryToken The recovery token acquired via email/sms from the key server + * @param {Function} callback(error, encryptedPrivateKey) + */ +KeychainDAO.prototype.downloadPrivateKey = function(options, callback) { + this._privateKeyDao.download(options, callback); +}; + +/** + * This is called after the encrypted private key has successfully been downloaded and it's ready to be decrypted and stored in localstorage. + * @param {String} options._id The private PGP key id + * @param {String} options.userId The user's email address + * @param {String} options.code The randomly generated or self selected code used to derive the key for the decryption of the private PGP key + * @param {String} options.encryptedPrivateKey The encrypted private PGP key + * @param {String} options.salt The salt required to derive the code derived key + * @param {String} options.iv The iv used to encrypt the private PGP key + * @param {Function} callback(error, keyObject) + */ +KeychainDAO.prototype.decryptAndStorePrivateKeyLocally = function(options, callback) { + var self = this, + code = options.code, + salt = options.salt, + keySize = config.symKeySize; + + if (!options._id || !options.userId || !options.code || !options.salt || !options.encryptedPrivateKey || !options.iv) { + callback(new Error('Incomplete arguments!')); + return; + } + + // derive key from the code and the salt using PBKDF2 + self._crypto.deriveKey(code, salt, keySize, function(err, key) { + if (err) { + callback(err); + return; + } + + decryptAndStore(key); + }); + + function decryptAndStore(derivedKey) { + // decrypt the private key with the derived key + var ct = options.encryptedPrivateKey, + iv = options.iv; + + self._crypto.decrypt(ct, derivedKey, iv, function(err, privateKeyArmored) { + if (err) { + callback(new Error('Invalid keychain code!')); + return; + } + + // validate pgp key + var keyParams; + try { + keyParams = self._pgp.getKeyParams(privateKeyArmored); + } catch (e) { + callback(new Error('Error parsing private PGP key!')); + return; + } + + if (keyParams._id !== options._id || keyParams.userId !== options.userId) { + callback(new Error('Private key parameters don\'t match with public key\'s!')); + return; + } + + var keyObject = { + _id: options._id, + userId: options.userId, + encryptedKey: privateKeyArmored + }; + + // store private key locally + self.saveLocalPrivateKey(keyObject, function(err) { + if (err) { + callback(err); + return; + } + + callback(null, keyObject); + }); + }); + } +}; + +// +// Keypair functions +// + +/** + * Gets the local user's key either from local storage + * or fetches it from the cloud. The private key is encrypted. + * If no key pair exists, null is returned. + * return [Object] The user's key pair {publicKey, privateKey} + */ +KeychainDAO.prototype.getUserKeyPair = function(userId, callback) { + var self = this; + + // search for user's public key locally + self._localDbDao.list(DB_PUBLICKEY, 0, null, function(err, allPubkeys) { + if (err) { + callback(err); + return; + } + + var pubkey = _.findWhere(allPubkeys, { + userId: userId + }); + + if (pubkey && pubkey._id) { + // that user's public key is already in local storage... + // sync keypair to the cloud + syncKeypair(pubkey._id); + return; + } + + // no public key by that user id in storage + // find from cloud by email address + self._publicKeyDao.getByUserId(userId, function(err, cloudPubkey) { + if (err) { + callback(err); + return; + } + + if (cloudPubkey && cloudPubkey._id) { + // there is a public key for that user already in the cloud... + // sync keypair to local storage + syncKeypair(cloudPubkey._id); + return; + } + + // continue without keypair... generate in crypto.js + callback(); + }); + }); + + function syncKeypair(keypairId) { + // persist key pair in local storage + self.lookupPublicKey(keypairId, function(err, savedPubkey) { + if (err) { + callback(err); + return; + } + + // persist private key in local storage + self.lookupPrivateKey(keypairId, function(err, savedPrivkey) { + var keys = {}; + + if (err) { + callback(err); + return; + } + + if (savedPubkey && savedPubkey.publicKey) { + keys.publicKey = savedPubkey; + } + + if (savedPrivkey && savedPrivkey.encryptedKey) { + keys.privateKey = savedPrivkey; + } + + callback(null, keys); + }); + }); + } +}; + +/** + * Checks to see if the user's key pair is stored both + * locally and in the cloud and persist arccordingly + * @param [Object] The user's key pair {publicKey, privateKey} + */ +KeychainDAO.prototype.putUserKeyPair = function(keypair, callback) { + var self = this; + + // validate input + if (!keypair || !keypair.publicKey || !keypair.privateKey || !keypair.publicKey.userId || keypair.publicKey.userId !== keypair.privateKey.userId) { + callback({ + errMsg: 'Incorrect input!' + }); + return; + } + + // don't check the user's own public key for deletion in refreshKeyForUserId + keypair.publicKey.imported = true; + + // store public key locally + self.saveLocalPublicKey(keypair.publicKey, function(err) { + if (err) { + callback(err); + return; + } + + // persist public key in cloud storage + self._publicKeyDao.put(keypair.publicKey, function(err) { + // validate result + if (err) { + callback(err); + return; + } + + // store private key locally + self.saveLocalPrivateKey(keypair.privateKey, callback); + }); + }); +}; + +// +// Helper functions +// + +KeychainDAO.prototype.lookupPublicKey = function(id, callback) { + var self = this; + + if (!id) { + callback({ + errMsg: 'ID must be set for public key query!' + }); + return; + } + + // lookup in local storage + self._localDbDao.read(DB_PUBLICKEY + '_' + id, function(err, pubkey) { + if (err) { + callback(err); + return; + } + + if (pubkey) { + callback(null, pubkey); + return; + } + + // fetch from cloud storage + self._publicKeyDao.get(id, function(err, cloudPubkey) { + if (err) { + callback(err); + return; + } + + // cache public key in cache self.saveLocalPublicKey(cloudPubkey, function(err) { if (err) { callback(err); @@ -245,694 +900,37 @@ define(function(require) { callback(null, cloudPubkey); }); - } - }; - - // - // Device registration functions - // - - /** - * Set the device's memorable name e.g 'iPhone Work' - * @param {String} deviceName The device name - * @param {Function} callback(error) - */ - KeychainDAO.prototype.setDeviceName = function(deviceName, callback) { - if (!deviceName) { - callback(new Error('Please set a device name!')); - return; - } - - this._localDbDao.persist(DB_DEVICENAME, deviceName, callback); - }; - - /** - * Get the device' memorable name from local storage. Throws an error if not set - * @param {Function} callback(error, deviceName) - * @return {String} The device name - */ - KeychainDAO.prototype.getDeviceName = function(callback) { - // check if deviceName is already persisted in storage - this._localDbDao.read(DB_DEVICENAME, function(err, deviceName) { - if (err) { - callback(err); - return; - } - - if (!deviceName) { - callback(new Error('Device name not set!')); - return; - } - - callback(null, deviceName); }); - }; - - /** - * Geneate a device specific key and secret to authenticate to the private key service. - * @param {Function} callback(error, deviceSecret:[base64 encoded string]) - */ - KeychainDAO.prototype.getDeviceSecret = function(callback) { - var self = this; - - // generate random deviceSecret or get from storage - self._localDbDao.read(DB_DEVICE_SECRET, function(err, storedDevSecret) { - if (err) { - callback(err); - return; - } - - if (storedDevSecret) { - // a device key is already available locally - callback(null, storedDevSecret); - return; - } - - // generate random deviceSecret - var deviceSecret = util.random(config.symKeySize); - // persist deviceSecret to local storage (in plaintext) - self._localDbDao.persist(DB_DEVICE_SECRET, deviceSecret, function(err) { - if (err) { - callback(err); - return; - } - - callback(null, deviceSecret); - }); - }); - }; - - /** - * Register the device on the private key server. This will give the device access to upload an encrypted private key. - * @param {String} options.userId The user's email address - * @param {Function} callback(error) - */ - KeychainDAO.prototype.registerDevice = function(options, callback) { - var self = this, - devName; - - // check if deviceName is already persisted in storage - self.getDeviceName(function(err, deviceName) { - if (err) { - callback(err); - return; - } - - requestDeviceRegistration(deviceName); - }); - - function requestDeviceRegistration(deviceName) { - devName = deviceName; - - // request device registration session key - self._privateKeyDao.requestDeviceRegistration({ - userId: options.userId, - deviceName: deviceName - }, function(err, regSessionKey) { - if (err) { - callback(err); - return; - } - - if (!regSessionKey.encryptedRegSessionKey) { - callback(new Error('Invalid format for session key!')); - return; - } - - decryptSessionKey(regSessionKey); - }); - } - - function decryptSessionKey(regSessionKey) { - self.lookupPublicKey(config.serverPrivateKeyId, function(err, serverPubkey) { - if (err) { - callback(err); - return; - } - - if (!serverPubkey || !serverPubkey.publicKey) { - callback(new Error('Server public key for device registration not found!')); - return; - } - - // decrypt the session key - var ct = regSessionKey.encryptedRegSessionKey; - self._pgp.decrypt(ct, serverPubkey.publicKey, function(err, decrypedSessionKey, signaturesValid) { - if (err || !signaturesValid) { - return callback(err || new Error('Verifying PGP signature failed!')); - } - - uploadDeviceSecret(decrypedSessionKey); - }); - }); - } - - function uploadDeviceSecret(regSessionKey) { - // read device secret from local storage - self.getDeviceSecret(function(err, deviceSecret) { - if (err) { - callback(err); - return; - } - - // generate iv - var iv = util.random(config.symIvSize); - // encrypt deviceSecret - self._crypto.encrypt(deviceSecret, regSessionKey, iv, function(err, encryptedDeviceSecret) { - if (err) { - callback(err); - return; - } - - // upload encryptedDeviceSecret - self._privateKeyDao.uploadDeviceSecret({ - userId: options.userId, - deviceName: devName, - encryptedDeviceSecret: encryptedDeviceSecret, - iv: iv - }, callback); - }); - }); - } - }; - - // - // Private key functions - // - - /** - * Authenticate to the private key server (required before private PGP key upload). - * @param {String} userId The user's email address - * @param {Function} callback(error, authSessionKey) - * @return {Object} {sessionId:String, sessionKey:[base64 encoded]} - */ - KeychainDAO.prototype._authenticateToPrivateKeyServer = function(userId, callback) { - var self = this, - sessionId; - - // request auth session key required for upload - self._privateKeyDao.requestAuthSessionKey({ - userId: userId - }, function(err, authSessionKey) { - if (err) { - callback(err); - return; - } - - if (!authSessionKey.encryptedAuthSessionKey || !authSessionKey.encryptedChallenge || !authSessionKey.sessionId) { - callback(new Error('Invalid format for session key!')); - return; - } - - // remember session id for verification - sessionId = authSessionKey.sessionId; - - decryptSessionKey(authSessionKey); - }); - - function decryptSessionKey(authSessionKey) { - self.lookupPublicKey(config.serverPrivateKeyId, function(err, serverPubkey) { - if (err) { - callback(err); - return; - } - - if (!serverPubkey || !serverPubkey.publicKey) { - callback(new Error('Server public key for authentication not found!')); - return; - } - - // decrypt the session key - var ct1 = authSessionKey.encryptedAuthSessionKey; - self._pgp.decrypt(ct1, serverPubkey.publicKey, function(err, decryptedSessionKey, signaturesValid) { - if (err || !signaturesValid) { - return callback(err || new Error('Verifying PGP signature failed!')); - } - - // decrypt the challenge - var ct2 = authSessionKey.encryptedChallenge; - self._pgp.decrypt(ct2, serverPubkey.publicKey, function(err, decryptedChallenge, signaturesValid) { - if (err || !signaturesValid) { - return callback(err || new Error('Verifying PGP signature failed!')); - } - - encryptChallenge(decryptedSessionKey, decryptedChallenge); - }); - }); - }); - } - - function encryptChallenge(sessionKey, challenge) { - // get device secret - self.getDeviceSecret(function(err, deviceSecret) { - if (err) { - callback(err); - return; - } - - var iv = util.random(config.symIvSize); - // encrypt the challenge - self._crypto.encrypt(challenge, sessionKey, iv, function(err, encryptedChallenge) { - if (err) { - callback(err); - return; - } - - // encrypt the device secret - self._crypto.encrypt(deviceSecret, sessionKey, iv, function(err, encryptedDeviceSecret) { - if (err) { - callback(err); - return; - } - - replyChallenge({ - encryptedChallenge: encryptedChallenge, - encryptedDeviceSecret: encryptedDeviceSecret, - iv: iv - }, sessionKey); - }); - }); - }); - } - - function replyChallenge(response, sessionKey) { - // respond to challenge by uploading the with the session key encrypted challenge - self._privateKeyDao.verifyAuthentication({ - userId: userId, - sessionId: sessionId, - encryptedChallenge: response.encryptedChallenge, - encryptedDeviceSecret: response.encryptedDeviceSecret, - iv: response.iv - }, function(err) { - if (err) { - callback(err); - return; - } - - callback(null, { - sessionId: sessionId, - sessionKey: sessionKey - }); - }); - } - }; - - /** - * Encrypt and upload the private PGP key to the server. - * @param {String} options.userId The user's email address - * @param {String} options.code The randomly generated or self selected code used to derive the key for the encryption of the private PGP key - * @param {Function} callback(error) - */ - KeychainDAO.prototype.uploadPrivateKey = function(options, callback) { - var self = this, - keySize = config.symKeySize, - salt; - - if (!options.userId || !options.code) { - callback(new Error('Incomplete arguments!')); - return; - } - - deriveKey(options.code); - - function deriveKey(code) { - // generate random salt - salt = util.random(keySize); - // derive key from the code using PBKDF2 - self._crypto.deriveKey(code, salt, keySize, function(err, key) { - if (err) { - callback(err); - return; - } - - encryptPrivateKey(key); - }); - } - - function encryptPrivateKey(encryptionKey) { - // get private key from local storage - self.getUserKeyPair(options.userId, function(err, keypair) { - if (err) { - callback(err); - return; - } - - var privkeyId = keypair.privateKey._id, - pgpBlock = keypair.privateKey.encryptedKey; - - // encrypt the private key with the derived key - var iv = util.random(config.symIvSize); - self._crypto.encrypt(pgpBlock, encryptionKey, iv, function(err, ct) { - if (err) { - callback(err); - return; - } - - var payload = { - _id: privkeyId, - userId: options.userId, - encryptedPrivateKey: ct, - salt: salt, - iv: iv - }; - - uploadPrivateKey(payload); - }); - }); - } - - function uploadPrivateKey(payload) { - // authenticate to server for upload - self._authenticateToPrivateKeyServer(options.userId, function(err, authSessionKey) { - if (err) { - callback(err); - return; - } - - // encrypt encryptedPrivateKey again using authSessionKey - var pt = payload.encryptedPrivateKey, - iv = payload.iv, - key = authSessionKey.sessionKey; - self._crypto.encrypt(pt, key, iv, function(err, ct) { - if (err) { - callback(err); - return; - } - - // replace the encryptedPrivateKey with the double wrapped ciphertext - payload.encryptedPrivateKey = ct; - // set sessionId - payload.sessionId = authSessionKey.sessionId; - - // upload the encrypted priavet key - self._privateKeyDao.upload(payload, callback); - }); - }); - } - }; - - /** - * Request downloading the user's encrypted private key. This will initiate the server to send the recovery token via email/sms to the user. - * @param {String} options.userId The user's email address - * @param {String} options.keyId The private PGP key id - * @param {Function} callback(error) - */ - KeychainDAO.prototype.requestPrivateKeyDownload = function(options, callback) { - this._privateKeyDao.requestDownload(options, callback); - }; - - /** - * Query if an encrypted private PGP key exists on the server without initializing the recovery procedure - * @param {String} options.userId The user's email address - * @param {String} options.keyId The private PGP key id - * @param {Function} callback(error) - */ - KeychainDAO.prototype.hasPrivateKey = function(options, callback) { - this._privateKeyDao.hasPrivateKey(options, callback); - }; - - /** - * Download the encrypted private PGP key from the server using the recovery token. - * @param {String} options.userId The user's email address - * @param {String} options.keyId The user's email address - * @param {String} options.recoveryToken The recovery token acquired via email/sms from the key server - * @param {Function} callback(error, encryptedPrivateKey) - */ - KeychainDAO.prototype.downloadPrivateKey = function(options, callback) { - this._privateKeyDao.download(options, callback); - }; - - /** - * This is called after the encrypted private key has successfully been downloaded and it's ready to be decrypted and stored in localstorage. - * @param {String} options._id The private PGP key id - * @param {String} options.userId The user's email address - * @param {String} options.code The randomly generated or self selected code used to derive the key for the decryption of the private PGP key - * @param {String} options.encryptedPrivateKey The encrypted private PGP key - * @param {String} options.salt The salt required to derive the code derived key - * @param {String} options.iv The iv used to encrypt the private PGP key - * @param {Function} callback(error, keyObject) - */ - KeychainDAO.prototype.decryptAndStorePrivateKeyLocally = function(options, callback) { - var self = this, - code = options.code, - salt = options.salt, - keySize = config.symKeySize; - - if (!options._id || !options.userId || !options.code || !options.salt || !options.encryptedPrivateKey || !options.iv) { - callback(new Error('Incomplete arguments!')); - return; - } - - // derive key from the code and the salt using PBKDF2 - self._crypto.deriveKey(code, salt, keySize, function(err, key) { - if (err) { - callback(err); - return; - } - - decryptAndStore(key); - }); - - function decryptAndStore(derivedKey) { - // decrypt the private key with the derived key - var ct = options.encryptedPrivateKey, - iv = options.iv; - - self._crypto.decrypt(ct, derivedKey, iv, function(err, privateKeyArmored) { - if (err) { - callback(new Error('Invalid keychain code!')); - return; - } - - // validate pgp key - var keyParams; - try { - keyParams = self._pgp.getKeyParams(privateKeyArmored); - } catch (e) { - callback(new Error('Error parsing private PGP key!')); - return; - } - - if (keyParams._id !== options._id || keyParams.userId !== options.userId) { - callback(new Error('Private key parameters don\'t match with public key\'s!')); - return; - } - - var keyObject = { - _id: options._id, - userId: options.userId, - encryptedKey: privateKeyArmored - }; - - // store private key locally - self.saveLocalPrivateKey(keyObject, function(err) { - if (err) { - callback(err); - return; - } - - callback(null, keyObject); - }); - }); - } - }; - - // - // Keypair functions - // - - /** - * Gets the local user's key either from local storage - * or fetches it from the cloud. The private key is encrypted. - * If no key pair exists, null is returned. - * return [Object] The user's key pair {publicKey, privateKey} - */ - KeychainDAO.prototype.getUserKeyPair = function(userId, callback) { - var self = this; - - // search for user's public key locally - self._localDbDao.list(DB_PUBLICKEY, 0, null, function(err, allPubkeys) { - if (err) { - callback(err); - return; - } - - var pubkey = _.findWhere(allPubkeys, { - userId: userId - }); - - if (pubkey && pubkey._id) { - // that user's public key is already in local storage... - // sync keypair to the cloud - syncKeypair(pubkey._id); - return; - } - - // no public key by that user id in storage - // find from cloud by email address - self._publicKeyDao.getByUserId(userId, function(err, cloudPubkey) { - if (err) { - callback(err); - return; - } - - if (cloudPubkey && cloudPubkey._id) { - // there is a public key for that user already in the cloud... - // sync keypair to local storage - syncKeypair(cloudPubkey._id); - return; - } - - // continue without keypair... generate in crypto.js - callback(); - }); - }); - - function syncKeypair(keypairId) { - // persist key pair in local storage - self.lookupPublicKey(keypairId, function(err, savedPubkey) { - if (err) { - callback(err); - return; - } - - // persist private key in local storage - self.lookupPrivateKey(keypairId, function(err, savedPrivkey) { - var keys = {}; - - if (err) { - callback(err); - return; - } - - if (savedPubkey && savedPubkey.publicKey) { - keys.publicKey = savedPubkey; - } - - if (savedPrivkey && savedPrivkey.encryptedKey) { - keys.privateKey = savedPrivkey; - } - - callback(null, keys); - }); - }); - } - }; - - /** - * Checks to see if the user's key pair is stored both - * locally and in the cloud and persist arccordingly - * @param [Object] The user's key pair {publicKey, privateKey} - */ - KeychainDAO.prototype.putUserKeyPair = function(keypair, callback) { - var self = this; - - // validate input - if (!keypair || !keypair.publicKey || !keypair.privateKey || !keypair.publicKey.userId || keypair.publicKey.userId !== keypair.privateKey.userId) { - callback({ - errMsg: 'Incorrect input!' - }); - return; - } - - // don't check the user's own public key for deletion in refreshKeyForUserId - keypair.publicKey.imported = true; - - // store public key locally - self.saveLocalPublicKey(keypair.publicKey, function(err) { - if (err) { - callback(err); - return; - } - - // persist public key in cloud storage - self._publicKeyDao.put(keypair.publicKey, function(err) { - // validate result - if (err) { - callback(err); - return; - } - - // store private key locally - self.saveLocalPrivateKey(keypair.privateKey, callback); - }); - }); - }; - - // - // Helper functions - // - - KeychainDAO.prototype.lookupPublicKey = function(id, callback) { - var self = this; - - if (!id) { - callback({ - errMsg: 'ID must be set for public key query!' - }); - return; - } - - // lookup in local storage - self._localDbDao.read(DB_PUBLICKEY + '_' + id, function(err, pubkey) { - if (err) { - callback(err); - return; - } - - if (pubkey) { - callback(null, pubkey); - return; - } - - // fetch from cloud storage - self._publicKeyDao.get(id, function(err, cloudPubkey) { - if (err) { - callback(err); - return; - } - - // cache public key in cache - self.saveLocalPublicKey(cloudPubkey, function(err) { - if (err) { - callback(err); - return; - } - - callback(null, cloudPubkey); - }); - }); - }); - }; - - /** - * List all the locally stored public keys - */ - KeychainDAO.prototype.listLocalPublicKeys = function(callback) { - // search local keyring for public key - this._localDbDao.list(DB_PUBLICKEY, 0, null, callback); - }; - - KeychainDAO.prototype.removeLocalPublicKey = function(id, callback) { - this._localDbDao.remove(DB_PUBLICKEY + '_' + id, callback); - }; - - KeychainDAO.prototype.lookupPrivateKey = function(id, callback) { - // lookup in local storage - this._localDbDao.read(DB_PRIVATEKEY + '_' + id, callback); - }; - - KeychainDAO.prototype.saveLocalPublicKey = function(pubkey, callback) { - // persist public key (email, _id) - var pkLookupKey = DB_PUBLICKEY + '_' + pubkey._id; - this._localDbDao.persist(pkLookupKey, pubkey, callback); - }; - - KeychainDAO.prototype.saveLocalPrivateKey = function(privkey, callback) { - // persist private key (email, _id) - var prkLookupKey = DB_PRIVATEKEY + '_' + privkey._id; - this._localDbDao.persist(prkLookupKey, privkey, callback); - }; - - return KeychainDAO; -}); \ No newline at end of file + }); +}; + +/** + * List all the locally stored public keys + */ +KeychainDAO.prototype.listLocalPublicKeys = function(callback) { + // search local keyring for public key + this._localDbDao.list(DB_PUBLICKEY, 0, null, callback); +}; + +KeychainDAO.prototype.removeLocalPublicKey = function(id, callback) { + this._localDbDao.remove(DB_PUBLICKEY + '_' + id, callback); +}; + +KeychainDAO.prototype.lookupPrivateKey = function(id, callback) { + // lookup in local storage + this._localDbDao.read(DB_PRIVATEKEY + '_' + id, callback); +}; + +KeychainDAO.prototype.saveLocalPublicKey = function(pubkey, callback) { + // persist public key (email, _id) + var pkLookupKey = DB_PUBLICKEY + '_' + pubkey._id; + this._localDbDao.persist(pkLookupKey, pubkey, callback); +}; + +KeychainDAO.prototype.saveLocalPrivateKey = function(privkey, callback) { + // persist private key (email, _id) + var prkLookupKey = DB_PRIVATEKEY + '_' + privkey._id; + this._localDbDao.persist(prkLookupKey, privkey, callback); +}; + +exports = KeychainDAO; \ No newline at end of file diff --git a/src/js/dao/lawnchair-dao.js b/src/js/dao/lawnchair-dao.js index bee484f..d5812ae 100644 --- a/src/js/dao/lawnchair-dao.js +++ b/src/js/dao/lawnchair-dao.js @@ -1,222 +1,216 @@ /** * Handles generic caching of JSON objects in a lawnchair adapter */ -define(function(require) { - 'use strict'; - var _ = require('underscore'), - Lawnchair = require('lawnchair'); - require('lawnchairSQL'); - require('lawnchairIDB'); +'use strict'; - var LawnchairDAO = function() {}; +var LawnchairDAO = function() {}; - LawnchairDAO.prototype.init = function(dbName, callback) { - if (!dbName) { +LawnchairDAO.prototype.init = function(dbName, callback) { + if (!dbName) { + callback({ + errMsg: 'Lawnchair DB name must be specified!' + }); + return; + } + + this._db = new Lawnchair({ + name: dbName + }, function(lc) { + if (!lc) { callback({ - errMsg: 'Lawnchair DB name must be specified!' + errMsg: 'Lawnchair init failed!' }); return; } - this._db = new Lawnchair({ - name: dbName - }, function(lc) { - if (!lc) { - callback({ - errMsg: 'Lawnchair init failed!' - }); - return; - } + callback(); + }); +}; +/** + * Create or update an object + */ +LawnchairDAO.prototype.persist = function(key, object, callback) { + if (!key || !object) { + callback({ + errMsg: 'Key and Object must be set!' + }); + return; + } + + this._db.save({ + key: key, + object: object + }, function(persisted) { + if (persisted.key !== key) { + callback({ + errMsg: 'Persisting failed!' + }); + return; + } + + callback(); + }); +}; + +/** + * Persist a bunch of items at once + */ +LawnchairDAO.prototype.batch = function(list, callback) { + if (!(list instanceof Array)) { + callback({ + errMsg: 'Input must be of type Array!' + }); + return; + } + + this._db.batch(list, function(res) { + if (!res) { + callback({ + errMsg: 'Persisting batch failed!' + }); + return; + } + + callback(); + }); +}; + +/** + * Read a single item by its key + */ +LawnchairDAO.prototype.read = function(key, callback) { + if (!key) { + callback({ + errMsg: 'Key must be specified!' + }); + return; + } + + this._db.get(key, function(o) { + if (o) { + callback(null, o.object); + } else { callback(); - }); - }; + } + }); +}; - /** - * Create or update an object - */ - LawnchairDAO.prototype.persist = function(key, object, callback) { - if (!key || !object) { - callback({ - errMsg: 'Key and Object must be set!' - }); +/** + * List all the items of a certain type + * @param type [String] The type of item e.g. 'email' + * @param offset [Number] The offset of items to fetch (0 is the last stored item) + * @param num [Number] The number of items to fetch (null means fetch all) + */ +LawnchairDAO.prototype.list = function(type, offset, num, callback) { + var self = this, + i, from, to, + matchingKeys = [], + intervalKeys = [], + list = []; + + // validate input + if (!type || typeof offset === 'undefined' || typeof num === 'undefined') { + callback({ + errMsg: 'Args not is not set!' + }); + return; + } + + // get all keys + self._db.keys(function(keys) { + + // check if key begins with type + keys.forEach(function(key) { + if (key.indexOf(type) === 0) { + matchingKeys.push(key); + } + }); + + // sort keys + matchingKeys.sort(); + + // set window of items to fetch + // if num is null, list all items + from = (num) ? matchingKeys.length - offset - num : 0; + to = matchingKeys.length - 1 - offset; + // filter items within requested interval + for (i = 0; i < matchingKeys.length; i++) { + if (i >= from && i <= to) { + intervalKeys.push(matchingKeys[i]); + } + } + + // return if there are no matching keys + if (intervalKeys.length === 0) { + callback(null, list); return; } - this._db.save({ - key: key, - object: object - }, function(persisted) { - if (persisted.key !== key) { - callback({ - errMsg: 'Persisting failed!' - }); - return; - } + // fetch all items from data-store with matching key + self._db.get(intervalKeys, function(intervalList) { + intervalList.forEach(function(item) { + list.push(item.object); + }); + // return only the interval between offset and num + callback(null, list); + }); + + }); +}; + +/** + * Removes an object liter from local storage by its key (delete) + */ +LawnchairDAO.prototype.remove = function(key, callback) { + this._db.remove(key, callback); +}; + +/** + * Removes an object liter from local storage by its key (delete) + */ +LawnchairDAO.prototype.removeList = function(type, callback) { + var self = this, + matchingKeys = [], + after; + + // validate type + if (!type) { + callback({ + errMsg: 'Type is not set!' + }); + return; + } + + // get all keys + self._db.keys(function(keys) { + // check if key begins with type + keys.forEach(function(key) { + if (key.indexOf(type) === 0) { + matchingKeys.push(key); + } + }); + + if (matchingKeys.length < 1) { callback(); - }); - }; - - /** - * Persist a bunch of items at once - */ - LawnchairDAO.prototype.batch = function(list, callback) { - if (!(list instanceof Array)) { - callback({ - errMsg: 'Input must be of type Array!' - }); return; } - this._db.batch(list, function(res) { - if (!res) { - callback({ - errMsg: 'Persisting batch failed!' - }); - return; - } - - callback(); + // remove all matching keys + after = _.after(matchingKeys.length, callback); + _.each(matchingKeys, function(key) { + self._db.remove(key, after); }); - }; + }); +}; - /** - * Read a single item by its key - */ - LawnchairDAO.prototype.read = function(key, callback) { - if (!key) { - callback({ - errMsg: 'Key must be specified!' - }); - return; - } +/** + * Clears the whole local storage cache + */ +LawnchairDAO.prototype.clear = function(callback) { + this._db.nuke(callback); +}; - this._db.get(key, function(o) { - if (o) { - callback(null, o.object); - } else { - callback(); - } - }); - }; - - /** - * List all the items of a certain type - * @param type [String] The type of item e.g. 'email' - * @param offset [Number] The offset of items to fetch (0 is the last stored item) - * @param num [Number] The number of items to fetch (null means fetch all) - */ - LawnchairDAO.prototype.list = function(type, offset, num, callback) { - var self = this, - i, from, to, - matchingKeys = [], - intervalKeys = [], - list = []; - - // validate input - if (!type || typeof offset === 'undefined' || typeof num === 'undefined') { - callback({ - errMsg: 'Args not is not set!' - }); - return; - } - - // get all keys - self._db.keys(function(keys) { - - // check if key begins with type - keys.forEach(function(key) { - if (key.indexOf(type) === 0) { - matchingKeys.push(key); - } - }); - - // sort keys - matchingKeys.sort(); - - // set window of items to fetch - // if num is null, list all items - from = (num) ? matchingKeys.length - offset - num : 0; - to = matchingKeys.length - 1 - offset; - // filter items within requested interval - for (i = 0; i < matchingKeys.length; i++) { - if (i >= from && i <= to) { - intervalKeys.push(matchingKeys[i]); - } - } - - // return if there are no matching keys - if (intervalKeys.length === 0) { - callback(null, list); - return; - } - - // fetch all items from data-store with matching key - self._db.get(intervalKeys, function(intervalList) { - intervalList.forEach(function(item) { - list.push(item.object); - }); - - // return only the interval between offset and num - callback(null, list); - }); - - }); - }; - - /** - * Removes an object liter from local storage by its key (delete) - */ - LawnchairDAO.prototype.remove = function(key, callback) { - this._db.remove(key, callback); - }; - - /** - * Removes an object liter from local storage by its key (delete) - */ - LawnchairDAO.prototype.removeList = function(type, callback) { - var self = this, - matchingKeys = [], - after; - - // validate type - if (!type) { - callback({ - errMsg: 'Type is not set!' - }); - return; - } - - // get all keys - self._db.keys(function(keys) { - // check if key begins with type - keys.forEach(function(key) { - if (key.indexOf(type) === 0) { - matchingKeys.push(key); - } - }); - - if (matchingKeys.length < 1) { - callback(); - return; - } - - // remove all matching keys - after = _.after(matchingKeys.length, callback); - _.each(matchingKeys, function(key) { - self._db.remove(key, after); - }); - }); - }; - - /** - * Clears the whole local storage cache - */ - LawnchairDAO.prototype.clear = function(callback) { - this._db.nuke(callback); - }; - - return LawnchairDAO; -}); \ No newline at end of file +exports = LawnchairDAO; \ No newline at end of file diff --git a/src/js/dao/privatekey-dao.js b/src/js/dao/privatekey-dao.js index 5d8ba49..10979d7 100644 --- a/src/js/dao/privatekey-dao.js +++ b/src/js/dao/privatekey-dao.js @@ -1,198 +1,196 @@ -define(function() { - 'use strict'; +'use strict'; - var PrivateKeyDAO = function(restDao) { - this._restDao = restDao; - }; +var PrivateKeyDAO = function(restDao) { + this._restDao = restDao; +}; - // - // Device registration functions - // +// +// Device registration functions +// - /** - * Request registration of a new device by fetching registration session key. - * @param {String} options.userId The user's email address - * @param {String} options.deviceName The device's memorable name - * @param {Function} callback(error, regSessionKey) - * @return {Object} {encryptedRegSessionKey:[base64]} - */ - PrivateKeyDAO.prototype.requestDeviceRegistration = function(options, callback) { - var uri; +/** + * Request registration of a new device by fetching registration session key. + * @param {String} options.userId The user's email address + * @param {String} options.deviceName The device's memorable name + * @param {Function} callback(error, regSessionKey) + * @return {Object} {encryptedRegSessionKey:[base64]} + */ +PrivateKeyDAO.prototype.requestDeviceRegistration = function(options, callback) { + var uri; - if (!options.userId || !options.deviceName) { - callback(new Error('Incomplete arguments!')); + if (!options.userId || !options.deviceName) { + callback(new Error('Incomplete arguments!')); + return; + } + + uri = '/device/user/' + options.userId + '/devicename/' + options.deviceName; + this._restDao.post(undefined, uri, callback); +}; + +/** + * Authenticate device registration by uploading the deviceSecret encrypted with the regSessionKeys. + * @param {String} options.userId The user's email address + * @param {String} options.deviceName The device's memorable name + * @param {String} options.encryptedDeviceSecret The base64 encoded encrypted device secret + * @param {String} options.iv The iv used for encryption + * @param {Function} callback(error) + */ +PrivateKeyDAO.prototype.uploadDeviceSecret = function(options, callback) { + var uri; + + if (!options.userId || !options.deviceName || !options.encryptedDeviceSecret || !options.iv) { + callback(new Error('Incomplete arguments!')); + return; + } + + uri = '/device/user/' + options.userId + '/devicename/' + options.deviceName; + this._restDao.put(options, uri, callback); +}; + +// +// Private key functions +// + +/** + * Request authSessionKeys required for upload the encrypted private PGP key. + * @param {String} options.userId The user's email address + * @param {Function} callback(error, authSessionKey) + * @return {Object} {sessionId, encryptedAuthSessionKey:[base64 encoded], encryptedChallenge:[base64 encoded]} + */ +PrivateKeyDAO.prototype.requestAuthSessionKey = function(options, callback) { + var uri; + + if (!options.userId) { + callback(new Error('Incomplete arguments!')); + return; + } + + uri = '/auth/user/' + options.userId; + this._restDao.post(undefined, uri, callback); +}; + +/** + * Verifiy authentication by uploading the challenge and deviceSecret encrypted with the authSessionKeys as a response. + * @param {String} options.userId The user's email address + * @param {String} options.encryptedChallenge The server's base64 encoded challenge encrypted using the authSessionKey + * @param {String} options.encryptedDeviceSecret The server's base64 encoded deviceSecret encrypted using the authSessionKey + * @param {String} options.iv The iv used for encryption + * @param {Function} callback(error) + */ +PrivateKeyDAO.prototype.verifyAuthentication = function(options, callback) { + var uri; + + if (!options.userId || !options.sessionId || !options.encryptedChallenge || !options.encryptedDeviceSecret || !options.iv) { + callback(new Error('Incomplete arguments!')); + return; + } + + uri = '/auth/user/' + options.userId + '/session/' + options.sessionId; + this._restDao.put(options, uri, callback); +}; + +/** + * Upload the encrypted private PGP key. + * @param {String} options._id The hex encoded capital 16 char key id + * @param {String} options.userId The user's email address + * @param {String} options.encryptedPrivateKey The base64 encoded encrypted private PGP key + * @param {String} options.sessionId The session id + * @param {Function} callback(error) + */ +PrivateKeyDAO.prototype.upload = function(options, callback) { + var uri; + + if (!options._id || !options.userId || !options.encryptedPrivateKey || !options.sessionId || !options.salt || !options.iv) { + callback(new Error('Incomplete arguments!')); + return; + } + + uri = '/privatekey/user/' + options.userId + '/session/' + options.sessionId; + this._restDao.post(options, uri, callback); +}; + +/** + * Query if an encrypted private PGP key exists on the server without initializing the recovery procedure. + * @param {String} options.userId The user's email address + * @param {String} options.keyId The private PGP key id + * @param {Function} callback(error, found) + * @return {Boolean} whether the key was found on the server or not. + */ +PrivateKeyDAO.prototype.hasPrivateKey = function(options, callback) { + if (!options.userId || !options.keyId) { + callback(new Error('Incomplete arguments!')); + return; + } + + this._restDao.get({ + uri: '/privatekey/user/' + options.userId + '/key/' + options.keyId + '?ignoreRecovery=true', + }, function(err) { + // 404: there is no encrypted private key on the server + if (err && err.code !== 200) { + callback(null, false); return; } - uri = '/device/user/' + options.userId + '/devicename/' + options.deviceName; - this._restDao.post(undefined, uri, callback); - }; - - /** - * Authenticate device registration by uploading the deviceSecret encrypted with the regSessionKeys. - * @param {String} options.userId The user's email address - * @param {String} options.deviceName The device's memorable name - * @param {String} options.encryptedDeviceSecret The base64 encoded encrypted device secret - * @param {String} options.iv The iv used for encryption - * @param {Function} callback(error) - */ - PrivateKeyDAO.prototype.uploadDeviceSecret = function(options, callback) { - var uri; - - if (!options.userId || !options.deviceName || !options.encryptedDeviceSecret || !options.iv) { - callback(new Error('Incomplete arguments!')); + if (err) { + callback(err); return; } - uri = '/device/user/' + options.userId + '/devicename/' + options.deviceName; - this._restDao.put(options, uri, callback); - }; + callback(null, true); + }); +}; - // - // Private key functions - // +/** + * Request download for the encrypted private PGP key. + * @param {String} options.userId The user's email address + * @param {String} options.keyId The private PGP key id + * @param {Function} callback(error, found) + * @return {Boolean} whether the key was found on the server or not. + */ +PrivateKeyDAO.prototype.requestDownload = function(options, callback) { + if (!options.userId || !options.keyId) { + callback(new Error('Incomplete arguments!')); + return; + } - /** - * Request authSessionKeys required for upload the encrypted private PGP key. - * @param {String} options.userId The user's email address - * @param {Function} callback(error, authSessionKey) - * @return {Object} {sessionId, encryptedAuthSessionKey:[base64 encoded], encryptedChallenge:[base64 encoded]} - */ - PrivateKeyDAO.prototype.requestAuthSessionKey = function(options, callback) { - var uri; - - if (!options.userId) { - callback(new Error('Incomplete arguments!')); + this._restDao.get({ + uri: '/privatekey/user/' + options.userId + '/key/' + options.keyId + }, function(err) { + // 404: there is no encrypted private key on the server + if (err && err.code !== 200) { + callback(null, false); return; } - uri = '/auth/user/' + options.userId; - this._restDao.post(undefined, uri, callback); - }; - - /** - * Verifiy authentication by uploading the challenge and deviceSecret encrypted with the authSessionKeys as a response. - * @param {String} options.userId The user's email address - * @param {String} options.encryptedChallenge The server's base64 encoded challenge encrypted using the authSessionKey - * @param {String} options.encryptedDeviceSecret The server's base64 encoded deviceSecret encrypted using the authSessionKey - * @param {String} options.iv The iv used for encryption - * @param {Function} callback(error) - */ - PrivateKeyDAO.prototype.verifyAuthentication = function(options, callback) { - var uri; - - if (!options.userId || !options.sessionId || !options.encryptedChallenge || !options.encryptedDeviceSecret || !options.iv) { - callback(new Error('Incomplete arguments!')); + if (err) { + callback(err); return; } - uri = '/auth/user/' + options.userId + '/session/' + options.sessionId; - this._restDao.put(options, uri, callback); - }; + callback(null, true); + }); +}; - /** - * Upload the encrypted private PGP key. - * @param {String} options._id The hex encoded capital 16 char key id - * @param {String} options.userId The user's email address - * @param {String} options.encryptedPrivateKey The base64 encoded encrypted private PGP key - * @param {String} options.sessionId The session id - * @param {Function} callback(error) - */ - PrivateKeyDAO.prototype.upload = function(options, callback) { - var uri; +/** + * Verify the download request for the private PGP key using the recovery token sent via email. This downloads the actual encrypted private key. + * @param {String} options.userId The user's email address + * @param {String} options.keyId The private key id + * @param {String} options.recoveryToken The token proving the user own the email account + * @param {Function} callback(error, encryptedPrivateKey) + * @return {Object} {_id:[hex encoded capital 16 char key id], encryptedPrivateKey:[base64 encoded], encryptedUserId: [base64 encoded]} + */ +PrivateKeyDAO.prototype.download = function(options, callback) { + var uri; - if (!options._id || !options.userId || !options.encryptedPrivateKey || !options.sessionId || !options.salt || !options.iv) { - callback(new Error('Incomplete arguments!')); - return; - } + if (!options.userId || !options.keyId || !options.recoveryToken) { + callback(new Error('Incomplete arguments!')); + return; + } - uri = '/privatekey/user/' + options.userId + '/session/' + options.sessionId; - this._restDao.post(options, uri, callback); - }; + uri = '/privatekey/user/' + options.userId + '/key/' + options.keyId + '/recovery/' + options.recoveryToken; + this._restDao.get({ + uri: uri + }, callback); +}; - /** - * Query if an encrypted private PGP key exists on the server without initializing the recovery procedure. - * @param {String} options.userId The user's email address - * @param {String} options.keyId The private PGP key id - * @param {Function} callback(error, found) - * @return {Boolean} whether the key was found on the server or not. - */ - PrivateKeyDAO.prototype.hasPrivateKey = function(options, callback) { - if (!options.userId || !options.keyId) { - callback(new Error('Incomplete arguments!')); - return; - } - - this._restDao.get({ - uri: '/privatekey/user/' + options.userId + '/key/' + options.keyId + '?ignoreRecovery=true', - }, function(err) { - // 404: there is no encrypted private key on the server - if (err && err.code !== 200) { - callback(null, false); - return; - } - - if (err) { - callback(err); - return; - } - - callback(null, true); - }); - }; - - /** - * Request download for the encrypted private PGP key. - * @param {String} options.userId The user's email address - * @param {String} options.keyId The private PGP key id - * @param {Function} callback(error, found) - * @return {Boolean} whether the key was found on the server or not. - */ - PrivateKeyDAO.prototype.requestDownload = function(options, callback) { - if (!options.userId || !options.keyId) { - callback(new Error('Incomplete arguments!')); - return; - } - - this._restDao.get({ - uri: '/privatekey/user/' + options.userId + '/key/' + options.keyId - }, function(err) { - // 404: there is no encrypted private key on the server - if (err && err.code !== 200) { - callback(null, false); - return; - } - - if (err) { - callback(err); - return; - } - - callback(null, true); - }); - }; - - /** - * Verify the download request for the private PGP key using the recovery token sent via email. This downloads the actual encrypted private key. - * @param {String} options.userId The user's email address - * @param {String} options.keyId The private key id - * @param {String} options.recoveryToken The token proving the user own the email account - * @param {Function} callback(error, encryptedPrivateKey) - * @return {Object} {_id:[hex encoded capital 16 char key id], encryptedPrivateKey:[base64 encoded], encryptedUserId: [base64 encoded]} - */ - PrivateKeyDAO.prototype.download = function(options, callback) { - var uri; - - if (!options.userId || !options.keyId || !options.recoveryToken) { - callback(new Error('Incomplete arguments!')); - return; - } - - uri = '/privatekey/user/' + options.userId + '/key/' + options.keyId + '/recovery/' + options.recoveryToken; - this._restDao.get({ - uri: uri - }, callback); - }; - - return PrivateKeyDAO; -}); \ No newline at end of file +exports = PrivateKeyDAO; \ No newline at end of file diff --git a/src/js/dao/publickey-dao.js b/src/js/dao/publickey-dao.js index a2ebba0..f4d6bff 100644 --- a/src/js/dao/publickey-dao.js +++ b/src/js/dao/publickey-dao.js @@ -1,105 +1,103 @@ -define(function() { - 'use strict'; +'use strict'; - var PublicKeyDAO = function(restDao) { - this._restDao = restDao; - }; +var PublicKeyDAO = function(restDao) { + this._restDao = restDao; +}; - /** - * Verify the public key behind the given uuid - */ - PublicKeyDAO.prototype.verify = function(uuid, callback) { - var uri = '/verify/' + uuid; +/** + * Verify the public key behind the given uuid + */ +PublicKeyDAO.prototype.verify = function(uuid, callback) { + var uri = '/verify/' + uuid; - this._restDao.get({ - uri: uri, - type: 'text' - }, function(err, res, status) { - if (err && err.code === 400) { - // there was an attempt to verify a non-existing public key - callback(); - return; - } + this._restDao.get({ + uri: uri, + type: 'text' + }, function(err, res, status) { + if (err && err.code === 400) { + // there was an attempt to verify a non-existing public key + callback(); + return; + } - callback(err, res, status); - }); - }; + callback(err, res, status); + }); +}; - /** - * Find the user's corresponding public key - */ - PublicKeyDAO.prototype.get = function(keyId, callback) { - var uri = '/publickey/key/' + keyId; +/** + * Find the user's corresponding public key + */ +PublicKeyDAO.prototype.get = function(keyId, callback) { + var uri = '/publickey/key/' + keyId; - this._restDao.get({ - uri: uri - }, function(err, key) { - if (err && err.code === 404) { - callback(); - return; - } + this._restDao.get({ + uri: uri + }, function(err, key) { + if (err && err.code === 404) { + callback(); + return; + } - if (err) { - callback(err); - return; - } + if (err) { + callback(err); + return; + } - callback(null, (key && key._id) ? key : undefined); - }); - }; + callback(null, (key && key._id) ? key : undefined); + }); +}; - /** - * Find the user's corresponding public key by email - */ - PublicKeyDAO.prototype.getByUserId = function(userId, callback) { - var uri = '/publickey/user/' + userId; +/** + * Find the user's corresponding public key by email + */ +PublicKeyDAO.prototype.getByUserId = function(userId, callback) { + var uri = '/publickey/user/' + userId; - this._restDao.get({ - uri: uri - }, function(err, keys) { - // not found - if (err && err.code === 404) { - callback(); - return; - } + this._restDao.get({ + uri: uri + }, function(err, keys) { + // not found + if (err && err.code === 404) { + callback(); + return; + } - if (err) { - callback(err); - return; - } + if (err) { + callback(err); + return; + } - if (!keys || keys.length < 1) { - // 'No public key for that user!' - callback(); - return; - } + if (!keys || keys.length < 1) { + // 'No public key for that user!' + callback(); + return; + } - if (keys.length > 1) { - callback({ - errMsg: 'That user has multiple public keys!' - }); - return; - } + if (keys.length > 1) { + callback({ + errMsg: 'That user has multiple public keys!' + }); + return; + } - callback(null, keys[0]); - }); - }; + callback(null, keys[0]); + }); +}; - /** - * Persist the user's publc key - */ - PublicKeyDAO.prototype.put = function(pubkey, callback) { - var uri = '/publickey/user/' + pubkey.userId + '/key/' + pubkey._id; - this._restDao.put(pubkey, uri, callback); - }; +/** + * Persist the user's publc key + */ +PublicKeyDAO.prototype.put = function(pubkey, callback) { + var uri = '/publickey/user/' + pubkey.userId + '/key/' + pubkey._id; + this._restDao.put(pubkey, uri, callback); +}; - /** - * Delete the public key from the cloud storage service - */ - PublicKeyDAO.prototype.remove = function(keyId, callback) { - var uri = '/publickey/key/' + keyId; - this._restDao.remove(uri, callback); - }; +/** + * Delete the public key from the cloud storage service + */ +PublicKeyDAO.prototype.remove = function(keyId, callback) { + var uri = '/publickey/key/' + keyId; + this._restDao.remove(uri, callback); +}; - return PublicKeyDAO; -}); \ No newline at end of file +exports = PublicKeyDAO; \ No newline at end of file diff --git a/src/js/dao/rest-dao.js b/src/js/dao/rest-dao.js index 686ebc1..17b3ce1 100644 --- a/src/js/dao/rest-dao.js +++ b/src/js/dao/rest-dao.js @@ -1,123 +1,121 @@ -define(function(require) { - 'use strict'; +'use strict'; - var config = require('js/app-config').config; +var config = require('../app-config').config; - var RestDAO = function(baseUri) { - if (baseUri) { - this._baseUri = baseUri; - } else { - this._baseUri = config.cloudUrl; - } - }; +var RestDAO = function(baseUri) { + if (baseUri) { + this._baseUri = baseUri; + } else { + this._baseUri = config.cloudUrl; + } +}; - /** - * GET (read) request - * @param {String} options.uri URI relative to the base uri to perform the GET request with. - * @param {String} options.type (optional) The type of data that you're expecting back from the server: json, xml, text. Default: json. - */ - RestDAO.prototype.get = function(options, callback) { - options.method = 'GET'; - this._processRequest(options, callback); - }; +/** + * GET (read) request + * @param {String} options.uri URI relative to the base uri to perform the GET request with. + * @param {String} options.type (optional) The type of data that you're expecting back from the server: json, xml, text. Default: json. + */ +RestDAO.prototype.get = function(options, callback) { + options.method = 'GET'; + this._processRequest(options, callback); +}; - /** - * POST (create) request - */ - RestDAO.prototype.post = function(item, uri, callback) { - this._processRequest({ - method: 'POST', - payload: item, - uri: uri - }, callback); - }; +/** + * POST (create) request + */ +RestDAO.prototype.post = function(item, uri, callback) { + this._processRequest({ + method: 'POST', + payload: item, + uri: uri + }, callback); +}; - /** - * PUT (update) request - */ - RestDAO.prototype.put = function(item, uri, callback) { - this._processRequest({ - method: 'PUT', - payload: item, - uri: uri - }, callback); - }; +/** + * PUT (update) request + */ +RestDAO.prototype.put = function(item, uri, callback) { + this._processRequest({ + method: 'PUT', + payload: item, + uri: uri + }, callback); +}; - /** - * DELETE (remove) request - */ - RestDAO.prototype.remove = function(uri, callback) { - this._processRequest({ - method: 'DELETE', - uri: uri - }, callback); - }; +/** + * DELETE (remove) request + */ +RestDAO.prototype.remove = function(uri, callback) { + this._processRequest({ + method: 'DELETE', + uri: uri + }, callback); +}; - // - // helper functions - // +// +// helper functions +// - RestDAO.prototype._processRequest = function(options, callback) { - var xhr, format; +RestDAO.prototype._processRequest = function(options, callback) { + var xhr, format; - if (typeof options.uri === 'undefined') { - callback({ - code: 400, - errMsg: 'Bad Request! URI is a mandatory parameter.' - }); - return; - } + if (typeof options.uri === 'undefined') { + callback({ + code: 400, + errMsg: 'Bad Request! URI is a mandatory parameter.' + }); + return; + } - options.type = options.type || 'json'; + options.type = options.type || 'json'; - if (options.type === 'json') { - format = 'application/json'; - } else if (options.type === 'xml') { - format = 'application/xml'; - } else if (options.type === 'text') { - format = 'text/plain'; - } else { - callback({ - code: 400, - errMsg: 'Bad Request! Unhandled data type.' - }); - return; - } + if (options.type === 'json') { + format = 'application/json'; + } else if (options.type === 'xml') { + format = 'application/xml'; + } else if (options.type === 'text') { + format = 'text/plain'; + } else { + callback({ + code: 400, + errMsg: 'Bad Request! Unhandled data type.' + }); + return; + } - xhr = new XMLHttpRequest(); - xhr.open(options.method, this._baseUri + options.uri); - xhr.setRequestHeader('Accept', format); - xhr.setRequestHeader('Content-Type', format); + xhr = new XMLHttpRequest(); + xhr.open(options.method, this._baseUri + options.uri); + xhr.setRequestHeader('Accept', format); + xhr.setRequestHeader('Content-Type', format); - xhr.onload = function() { - var res; + xhr.onload = function() { + var res; - if (xhr.readyState === 4 && (xhr.status === 200 || xhr.status === 201 || xhr.status === 304)) { - if (options.type === 'json') { - res = xhr.responseText ? JSON.parse(xhr.responseText) : xhr.responseText; - } else { - res = xhr.responseText; - } - - callback(null, res, xhr.status); - return; + if (xhr.readyState === 4 && (xhr.status === 200 || xhr.status === 201 || xhr.status === 304)) { + if (options.type === 'json') { + res = xhr.responseText ? JSON.parse(xhr.responseText) : xhr.responseText; + } else { + res = xhr.responseText; } - callback({ - code: xhr.status, - errMsg: xhr.statusText - }); - }; + callback(null, res, xhr.status); + return; + } - xhr.onerror = function() { - callback({ - code: 42, - errMsg: 'Error calling ' + options.method + ' on ' + options.uri - }); - }; - - xhr.send(options.payload ? JSON.stringify(options.payload) : undefined); + callback({ + code: xhr.status, + errMsg: xhr.statusText + }); }; - return RestDAO; -}); \ No newline at end of file + xhr.onerror = function() { + callback({ + code: 42, + errMsg: 'Error calling ' + options.method + ' on ' + options.uri + }); + }; + + xhr.send(options.payload ? JSON.stringify(options.payload) : undefined); +}; + +exports = RestDAO; \ No newline at end of file diff --git a/src/js/util/backbutton-handler.js b/src/js/util/backbutton-handler.js index 66b92a7..930e61a 100644 --- a/src/js/util/backbutton-handler.js +++ b/src/js/util/backbutton-handler.js @@ -1,57 +1,55 @@ -define(function(require) { - 'use strict'; +'use strict'; - var axe = require('axe'), - DEBUG_TAG = 'backbutton handler'; +var axe = require('axe-logger'), + DEBUG_TAG = 'backbutton handler'; - /** - * The back button handler introduces meaningful behavior fo rthe back button: - * if there's an open lightbox, close it; - * if the reader is open in mobile mode, close it; - * if the navigation is open, close it; - * if there's nothing else open, shut down the app; - * - * @type {Object} - */ - var backBtnHandler = { - attachHandler: function(scope) { - this.scope = scope; - }, - start: function() { - document.addEventListener("backbutton", handleBackButton, false); - }, - stop: function() { - document.removeEventListener("backbutton", handleBackButton, false); - } - }; - - function handleBackButton(event) { - axe.debug(DEBUG_TAG, 'back button pressed'); - - // this disarms the default behavior which we NEVER want - event.preventDefault(); - event.stopPropagation(); - - if (backBtnHandler.scope.state.lightbox) { - // closes the lightbox (error msgs, writer, ...) - backBtnHandler.scope.state.lightbox = undefined; - axe.debug(DEBUG_TAG, 'lightbox closed'); - backBtnHandler.scope.$apply(); - } else if (backBtnHandler.scope.state.read && backBtnHandler.scope.state.read.open) { - // closes the reader - backBtnHandler.scope.state.read.toggle(false); - axe.debug(DEBUG_TAG, 'reader closed'); - backBtnHandler.scope.$apply(); - } else if (backBtnHandler.scope.state.nav && backBtnHandler.scope.state.nav.open) { - // closes the navigation - backBtnHandler.scope.state.nav.toggle(false); - axe.debug(DEBUG_TAG, 'navigation closed'); - backBtnHandler.scope.$apply(); - } else { - // exits the app - navigator.app.exitApp(); - } +/** + * The back button handler introduces meaningful behavior fo rthe back button: + * if there's an open lightbox, close it; + * if the reader is open in mobile mode, close it; + * if the navigation is open, close it; + * if there's nothing else open, shut down the app; + * + * @type {Object} + */ +var backBtnHandler = { + attachHandler: function(scope) { + this.scope = scope; + }, + start: function() { + document.addEventListener("backbutton", handleBackButton, false); + }, + stop: function() { + document.removeEventListener("backbutton", handleBackButton, false); } +}; - return backBtnHandler; -}); \ No newline at end of file +function handleBackButton(event) { + axe.debug(DEBUG_TAG, 'back button pressed'); + + // this disarms the default behavior which we NEVER want + event.preventDefault(); + event.stopPropagation(); + + if (backBtnHandler.scope.state.lightbox) { + // closes the lightbox (error msgs, writer, ...) + backBtnHandler.scope.state.lightbox = undefined; + axe.debug(DEBUG_TAG, 'lightbox closed'); + backBtnHandler.scope.$apply(); + } else if (backBtnHandler.scope.state.read && backBtnHandler.scope.state.read.open) { + // closes the reader + backBtnHandler.scope.state.read.toggle(false); + axe.debug(DEBUG_TAG, 'reader closed'); + backBtnHandler.scope.$apply(); + } else if (backBtnHandler.scope.state.nav && backBtnHandler.scope.state.nav.open) { + // closes the navigation + backBtnHandler.scope.state.nav.toggle(false); + axe.debug(DEBUG_TAG, 'navigation closed'); + backBtnHandler.scope.$apply(); + } else { + // exits the app + navigator.app.exitApp(); + } +} + +exports = backBtnHandler; \ No newline at end of file diff --git a/src/js/util/connection-doctor.js b/src/js/util/connection-doctor.js index 43e047a..acd5a23 100644 --- a/src/js/util/connection-doctor.js +++ b/src/js/util/connection-doctor.js @@ -1,295 +1,293 @@ -define(function(require) { - 'use strict'; +'use strict'; - var TCPSocket = require('tcp-socket'), - appConfig = require('js/app-config'), - cfg = appConfig.config, - strings = appConfig.string, - ImapClient = require('imap-client'), - SmtpClient = require('smtpclient'); +var TCPSocket = require('tcp-socket'), + appConfig = require('../app-config'), + cfg = appConfig.config, + strings = appConfig.string, + ImapClient = require('imap-client'), + SmtpClient = require('wo-smtpclient'); - /** - * The connection doctor can check your connection. In essence, it reconstructs what happens when - * the app goes online in an abbreviated way. You need to configure() the instance with the IMAP/SMTP - * credentials before running check()! - * - * @constructor - */ - var ConnectionDoctor = function() {}; +/** + * The connection doctor can check your connection. In essence, it reconstructs what happens when + * the app goes online in an abbreviated way. You need to configure() the instance with the IMAP/SMTP + * credentials before running check()! + * + * @constructor + */ +var ConnectionDoctor = function() {}; - // - // Error codes - // +// +// Error codes +// - var OFFLINE = ConnectionDoctor.OFFLINE = 42; - var TLS_WRONG_CERT = ConnectionDoctor.TLS_WRONG_CERT = 43; - var HOST_UNREACHABLE = ConnectionDoctor.HOST_UNREACHABLE = 44; - var HOST_TIMEOUT = ConnectionDoctor.HOST_TIMEOUT = 45; - var AUTH_REJECTED = ConnectionDoctor.AUTH_REJECTED = 46; - var NO_INBOX = ConnectionDoctor.NO_INBOX = 47; - var GENERIC_ERROR = ConnectionDoctor.GENERIC_ERROR = 48; +var OFFLINE = ConnectionDoctor.OFFLINE = 42; +var TLS_WRONG_CERT = ConnectionDoctor.TLS_WRONG_CERT = 43; +var HOST_UNREACHABLE = ConnectionDoctor.HOST_UNREACHABLE = 44; +var HOST_TIMEOUT = ConnectionDoctor.HOST_TIMEOUT = 45; +var AUTH_REJECTED = ConnectionDoctor.AUTH_REJECTED = 46; +var NO_INBOX = ConnectionDoctor.NO_INBOX = 47; +var GENERIC_ERROR = ConnectionDoctor.GENERIC_ERROR = 48; - // - // Public API - // +// +// Public API +// - /** - * Configures the connection doctor - * - * @param {Object} credentials.imap IMAP configuration (host:string, port:number, secure:boolean, ignoreTLS:boolean) - * @param {Object} credentials.smtp SMTP configuration (host:string, port:number, secure:boolean, ignoreTLS:boolean) - * @param {String} credentials.username - * @param {String} credentials.password - */ - ConnectionDoctor.prototype.configure = function(credentials) { - this.credentials = credentials; +/** + * Configures the connection doctor + * + * @param {Object} credentials.imap IMAP configuration (host:string, port:number, secure:boolean, ignoreTLS:boolean) + * @param {Object} credentials.smtp SMTP configuration (host:string, port:number, secure:boolean, ignoreTLS:boolean) + * @param {String} credentials.username + * @param {String} credentials.password + */ +ConnectionDoctor.prototype.configure = function(credentials) { + this.credentials = credentials; - // internal members - this._imap = new ImapClient({ - host: this.credentials.imap.host, - port: this.credentials.imap.port, - secure: this.credentials.imap.secure, - ignoreTLS: this.credentials.imap.ignoreTLS, - ca: this.credentials.imap.ca, - auth: { - user: this.credentials.username, - pass: this.credentials.password, - xoauth2: this.credentials.xoauth2 - } - }); + // internal members + this._imap = new ImapClient({ + host: this.credentials.imap.host, + port: this.credentials.imap.port, + secure: this.credentials.imap.secure, + ignoreTLS: this.credentials.imap.ignoreTLS, + ca: this.credentials.imap.ca, + auth: { + user: this.credentials.username, + pass: this.credentials.password, + xoauth2: this.credentials.xoauth2 + } + }); - this._smtp = new SmtpClient(this.credentials.smtp.host, this.credentials.smtp.port, { - useSecureTransport: this.credentials.smtp.secure, - ignoreTLS: this.credentials.smtp.ignoreTLS, - ca: this.credentials.smtp.ca, - auth: { - user: this.credentials.username, - pass: this.credentials.password, - xoauth2: this.credentials.xoauth2 - } - }); - }; + this._smtp = new SmtpClient(this.credentials.smtp.host, this.credentials.smtp.port, { + useSecureTransport: this.credentials.smtp.secure, + ignoreTLS: this.credentials.smtp.ignoreTLS, + ca: this.credentials.smtp.ca, + auth: { + user: this.credentials.username, + pass: this.credentials.password, + xoauth2: this.credentials.xoauth2 + } + }); +}; - /** - * It conducts the following tests for IMAP and SMTP, respectively: - * 1) Check if browser is online - * 2) Connect to host:port via TCP/TLS - * 3) Login to the server - * 4) Perform some basic commands (e.g. list folders) - * 5) Exposes error codes - * - * @param {Function} callback(error) Invoked when the test suite passed, or with an error object if something went wrong - */ - ConnectionDoctor.prototype.check = function(callback) { - var self = this; +/** + * It conducts the following tests for IMAP and SMTP, respectively: + * 1) Check if browser is online + * 2) Connect to host:port via TCP/TLS + * 3) Login to the server + * 4) Perform some basic commands (e.g. list folders) + * 5) Exposes error codes + * + * @param {Function} callback(error) Invoked when the test suite passed, or with an error object if something went wrong + */ +ConnectionDoctor.prototype.check = function(callback) { + var self = this; - if (!self.credentials) { - return callback(new Error('You need to configure() the connection doctor first!')); + if (!self.credentials) { + return callback(new Error('You need to configure() the connection doctor first!')); + } + + self._checkOnline(function(error) { + if (error) { + return callback(error); } - self._checkOnline(function(error) { + self._checkReachable(self.credentials.imap, function(error) { if (error) { return callback(error); } - self._checkReachable(self.credentials.imap, function(error) { + self._checkReachable(self.credentials.smtp, function(error) { if (error) { return callback(error); } - self._checkReachable(self.credentials.smtp, function(error) { + self._checkImap(function(error) { if (error) { return callback(error); } - self._checkImap(function(error) { - if (error) { - return callback(error); - } - - self._checkSmtp(callback); - }); + self._checkSmtp(callback); }); }); }); - }; + }); +}; - // - // Internal API - // +// +// Internal API +// - /** - * Checks if the browser is online - * - * @param {Function} callback(error) Invoked when the test suite passed, or with an error object if browser is offline - */ - ConnectionDoctor.prototype._checkOnline = function(callback) { - if (navigator.onLine) { - callback(); - } else { - callback(createError(OFFLINE, strings.connDocOffline)); +/** + * Checks if the browser is online + * + * @param {Function} callback(error) Invoked when the test suite passed, or with an error object if browser is offline + */ +ConnectionDoctor.prototype._checkOnline = function(callback) { + if (navigator.onLine) { + callback(); + } else { + callback(createError(OFFLINE, strings.connDocOffline)); + } +}; + +/** + * Checks if a host is reachable via TCP + * + * @param {String} options.host + * @param {Number} options.port + * @param {Boolean} options.secure + * @param {Function} callback(error) Invoked when the test suite passed, or with an error object if something went wrong + */ +ConnectionDoctor.prototype._checkReachable = function(options, callback) { + var socket, + error, // remember the error message + timeout, // remember the timeout object + host = options.host + ':' + options.port, + hasTimedOut = false; // prevents multiple callbacks + + timeout = setTimeout(function() { + hasTimedOut = true; + callback(createError(HOST_TIMEOUT, strings.connDocHostTimeout.replace('{0}', host).replace('{1}', cfg.connDocTimeout))); + }, cfg.connDocTimeout); + + socket = TCPSocket.open(options.host, options.port, { + binaryType: 'arraybuffer', + useSecureTransport: options.secure, + ca: options.ca + }); + + socket.ondata = function() {}; // we don't actually care about the data + + // [WO-625] Mozilla forbids extensions to the TCPSocket object, + // throws an exception when assigned unexpected callback functions. + // The exception can be safely ignored since we need the callback + // for the other shims + try { + socket.oncert = function() { + if (options.ca) { + // the certificate we already have is outdated + error = createError(TLS_WRONG_CERT, strings.connDocTlsWrongCert.replace('{0}', host)); + } + }; + } catch (e) {} + + socket.onerror = function(e) { + if (!error) { + error = createError(HOST_UNREACHABLE, strings.connDocHostUnreachable.replace('{0}', host), e.data); } }; - /** - * Checks if a host is reachable via TCP - * - * @param {String} options.host - * @param {Number} options.port - * @param {Boolean} options.secure - * @param {Function} callback(error) Invoked when the test suite passed, or with an error object if something went wrong - */ - ConnectionDoctor.prototype._checkReachable = function(options, callback) { - var socket, - error, // remember the error message - timeout, // remember the timeout object - host = options.host + ':' + options.port, - hasTimedOut = false; // prevents multiple callbacks - - timeout = setTimeout(function() { - hasTimedOut = true; - callback(createError(HOST_TIMEOUT, strings.connDocHostTimeout.replace('{0}', host).replace('{1}', cfg.connDocTimeout))); - }, cfg.connDocTimeout); - - socket = TCPSocket.open(options.host, options.port, { - binaryType: 'arraybuffer', - useSecureTransport: options.secure, - ca: options.ca - }); - - socket.ondata = function() {}; // we don't actually care about the data - - // [WO-625] Mozilla forbids extensions to the TCPSocket object, - // throws an exception when assigned unexpected callback functions. - // The exception can be safely ignored since we need the callback - // for the other shims - try { - socket.oncert = function() { - if (options.ca) { - // the certificate we already have is outdated - error = createError(TLS_WRONG_CERT, strings.connDocTlsWrongCert.replace('{0}', host)); - } - }; - } catch (e) {} - - socket.onerror = function(e) { - if (!error) { - error = createError(HOST_UNREACHABLE, strings.connDocHostUnreachable.replace('{0}', host), e.data); - } - }; - - socket.onopen = function() { - socket.close(); - }; - - socket.onclose = function() { - if (!hasTimedOut) { - clearTimeout(timeout); - callback(error); - } - }; + socket.onopen = function() { + socket.close(); }; - /** - * Checks if an IMAP server is reachable, accepts the credentials, can list folders and has an inbox and logs out. - * Adds the certificate to the IMAP settings if not provided. - * - * @param {Function} callback(error) Invoked when the test suite passed, or with an error object if something went wrong - */ - ConnectionDoctor.prototype._checkImap = function(callback) { - var self = this, - loggedIn = false, - host = self.credentials.imap.host + ':' + self.credentials.imap.port; + socket.onclose = function() { + if (!hasTimedOut) { + clearTimeout(timeout); + callback(error); + } + }; +}; + +/** + * Checks if an IMAP server is reachable, accepts the credentials, can list folders and has an inbox and logs out. + * Adds the certificate to the IMAP settings if not provided. + * + * @param {Function} callback(error) Invoked when the test suite passed, or with an error object if something went wrong + */ +ConnectionDoctor.prototype._checkImap = function(callback) { + var self = this, + loggedIn = false, + host = self.credentials.imap.host + ':' + self.credentials.imap.port; - self._imap.onCert = function(pemEncodedCert) { - if (!self.credentials.imap.ca) { - self.credentials.imap.ca = pemEncodedCert; + self._imap.onCert = function(pemEncodedCert) { + if (!self.credentials.imap.ca) { + self.credentials.imap.ca = pemEncodedCert; + } + }; + + // login and logout do not use error objects in the callback, but rather invoke + // the global onError handler, so we need to track if login was successful + self._imap.onError = function(error) { + if (!loggedIn) { + callback(createError(AUTH_REJECTED, strings.connDocAuthRejected.replace('{0}', host), error)); + } else { + callback(createError(GENERIC_ERROR, strings.connDocGenericError.replace('{0}', host).replace('{1}', error.message), error)); + } + }; + + self._imap.login(function() { + loggedIn = true; + + self._imap.listWellKnownFolders(function(error, wellKnownFolders) { + if (error) { + return callback(createError(GENERIC_ERROR, strings.connDocGenericError.replace('{0}', host).replace('{1}', error.message), error)); } - }; - // login and logout do not use error objects in the callback, but rather invoke - // the global onError handler, so we need to track if login was successful - self._imap.onError = function(error) { - if (!loggedIn) { - callback(createError(AUTH_REJECTED, strings.connDocAuthRejected.replace('{0}', host), error)); - } else { - callback(createError(GENERIC_ERROR, strings.connDocGenericError.replace('{0}', host).replace('{1}', error.message), error)); + if (wellKnownFolders.Inbox.length === 0) { + // the client needs at least an inbox folder to work properly + return callback(createError(NO_INBOX, strings.connDocNoInbox.replace('{0}', host))); } - }; - self._imap.login(function() { - loggedIn = true; - - self._imap.listWellKnownFolders(function(error, wellKnownFolders) { - if (error) { - return callback(createError(GENERIC_ERROR, strings.connDocGenericError.replace('{0}', host).replace('{1}', error.message), error)); - } - - if (wellKnownFolders.Inbox.length === 0) { - // the client needs at least an inbox folder to work properly - return callback(createError(NO_INBOX, strings.connDocNoInbox.replace('{0}', host))); - } - - self._imap.logout(function() { - callback(); - }); + self._imap.logout(function() { + callback(); }); }); + }); +}; + +/** + * Checks if an SMTP server is reachable and accepts the credentials and logs out. + * Adds the certificate to the SMTP settings if not provided. + * + * @param {Function} callback(error) Invoked when the test suite passed, or with an error object if something went wrong + */ +ConnectionDoctor.prototype._checkSmtp = function(callback) { + var self = this, + host = self.credentials.smtp.host + ':' + self.credentials.smtp.port, + errored = false; // tracks if we need to invoke the callback at onclose or not + + self._smtp.oncert = function(pemEncodedCert) { + if (!self.credentials.smtp.ca) { + self.credentials.smtp.ca = pemEncodedCert; + } }; - /** - * Checks if an SMTP server is reachable and accepts the credentials and logs out. - * Adds the certificate to the SMTP settings if not provided. - * - * @param {Function} callback(error) Invoked when the test suite passed, or with an error object if something went wrong - */ - ConnectionDoctor.prototype._checkSmtp = function(callback) { - var self = this, - host = self.credentials.smtp.host + ':' + self.credentials.smtp.port, - errored = false; // tracks if we need to invoke the callback at onclose or not - - self._smtp.oncert = function(pemEncodedCert) { - if (!self.credentials.smtp.ca) { - self.credentials.smtp.ca = pemEncodedCert; - } - }; - - self._smtp.onerror = function(error) { - if (error) { - errored = true; - callback(createError(AUTH_REJECTED, strings.connDocAuthRejected.replace('{0}', host), error)); - } - }; - - self._smtp.onidle = function() { - self._smtp.quit(); - }; - - self._smtp.onclose = function() { - if (!errored) { - callback(); - } - }; - - self._smtp.connect(); + self._smtp.onerror = function(error) { + if (error) { + errored = true; + callback(createError(AUTH_REJECTED, strings.connDocAuthRejected.replace('{0}', host), error)); + } }; + self._smtp.onidle = function() { + self._smtp.quit(); + }; - // - // Helper Functions - // + self._smtp.onclose = function() { + if (!errored) { + callback(); + } + }; - function createError(code, message, underlyingError) { - var error = new Error(message); - error.code = code; - error.underlyingError = underlyingError; + self._smtp.connect(); +}; - return error; - } - return ConnectionDoctor; -}); \ No newline at end of file +// +// Helper Functions +// + +function createError(code, message, underlyingError) { + var error = new Error(message); + error.code = code; + error.underlyingError = underlyingError; + + return error; +} + +exports = ConnectionDoctor; \ No newline at end of file diff --git a/src/js/util/download.js b/src/js/util/download.js index 2ca1846..5d9d2c1 100644 --- a/src/js/util/download.js +++ b/src/js/util/download.js @@ -1,60 +1,58 @@ -define(function(require) { - 'use strict'; +'use strict'; - var util = require('js/crypto/util'); +var util = require('crypto-lib').util; - var dl = {}; +var dl = {}; - dl.createDownload = function(options) { - var contentType = options.contentType || 'application/octet-stream'; - var filename = options.filename || 'file'; - var content = options.content; - var a = document.createElement('a'); - var supportsBlob; +dl.createDownload = function(options) { + var contentType = options.contentType || 'application/octet-stream'; + var filename = options.filename || 'file'; + var content = options.content; + var a = document.createElement('a'); + var supportsBlob; - try { - supportsBlob = !!new Blob(); - } catch (e) {} + try { + supportsBlob = !!new Blob(); + } catch (e) {} - if (typeof a.download !== "undefined" && supportsBlob) { - // ff 30+, chrome 27+ (android: 37+) - document.body.appendChild(a); - a.style = "display: none"; - a.href = window.URL.createObjectURL(new Blob([content], { - type: contentType - })); - a.download = filename; - a.click(); - setTimeout(function() { - window.URL.revokeObjectURL(a.href); - document.body.removeChild(a); - }, 10); // arbitrary, just get it off the main thread - } else if (window.navigator.msSaveBlob) { - // ie 10+ - window.navigator.msSaveBlob(new Blob([content], { - type: contentType - }), filename); - } else if (supportsBlob) { - // safari actually makes no sense: - // - you can't open a new window - // - the file system api is dead - // - download attribute doesn't work - // - behaves randomly (opens a new tab or doesn't, downloads stuff or doesn't, ...) - var url = window.URL.createObjectURL(new Blob([content], { - type: contentType - })); - var newTab = window.open(url, "_blank"); - if (!newTab) { - window.location.href = url; - } - } else { - // anything else, where anything at all is better than nothing - if (typeof content !== "string" && content.buffer) { - content = util.arrBuf2BinStr(content.buffer); - } - window.open('data:' + contentType + ';base64,' + btoa(content), "_blank"); + if (typeof a.download !== "undefined" && supportsBlob) { + // ff 30+, chrome 27+ (android: 37+) + document.body.appendChild(a); + a.style = "display: none"; + a.href = window.URL.createObjectURL(new Blob([content], { + type: contentType + })); + a.download = filename; + a.click(); + setTimeout(function() { + window.URL.revokeObjectURL(a.href); + document.body.removeChild(a); + }, 10); // arbitrary, just get it off the main thread + } else if (window.navigator.msSaveBlob) { + // ie 10+ + window.navigator.msSaveBlob(new Blob([content], { + type: contentType + }), filename); + } else if (supportsBlob) { + // safari actually makes no sense: + // - you can't open a new window + // - the file system api is dead + // - download attribute doesn't work + // - behaves randomly (opens a new tab or doesn't, downloads stuff or doesn't, ...) + var url = window.URL.createObjectURL(new Blob([content], { + type: contentType + })); + var newTab = window.open(url, "_blank"); + if (!newTab) { + window.location.href = url; } - }; + } else { + // anything else, where anything at all is better than nothing + if (typeof content !== "string" && content.buffer) { + content = util.arrBuf2BinStr(content.buffer); + } + window.open('data:' + contentType + ';base64,' + btoa(content), "_blank"); + } +}; - return dl; -}); \ No newline at end of file +exports = dl; \ No newline at end of file diff --git a/src/js/util/error.js b/src/js/util/error.js index 965557b..7df0e6b 100644 --- a/src/js/util/error.js +++ b/src/js/util/error.js @@ -1,35 +1,33 @@ -define(function(require) { - 'use strict'; +'use strict'; - var axe = require('axe'); +var axe = require('axe-logger'); - var er = {}; - er.attachHandler = function(scope) { - scope.onError = function(options) { - if (!options) { - scope.$apply(); - return; - } +var er = {}; +er.attachHandler = function(scope) { + scope.onError = function(options) { + if (!options) { + scope.$apply(); + return; + } - axe.error((options.errMsg || options.message) + (options.stack ? ('\n' + options.stack) : '')); + axe.error((options.errMsg || options.message) + (options.stack ? ('\n' + options.stack) : '')); - scope.state.dialog = { - open: true, - title: options.title || 'Error', - message: options.errMsg || options.message, - faqLink: options.faqLink, - positiveBtnStr: options.positiveBtnStr || 'Ok', - negativeBtnStr: options.negativeBtnStr || 'Cancel', - showNegativeBtn: options.showNegativeBtn || false, - showBugReporter: (typeof options.showBugReporter !== 'undefined' ? options.showBugReporter : !options.title), // if title is set, presume it's not an error by default - callback: options.callback - }; - // don't call apply for synchronous calls - if (!options.sync) { - scope.$apply(); - } + scope.state.dialog = { + open: true, + title: options.title || 'Error', + message: options.errMsg || options.message, + faqLink: options.faqLink, + positiveBtnStr: options.positiveBtnStr || 'Ok', + negativeBtnStr: options.negativeBtnStr || 'Cancel', + showNegativeBtn: options.showNegativeBtn || false, + showBugReporter: (typeof options.showBugReporter !== 'undefined' ? options.showBugReporter : !options.title), // if title is set, presume it's not an error by default + callback: options.callback }; + // don't call apply for synchronous calls + if (!options.sync) { + scope.$apply(); + } }; +}; - return er; -}); \ No newline at end of file +exports = er; \ No newline at end of file diff --git a/src/js/util/notification.js b/src/js/util/notification.js index 8c4a7cc..e2cdd16 100644 --- a/src/js/util/notification.js +++ b/src/js/util/notification.js @@ -1,60 +1,58 @@ -define(function(require) { - 'use strict'; +'use strict'; - var cfg = require('js/app-config').config; +var cfg = require('../app-config').config; - var self = {}; +var self = {}; - if (window.Notification) { - self.hasPermission = Notification.permission === "granted"; +if (window.Notification) { + self.hasPermission = Notification.permission === "granted"; +} + +/** + * Creates a notification. Requests permission if not already granted + * + * @param {String} options.title The notification title + * @param {String} options.message The notification message + * @param {Number} options.timeout (optional) Timeout when the notification is closed in milliseconds + * @param {Function} options.onClick (optional) callback when the notification is clicked + * @returns {Notification} A notification instance + */ +self.create = function(options) { + options.onClick = options.onClick || function() {}; + + if (!window.Notification) { + return; } - /** - * Creates a notification. Requests permission if not already granted - * - * @param {String} options.title The notification title - * @param {String} options.message The notification message - * @param {Number} options.timeout (optional) Timeout when the notification is closed in milliseconds - * @param {Function} options.onClick (optional) callback when the notification is clicked - * @returns {Notification} A notification instance - */ - self.create = function(options) { - options.onClick = options.onClick || function() {}; - - if (!window.Notification) { - return; - } - - if (!self.hasPermission) { - // don't wait until callback returns - Notification.requestPermission(function(permission) { - if (permission === "granted") { - self.hasPermission = true; - } - }); - } - - var notification = new Notification(options.title, { - body: options.message, - icon: cfg.iconPath + if (!self.hasPermission) { + // don't wait until callback returns + Notification.requestPermission(function(permission) { + if (permission === "granted") { + self.hasPermission = true; + } }); - notification.onclick = function() { - window.focus(); - options.onClick(); - }; + } - if (options.timeout > 0) { - setTimeout(function() { - notification.close(); - }, options.timeout); - } - - return notification; + var notification = new Notification(options.title, { + body: options.message, + icon: cfg.iconPath + }); + notification.onclick = function() { + window.focus(); + options.onClick(); }; - self.close = function(notification) { - notification.close(); - }; + if (options.timeout > 0) { + setTimeout(function() { + notification.close(); + }, options.timeout); + } - return self; -}); \ No newline at end of file + return notification; +}; + +self.close = function(notification) { + notification.close(); +}; + +exports = self; \ No newline at end of file diff --git a/src/js/util/oauth.js b/src/js/util/oauth.js index 8762347..acc801a 100644 --- a/src/js/util/oauth.js +++ b/src/js/util/oauth.js @@ -1,101 +1,99 @@ -define(function() { - 'use strict'; +'use strict'; - var OAuth = function(googleApi) { - this._googleApi = googleApi; +var OAuth = function(googleApi) { + this._googleApi = googleApi; +}; + +/** + * Check if chrome.identity api is supported + * @return {Boolean} If is supported + */ +OAuth.prototype.isSupported = function() { + return !!(window.chrome && chrome.identity); +}; + +/** + * Request an OAuth token from chrome for gmail users + * @param {String} emailAddress The user's email address (optional) + */ +OAuth.prototype.getOAuthToken = function(emailAddress, callback) { + var idOptions = { + interactive: true }; - /** - * Check if chrome.identity api is supported - * @return {Boolean} If is supported - */ - OAuth.prototype.isSupported = function() { - return !!(window.chrome && chrome.identity); - }; - - /** - * Request an OAuth token from chrome for gmail users - * @param {String} emailAddress The user's email address (optional) - */ - OAuth.prototype.getOAuthToken = function(emailAddress, callback) { - var idOptions = { - interactive: true - }; - - // check which runtime the app is running under - chrome.runtime.getPlatformInfo(function(platformInfo) { - if (chrome.runtime.lastError || !platformInfo) { - callback(new Error('Error getting chrome platform info!')); - return; - } - - if (emailAddress && platformInfo.os.indexOf('android') !== -1) { - // set accountHint so that native Android account picker does not show up each time - idOptions.accountHint = emailAddress; - } - - // get OAuth Token from chrome - chrome.identity.getAuthToken(idOptions, function(token) { - if (chrome.runtime.lastError || !token) { - callback({ - errMsg: 'Error fetching an OAuth token for the user!' - }); - return; - } - - callback(null, token); - }); - }); - }; - - /** - * Remove an old OAuth token and get a new one. - * @param {String} options.oldToken The old token to be removed - * @param {String} options.emailAddress The user's email address (optional) - */ - OAuth.prototype.refreshToken = function(options, callback) { - var self = this; - - if (!options.oldToken) { - callback(new Error('oldToken option not set!')); + // check which runtime the app is running under + chrome.runtime.getPlatformInfo(function(platformInfo) { + if (chrome.runtime.lastError || !platformInfo) { + callback(new Error('Error getting chrome platform info!')); return; } - // remove cached token - chrome.identity.removeCachedAuthToken({ - token: options.oldToken - }, function() { - // get a new token - self.getOAuthToken(options.emailAddress, callback); - }); - }; - - /** - * Get email address from google api - * @param {String} token The oauth token - */ - OAuth.prototype.queryEmailAddress = function(token, callback) { - if (!token) { - callback({ - errMsg: 'Invalid OAuth token!' - }); - return; + if (emailAddress && platformInfo.os.indexOf('android') !== -1) { + // set accountHint so that native Android account picker does not show up each time + idOptions.accountHint = emailAddress; } - // fetch gmail user's email address from the Google Authorization Server - this._googleApi.get({ - uri: '/oauth2/v3/userinfo?access_token=' + token - }, function(err, info) { - if (err || !info || !info.email) { + // get OAuth Token from chrome + chrome.identity.getAuthToken(idOptions, function(token) { + if (chrome.runtime.lastError || !token) { callback({ - errMsg: 'Error looking up email address on google api!' + errMsg: 'Error fetching an OAuth token for the user!' }); return; } - callback(null, info.email); + callback(null, token); }); - }; + }); +}; - return OAuth; -}); \ No newline at end of file +/** + * Remove an old OAuth token and get a new one. + * @param {String} options.oldToken The old token to be removed + * @param {String} options.emailAddress The user's email address (optional) + */ +OAuth.prototype.refreshToken = function(options, callback) { + var self = this; + + if (!options.oldToken) { + callback(new Error('oldToken option not set!')); + return; + } + + // remove cached token + chrome.identity.removeCachedAuthToken({ + token: options.oldToken + }, function() { + // get a new token + self.getOAuthToken(options.emailAddress, callback); + }); +}; + +/** + * Get email address from google api + * @param {String} token The oauth token + */ +OAuth.prototype.queryEmailAddress = function(token, callback) { + if (!token) { + callback({ + errMsg: 'Invalid OAuth token!' + }); + return; + } + + // fetch gmail user's email address from the Google Authorization Server + this._googleApi.get({ + uri: '/oauth2/v3/userinfo?access_token=' + token + }, function(err, info) { + if (err || !info || !info.email) { + callback({ + errMsg: 'Error looking up email address on google api!' + }); + return; + } + + callback(null, info.email); + }); +}; + +exports = OAuth; \ No newline at end of file diff --git a/src/js/util/update/update-handler.js b/src/js/util/update/update-handler.js index a34252a..e070cb4 100644 --- a/src/js/util/update/update-handler.js +++ b/src/js/util/update/update-handler.js @@ -1,132 +1,130 @@ -define(function(require) { - 'use strict'; +'use strict'; - var axe = require('axe'), - cfg = require('js/app-config').config, - updateV1 = require('js/util/update/update-v1'), - updateV2 = require('js/util/update/update-v2'), - updateV3 = require('js/util/update/update-v3'), - updateV4 = require('js/util/update/update-v4'), - updateV5 = require('js/util/update/update-v5'); +var axe = require('axe-logger'), + cfg = require('../../app-config').config, + updateV1 = require('./update-v1'), + updateV2 = require('./update-v2'), + updateV3 = require('./update-v3'), + updateV4 = require('./update-v4'), + updateV5 = require('./update-v5'); - /** - * Handles database migration - */ - var UpdateHandler = function(appConfigStorage, userStorage, auth) { - this._appConfigStorage = appConfigStorage; - this._userStorage = userStorage; - this._updateScripts = [updateV1, updateV2, updateV3, updateV4, updateV5]; - this._auth = auth; +/** + * Handles database migration + */ +var UpdateHandler = function(appConfigStorage, userStorage, auth) { + this._appConfigStorage = appConfigStorage; + this._userStorage = userStorage; + this._updateScripts = [updateV1, updateV2, updateV3, updateV4, updateV5]; + this._auth = auth; +}; + +/** + * Executes all the necessary updates + * @param {Function} callback(error) Invoked when all the database updates were executed, or if an error occurred + */ +UpdateHandler.prototype.update = function(callback) { + var self = this, + currentVersion = 0, + targetVersion = cfg.dbVersion, + versionDbType = 'dbVersion'; + + self._appConfigStorage.listItems(versionDbType, 0, null, function(err, items) { + if (err) { + callback(err); + return; + } + + // parse the database version number + if (items && items.length > 0) { + currentVersion = parseInt(items[0], 10); + } + + self._applyUpdate({ + currentVersion: currentVersion, + targetVersion: targetVersion + }, callback); + }); +}; + +/** + * Schedules necessary updates and executes thom in order + */ +UpdateHandler.prototype._applyUpdate = function(options, callback) { + var self = this, + scriptOptions, + queue = []; + + if (options.currentVersion >= options.targetVersion) { + // the current database version is up to date + callback(); + return; + } + + scriptOptions = { + appConfigStorage: self._appConfigStorage, + userStorage: self._userStorage, + auth: self._auth }; - /** - * Executes all the necessary updates - * @param {Function} callback(error) Invoked when all the database updates were executed, or if an error occurred - */ - UpdateHandler.prototype.update = function(callback) { - var self = this, - currentVersion = 0, - targetVersion = cfg.dbVersion, - versionDbType = 'dbVersion'; + // add all the necessary database updates to the queue + for (var i = options.currentVersion; i < options.targetVersion; i++) { + queue.push(self._updateScripts[i]); + } - self._appConfigStorage.listItems(versionDbType, 0, null, function(err, items) { - if (err) { - callback(err); - return; - } + // takes the next update from the queue and executes it + function executeNextUpdate(err) { + if (err) { + callback(err); + return; + } - // parse the database version number - if (items && items.length > 0) { - currentVersion = parseInt(items[0], 10); - } - - self._applyUpdate({ - currentVersion: currentVersion, - targetVersion: targetVersion - }, callback); - }); - }; - - /** - * Schedules necessary updates and executes thom in order - */ - UpdateHandler.prototype._applyUpdate = function(options, callback) { - var self = this, - scriptOptions, - queue = []; - - if (options.currentVersion >= options.targetVersion) { - // the current database version is up to date + if (queue.length < 1) { + // we're done callback(); return; } - scriptOptions = { - appConfigStorage: self._appConfigStorage, - userStorage: self._userStorage, - auth: self._auth - }; + // process next update + var script = queue.shift(); + script(scriptOptions, executeNextUpdate); + } - // add all the necessary database updates to the queue - for (var i = options.currentVersion; i < options.targetVersion; i++) { - queue.push(self._updateScripts[i]); - } + executeNextUpdate(); +}; - // takes the next update from the queue and executes it - function executeNextUpdate(err) { - if (err) { - callback(err); - return; - } - - if (queue.length < 1) { - // we're done - callback(); - return; - } - - // process next update - var script = queue.shift(); - script(scriptOptions, executeNextUpdate); - } - - executeNextUpdate(); - }; - - /** - * Check application version and update correspondingly - */ - UpdateHandler.prototype.checkForUpdate = function(dialog) { - // Chrome Packaged App - if (typeof window.chrome !== 'undefined' && chrome.runtime && chrome.runtime.onUpdateAvailable) { - // check for Chrome app update and restart - chrome.runtime.onUpdateAvailable.addListener(function(details) { - axe.debug('New Chrome App update... requesting reload.'); - // Chrome downloaded a new app version - dialog({ - title: 'Update available', - message: 'A new version ' + details.version + ' of the app is available. Restart the app to update?', - positiveBtnStr: 'Restart', - negativeBtnStr: 'Not now', - showNegativeBtn: true, - callback: function(agree) { - if (agree) { - chrome.runtime.reload(); - } +/** + * Check application version and update correspondingly + */ +UpdateHandler.prototype.checkForUpdate = function(dialog) { + // Chrome Packaged App + if (typeof window.chrome !== 'undefined' && chrome.runtime && chrome.runtime.onUpdateAvailable) { + // check for Chrome app update and restart + chrome.runtime.onUpdateAvailable.addListener(function(details) { + axe.debug('New Chrome App update... requesting reload.'); + // Chrome downloaded a new app version + dialog({ + title: 'Update available', + message: 'A new version ' + details.version + ' of the app is available. Restart the app to update?', + positiveBtnStr: 'Restart', + negativeBtnStr: 'Not now', + showNegativeBtn: true, + callback: function(agree) { + if (agree) { + chrome.runtime.reload(); } - }); - }); - chrome.runtime.requestUpdateCheck(function(status) { - if (status === "update_found") { - axe.debug("Update pending..."); - } else if (status === "no_update") { - axe.debug("No update found."); - } else if (status === "throttled") { - axe.debug("Checking updates too frequently."); } }); - } - }; + }); + chrome.runtime.requestUpdateCheck(function(status) { + if (status === "update_found") { + axe.debug("Update pending..."); + } else if (status === "no_update") { + axe.debug("No update found."); + } else if (status === "throttled") { + axe.debug("Checking updates too frequently."); + } + }); + } +}; - return UpdateHandler; -}); \ No newline at end of file +exports = UpdateHandler; \ No newline at end of file diff --git a/src/js/util/update/update-v1.js b/src/js/util/update/update-v1.js index 942b9a6..77c2b61 100644 --- a/src/js/util/update/update-v1.js +++ b/src/js/util/update/update-v1.js @@ -1,29 +1,27 @@ -define(function() { - 'use strict'; +'use strict'; - /** - * Update handler for transition database version 0 -> 1 - * - * In database version 1, the stored email objects have to be purged, otherwise - * every non-prefixed mail in the IMAP folders would be nuked due to the implementation - * of the delta sync. - */ - function updateV1(options, callback) { - var emailDbType = 'email_', - versionDbType = 'dbVersion', - postUpdateDbVersion = 1; +/** + * Update handler for transition database version 0 -> 1 + * + * In database version 1, the stored email objects have to be purged, otherwise + * every non-prefixed mail in the IMAP folders would be nuked due to the implementation + * of the delta sync. + */ +function updateV1(options, callback) { + var emailDbType = 'email_', + versionDbType = 'dbVersion', + postUpdateDbVersion = 1; - // remove the emails - options.userStorage.removeList(emailDbType, function(err) { - if (err) { - callback(err); - return; - } + // remove the emails + options.userStorage.removeList(emailDbType, function(err) { + if (err) { + callback(err); + return; + } - // update the database version to postUpdateDbVersion - options.appConfigStorage.storeList([postUpdateDbVersion], versionDbType, callback); - }); - } + // update the database version to postUpdateDbVersion + options.appConfigStorage.storeList([postUpdateDbVersion], versionDbType, callback); + }); +} - return updateV1; -}); \ No newline at end of file +exports = updateV1; \ No newline at end of file diff --git a/src/js/util/update/update-v2.js b/src/js/util/update/update-v2.js index 03fdf50..74f62a7 100644 --- a/src/js/util/update/update-v2.js +++ b/src/js/util/update/update-v2.js @@ -1,28 +1,26 @@ -define(function() { - 'use strict'; +'use strict'; - /** - * Update handler for transition database version 1 -> 2 - * - * In database version 2, the stored email objects have to be purged, because the - * new data model stores information about the email structure in the property 'bodyParts'. - */ - function updateV2(options, callback) { - var emailDbType = 'email_', - versionDbType = 'dbVersion', - postUpdateDbVersion = 2; +/** + * Update handler for transition database version 1 -> 2 + * + * In database version 2, the stored email objects have to be purged, because the + * new data model stores information about the email structure in the property 'bodyParts'. + */ +function updateV2(options, callback) { + var emailDbType = 'email_', + versionDbType = 'dbVersion', + postUpdateDbVersion = 2; - // remove the emails - options.userStorage.removeList(emailDbType, function(err) { - if (err) { - callback(err); - return; - } + // remove the emails + options.userStorage.removeList(emailDbType, function(err) { + if (err) { + callback(err); + return; + } - // update the database version to postUpdateDbVersion - options.appConfigStorage.storeList([postUpdateDbVersion], versionDbType, callback); - }); - } + // update the database version to postUpdateDbVersion + options.appConfigStorage.storeList([postUpdateDbVersion], versionDbType, callback); + }); +} - return updateV2; -}); \ No newline at end of file +exports = updateV2; \ No newline at end of file diff --git a/src/js/util/update/update-v3.js b/src/js/util/update/update-v3.js index dfc49ff..82cbf01 100644 --- a/src/js/util/update/update-v3.js +++ b/src/js/util/update/update-v3.js @@ -1,28 +1,26 @@ -define(function() { - 'use strict'; +'use strict'; - /** - * Update handler for transition database version 2 -> 3 - * - * In database version 3, we introduced new flags to the messages, also - * the outbox uses artificial uids - */ - function update(options, callback) { - var emailDbType = 'email_', - versionDbType = 'dbVersion', - postUpdateDbVersion = 3; +/** + * Update handler for transition database version 2 -> 3 + * + * In database version 3, we introduced new flags to the messages, also + * the outbox uses artificial uids + */ +function update(options, callback) { + var emailDbType = 'email_', + versionDbType = 'dbVersion', + postUpdateDbVersion = 3; - // remove the emails - options.userStorage.removeList(emailDbType, function(err) { - if (err) { - callback(err); - return; - } + // remove the emails + options.userStorage.removeList(emailDbType, function(err) { + if (err) { + callback(err); + return; + } - // update the database version to postUpdateDbVersion - options.appConfigStorage.storeList([postUpdateDbVersion], versionDbType, callback); - }); - } + // update the database version to postUpdateDbVersion + options.appConfigStorage.storeList([postUpdateDbVersion], versionDbType, callback); + }); +} - return update; -}); \ No newline at end of file +exports = update; \ No newline at end of file diff --git a/src/js/util/update/update-v4.js b/src/js/util/update/update-v4.js index 54af103..b481aac 100644 --- a/src/js/util/update/update-v4.js +++ b/src/js/util/update/update-v4.js @@ -1,91 +1,89 @@ -define(function(require) { - 'use strict'; +'use strict'; - var config = require('js/app-config').config; +var config = require('../../app-config').config; - /** - * Update handler for transition database version 3 -> 4 - * - * In database version 4, we need to add a "provider" flag to the - * indexeddb. only gmail was allowed as a mail service provider before, - * so let's add this... - */ - function update(options, callback) { - var VERSION_DB_TYPE = 'dbVersion', - EMAIL_ADDR_DB_KEY = 'emailaddress', - USERNAME_DB_KEY = 'username', - PROVIDER_DB_KEY = 'provider', - IMAP_DB_KEY = 'imap', - SMTP_DB_KEY = 'smtp', - REALNAME_DB_KEY = 'realname', - POST_UPDATE_DB_VERSION = 4; +/** + * Update handler for transition database version 3 -> 4 + * + * In database version 4, we need to add a "provider" flag to the + * indexeddb. only gmail was allowed as a mail service provider before, + * so let's add this... + */ +function update(options, callback) { + var VERSION_DB_TYPE = 'dbVersion', + EMAIL_ADDR_DB_KEY = 'emailaddress', + USERNAME_DB_KEY = 'username', + PROVIDER_DB_KEY = 'provider', + IMAP_DB_KEY = 'imap', + SMTP_DB_KEY = 'smtp', + REALNAME_DB_KEY = 'realname', + POST_UPDATE_DB_VERSION = 4; - var imap = config.gmail.imap, - smtp = config.gmail.smtp; + var imap = config.gmail.imap, + smtp = config.gmail.smtp; - // load the email address (if existing) - loadFromDB(EMAIL_ADDR_DB_KEY, function(err, emailAddress) { + // load the email address (if existing) + loadFromDB(EMAIL_ADDR_DB_KEY, function(err, emailAddress) { + if (err) { + return callback(err); + } + + // load the provider (if existing) + loadFromDB(PROVIDER_DB_KEY, function(err, provider) { if (err) { return callback(err); } - // load the provider (if existing) - loadFromDB(PROVIDER_DB_KEY, function(err, provider) { + // if there is an email address without a provider, we need to add the missing provider entry + // for any other situation, we're good. + + if (!(emailAddress && !provider)) { + // update the database version to POST_UPDATE_DB_VERSION + return options.appConfigStorage.storeList([POST_UPDATE_DB_VERSION], VERSION_DB_TYPE, callback); + } + + // add the missing provider key + options.appConfigStorage.storeList(['gmail'], PROVIDER_DB_KEY, function(err) { if (err) { return callback(err); } - // if there is an email address without a provider, we need to add the missing provider entry - // for any other situation, we're good. - - if (!(emailAddress && !provider)) { - // update the database version to POST_UPDATE_DB_VERSION - return options.appConfigStorage.storeList([POST_UPDATE_DB_VERSION], VERSION_DB_TYPE, callback); - } - - // add the missing provider key - options.appConfigStorage.storeList(['gmail'], PROVIDER_DB_KEY, function(err) { + // add the missing user name key + options.appConfigStorage.storeList([emailAddress], USERNAME_DB_KEY, function(err) { if (err) { return callback(err); } - // add the missing user name key - options.appConfigStorage.storeList([emailAddress], USERNAME_DB_KEY, function(err) { + // add the missing imap host info key + options.appConfigStorage.storeList([imap], IMAP_DB_KEY, function(err) { if (err) { return callback(err); } - // add the missing imap host info key - options.appConfigStorage.storeList([imap], IMAP_DB_KEY, function(err) { + // add the missing empty real name + options.appConfigStorage.storeList([''], REALNAME_DB_KEY, function(err) { if (err) { return callback(err); } - // add the missing empty real name - options.appConfigStorage.storeList([''], REALNAME_DB_KEY, function(err) { + // add the missing smtp host info key + options.appConfigStorage.storeList([smtp], SMTP_DB_KEY, function(err) { if (err) { return callback(err); } - // add the missing smtp host info key - options.appConfigStorage.storeList([smtp], SMTP_DB_KEY, function(err) { + // reload the credentials + options.auth.initialized = false; + options.auth._loadCredentials(function(err) { if (err) { return callback(err); } - // reload the credentials - options.auth.initialized = false; - options.auth._loadCredentials(function(err) { - if (err) { - return callback(err); - } - - // update the database version to POST_UPDATE_DB_VERSION - options.appConfigStorage.storeList([POST_UPDATE_DB_VERSION], VERSION_DB_TYPE, callback); - }); + // update the database version to POST_UPDATE_DB_VERSION + options.appConfigStorage.storeList([POST_UPDATE_DB_VERSION], VERSION_DB_TYPE, callback); }); }); }); @@ -93,13 +91,13 @@ define(function(require) { }); }); }); + }); - function loadFromDB(key, callback) { - options.appConfigStorage.listItems(key, 0, null, function(err, cachedItems) { - callback(err, (!err && cachedItems && cachedItems[0])); - }); - } + function loadFromDB(key, callback) { + options.appConfigStorage.listItems(key, 0, null, function(err, cachedItems) { + callback(err, (!err && cachedItems && cachedItems[0])); + }); } +} - return update; -}); \ No newline at end of file +exports = update; \ No newline at end of file diff --git a/src/js/util/update/update-v5.js b/src/js/util/update/update-v5.js index ef3a035..338c470 100644 --- a/src/js/util/update/update-v5.js +++ b/src/js/util/update/update-v5.js @@ -1,56 +1,54 @@ -define(function() { - 'use strict'; +'use strict'; - var FOLDER_TYPE_INBOX = 'Inbox'; - var FOLDER_TYPE_SENT = 'Sent'; - var FOLDER_TYPE_DRAFTS = 'Drafts'; - var FOLDER_TYPE_TRASH = 'Trash'; +var FOLDER_TYPE_INBOX = 'Inbox'; +var FOLDER_TYPE_SENT = 'Sent'; +var FOLDER_TYPE_DRAFTS = 'Drafts'; +var FOLDER_TYPE_TRASH = 'Trash'; - var FOLDER_DB_TYPE = 'folders'; - var VERSION_DB_TYPE = 'dbVersion'; +var FOLDER_DB_TYPE = 'folders'; +var VERSION_DB_TYPE = 'dbVersion'; - var POST_UPDATE_DB_VERSION = 5; +var POST_UPDATE_DB_VERSION = 5; - /** - * Update handler for transition database version 4 -> 5 - * - * Due to an overlooked issue, there may be multiple folders, e.g. for sent mails. - * This removes the "duplicate" folders. - */ - function update(options, callback) { +/** + * Update handler for transition database version 4 -> 5 + * + * Due to an overlooked issue, there may be multiple folders, e.g. for sent mails. + * This removes the "duplicate" folders. + */ +function update(options, callback) { - // remove the emails - options.userStorage.listItems(FOLDER_DB_TYPE, 0, null, function(err, stored) { + // remove the emails + options.userStorage.listItems(FOLDER_DB_TYPE, 0, null, function(err, stored) { + if (err) { + return callback(err); + } + + var folders = stored[0] || []; + [FOLDER_TYPE_INBOX, FOLDER_TYPE_SENT, FOLDER_TYPE_DRAFTS, FOLDER_TYPE_TRASH].forEach(function(mbxType) { + var foldersForType = folders.filter(function(mbx) { + return mbx.type === mbxType; + }); + + if (foldersForType.length <= 1) { + return; // nothing to do here + } + + // remove duplicate folders + for (var i = 1; i < foldersForType.length; i++) { + folders.splice(folders.indexOf(foldersForType[i]), 1); + } + }); + + options.userStorage.storeList([folders], FOLDER_DB_TYPE, function(err) { if (err) { return callback(err); } - var folders = stored[0] || []; - [FOLDER_TYPE_INBOX, FOLDER_TYPE_SENT, FOLDER_TYPE_DRAFTS, FOLDER_TYPE_TRASH].forEach(function(mbxType) { - var foldersForType = folders.filter(function(mbx) { - return mbx.type === mbxType; - }); - - if (foldersForType.length <= 1) { - return; // nothing to do here - } - - // remove duplicate folders - for (var i = 1; i < foldersForType.length; i++) { - folders.splice(folders.indexOf(foldersForType[i]), 1); - } - }); - - options.userStorage.storeList([folders], FOLDER_DB_TYPE, function(err) { - if (err) { - return callback(err); - } - - // update the database version to POST_UPDATE_DB_VERSION - options.appConfigStorage.storeList([POST_UPDATE_DB_VERSION], VERSION_DB_TYPE, callback); - }); + // update the database version to POST_UPDATE_DB_VERSION + options.appConfigStorage.storeList([POST_UPDATE_DB_VERSION], VERSION_DB_TYPE, callback); }); - } + }); +} - return update; -}); \ No newline at end of file +exports = update; \ No newline at end of file diff --git a/src/lib/uuid/uuid.js b/src/lib/uuid/uuid.js deleted file mode 100644 index 4795b9d..0000000 --- a/src/lib/uuid/uuid.js +++ /dev/null @@ -1,245 +0,0 @@ -// uuid.js -// -// (c) 2010-2012 Robert Kieffer -// MIT License -// https://github.com/broofa/node-uuid -(function() { - var _global = this; - - // Unique ID creation requires a high quality random # generator. We feature - // detect to determine the best RNG source, normalizing to a function that - // returns 128-bits of randomness, since that's what's usually required - var _rng; - - // Node.js crypto-based RNG - http://nodejs.org/docs/v0.6.2/api/crypto.html - // - // Moderately fast, high quality - if (typeof(require) == 'function') { - try { - var _rb = require('crypto').randomBytes; - _rng = _rb && function() {return _rb(16);}; - } catch(e) {} - } - - if (!_rng && _global.crypto && crypto.getRandomValues) { - // WHATWG crypto-based RNG - http://wiki.whatwg.org/wiki/Crypto - // - // Moderately fast, high quality - var _rnds8 = new Uint8Array(16); - _rng = function whatwgRNG() { - crypto.getRandomValues(_rnds8); - return _rnds8; - }; - } - - if (!_rng) { - // Math.random()-based (RNG) - // - // If all else fails, use Math.random(). It's fast, but is of unspecified - // quality. - var _rnds = new Array(16); - _rng = function() { - for (var i = 0, r; i < 16; i++) { - if ((i & 0x03) === 0) r = Math.random() * 0x100000000; - _rnds[i] = r >>> ((i & 0x03) << 3) & 0xff; - } - - return _rnds; - }; - } - - // Buffer class to use - var BufferClass = typeof(Buffer) == 'function' ? Buffer : Array; - - // Maps for number <-> hex string conversion - var _byteToHex = []; - var _hexToByte = {}; - for (var i = 0; i < 256; i++) { - _byteToHex[i] = (i + 0x100).toString(16).substr(1); - _hexToByte[_byteToHex[i]] = i; - } - - // **`parse()` - Parse a UUID into it's component bytes** - function parse(s, buf, offset) { - var i = (buf && offset) || 0, ii = 0; - - buf = buf || []; - s.toLowerCase().replace(/[0-9a-f]{2}/g, function(oct) { - if (ii < 16) { // Don't overflow! - buf[i + ii++] = _hexToByte[oct]; - } - }); - - // Zero out remaining bytes if string was short - while (ii < 16) { - buf[i + ii++] = 0; - } - - return buf; - } - - // **`unparse()` - Convert UUID byte array (ala parse()) into a string** - function unparse(buf, offset) { - var i = offset || 0, bth = _byteToHex; - return bth[buf[i++]] + bth[buf[i++]] + - bth[buf[i++]] + bth[buf[i++]] + '-' + - bth[buf[i++]] + bth[buf[i++]] + '-' + - bth[buf[i++]] + bth[buf[i++]] + '-' + - bth[buf[i++]] + bth[buf[i++]] + '-' + - bth[buf[i++]] + bth[buf[i++]] + - bth[buf[i++]] + bth[buf[i++]] + - bth[buf[i++]] + bth[buf[i++]]; - } - - // **`v1()` - Generate time-based UUID** - // - // Inspired by https://github.com/LiosK/UUID.js - // and http://docs.python.org/library/uuid.html - - // random #'s we need to init node and clockseq - var _seedBytes = _rng(); - - // Per 4.5, create and 48-bit node id, (47 random bits + multicast bit = 1) - var _nodeId = [ - _seedBytes[0] | 0x01, - _seedBytes[1], _seedBytes[2], _seedBytes[3], _seedBytes[4], _seedBytes[5] - ]; - - // Per 4.2.2, randomize (14 bit) clockseq - var _clockseq = (_seedBytes[6] << 8 | _seedBytes[7]) & 0x3fff; - - // Previous uuid creation time - var _lastMSecs = 0, _lastNSecs = 0; - - // See https://github.com/broofa/node-uuid for API details - function v1(options, buf, offset) { - var i = buf && offset || 0; - var b = buf || []; - - options = options || {}; - - var clockseq = options.clockseq != null ? options.clockseq : _clockseq; - - // UUID timestamps are 100 nano-second units since the Gregorian epoch, - // (1582-10-15 00:00). JSNumbers aren't precise enough for this, so - // time is handled internally as 'msecs' (integer milliseconds) and 'nsecs' - // (100-nanoseconds offset from msecs) since unix epoch, 1970-01-01 00:00. - var msecs = options.msecs != null ? options.msecs : new Date().getTime(); - - // Per 4.2.1.2, use count of uuid's generated during the current clock - // cycle to simulate higher resolution clock - var nsecs = options.nsecs != null ? options.nsecs : _lastNSecs + 1; - - // Time since last uuid creation (in msecs) - var dt = (msecs - _lastMSecs) + (nsecs - _lastNSecs)/10000; - - // Per 4.2.1.2, Bump clockseq on clock regression - if (dt < 0 && options.clockseq == null) { - clockseq = clockseq + 1 & 0x3fff; - } - - // Reset nsecs if clock regresses (new clockseq) or we've moved onto a new - // time interval - if ((dt < 0 || msecs > _lastMSecs) && options.nsecs == null) { - nsecs = 0; - } - - // Per 4.2.1.2 Throw error if too many uuids are requested - if (nsecs >= 10000) { - throw new Error('uuid.v1(): Can\'t create more than 10M uuids/sec'); - } - - _lastMSecs = msecs; - _lastNSecs = nsecs; - _clockseq = clockseq; - - // Per 4.1.4 - Convert from unix epoch to Gregorian epoch - msecs += 12219292800000; - - // `time_low` - var tl = ((msecs & 0xfffffff) * 10000 + nsecs) % 0x100000000; - b[i++] = tl >>> 24 & 0xff; - b[i++] = tl >>> 16 & 0xff; - b[i++] = tl >>> 8 & 0xff; - b[i++] = tl & 0xff; - - // `time_mid` - var tmh = (msecs / 0x100000000 * 10000) & 0xfffffff; - b[i++] = tmh >>> 8 & 0xff; - b[i++] = tmh & 0xff; - - // `time_high_and_version` - b[i++] = tmh >>> 24 & 0xf | 0x10; // include version - b[i++] = tmh >>> 16 & 0xff; - - // `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant) - b[i++] = clockseq >>> 8 | 0x80; - - // `clock_seq_low` - b[i++] = clockseq & 0xff; - - // `node` - var node = options.node || _nodeId; - for (var n = 0; n < 6; n++) { - b[i + n] = node[n]; - } - - return buf ? buf : unparse(b); - } - - // **`v4()` - Generate random UUID** - - // See https://github.com/broofa/node-uuid for API details - function v4(options, buf, offset) { - // Deprecated - 'format' argument, as supported in v1.2 - var i = buf && offset || 0; - - if (typeof(options) == 'string') { - buf = options == 'binary' ? new BufferClass(16) : null; - options = null; - } - options = options || {}; - - var rnds = options.random || (options.rng || _rng)(); - - // Per 4.4, set bits for version and `clock_seq_hi_and_reserved` - rnds[6] = (rnds[6] & 0x0f) | 0x40; - rnds[8] = (rnds[8] & 0x3f) | 0x80; - - // Copy bytes to buffer, if provided - if (buf) { - for (var ii = 0; ii < 16; ii++) { - buf[i + ii] = rnds[ii]; - } - } - - return buf || unparse(rnds); - } - - // Export public API - var uuid = v4; - uuid.v1 = v1; - uuid.v4 = v4; - uuid.parse = parse; - uuid.unparse = unparse; - uuid.BufferClass = BufferClass; - - if (_global.define && define.amd) { - // Publish as AMD module - define(function() {return uuid;}); - } else if (typeof(module) != 'undefined' && module.exports) { - // Publish as node.js module - module.exports = uuid; - } else { - // Publish as global (in browsers) - var _previousRoot = _global.uuid; - - // **`noConflict()` - (browser only) to reset global 'uuid' var** - uuid.noConflict = function() { - _global.uuid = _previousRoot; - return uuid; - }; - - _global.uuid = uuid; - } -}()); diff --git a/src/package.json b/src/package.json deleted file mode 100644 index 637decf..0000000 --- a/src/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "Whiteout Mail", - "version": "0.0.1", - "main": "index.html", - "window": { - "toolbar": false, - "width": 1024, - "height": 768 - } -} \ No newline at end of file diff --git a/src/require-config.js b/src/require-config.js deleted file mode 100644 index 4a550a6..0000000 --- a/src/require-config.js +++ /dev/null @@ -1,68 +0,0 @@ -(function() { - 'use strict'; - - requirejs.config({ - nodeRequire: (typeof module !== 'undefined' && module.exports) ? require : undefined, - baseUrl: 'lib', - paths: { - js: '../js', - test: '../../test', - jquery: 'jquery.min', - underscore: 'underscore/underscore-min', - lawnchair: 'lawnchair/lawnchair-git', - lawnchairSQL: 'lawnchair/lawnchair-adapter-webkit-sqlite-git', - lawnchairIDB: 'lawnchair/lawnchair-adapter-indexed-db-git', - angular: 'angular/angular.min', - angularRoute: 'angular/angular-route.min', - angularAnimate: 'angular/angular-animate.min', - ngInfiniteScroll: 'ng-infinite-scroll.min', - ngTagsInput: 'ngtagsinput/ng-tags-input.min', - uuid: 'uuid/uuid', - forge: 'forge/forge.min', - punycode: 'punycode.min', - openpgp: 'openpgp/openpgp', - fastclick: 'fastclick/fastclick' - }, - shim: { - forge: { - exports: 'forge' - }, - jquery: { - exports: '$' - }, - angular: { - exports: 'angular', - deps: ['jquery'] - }, - angularRoute: { - exports: 'angular', - deps: ['angular'] - }, - angularAnimate: { - exports: 'angular', - deps: ['angular'] - }, - ngInfiniteScroll: { - exports: 'angular', - deps: ['jquery', 'angular'] - }, - ngTagsInput: { - exports: 'angular', - deps: ['angular'] - }, - lawnchair: { - exports: 'Lawnchair' - }, - lawnchairSQL: { - deps: ['lawnchair'] - }, - lawnchairIDB: { - deps: ['lawnchair', 'lawnchairSQL'] - }, - underscore: { - exports: '_' - } - } - }); - -}()); \ No newline at end of file