From 58ed8928e608d1ad2ffb9137ffc4a4219e9f2fe4 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Tue, 3 Dec 2013 19:21:50 +0100 Subject: [PATCH] integrate new email-dao into controllers and first attempt at starting app --- src/js/app-controller.js | 4 +- src/js/bo/outbox.js | 4 +- src/js/controller/login-existing.js | 6 +- src/js/controller/login.js | 10 +- src/js/controller/mail-list.js | 381 ++--- src/js/controller/navigation.js | 41 +- src/js/dao/email-dao-2.js | 872 ----------- src/js/dao/email-dao.js | 1157 ++++++++------ src/tpl/mail-list.html | 6 +- src/tpl/navigation.html | 2 +- test/new-unit/email-dao-2-test.js | 1683 -------------------- test/new-unit/email-dao-test.js | 2088 +++++++++++++++++-------- test/new-unit/login-ctrl-test.js | 13 +- test/new-unit/main.js | 1 - test/new-unit/navigation-ctrl-test.js | 21 +- test/new-unit/outbox-bo-test.js | 8 +- 16 files changed, 2325 insertions(+), 3972 deletions(-) delete mode 100644 src/js/dao/email-dao-2.js delete mode 100644 test/new-unit/email-dao-2-test.js diff --git a/src/js/app-controller.js b/src/js/app-controller.js index 2944ae4..26395f7 100644 --- a/src/js/app-controller.js +++ b/src/js/app-controller.js @@ -222,7 +222,9 @@ define(function(require) { asymKeySize: config.asymKeySize }; - self._emailDao.init(account, callback); + self._emailDao.init({ + account: account + }, callback); } }; diff --git a/src/js/bo/outbox.js b/src/js/bo/outbox.js index dce5102..25fe1f4 100644 --- a/src/js/bo/outbox.js +++ b/src/js/bo/outbox.js @@ -209,7 +209,7 @@ define(function(require) { }; // send invitation mail - self._emailDao.send(invitationMail, function(err) { + self._emailDao.sendPlaintext(invitationMail, function(err) { if (err) { self._outboxBusy = false; callback(err); @@ -222,7 +222,7 @@ define(function(require) { function sendEncrypted(email) { removeFromPendingMails(email); - self._emailDao.encryptedSend(email, function(err) { + self._emailDao.sendEncrypted(email, function(err) { if (err) { self._outboxBusy = false; callback(err); diff --git a/src/js/controller/login-existing.js b/src/js/controller/login-existing.js index de91124..ffc5bce 100644 --- a/src/js/controller/login-existing.js +++ b/src/js/controller/login-existing.js @@ -37,7 +37,11 @@ define(function(require) { handleError(err); return; } - emailDao.unlock(keypair, $scope.passphrase, onUnlock); + + emailDao.unlock({ + keypair: keypair, + passphrase: $scope.passphrase + }, onUnlock); }); } diff --git a/src/js/controller/login.js b/src/js/controller/login.js index 6c60ce5..d6b8c5c 100644 --- a/src/js/controller/login.js +++ b/src/js/controller/login.js @@ -45,15 +45,7 @@ define(function(require) { return; } - // login to imap backend - appController._emailDao.imapLogin(function(err) { - if (err) { - $scope.onError(err); - return; - } - - redirect(availableKeys); - }); + redirect(availableKeys); }); }); } diff --git a/src/js/controller/mail-list.js b/src/js/controller/mail-list.js index 1de286d..1bf39d8 100644 --- a/src/js/controller/mail-list.js +++ b/src/js/controller/mail-list.js @@ -1,8 +1,7 @@ define(function(require) { 'use strict'; - var _ = require('underscore'), - angular = require('angular'), + var angular = require('angular'), appController = require('js/app-controller'), IScroll = require('iscroll'), str = require('js/app-config').string, @@ -10,9 +9,7 @@ define(function(require) { emailDao, outboxBo; var MailListCtrl = function($scope) { - var offset = 0, - num = 100, - firstSelect = true; + var firstSelect = true; // // Init @@ -21,36 +18,37 @@ define(function(require) { emailDao = appController._emailDao; outboxBo = appController._outboxBo; - if (emailDao) { - emailDao.onIncomingMessage = function(email) { - if (email.subject.indexOf(str.subjectPrefix) === -1) { - return; - } + // // push handler + // if (emailDao) { + // emailDao.onIncomingMessage = function(email) { + // if (email.subject.indexOf(str.subjectPrefix) === -1) { + // return; + // } - // sync - $scope.synchronize(function() { - // show notification - notificationForEmail(email); - }); - }; - chrome.notifications.onClicked.addListener(notificationClicked); - } + // // sync + // $scope.synchronize(function() { + // // show notification + // notificationForEmail(email); + // }); + // }; + // chrome.notifications.onClicked.addListener(notificationClicked); + // } - function notificationClicked(uidString) { - var email, uid = parseInt(uidString, 10); + // function notificationClicked(uidString) { + // var email, uid = parseInt(uidString, 10); - if (isNaN(uid)) { - return; - } + // if (isNaN(uid)) { + // return; + // } - email = _.findWhere($scope.emails, { - uid: uid - }); + // email = _.findWhere(getFolder().messages, { + // uid: uid + // }); - if (email) { - $scope.select(email); - } - } + // if (email) { + // $scope.select(email); + // } + // } // // scope functions @@ -64,11 +62,11 @@ define(function(require) { $scope.state.mailList.selected = email; - // mark selected message as 'read' - markAsRead(email); + // // mark selected message as 'read' + // markAsRead(email); }; - $scope.synchronize = function(callback) { + $scope.synchronize = function() { // if we're in the outbox, don't do an imap sync if (getFolder().type === 'Outbox') { updateStatus('Last update: ', new Date()); @@ -77,23 +75,22 @@ define(function(require) { } updateStatus('Syncing ...'); - // sync from imap to local db - syncImapFolder({ - folder: getFolder().path, - offset: -num, - num: offset - }, function() { - // list again from local db after syncing - listLocalMessages({ - folder: getFolder().path, - offset: offset, - num: num - }, function() { - updateStatus('Last update: ', new Date()); - if (callback) { - callback(); - } - }); + + // let email dao handle sync transparently + emailDao.sync({ + folder: getFolder().path + }, function(err) { + if (err) { + updateStatus('Error on sync!'); + $scope.onError(err); + return; + } + + // sort emails + displayEmails(getFolder().messages); + + // display last update + updateStatus('Last update: ', new Date()); }); }; @@ -102,83 +99,83 @@ define(function(require) { return; } - var index, currentFolder, trashFolder, outboxFolder; + // var index, currentFolder, trashFolder, outboxFolder; - currentFolder = getFolder(); + // currentFolder = getFolder(); - trashFolder = _.findWhere($scope.folders, { - type: 'Trash' - }); + // trashFolder = _.findWhere($scope.folders, { + // type: 'Trash' + // }); - outboxFolder = _.findWhere($scope.folders, { - type: 'Outbox' - }); + // outboxFolder = _.findWhere($scope.folders, { + // type: 'Outbox' + // }); - if (currentFolder === outboxFolder) { - $scope.onError({ - errMsg: 'Deleting messages from the outbox is not yet supported.' - }); - return; - } + // if (currentFolder === outboxFolder) { + // $scope.onError({ + // errMsg: 'Deleting messages from the outbox is not yet supported.' + // }); + // return; + // } - if (currentFolder === trashFolder) { - $scope.state.dialog = { - open: true, - title: 'Delete', - message: 'Delete this message permanently?', - callback: function(ok) { - if (!ok) { - return; - } + // if (currentFolder === trashFolder) { + // $scope.state.dialog = { + // open: true, + // title: 'Delete', + // message: 'Delete this message permanently?', + // callback: function(ok) { + // if (!ok) { + // return; + // } - removeLocalAndShowNext(); - removeRemote(); - } - }; - return; - } + // removeLocalAndShowNext(); + // removeRemote(); + // } + // }; + // return; + // } - removeLocalAndShowNext(); - removeRemote(); + // removeLocalAndShowNext(); + // removeRemote(); - function removeLocalAndShowNext() { - index = $scope.emails.indexOf(email); - // show the next mail - if ($scope.emails.length > 1) { - // if we're about to delete the last entry of the array, show the previous (i.e. the one below in the list), - // otherwise show the next one (i.e. the one above in the list) - $scope.select(_.last($scope.emails) === email ? $scope.emails[index - 1] : $scope.emails[index + 1]); - } else { - // if we have only one email in the array, show nothing - $scope.select(); - $scope.state.mailList.selected = undefined; - } - $scope.emails.splice(index, 1); - } + // function removeLocalAndShowNext() { + // index = getFolder().messages.indexOf(email); + // // show the next mail + // if (getFolder().messages.length > 1) { + // // if we're about to delete the last entry of the array, show the previous (i.e. the one below in the list), + // // otherwise show the next one (i.e. the one above in the list) + // $scope.select(_.last(getFolder().messages) === email ? getFolder().messages[index - 1] : getFolder().messages[index + 1]); + // } else { + // // if we have only one email in the array, show nothing + // $scope.select(); + // $scope.state.mailList.selected = undefined; + // } + // getFolder().messages.splice(index, 1); + // } - function removeRemote() { - if (getFolder() === trashFolder) { - emailDao.imapDeleteMessage({ - folder: getFolder().path, - uid: email.uid - }, moved); - return; - } + // function removeRemote() { + // if (getFolder() === trashFolder) { + // emailDao.imapDeleteMessage({ + // folder: getFolder().path, + // uid: email.uid + // }, moved); + // return; + // } - emailDao.imapMoveMessage({ - folder: getFolder().path, - uid: email.uid, - destination: trashFolder.path - }, moved); - } + // emailDao.imapMoveMessage({ + // folder: getFolder().path, + // uid: email.uid, + // destination: trashFolder.path + // }, moved); + // } - function moved(err) { - if (err) { - $scope.emails.splice(index, 0, email); - $scope.onError(err); - return; - } - } + // function moved(err) { + // if (err) { + // getFolder().messages.splice(index, 0, email); + // $scope.onError(err); + // return; + // } + // } }; $scope._stopWatchTask = $scope.$watch('state.nav.currentFolder', function() { @@ -190,8 +187,8 @@ define(function(require) { if (!window.chrome || !chrome.identity) { firstSelect = true; updateStatus('Last update: ', new Date()); - $scope.emails = createDummyMails(); - $scope.select($scope.emails[0]); + getFolder().messages = createDummyMails(); + displayEmails(getFolder().messages); return; } @@ -204,21 +201,9 @@ define(function(require) { return; } - updateStatus('Read cache ...'); - - // list messaged from local db - listLocalMessages({ - folder: getFolder().path, - offset: offset, - num: num - }, function sync() { - updateStatus('Syncing ...'); - $scope.$apply(); - - // sync imap folder to local db - $scope.synchronize(); - }); + displayEmails(getFolder().messages); + $scope.synchronize(); }); // share local scope functions with root state @@ -231,51 +216,14 @@ define(function(require) { // helper functions // - function notificationForEmail(email) { - chrome.notifications.create('' + email.uid, { - type: 'basic', - title: email.from[0].address, - message: email.subject.split(str.subjectPrefix)[1], - iconUrl: chrome.runtime.getURL(cfg.iconPath) - }, function() {}); - } - - function syncImapFolder(options, callback) { - emailDao.unreadMessages(getFolder().path, function(err, unreadCount) { - if (err) { - updateStatus('Error on sync!'); - $scope.onError(err); - return; - } - // set unread count in folder model - getFolder().count = unreadCount; - $scope.$apply(); - - emailDao.imapSync(options, function(err) { - if (err) { - updateStatus('Error on sync!'); - $scope.onError(err); - return; - } - - callback(); - }); - }); - } - - function listLocalMessages(options, callback) { - firstSelect = true; - emailDao.listMessages(options, function(err, emails) { - if (err) { - updateStatus('Error listing cache!'); - $scope.onError(err); - return; - } - - callback(emails); - displayEmails(emails); - }); - } + // function notificationForEmail(email) { + // chrome.notifications.create('' + email.uid, { + // type: 'basic', + // title: email.from[0].address, + // message: email.subject.split(str.subjectPrefix)[1], + // iconUrl: chrome.runtime.getURL(cfg.iconPath) + // }, function() {}); + // } function updateStatus(lbl, time) { $scope.lastUpdateLbl = lbl; @@ -284,69 +232,58 @@ define(function(require) { function displayEmails(emails) { if (!emails || emails.length < 1) { - $scope.emails = []; $scope.select(); - $scope.$apply(); return; } - // sort by uid - emails = _.sortBy(emails, function(e) { - return -e.uid; - }); - - $scope.emails = emails; - $scope.select($scope.emails[0]); - - // syncing from the outbox is a synchronous call, so we mustn't call $scope.$apply - // for every other IMAP folder, this call is asynchronous, hence we have to call $scope.$apply... - if (getFolder().type !== 'Outbox') { - $scope.$apply(); - } + // select first message + $scope.select(emails[emails.length - 1]); } function getFolder() { return $scope.state.nav.currentFolder; } - function markAsRead(email) { - // marking mails as read is meaningless in the outbox - if (getFolder().type === 'Outbox') { - return; - } + // function markAsRead(email) { + // // marking mails as read is meaningless in the outbox + // if (getFolder().type === 'Outbox') { + // return; + // } - // don't mark top selected email automatically - if (firstSelect) { - firstSelect = false; - return; - } + // // don't mark top selected email automatically + // if (firstSelect) { + // firstSelect = false; + // return; + // } - $scope.state.read.toggle(true); - if (!window.chrome || !chrome.socket) { - return; - } + // $scope.state.read.toggle(true); + // if (!window.chrome || !chrome.socket) { + // return; + // } - if (!email.unread) { - return; - } + // if (!email.unread) { + // return; + // } - email.unread = false; - emailDao.imapMarkMessageRead({ - folder: getFolder().path, - uid: email.uid - }, function(err) { - if (err) { - updateStatus('Error marking read!'); - $scope.onError(err); - return; - } - }); - } + // email.unread = false; + // emailDao.imapMarkMessageRead({ + // folder: getFolder().path, + // uid: email.uid + // }, function(err) { + // if (err) { + // updateStatus('Error marking read!'); + // $scope.onError(err); + // return; + // } + // }); + // } }; function createDummyMails() { + var uid = 0; + var Email = function(unread, attachments, answered, html) { - this.uid = '1'; + this.uid = uid++; this.from = [{ name: 'Whiteout Support', address: 'support@whiteout.io' diff --git a/src/js/controller/navigation.js b/src/js/controller/navigation.js index 09ae961..9f08a96 100644 --- a/src/js/controller/navigation.js +++ b/src/js/controller/navigation.js @@ -42,7 +42,7 @@ define(function(require) { return; } - var outbox = _.findWhere($scope.folders, { + var outbox = _.findWhere($scope.account.folders, { type: 'Outbox' }); outbox.count = count; @@ -54,40 +54,29 @@ define(function(require) { // // init folders - initFolders(function(folders) { - $scope.folders = folders; - // select inbox as the current folder on init - $scope.openFolder($scope.folders[0]); - }); + initFolders(); + // select inbox as the current folder on init + $scope.openFolder($scope.account.folders[0]); // // helper functions // - function initFolders(callback) { + function initFolders() { if (window.chrome && chrome.identity) { - emailDao.imapListFolders(function(err, folders) { - if (err) { - $scope.onError(err); - return; - } + // get pointer to account/folder/message tree on root scope + $scope.$root.account = emailDao._account; - folders.forEach(function(f) { - f.count = 0; - }); - - // start checking outbox periodically - outboxBo.startChecking($scope.onOutboxUpdate); - // make function available globally for write controller - $scope.emptyOutbox = outboxBo._processOutbox.bind(outboxBo); - - callback(folders); - $scope.$apply(); - }); + // start checking outbox periodically + outboxBo.startChecking($scope.onOutboxUpdate); + // make function available globally for write controller + $scope.emptyOutbox = outboxBo._processOutbox.bind(outboxBo); return; } - callback([{ + // attach dummy folders for development + $scope.$root.account = {}; + $scope.account.folders = [{ type: 'Inbox', count: 2, path: 'INBOX' @@ -107,7 +96,7 @@ define(function(require) { type: 'Trash', count: 0, path: 'TRASH' - }]); + }]; } }; diff --git a/src/js/dao/email-dao-2.js b/src/js/dao/email-dao-2.js deleted file mode 100644 index daf0add..0000000 --- a/src/js/dao/email-dao-2.js +++ /dev/null @@ -1,872 +0,0 @@ -define(function(require) { - 'use strict'; - - var util = require('cryptoLib/util'), - _ = require('underscore'), - str = require('js/app-config').string, - config = require('js/app-config').config; - - var EmailDAO = function(keychain, imapClient, smtpClient, crypto, devicestorage) { - var self = this; - - self._keychain = keychain; - self._imapClient = imapClient; - self._smtpClient = smtpClient; - self._crypto = crypto; - self._devicestorage = devicestorage; - - // delegation-esque pattern to mitigate between node-style events and plain js - self._imapClient.onIncomingMessage = function(message) { - if (typeof self.onIncomingMessage === 'function') { - self.onIncomingMessage(message); - } - }; - }; - - // - // External API - // - - EmailDAO.prototype.init = function(options, callback) { - var self = this, - keypair; - - self._account = options.account; - self._account.busy = false; - - // 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() { - // init user's local database - self._devicestorage.init(emailAddress, function() { - // 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() { - self._imapLogin(function(err) { - if (err) { - callback(err); - return; - } - - self._imapListFolders(function(err, folders) { - if (err) { - callback(err); - return; - } - - self._account.folders = folders; - callback(null, keypair); - }); - }); - } - }; - - - EmailDAO.prototype.unlock = function(options, callback) { - var self = this; - - if (options.keypair) { - // import existing key pair into crypto module - self._crypto.importKeys({ - passphrase: options.passphrase, - privateKeyArmored: options.keypair.privateKey.encryptedKey, - publicKeyArmored: options.keypair.publicKey.publicKey - }, callback); - return; - } - - // no keypair for is stored for the user... generate a new one - self._crypto.generateKeys({ - emailAddress: self._account.emailAddress, - keySize: self._account.asymKeySize, - passphrase: options.passphrase - }, function(err, generatedKeypair) { - if (err) { - callback(err); - return; - } - - handleGenerated(generatedKeypair); - }); - - function handleGenerated(generatedKeypair) { - // import the new key pair into crypto module - self._crypto.importKeys({ - passphrase: options.passphrase, - privateKeyArmored: generatedKeypair.privateKeyArmored, - publicKeyArmored: generatedKeypair.publicKeyArmored - }, function(err) { - if (err) { - callback(err); - return; - } - - // persist newly generated keypair - var newKeypair = { - publicKey: { - _id: generatedKeypair.keyId, - userId: self._account.emailAddress, - publicKey: generatedKeypair.publicKeyArmored - }, - privateKey: { - _id: generatedKeypair.keyId, - userId: self._account.emailAddress, - encryptedKey: generatedKeypair.privateKeyArmored - } - }; - self._keychain.putUserKeyPair(newKeypair, callback); - }); - } - }; - - EmailDAO.prototype.sync = function(options, callback) { - /* - * Here's how delta sync works: - * delta1: storage > memory => we deleted messages, remove from remote - * delta2: memory > storage => we added messages, push to remote <<< not supported yet - * delta3: memory > imap => we deleted messages directly from the remote, remove from memory and storage - * delta4: imap > memory => we have new messages available, fetch to memory and storage - */ - - var self = this, - folder, - delta1 /*, delta2 */ , delta3, delta4, - isFolderInitialized; - - - // validate options - if (!options.folder) { - callback({ - errMsg: 'Invalid options!' - }); - return; - } - - if (self._account.busy) { - callback({ - errMsg: 'Sync aborted: Previous sync still in progress' - }); - return; - } - - self._account.busy = true; - - folder = _.findWhere(self._account.folders, { - path: options.folder - }); - isFolderInitialized = !! folder.messages; - - // initial filling from local storage is an exception from the normal sync. - // after reading from local storage, do imap sync - if (!isFolderInitialized) { - initFolderMessages(); - return; - } - - doLocalDelta(); - - function initFolderMessages() { - folder.messages = []; - self._localListMessages({ - folder: folder.path - }, function(err, messages) { - if (err) { - self._account.busy = false; - callback(err); - return; - } - - if (_.isEmpty(messages)) { - // if there's nothing here, we're good - callback(); - doImapDelta(); - return; - } - - var after = _.after(messages.length, function() { - callback(); - doImapDelta(); - }); - - messages.forEach(function(message) { - handleMessage(message, function(err, cleartextMessage) { - if (err) { - self._account.busy = false; - callback(err); - return; - } - - folder.messages.push(cleartextMessage); - after(); - }); - }); - }); - } - - function doLocalDelta() { - self._localListMessages({ - folder: folder.path - }, function(err, messages) { - if (err) { - self._account.busy = false; - callback(err); - return; - } - - /* - * delta1: storage > memory => we deleted messages, remove from remote - * delta2: memory > storage => we added messages, push to remote - */ - delta1 = checkDelta(messages, folder.messages); - // delta2 = checkDelta(folder.messages, messages); // not supported yet - - if (_.isEmpty(delta1) /* && _.isEmpty(delta2)*/ ) { - // if there is no delta, head directly to imap sync - callback(); - doImapDelta(); - return; - } - - doDelta1(); - - function doDelta1() { - var after = _.after(delta1.length, function() { - // doDelta2(); when it is implemented - callback(); - doImapDelta(); - }); - - delta1.forEach(function(message) { - var deleteMe = { - folder: folder.path, - uid: message.uid - }; - - self._imapDeleteMessage(deleteMe, function(err) { - if (err) { - self._account.busy = false; - callback(err); - return; - } - - self._localDeleteMessage(deleteMe, function(err) { - if (err) { - self._account.busy = false; - callback(err); - return; - } - - after(); - }); - }); - }); - } - }); - } - - function doImapDelta() { - self._imapListMessages({ - folder: folder.path - }, function(err, headers) { - if (err) { - self._account.busy = false; - callback(err); - return; - } - - // ignore non-whiteout mails - headers = _.without(headers, _.filter(headers, function(header) { - return header.subject.indexOf(str.subjectPrefix) === -1; - })); - - /* - * delta3: memory > imap => we deleted messages directly from the remote, remove from memory and storage - * delta4: imap > memory => we have new messages available, fetch to memory and storage - */ - delta3 = checkDelta(folder.messages, headers); - delta4 = checkDelta(headers, folder.messages); - - if (_.isEmpty(delta3) && _.isEmpty(delta4)) { - // if there is no delta, we're done - self._account.busy = false; - callback(); - return; - } - - doDelta3(); - - // we deleted messages directly from the remote, remove from memory and storage - function doDelta3() { - if (_.isEmpty(delta3)) { - doDelta4(); - return; - } - - var after = _.after(delta3.length, function() { - // we're done with delta 3, so let's continue - doDelta4(); - }); - - delta3.forEach(function(header) { - // remove delta3 from memory - var idx = folder.messages.indexOf(header); - folder.messages.splice(idx, 1); - - // remove delta3 from local storage - self._localDeleteMessage({ - folder: folder.path, - uid: header.uid - }, function(err) { - if (err) { - self._account.busy = false; - callback(err); - return; - } - - after(); - }); - }); - } - - // we have new messages available, fetch to memory and storage - // (downstream sync) - function doDelta4() { - // no delta, we're done here - if (_.isEmpty(delta4)) { - self._account.busy = false; - callback(); - } - - var after = _.after(delta4.length, function() { - self._account.busy = false; - callback(); - }); - - delta4.forEach(function(header) { - // get the whole message - self._imapGetMessage({ - folder: folder.path, - uid: header.uid - }, function(err, message) { - if (err) { - self._account.busy = false; - callback(err); - return; - } - - if (isVerificationMail(message)) { - verify(message, function(err) { - if (err) { - self._account.busy = false; - callback(err); - return; - } - - after(); - }); - return; - } - - // add the encrypted message to the local storage - self._localStoreMessages({ - folder: folder.path, - emails: [message] - }, function(err) { - if (err) { - self._account.busy = false; - callback(err); - return; - } - - // decrypt and add to folder in memory - handleMessage(message, function(err, cleartextMessage) { - if (err) { - self._account.busy = false; - callback(err); - return; - } - - folder.messages.push(cleartextMessage); - after(); - }); - }); - }); - }); - } - }); - } - - /* - * Checks which messages are included in a, but not in b - */ - function checkDelta(a, b) { - var i, msg, exists, - delta = []; - - // find the delta - for (i = a.length - 1; i >= 0; i--) { - msg = a[i]; - exists = _.findWhere(b, { - uid: msg.uid - }); - if (!exists) { - delta.push(msg); - } - } - - return delta; - } - - function isVerificationMail(email) { - return email.subject === str.subjectPrefix + str.verificationSubject; - } - - function verify(email, localCallback) { - var uuid, index, verifyUrlPrefix = config.cloudUrl + config.verificationUrl; - - if (!email.unread) { - // don't bother if the email was already marked as read - localCallback(); - return; - } - - index = email.body.indexOf(verifyUrlPrefix); - if (index === -1) { - // there's no url in the email, so forget about that. - localCallback(); - return; - } - - - uuid = email.body.substr(index + verifyUrlPrefix.length, config.verificationUuidLength); - self._keychain.verifyPublicKey(uuid, function(err) { - if (err) { - localCallback({ - errMsg: 'Verifying your public key failed: ' + err.errMsg - }); - return; - } - - // public key has been verified, mark the message as read, delete it, and ignore it in the future - self.markRead({ - folder: options.folder, - uid: email.uid - }, function(err) { - if (err) { - localCallback(err); - return; - } - - self._imapDeleteMessage({ - folder: options.folder, - uid: email.uid - }, localCallback); - }); - }); - } - - function handleMessage(message, localCallback) { - if (containsArmoredCiphertext(message)) { - decrypt(message, localCallback); - return; - } - - // cleartext mail - localCallback(null, message); - after(); - } - - function containsArmoredCiphertext(email) { - return typeof email.body === 'string' && email.body.indexOf(str.cryptPrefix) !== -1 && email.body.indexOf(str.cryptSuffix) !== -1; - } - - function decrypt(email, localCallback) { - var sender; - - extractArmoredContent(email); - - // fetch public key required to verify signatures - sender = email.from[0].address; - self._keychain.getReceiverPublicKey(sender, function(err, senderPubkey) { - if (err) { - localCallback(err); - return; - } - - if (!senderPubkey) { - // this should only happen if a mail from another channel is in the inbox - setBodyAndContinue('Public key for sender not found!'); - return; - } - - // decrypt and verfiy signatures - self._crypto.decrypt(email.body, senderPubkey.publicKey, function(err, decrypted) { - if (err) { - decrypted = err.errMsg; - } - - setBodyAndContinue(decrypted); - }); - }); - - function extractArmoredContent(email) { - var start = email.body.indexOf(str.cryptPrefix), - end = email.body.indexOf(str.cryptSuffix) + str.cryptSuffix.length; - - // parse email body for encrypted message block - email.body = email.body.substring(start, end); - } - - function setBodyAndContinue(text) { - email.body = text; - localCallback(null, email); - } - } - }; - - EmailDAO.prototype.markRead = function(options, callback) { - this._imapClient.updateFlags({ - path: options.folder, - uid: options.uid, - unread: false - }, callback); - }; - - EmailDAO.prototype.markAnswered = function(options, callback) { - this._imapClient.updateFlags({ - path: options.folder, - uid: options.uid, - answered: true - }, callback); - }; - - EmailDAO.prototype.move = function(options, callback) { - this._imapClient.moveMessage({ - path: options.folder, - uid: options.uid, - destination: options.destination - }, callback); - }; - - EmailDAO.prototype.sendEncrypted = function(options, callback) { - var self = this, - email = options.email; - - // validate the email input - if (!email.to || !email.from || !email.to[0].address || !email.from[0].address) { - callback({ - errMsg: 'Invalid email object!' - }); - return; - } - - // validate email addresses - for (var i = email.to.length - 1; i >= 0; i--) { - if (!util.validateEmailAddress(email.to[i].address)) { - callback({ - errMsg: 'Invalid recipient: ' + email.to[i].address - }); - return; - } - } - - if (!util.validateEmailAddress(email.from[0].address)) { - callback({ - errMsg: 'Invalid sender: ' + email.from - }); - return; - } - - // only support single recipient for e-2-e encryption - // check if receiver has a public key - self._keychain.getReceiverPublicKey(email.to[0].address, function(err, receiverPubkey) { - if (err) { - callback(err); - return; - } - - // validate public key - if (!receiverPubkey) { - callback({ - errMsg: 'User has no public key yet!' - }); - return; - } - - // public key found... encrypt and send - self._encrypt({ - email: email, - keys: receiverPubkey.publicKey - }, function(err, email) { - if (err) { - callback(err); - return; - } - - self._smtpClient.send(email, callback); - }); - }); - }; - - EmailDAO.prototype.sendPlaintext = function(options, callback) { - this._smtpClient.send(options.email, callback); - }; - - // - // Internal API - // - - // Encryption API - - EmailDAO.prototype._encrypt = function(options, callback) { - var self = this, - pt = options.email.body; - - options.keys = [options.keys] || []; - - // get own public key so send message can be read - self._crypto.exportKeys(function(err, ownKeys) { - if (err) { - callback(err); - return; - } - - // add own public key to receiver list - options.keys.push(ownKeys.publicKeyArmored); - // encrypt the email - self._crypto.encrypt(pt, options.keys, function(err, ct) { - if (err) { - callback(err); - return; - } - - // bundle encrypted email together for sending - frameEncryptedMessage(options.email, ct); - callback(null, options.email); - }); - }); - - function frameEncryptedMessage(email, ct) { - var greeting, - message = str.message + '\n\n\n', - signature = '\n\n' + str.signature + '\n\n'; - - // get first name of recipient - greeting = 'Hi ' + (email.to[0].name || email.to[0].address).split('@')[0].split('.')[0].split(' ')[0] + ',\n\n'; - - // build encrypted text body - email.body = greeting + message + ct + signature; - email.subject = email.subject; - } - }; - - // Local Storage API - - EmailDAO.prototype._localListMessages = function(options, callback) { - var dbType = 'email_' + options.folder; - this._devicestorage.listItems(dbType, 0, null, callback); - }; - - EmailDAO.prototype._localStoreMessages = function(options, callback) { - var dbType = 'email_' + options.folder; - this._devicestorage.storeList(options.emails, dbType, callback); - }; - - EmailDAO.prototype._localDeleteMessage = function(options, callback) { - if (!options.folder || !options.uid) { - callback({ - errMsg: 'Invalid options!' - }); - return; - } - var dbType = 'email_' + options.folder + '_' + options.uid; - this._devicestorage.removeList(dbType, callback); - }; - - - // IMAP API - - /** - * Login the imap client - */ - EmailDAO.prototype._imapLogin = function(callback) { - // login IMAP client if existent - this._imapClient.login(callback); - }; - - /** - * Cleanup by logging the user off. - */ - EmailDAO.prototype._imapLogout = function(callback) { - this._imapClient.logout(callback); - }; - - /** - * List messages from an imap folder. This will not yet fetch the email body. - * @param {String} options.folderName The name of the imap folder. - */ - EmailDAO.prototype._imapListMessages = function(options, callback) { - this._imapClient.listMessages({ - path: options.folder, - offset: 0, - length: 100 - }, callback); - }; - - EmailDAO.prototype._imapDeleteMessage = function(options, callback) { - this._imapClient.deleteMessage({ - path: options.folder, - uid: options.uid - }, callback); - }; - - /** - * Get an email messsage including the email body from imap - * @param {String} options.messageId The - */ - EmailDAO.prototype._imapGetMessage = function(options, callback) { - this._imapClient.getMessagePreview({ - path: options.folder, - uid: options.uid - }, callback); - }; - - /** - * List the folders in the user's IMAP mailbox. - */ - EmailDAO.prototype._imapListFolders = function(callback) { - var self = this, - dbType = 'folders'; - - // check local cache - self._devicestorage.listItems(dbType, 0, null, function(err, stored) { - if (err) { - callback(err); - return; - } - - if (!stored || stored.length < 1) { - // no folders cached... fetch from server - fetchFromServer(); - return; - } - - callback(null, stored[0]); - }); - - function fetchFromServer() { - var folders; - - // fetch list from imap server - self._imapClient.listWellKnownFolders(function(err, wellKnownFolders) { - if (err) { - callback(err); - return; - } - - folders = [ - wellKnownFolders.inbox, - wellKnownFolders.sent, { - type: 'Outbox', - path: 'OUTBOX' - }, - wellKnownFolders.drafts, - wellKnownFolders.trash - ]; - - // cache locally - // persist encrypted list in device storage - self._devicestorage.storeList([folders], dbType, function(err) { - if (err) { - callback(err); - return; - } - - callback(null, folders); - }); - }); - } - }; - - // to be removed and solved with IMAP! - EmailDAO.prototype.store = function(email, callback) { - var self = this, - dbType = 'email_OUTBOX'; - - email.id = util.UUID(); - - // encrypt - self._encrypt({ - email: email - }, function(err, email) { - if (err) { - callback(err); - return; - } - - // store to local storage - self._devicestorage.storeList([email], dbType, callback); - }); - }; - - // to be removed and solved with IMAP! - EmailDAO.prototype.list = function(callback) { - var self = this, - dbType = 'email_OUTBOX'; - - self._devicestorage.listItems(dbType, 0, null, function(err, mails) { - if (err) { - callback(err); - return; - } - - if (mails.length === 0) { - callback(null, []); - return; - } - - self._crypto.exportKeys(function(err, ownKeys) { - if (err) { - callback(err); - return; - } - - var after = _.after(mails.length, function() { - callback(null, mails); - }); - - mails.forEach(function(mail) { - mail.body = str.cryptPrefix + mail.body.split(str.cryptPrefix)[1].split(str.cryptSuffix)[0] + str.cryptSuffix; - self._crypto.decrypt(mail.body, ownKeys.publicKeyArmored, function(err, decrypted) { - mail.body = err ? err.errMsg : decrypted; - after(); - }); - }); - - }); - }); - }; - - return EmailDAO; -}); \ No newline at end of file diff --git a/src/js/dao/email-dao.js b/src/js/dao/email-dao.js index 951e8ab..1018ba0 100644 --- a/src/js/dao/email-dao.js +++ b/src/js/dao/email-dao.js @@ -1,15 +1,11 @@ define(function(require) { 'use strict'; - var _ = require('underscore'), - util = require('cryptoLib/util'), + var util = require('cryptoLib/util'), + _ = require('underscore'), str = require('js/app-config').string, config = require('js/app-config').config; - /** - * A high-level Data-Access Api for handling Email synchronization - * between the cloud service and the device's local storage - */ var EmailDAO = function(keychain, imapClient, smtpClient, crypto, devicestorage) { var self = this; @@ -19,21 +15,24 @@ define(function(require) { self._crypto = crypto; self._devicestorage = devicestorage; - // delegation-esque pattern to mitigate between node-style events and plain js - self._imapClient.onIncomingMessage = function(message) { - if (typeof self.onIncomingMessage === 'function') { - self.onIncomingMessage(message); - } - }; + // // delegation-esque pattern to mitigate between node-style events and plain js + // self._imapClient.onIncomingMessage = function(message) { + // if (typeof self.onIncomingMessage === 'function') { + // self.onIncomingMessage(message); + // } + // }; }; - /** - * Inits all dependencies - */ - EmailDAO.prototype.init = function(account, callback) { - var self = this; + // + // External API + // - self._account = account; + EmailDAO.prototype.init = function(options, callback) { + var self = this, + keypair; + + self._account = options.account; + self._account.busy = false; // validate email address var emailAddress = self._account.emailAddress; @@ -56,21 +55,43 @@ define(function(require) { callback(err); return; } - callback(null, storedKeypair); + + keypair = storedKeypair; + initFolders(); + }); + }); + } + + function initFolders() { + self._imapLogin(function(err) { + if (err) { + callback(err); + return; + } + + self._imapListFolders(function(err, folders) { + if (err) { + callback(err); + return; + } + + self._account.folders = folders; + callback(null, keypair); }); }); } }; - EmailDAO.prototype.unlock = function(keypair, passphrase, callback) { + + EmailDAO.prototype.unlock = function(options, callback) { var self = this; - if (keypair && keypair.privateKey && keypair.publicKey) { + if (options.keypair) { // import existing key pair into crypto module self._crypto.importKeys({ - passphrase: passphrase, - privateKeyArmored: keypair.privateKey.encryptedKey, - publicKeyArmored: keypair.publicKey.publicKey + passphrase: options.passphrase, + privateKeyArmored: options.keypair.privateKey.encryptedKey, + publicKeyArmored: options.keypair.publicKey.publicKey }, callback); return; } @@ -79,16 +100,20 @@ define(function(require) { self._crypto.generateKeys({ emailAddress: self._account.emailAddress, keySize: self._account.asymKeySize, - passphrase: passphrase + passphrase: options.passphrase }, function(err, generatedKeypair) { if (err) { callback(err); return; } + handleGenerated(generatedKeypair); + }); + + function handleGenerated(generatedKeypair) { // import the new key pair into crypto module self._crypto.importKeys({ - passphrase: passphrase, + passphrase: options.passphrase, privateKeyArmored: generatedKeypair.privateKeyArmored, publicKeyArmored: generatedKeypair.publicKeyArmored }, function(err) { @@ -112,36 +137,624 @@ define(function(require) { }; self._keychain.putUserKeyPair(newKeypair, callback); }); + } + }; + + EmailDAO.prototype.sync = function(options, callback) { + /* + * Here's how delta sync works: + * delta1: storage > memory => we deleted messages, remove from remote + * delta2: memory > storage => we added messages, push to remote <<< not supported yet + * delta3: memory > imap => we deleted messages directly from the remote, remove from memory and storage + * delta4: imap > memory => we have new messages available, fetch to memory and storage + */ + + var self = this, + folder, + delta1 /*, delta2 */ , delta3, delta4, + isFolderInitialized; + + + // validate options + if (!options.folder) { + callback({ + errMsg: 'Invalid options!' + }); + return; + } + + if (self._account.busy) { + callback({ + errMsg: 'Sync aborted: Previous sync still in progress' + }); + return; + } + + self._account.busy = true; + + folder = _.findWhere(self._account.folders, { + path: options.folder + }); + isFolderInitialized = !! folder.messages; + + // initial filling from local storage is an exception from the normal sync. + // after reading from local storage, do imap sync + if (!isFolderInitialized) { + initFolderMessages(); + return; + } + + doLocalDelta(); + + function initFolderMessages() { + folder.messages = []; + self._localListMessages({ + folder: folder.path + }, function(err, messages) { + if (err) { + self._account.busy = false; + callback(err); + return; + } + + if (_.isEmpty(messages)) { + // if there's nothing here, we're good + callback(); + doImapDelta(); + return; + } + + var after = _.after(messages.length, function() { + callback(); + doImapDelta(); + }); + + messages.forEach(function(message) { + handleMessage(message, function(err, cleartextMessage) { + if (err) { + self._account.busy = false; + callback(err); + return; + } + + folder.messages.push(cleartextMessage); + after(); + }); + }); + }); + } + + function doLocalDelta() { + self._localListMessages({ + folder: folder.path + }, function(err, messages) { + if (err) { + self._account.busy = false; + callback(err); + return; + } + + /* + * delta1: storage > memory => we deleted messages, remove from remote + * delta2: memory > storage => we added messages, push to remote + */ + delta1 = checkDelta(messages, folder.messages); + // delta2 = checkDelta(folder.messages, messages); // not supported yet + + if (_.isEmpty(delta1) /* && _.isEmpty(delta2)*/ ) { + // if there is no delta, head directly to imap sync + callback(); + doImapDelta(); + return; + } + + doDelta1(); + + function doDelta1() { + var after = _.after(delta1.length, function() { + // doDelta2(); when it is implemented + callback(); + doImapDelta(); + }); + + delta1.forEach(function(message) { + var deleteMe = { + folder: folder.path, + uid: message.uid + }; + + self._imapDeleteMessage(deleteMe, function(err) { + if (err) { + self._account.busy = false; + callback(err); + return; + } + + self._localDeleteMessage(deleteMe, function(err) { + if (err) { + self._account.busy = false; + callback(err); + return; + } + + after(); + }); + }); + }); + } + }); + } + + function doImapDelta() { + self._imapListMessages({ + folder: folder.path + }, function(err, headers) { + if (err) { + self._account.busy = false; + callback(err); + return; + } + + // ignore non-whiteout mails + headers = _.without(headers, _.filter(headers, function(header) { + return header.subject.indexOf(str.subjectPrefix) === -1; + })); + + /* + * delta3: memory > imap => we deleted messages directly from the remote, remove from memory and storage + * delta4: imap > memory => we have new messages available, fetch to memory and storage + */ + delta3 = checkDelta(folder.messages, headers); + delta4 = checkDelta(headers, folder.messages); + + if (_.isEmpty(delta3) && _.isEmpty(delta4)) { + // if there is no delta, we're done + self._account.busy = false; + callback(); + return; + } + + doDelta3(); + + // we deleted messages directly from the remote, remove from memory and storage + function doDelta3() { + if (_.isEmpty(delta3)) { + doDelta4(); + return; + } + + var after = _.after(delta3.length, function() { + // we're done with delta 3, so let's continue + doDelta4(); + }); + + delta3.forEach(function(header) { + // remove delta3 from memory + var idx = folder.messages.indexOf(header); + folder.messages.splice(idx, 1); + + // remove delta3 from local storage + self._localDeleteMessage({ + folder: folder.path, + uid: header.uid + }, function(err) { + if (err) { + self._account.busy = false; + callback(err); + return; + } + + after(); + }); + }); + } + + // we have new messages available, fetch to memory and storage + // (downstream sync) + function doDelta4() { + // no delta, we're done here + if (_.isEmpty(delta4)) { + self._account.busy = false; + callback(); + } + + var after = _.after(delta4.length, function() { + self._account.busy = false; + callback(); + }); + + delta4.forEach(function(header) { + // get the whole message + self._imapGetMessage({ + folder: folder.path, + uid: header.uid + }, function(err, message) { + if (err) { + self._account.busy = false; + callback(err); + return; + } + + if (isVerificationMail(message)) { + verify(message, function(err) { + if (err) { + self._account.busy = false; + callback(err); + return; + } + + after(); + }); + return; + } + + // add the encrypted message to the local storage + self._localStoreMessages({ + folder: folder.path, + emails: [message] + }, function(err) { + if (err) { + self._account.busy = false; + callback(err); + return; + } + + // decrypt and add to folder in memory + handleMessage(message, function(err, cleartextMessage) { + if (err) { + self._account.busy = false; + callback(err); + return; + } + + folder.messages.push(cleartextMessage); + after(); + }); + }); + }); + }); + } + }); + } + + /* + * Checks which messages are included in a, but not in b + */ + function checkDelta(a, b) { + var i, msg, exists, + delta = []; + + // find the delta + for (i = a.length - 1; i >= 0; i--) { + msg = a[i]; + exists = _.findWhere(b, { + uid: msg.uid + }); + if (!exists) { + delta.push(msg); + } + } + + return delta; + } + + function isVerificationMail(email) { + return email.subject === str.subjectPrefix + str.verificationSubject; + } + + function verify(email, localCallback) { + var uuid, index, verifyUrlPrefix = config.cloudUrl + config.verificationUrl; + + if (!email.unread) { + // don't bother if the email was already marked as read + localCallback(); + return; + } + + index = email.body.indexOf(verifyUrlPrefix); + if (index === -1) { + // there's no url in the email, so forget about that. + localCallback(); + return; + } + + + uuid = email.body.substr(index + verifyUrlPrefix.length, config.verificationUuidLength); + self._keychain.verifyPublicKey(uuid, function(err) { + if (err) { + localCallback({ + errMsg: 'Verifying your public key failed: ' + err.errMsg + }); + return; + } + + // public key has been verified, mark the message as read, delete it, and ignore it in the future + self.markRead({ + folder: options.folder, + uid: email.uid + }, function(err) { + if (err) { + localCallback(err); + return; + } + + self._imapDeleteMessage({ + folder: options.folder, + uid: email.uid + }, localCallback); + }); + }); + } + + function handleMessage(message, localCallback) { + if (containsArmoredCiphertext(message)) { + decrypt(message, localCallback); + return; + } + + // cleartext mail + localCallback(null, message); + } + + function containsArmoredCiphertext(email) { + return typeof email.body === 'string' && email.body.indexOf(str.cryptPrefix) !== -1 && email.body.indexOf(str.cryptSuffix) !== -1; + } + + function decrypt(email, localCallback) { + var sender; + + extractArmoredContent(email); + + // fetch public key required to verify signatures + sender = email.from[0].address; + self._keychain.getReceiverPublicKey(sender, function(err, senderPubkey) { + if (err) { + localCallback(err); + return; + } + + if (!senderPubkey) { + // this should only happen if a mail from another channel is in the inbox + setBodyAndContinue('Public key for sender not found!'); + return; + } + + // decrypt and verfiy signatures + self._crypto.decrypt(email.body, senderPubkey.publicKey, function(err, decrypted) { + if (err) { + decrypted = err.errMsg; + } + + setBodyAndContinue(decrypted); + }); + }); + + function extractArmoredContent(email) { + var start = email.body.indexOf(str.cryptPrefix), + end = email.body.indexOf(str.cryptSuffix) + str.cryptSuffix.length; + + // parse email body for encrypted message block + email.body = email.body.substring(start, end); + } + + function setBodyAndContinue(text) { + email.body = text; + localCallback(null, email); + } + } + }; + + EmailDAO.prototype.markRead = function(options, callback) { + this._imapClient.updateFlags({ + path: options.folder, + uid: options.uid, + unread: false + }, callback); + }; + + EmailDAO.prototype.markAnswered = function(options, callback) { + this._imapClient.updateFlags({ + path: options.folder, + uid: options.uid, + answered: true + }, callback); + }; + + EmailDAO.prototype.move = function(options, callback) { + this._imapClient.moveMessage({ + path: options.folder, + uid: options.uid, + destination: options.destination + }, callback); + }; + + EmailDAO.prototype.sendEncrypted = function(options, callback) { + var self = this, + email = options.email; + + // validate the email input + if (!email.to || !email.from || !email.to[0].address || !email.from[0].address) { + callback({ + errMsg: 'Invalid email object!' + }); + return; + } + + // validate email addresses + for (var i = email.to.length - 1; i >= 0; i--) { + if (!util.validateEmailAddress(email.to[i].address)) { + callback({ + errMsg: 'Invalid recipient: ' + email.to[i].address + }); + return; + } + } + + if (!util.validateEmailAddress(email.from[0].address)) { + callback({ + errMsg: 'Invalid sender: ' + email.from + }); + return; + } + + // only support single recipient for e-2-e encryption + // check if receiver has a public key + self._keychain.getReceiverPublicKey(email.to[0].address, function(err, receiverPubkey) { + if (err) { + callback(err); + return; + } + + // validate public key + if (!receiverPubkey) { + callback({ + errMsg: 'User has no public key yet!' + }); + return; + } + + // public key found... encrypt and send + self._encrypt({ + email: email, + keys: receiverPubkey.publicKey + }, function(err, email) { + if (err) { + callback(err); + return; + } + + self._smtpClient.send(email, callback); + }); }); }; + EmailDAO.prototype.sendPlaintext = function(options, callback) { + this._smtpClient.send(options.email, callback); + }; + // - // IMAP Apis + // Internal API // + // Encryption API + + EmailDAO.prototype._encrypt = function(options, callback) { + var self = this, + pt = options.email.body; + + options.keys = [options.keys] || []; + + // get own public key so send message can be read + self._crypto.exportKeys(function(err, ownKeys) { + if (err) { + callback(err); + return; + } + + // add own public key to receiver list + options.keys.push(ownKeys.publicKeyArmored); + // encrypt the email + self._crypto.encrypt(pt, options.keys, function(err, ct) { + if (err) { + callback(err); + return; + } + + // bundle encrypted email together for sending + frameEncryptedMessage(options.email, ct); + callback(null, options.email); + }); + }); + + function frameEncryptedMessage(email, ct) { + var greeting, + message = str.message + '\n\n\n', + signature = '\n\n' + str.signature + '\n\n'; + + // get first name of recipient + greeting = 'Hi ' + (email.to[0].name || email.to[0].address).split('@')[0].split('.')[0].split(' ')[0] + ',\n\n'; + + // build encrypted text body + email.body = greeting + message + ct + signature; + email.subject = email.subject; + } + }; + + // Local Storage API + + EmailDAO.prototype._localListMessages = function(options, callback) { + var dbType = 'email_' + options.folder; + this._devicestorage.listItems(dbType, 0, null, callback); + }; + + EmailDAO.prototype._localStoreMessages = function(options, callback) { + var dbType = 'email_' + options.folder; + this._devicestorage.storeList(options.emails, dbType, callback); + }; + + EmailDAO.prototype._localDeleteMessage = function(options, callback) { + if (!options.folder || !options.uid) { + callback({ + errMsg: 'Invalid options!' + }); + return; + } + var dbType = 'email_' + options.folder + '_' + options.uid; + this._devicestorage.removeList(dbType, callback); + }; + + + // IMAP API + /** * Login the imap client */ - EmailDAO.prototype.imapLogin = function(callback) { - var self = this; - + EmailDAO.prototype._imapLogin = function(callback) { // login IMAP client if existent - self._imapClient.login(callback); + this._imapClient.login(callback); }; /** * Cleanup by logging the user off. */ - EmailDAO.prototype.destroy = function(callback) { - var self = this; + EmailDAO.prototype._imapLogout = function(callback) { + this._imapClient.logout(callback); + }; - self._imapClient.logout(callback); + /** + * List messages from an imap folder. This will not yet fetch the email body. + * @param {String} options.folderName The name of the imap folder. + */ + EmailDAO.prototype._imapListMessages = function(options, callback) { + this._imapClient.listMessages({ + path: options.folder, + offset: 0, + length: 100 + }, callback); + }; + + EmailDAO.prototype._imapDeleteMessage = function(options, callback) { + this._imapClient.deleteMessage({ + path: options.folder, + uid: options.uid + }, callback); + }; + + /** + * Get an email messsage including the email body from imap + * @param {String} options.messageId The + */ + EmailDAO.prototype._imapGetMessage = function(options, callback) { + this._imapClient.getMessagePreview({ + path: options.folder, + uid: options.uid + }, callback); }; /** * List the folders in the user's IMAP mailbox. */ - EmailDAO.prototype.imapListFolders = function(callback) { + EmailDAO.prototype._imapListFolders = function(callback) { var self = this, dbType = 'folders'; @@ -195,475 +808,7 @@ define(function(require) { } }; - /** - * Get the number of unread message for a folder - */ - EmailDAO.prototype.unreadMessages = function(path, callback) { - var self = this; - - self._imapClient.unreadMessages(path, callback); - }; - - /** - * Fetch a list of emails from the device's local storage - */ - EmailDAO.prototype.listMessages = function(options, callback) { - var self = this, - cleartextList = []; - - // validate options - if (!options.folder) { - callback({ - errMsg: 'Invalid options!' - }); - return; - } - options.offset = (typeof options.offset === 'undefined') ? 0 : options.offset; - options.num = (typeof options.num === 'undefined') ? null : options.num; - - // fetch items from device storage - self._devicestorage.listItems('email_' + options.folder, options.offset, options.num, function(err, emails) { - if (err) { - callback(err); - return; - } - - if (emails.length === 0) { - callback(null, cleartextList); - return; - } - - var after = _.after(emails.length, function() { - callback(null, cleartextList); - }); - - _.each(emails, function(email) { - handleMail(email, after); - }); - }); - - function handleMail(email, localCallback) { - // remove subject filter prefix - email.subject = email.subject.split(str.subjectPrefix)[1]; - - // encrypted mail - if (isPGPMail(email)) { - email = parseMessageBlock(email); - decrypt(email, localCallback); - return; - } - - // verification mail - if (isVerificationMail(email)) { - verify(email, localCallback); - return; - } - - // cleartext mail - cleartextList.push(email); - localCallback(); - } - - function isPGPMail(email) { - return typeof email.body === 'string' && email.body.indexOf(str.cryptPrefix) !== -1 && email.body.indexOf(str.cryptSuffix) !== -1; - } - - function isVerificationMail(email) { - return email.subject === str.verificationSubject; - } - - function parseMessageBlock(email) { - var messageBlock; - - // parse email body for encrypted message block - // get ascii armored message block by prefix and suffix - messageBlock = email.body.split(str.cryptPrefix)[1].split(str.cryptSuffix)[0]; - // add prefix and suffix again - email.body = str.cryptPrefix + messageBlock + str.cryptSuffix; - - return email; - } - - function decrypt(email, localCallback) { - // fetch public key required to verify signatures - var sender = email.from[0].address; - self._keychain.getReceiverPublicKey(sender, function(err, senderPubkey) { - if (err) { - callback(err); - return; - } - - if (!senderPubkey) { - // this should only happen if a mail from another channel is in the inbox - setBodyAndContinue('Public key for sender not found!'); - return; - } - - // decrypt and verfiy signatures - self._crypto.decrypt(email.body, senderPubkey.publicKey, function(err, decrypted) { - if (err) { - decrypted = err.errMsg; - } - - setBodyAndContinue(decrypted); - }); - }); - - function setBodyAndContinue(text) { - email.body = text; - cleartextList.push(email); - localCallback(); - } - } - - function verify(email, localCallback) { - var uuid, index, - verifiyUrlPrefix = config.cloudUrl + config.verificationUrl; - - if (!email.unread) { - // don't bother if the email was already marked as read - localCallback(); - return; - } - - index = email.body.indexOf(verifiyUrlPrefix); - if (index === -1) { - localCallback(); - return; - } - - uuid = email.body.split(config.verificationUrl)[1]; - self._keychain.verifyPublicKey(uuid, function(err) { - if (err) { - callback({ - errMsg: 'Verifying your public key failed: ' + err.errMsg - }); - return; - } - - // public key has been verified, mark the message as read, delete it, and ignore it in the future - self.imapMarkMessageRead({ - folder: options.folder, - uid: email.uid - }, function(err) { - if (err) { - // if marking the mail as read failed, don't bother - localCallback(); - return; - } - - self.imapDeleteMessage({ - folder: options.folder, - uid: email.uid - }, function() { - localCallback(); - }); - }); - }); - } - }; - - /** - * High level sync operation for the delta from the user's IMAP inbox - */ - EmailDAO.prototype.imapSync = function(options, callback) { - var self = this, - dbType = 'email_' + options.folder; - - fetchList(function(err, emails) { - if (err) { - callback(err); - return; - } - - // delete old items from db - self._devicestorage.removeList(dbType, function(err) { - if (err) { - callback(err); - return; - } - - // persist encrypted list in device storage - self._devicestorage.storeList(emails, dbType, callback); - }); - }); - - function fetchList(callback) { - var headers = []; - - // fetch imap folder's message list - self.imapListMessages({ - folder: options.folder, - offset: options.offset, - num: options.num - }, function(err, emails) { - if (err) { - callback(err); - return; - } - - // find encrypted messages by subject - emails.forEach(function(i) { - if (typeof i.subject === 'string' && i.subject.indexOf(str.subjectPrefix) !== -1) { - headers.push(i); - } - }); - - // fetch message bodies - fetchBodies(headers, callback); - }); - } - - function fetchBodies(headers, callback) { - var emails = []; - - if (headers.length < 1) { - callback(null, emails); - return; - } - - var after = _.after(headers.length, function() { - callback(null, emails); - }); - - _.each(headers, function(header) { - self.imapGetMessage({ - folder: options.folder, - uid: header.uid - }, function(err, message) { - if (err) { - callback(err); - return; - } - - // set gotten attributes like body to message object containing list meta data like 'unread' or 'replied' - header.id = message.id; - header.body = message.body; - header.html = message.html; - header.attachments = message.attachments; - - emails.push(header); - after(); - }); - }); - } - }; - - /** - * List messages from an imap folder. This will not yet fetch the email body. - * @param {String} options.folderName The name of the imap folder. - * @param {Number} options.offset The offset of items to fetch (0 is the last stored item) - * @param {Number} options.num The number of items to fetch (null means fetch all) - */ - EmailDAO.prototype.imapListMessages = function(options, callback) { - var self = this; - - self._imapClient.listMessages({ - path: options.folder, - offset: options.offset, - length: options.num - }, callback); - }; - - /** - * Get an email messsage including the email body from imap - * @param {String} options.messageId The - */ - EmailDAO.prototype.imapGetMessage = function(options, callback) { - var self = this; - - self._imapClient.getMessagePreview({ - path: options.folder, - uid: options.uid - }, callback); - }; - - EmailDAO.prototype.imapMoveMessage = function(options, callback) { - var self = this; - - self._imapClient.moveMessage({ - path: options.folder, - uid: options.uid, - destination: options.destination - }, moved); - - function moved(err) { - if (err) { - callback(err); - return; - } - - // delete from local db - self._devicestorage.removeList('email_' + options.folder + '_' + options.uid, callback); - } - }; - - EmailDAO.prototype.imapDeleteMessage = function(options, callback) { - var self = this; - - self._imapClient.deleteMessage({ - path: options.folder, - uid: options.uid - }, moved); - - function moved(err) { - if (err) { - callback(err); - return; - } - - // delete from local db - self._devicestorage.removeList('email_' + options.folder + '_' + options.uid, callback); - } - }; - - EmailDAO.prototype.imapMarkMessageRead = function(options, callback) { - var self = this; - - self._imapClient.updateFlags({ - path: options.folder, - uid: options.uid, - unread: false - }, callback); - }; - - EmailDAO.prototype.imapMarkAnswered = function(options, callback) { - var self = this; - - self._imapClient.updateFlags({ - path: options.folder, - uid: options.uid, - answered: true - }, callback); - }; - - // - // SMTP Apis - // - - /** - * Send an email client side via STMP. - */ - EmailDAO.prototype.encryptedSend = function(email, callback) { - var self = this, - invalidRecipient; - - // validate the email input - if (!email.to || !email.from || !email.to[0].address || !email.from[0].address) { - callback({ - errMsg: 'Invalid email object!' - }); - return; - } - - // validate email addresses - _.each(email.to, function(i) { - if (!util.validateEmailAddress(i.address)) { - invalidRecipient = i.address; - } - }); - if (invalidRecipient) { - callback({ - errMsg: 'Invalid recipient: ' + invalidRecipient - }); - return; - } - if (!util.validateEmailAddress(email.from[0].address)) { - callback({ - errMsg: 'Invalid sender: ' + email.from - }); - return; - } - - // only support single recipient for e-2-e encryption - // check if receiver has a public key - self._keychain.getReceiverPublicKey(email.to[0].address, function(err, receiverPubkey) { - if (err) { - callback(err); - return; - } - - // validate public key - if (!receiverPubkey) { - callback({ - errMsg: 'User has no public key yet!' - }); - return; - } - - // public key found... encrypt and send - self.encryptForUser({ - email: email, - receiverPubkey: receiverPubkey.publicKey - }, function(err, email) { - if (err) { - callback(err); - return; - } - - self.send(email, callback); - }); - }); - }; - - /** - * Encrypt an email asymmetrically for an exisiting user with their public key - */ - EmailDAO.prototype.encryptForUser = function(options, callback) { - var self = this, - pt = options.email.body, - receiverPubkeys = options.receiverPubkey ? [options.receiverPubkey] : []; - - // get own public key so send message can be read - self._crypto.exportKeys(function(err, ownKeys) { - if (err) { - callback(err); - return; - } - - // add own public key to receiver list - receiverPubkeys.push(ownKeys.publicKeyArmored); - // encrypt the email - self._crypto.encrypt(pt, receiverPubkeys, function(err, ct) { - if (err) { - callback(err); - return; - } - - // bundle encrypted email together for sending - frameEncryptedMessage(options.email, ct); - callback(null, options.email); - }); - }); - }; - - /** - * Frames an encrypted message in base64 Format. - */ - function frameEncryptedMessage(email, ct) { - var to, greeting; - - var MESSAGE = str.message + '\n\n\n', - SIGNATURE = '\n\n' + str.signature + '\n\n'; - - // get first name of recipient - to = (email.to[0].name || email.to[0].address).split('@')[0].split('.')[0].split(' ')[0]; - greeting = 'Hi ' + to + ',\n\n'; - - // build encrypted text body - email.body = greeting + MESSAGE + ct + SIGNATURE; - email.subject = str.subjectPrefix + email.subject; - } - - /** - * Send an actual message object via smtp - */ - EmailDAO.prototype.send = function(email, callback) { - var self = this; - - self._smtpClient.send(email, callback); - }; - + // to be removed and solved with IMAP! EmailDAO.prototype.store = function(email, callback) { var self = this, dbType = 'email_OUTBOX'; @@ -671,7 +816,7 @@ define(function(require) { email.id = util.UUID(); // encrypt - self.encryptForUser({ + self._encrypt({ email: email }, function(err, email) { if (err) { @@ -684,6 +829,7 @@ define(function(require) { }); }; + // to be removed and solved with IMAP! EmailDAO.prototype.list = function(callback) { var self = this, dbType = 'email_OUTBOX'; @@ -713,7 +859,6 @@ define(function(require) { mail.body = str.cryptPrefix + mail.body.split(str.cryptPrefix)[1].split(str.cryptSuffix)[0] + str.cryptSuffix; self._crypto.decrypt(mail.body, ownKeys.publicKeyArmored, function(err, decrypted) { mail.body = err ? err.errMsg : decrypted; - mail.subject = mail.subject.split(str.subjectPrefix)[1]; after(); }); }); diff --git a/src/tpl/mail-list.html b/src/tpl/mail-list.html index aaae82c..96a564c 100644 --- a/src/tpl/mail-list.html +++ b/src/tpl/mail-list.html @@ -4,9 +4,9 @@

{{state.nav.currentFolder.type}}

-
+
    -
  • +
  • {{email.from[0].name || email.from[0].address}}

    {{email.subject || 'No subject'}}

    @@ -17,7 +17,7 @@
-