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 @@
-