mirror of
https://github.com/moparisthebest/mail
synced 2024-11-26 02:42:17 -05:00
integrate new email-dao into controllers and first attempt at starting app
This commit is contained in:
parent
7542cf8589
commit
58ed8928e6
@ -222,7 +222,9 @@ define(function(require) {
|
|||||||
asymKeySize: config.asymKeySize
|
asymKeySize: config.asymKeySize
|
||||||
};
|
};
|
||||||
|
|
||||||
self._emailDao.init(account, callback);
|
self._emailDao.init({
|
||||||
|
account: account
|
||||||
|
}, callback);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -209,7 +209,7 @@ define(function(require) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// send invitation mail
|
// send invitation mail
|
||||||
self._emailDao.send(invitationMail, function(err) {
|
self._emailDao.sendPlaintext(invitationMail, function(err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
self._outboxBusy = false;
|
self._outboxBusy = false;
|
||||||
callback(err);
|
callback(err);
|
||||||
@ -222,7 +222,7 @@ define(function(require) {
|
|||||||
|
|
||||||
function sendEncrypted(email) {
|
function sendEncrypted(email) {
|
||||||
removeFromPendingMails(email);
|
removeFromPendingMails(email);
|
||||||
self._emailDao.encryptedSend(email, function(err) {
|
self._emailDao.sendEncrypted(email, function(err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
self._outboxBusy = false;
|
self._outboxBusy = false;
|
||||||
callback(err);
|
callback(err);
|
||||||
|
@ -37,7 +37,11 @@ define(function(require) {
|
|||||||
handleError(err);
|
handleError(err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
emailDao.unlock(keypair, $scope.passphrase, onUnlock);
|
|
||||||
|
emailDao.unlock({
|
||||||
|
keypair: keypair,
|
||||||
|
passphrase: $scope.passphrase
|
||||||
|
}, onUnlock);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,17 +45,9 @@ define(function(require) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// login to imap backend
|
|
||||||
appController._emailDao.imapLogin(function(err) {
|
|
||||||
if (err) {
|
|
||||||
$scope.onError(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
redirect(availableKeys);
|
redirect(availableKeys);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function redirect(availableKeys) {
|
function redirect(availableKeys) {
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
define(function(require) {
|
define(function(require) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var _ = require('underscore'),
|
var angular = require('angular'),
|
||||||
angular = require('angular'),
|
|
||||||
appController = require('js/app-controller'),
|
appController = require('js/app-controller'),
|
||||||
IScroll = require('iscroll'),
|
IScroll = require('iscroll'),
|
||||||
str = require('js/app-config').string,
|
str = require('js/app-config').string,
|
||||||
@ -10,9 +9,7 @@ define(function(require) {
|
|||||||
emailDao, outboxBo;
|
emailDao, outboxBo;
|
||||||
|
|
||||||
var MailListCtrl = function($scope) {
|
var MailListCtrl = function($scope) {
|
||||||
var offset = 0,
|
var firstSelect = true;
|
||||||
num = 100,
|
|
||||||
firstSelect = true;
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Init
|
// Init
|
||||||
@ -21,36 +18,37 @@ define(function(require) {
|
|||||||
emailDao = appController._emailDao;
|
emailDao = appController._emailDao;
|
||||||
outboxBo = appController._outboxBo;
|
outboxBo = appController._outboxBo;
|
||||||
|
|
||||||
if (emailDao) {
|
// // push handler
|
||||||
emailDao.onIncomingMessage = function(email) {
|
// if (emailDao) {
|
||||||
if (email.subject.indexOf(str.subjectPrefix) === -1) {
|
// emailDao.onIncomingMessage = function(email) {
|
||||||
return;
|
// if (email.subject.indexOf(str.subjectPrefix) === -1) {
|
||||||
}
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
// sync
|
// // sync
|
||||||
$scope.synchronize(function() {
|
// $scope.synchronize(function() {
|
||||||
// show notification
|
// // show notification
|
||||||
notificationForEmail(email);
|
// notificationForEmail(email);
|
||||||
});
|
// });
|
||||||
};
|
// };
|
||||||
chrome.notifications.onClicked.addListener(notificationClicked);
|
// chrome.notifications.onClicked.addListener(notificationClicked);
|
||||||
}
|
// }
|
||||||
|
|
||||||
function notificationClicked(uidString) {
|
// function notificationClicked(uidString) {
|
||||||
var email, uid = parseInt(uidString, 10);
|
// var email, uid = parseInt(uidString, 10);
|
||||||
|
|
||||||
if (isNaN(uid)) {
|
// if (isNaN(uid)) {
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
email = _.findWhere($scope.emails, {
|
// email = _.findWhere(getFolder().messages, {
|
||||||
uid: uid
|
// uid: uid
|
||||||
});
|
// });
|
||||||
|
|
||||||
if (email) {
|
// if (email) {
|
||||||
$scope.select(email);
|
// $scope.select(email);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
//
|
//
|
||||||
// scope functions
|
// scope functions
|
||||||
@ -64,11 +62,11 @@ define(function(require) {
|
|||||||
|
|
||||||
$scope.state.mailList.selected = email;
|
$scope.state.mailList.selected = email;
|
||||||
|
|
||||||
// mark selected message as 'read'
|
// // mark selected message as 'read'
|
||||||
markAsRead(email);
|
// markAsRead(email);
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.synchronize = function(callback) {
|
$scope.synchronize = function() {
|
||||||
// if we're in the outbox, don't do an imap sync
|
// if we're in the outbox, don't do an imap sync
|
||||||
if (getFolder().type === 'Outbox') {
|
if (getFolder().type === 'Outbox') {
|
||||||
updateStatus('Last update: ', new Date());
|
updateStatus('Last update: ', new Date());
|
||||||
@ -77,23 +75,22 @@ define(function(require) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateStatus('Syncing ...');
|
updateStatus('Syncing ...');
|
||||||
// sync from imap to local db
|
|
||||||
syncImapFolder({
|
// let email dao handle sync transparently
|
||||||
folder: getFolder().path,
|
emailDao.sync({
|
||||||
offset: -num,
|
folder: getFolder().path
|
||||||
num: offset
|
}, function(err) {
|
||||||
}, function() {
|
if (err) {
|
||||||
// list again from local db after syncing
|
updateStatus('Error on sync!');
|
||||||
listLocalMessages({
|
$scope.onError(err);
|
||||||
folder: getFolder().path,
|
return;
|
||||||
offset: offset,
|
|
||||||
num: num
|
|
||||||
}, function() {
|
|
||||||
updateStatus('Last update: ', new Date());
|
|
||||||
if (callback) {
|
|
||||||
callback();
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
// sort emails
|
||||||
|
displayEmails(getFolder().messages);
|
||||||
|
|
||||||
|
// display last update
|
||||||
|
updateStatus('Last update: ', new Date());
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -102,83 +99,83 @@ define(function(require) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var index, currentFolder, trashFolder, outboxFolder;
|
// var index, currentFolder, trashFolder, outboxFolder;
|
||||||
|
|
||||||
currentFolder = getFolder();
|
// currentFolder = getFolder();
|
||||||
|
|
||||||
trashFolder = _.findWhere($scope.folders, {
|
// trashFolder = _.findWhere($scope.folders, {
|
||||||
type: 'Trash'
|
// type: 'Trash'
|
||||||
});
|
// });
|
||||||
|
|
||||||
outboxFolder = _.findWhere($scope.folders, {
|
// outboxFolder = _.findWhere($scope.folders, {
|
||||||
type: 'Outbox'
|
// type: 'Outbox'
|
||||||
});
|
// });
|
||||||
|
|
||||||
if (currentFolder === outboxFolder) {
|
// if (currentFolder === outboxFolder) {
|
||||||
$scope.onError({
|
// $scope.onError({
|
||||||
errMsg: 'Deleting messages from the outbox is not yet supported.'
|
// errMsg: 'Deleting messages from the outbox is not yet supported.'
|
||||||
});
|
// });
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (currentFolder === trashFolder) {
|
// if (currentFolder === trashFolder) {
|
||||||
$scope.state.dialog = {
|
// $scope.state.dialog = {
|
||||||
open: true,
|
// open: true,
|
||||||
title: 'Delete',
|
// title: 'Delete',
|
||||||
message: 'Delete this message permanently?',
|
// message: 'Delete this message permanently?',
|
||||||
callback: function(ok) {
|
// callback: function(ok) {
|
||||||
if (!ok) {
|
// if (!ok) {
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
removeLocalAndShowNext();
|
// removeLocalAndShowNext();
|
||||||
removeRemote();
|
// removeRemote();
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
removeLocalAndShowNext();
|
// removeLocalAndShowNext();
|
||||||
removeRemote();
|
// removeRemote();
|
||||||
|
|
||||||
function removeLocalAndShowNext() {
|
// function removeLocalAndShowNext() {
|
||||||
index = $scope.emails.indexOf(email);
|
// index = getFolder().messages.indexOf(email);
|
||||||
// show the next mail
|
// // show the next mail
|
||||||
if ($scope.emails.length > 1) {
|
// 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),
|
// // 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)
|
// // 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]);
|
// $scope.select(_.last(getFolder().messages) === email ? getFolder().messages[index - 1] : getFolder().messages[index + 1]);
|
||||||
} else {
|
// } else {
|
||||||
// if we have only one email in the array, show nothing
|
// // if we have only one email in the array, show nothing
|
||||||
$scope.select();
|
// $scope.select();
|
||||||
$scope.state.mailList.selected = undefined;
|
// $scope.state.mailList.selected = undefined;
|
||||||
}
|
// }
|
||||||
$scope.emails.splice(index, 1);
|
// getFolder().messages.splice(index, 1);
|
||||||
}
|
// }
|
||||||
|
|
||||||
function removeRemote() {
|
// function removeRemote() {
|
||||||
if (getFolder() === trashFolder) {
|
// if (getFolder() === trashFolder) {
|
||||||
emailDao.imapDeleteMessage({
|
// emailDao.imapDeleteMessage({
|
||||||
folder: getFolder().path,
|
// folder: getFolder().path,
|
||||||
uid: email.uid
|
// uid: email.uid
|
||||||
}, moved);
|
// }, moved);
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
emailDao.imapMoveMessage({
|
// emailDao.imapMoveMessage({
|
||||||
folder: getFolder().path,
|
// folder: getFolder().path,
|
||||||
uid: email.uid,
|
// uid: email.uid,
|
||||||
destination: trashFolder.path
|
// destination: trashFolder.path
|
||||||
}, moved);
|
// }, moved);
|
||||||
}
|
// }
|
||||||
|
|
||||||
function moved(err) {
|
// function moved(err) {
|
||||||
if (err) {
|
// if (err) {
|
||||||
$scope.emails.splice(index, 0, email);
|
// getFolder().messages.splice(index, 0, email);
|
||||||
$scope.onError(err);
|
// $scope.onError(err);
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope._stopWatchTask = $scope.$watch('state.nav.currentFolder', function() {
|
$scope._stopWatchTask = $scope.$watch('state.nav.currentFolder', function() {
|
||||||
@ -190,8 +187,8 @@ define(function(require) {
|
|||||||
if (!window.chrome || !chrome.identity) {
|
if (!window.chrome || !chrome.identity) {
|
||||||
firstSelect = true;
|
firstSelect = true;
|
||||||
updateStatus('Last update: ', new Date());
|
updateStatus('Last update: ', new Date());
|
||||||
$scope.emails = createDummyMails();
|
getFolder().messages = createDummyMails();
|
||||||
$scope.select($scope.emails[0]);
|
displayEmails(getFolder().messages);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -204,23 +201,11 @@ define(function(require) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateStatus('Read cache ...');
|
displayEmails(getFolder().messages);
|
||||||
|
|
||||||
// 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();
|
$scope.synchronize();
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
// share local scope functions with root state
|
// share local scope functions with root state
|
||||||
$scope.state.mailList = {
|
$scope.state.mailList = {
|
||||||
remove: $scope.remove,
|
remove: $scope.remove,
|
||||||
@ -231,51 +216,14 @@ define(function(require) {
|
|||||||
// helper functions
|
// helper functions
|
||||||
//
|
//
|
||||||
|
|
||||||
function notificationForEmail(email) {
|
// function notificationForEmail(email) {
|
||||||
chrome.notifications.create('' + email.uid, {
|
// chrome.notifications.create('' + email.uid, {
|
||||||
type: 'basic',
|
// type: 'basic',
|
||||||
title: email.from[0].address,
|
// title: email.from[0].address,
|
||||||
message: email.subject.split(str.subjectPrefix)[1],
|
// message: email.subject.split(str.subjectPrefix)[1],
|
||||||
iconUrl: chrome.runtime.getURL(cfg.iconPath)
|
// iconUrl: chrome.runtime.getURL(cfg.iconPath)
|
||||||
}, function() {});
|
// }, 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 updateStatus(lbl, time) {
|
function updateStatus(lbl, time) {
|
||||||
$scope.lastUpdateLbl = lbl;
|
$scope.lastUpdateLbl = lbl;
|
||||||
@ -284,69 +232,58 @@ define(function(require) {
|
|||||||
|
|
||||||
function displayEmails(emails) {
|
function displayEmails(emails) {
|
||||||
if (!emails || emails.length < 1) {
|
if (!emails || emails.length < 1) {
|
||||||
$scope.emails = [];
|
|
||||||
$scope.select();
|
$scope.select();
|
||||||
$scope.$apply();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// sort by uid
|
// select first message
|
||||||
emails = _.sortBy(emails, function(e) {
|
$scope.select(emails[emails.length - 1]);
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFolder() {
|
function getFolder() {
|
||||||
return $scope.state.nav.currentFolder;
|
return $scope.state.nav.currentFolder;
|
||||||
}
|
}
|
||||||
|
|
||||||
function markAsRead(email) {
|
// function markAsRead(email) {
|
||||||
// marking mails as read is meaningless in the outbox
|
// // marking mails as read is meaningless in the outbox
|
||||||
if (getFolder().type === 'Outbox') {
|
// if (getFolder().type === 'Outbox') {
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
// don't mark top selected email automatically
|
// // don't mark top selected email automatically
|
||||||
if (firstSelect) {
|
// if (firstSelect) {
|
||||||
firstSelect = false;
|
// firstSelect = false;
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
$scope.state.read.toggle(true);
|
// $scope.state.read.toggle(true);
|
||||||
if (!window.chrome || !chrome.socket) {
|
// if (!window.chrome || !chrome.socket) {
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (!email.unread) {
|
// if (!email.unread) {
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
email.unread = false;
|
// email.unread = false;
|
||||||
emailDao.imapMarkMessageRead({
|
// emailDao.imapMarkMessageRead({
|
||||||
folder: getFolder().path,
|
// folder: getFolder().path,
|
||||||
uid: email.uid
|
// uid: email.uid
|
||||||
}, function(err) {
|
// }, function(err) {
|
||||||
if (err) {
|
// if (err) {
|
||||||
updateStatus('Error marking read!');
|
// updateStatus('Error marking read!');
|
||||||
$scope.onError(err);
|
// $scope.onError(err);
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
};
|
};
|
||||||
|
|
||||||
function createDummyMails() {
|
function createDummyMails() {
|
||||||
|
var uid = 0;
|
||||||
|
|
||||||
var Email = function(unread, attachments, answered, html) {
|
var Email = function(unread, attachments, answered, html) {
|
||||||
this.uid = '1';
|
this.uid = uid++;
|
||||||
this.from = [{
|
this.from = [{
|
||||||
name: 'Whiteout Support',
|
name: 'Whiteout Support',
|
||||||
address: 'support@whiteout.io'
|
address: 'support@whiteout.io'
|
||||||
|
@ -42,7 +42,7 @@ define(function(require) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var outbox = _.findWhere($scope.folders, {
|
var outbox = _.findWhere($scope.account.folders, {
|
||||||
type: 'Outbox'
|
type: 'Outbox'
|
||||||
});
|
});
|
||||||
outbox.count = count;
|
outbox.count = count;
|
||||||
@ -54,40 +54,29 @@ define(function(require) {
|
|||||||
//
|
//
|
||||||
|
|
||||||
// init folders
|
// init folders
|
||||||
initFolders(function(folders) {
|
initFolders();
|
||||||
$scope.folders = folders;
|
|
||||||
// select inbox as the current folder on init
|
// select inbox as the current folder on init
|
||||||
$scope.openFolder($scope.folders[0]);
|
$scope.openFolder($scope.account.folders[0]);
|
||||||
});
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// helper functions
|
// helper functions
|
||||||
//
|
//
|
||||||
|
|
||||||
function initFolders(callback) {
|
function initFolders() {
|
||||||
if (window.chrome && chrome.identity) {
|
if (window.chrome && chrome.identity) {
|
||||||
emailDao.imapListFolders(function(err, folders) {
|
// get pointer to account/folder/message tree on root scope
|
||||||
if (err) {
|
$scope.$root.account = emailDao._account;
|
||||||
$scope.onError(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
folders.forEach(function(f) {
|
|
||||||
f.count = 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
// start checking outbox periodically
|
// start checking outbox periodically
|
||||||
outboxBo.startChecking($scope.onOutboxUpdate);
|
outboxBo.startChecking($scope.onOutboxUpdate);
|
||||||
// make function available globally for write controller
|
// make function available globally for write controller
|
||||||
$scope.emptyOutbox = outboxBo._processOutbox.bind(outboxBo);
|
$scope.emptyOutbox = outboxBo._processOutbox.bind(outboxBo);
|
||||||
|
|
||||||
callback(folders);
|
|
||||||
$scope.$apply();
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
callback([{
|
// attach dummy folders for development
|
||||||
|
$scope.$root.account = {};
|
||||||
|
$scope.account.folders = [{
|
||||||
type: 'Inbox',
|
type: 'Inbox',
|
||||||
count: 2,
|
count: 2,
|
||||||
path: 'INBOX'
|
path: 'INBOX'
|
||||||
@ -107,7 +96,7 @@ define(function(require) {
|
|||||||
type: 'Trash',
|
type: 'Trash',
|
||||||
count: 0,
|
count: 0,
|
||||||
path: 'TRASH'
|
path: 'TRASH'
|
||||||
}]);
|
}];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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;
|
|
||||||
});
|
|
File diff suppressed because it is too large
Load Diff
@ -4,9 +4,9 @@
|
|||||||
<h2>{{state.nav.currentFolder.type}}</h2>
|
<h2>{{state.nav.currentFolder.type}}</h2>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="list-wrapper" ng-iscroll="emails">
|
<div class="list-wrapper" ng-iscroll="state.nav.currentFolder.messages">
|
||||||
<ul class="mail-list">
|
<ul class="mail-list">
|
||||||
<li ng-class="{'mail-list-active': email === state.mailList.selected, 'mail-list-attachment': email.attachments !== undefined && email.attachments.length > 0, 'mail-list-unread': email.unread && !email.answered, 'mail-list-replied': email.answered}" ng-click="select(email)" ng-repeat="email in emails">
|
<li ng-class="{'mail-list-active': email === state.mailList.selected, 'mail-list-attachment': email.attachments !== undefined && email.attachments.length > 0, 'mail-list-unread': email.unread && !email.answered, 'mail-list-replied': email.answered}" ng-click="select(email)" ng-repeat="email in state.nav.currentFolder.messages | orderBy:'uid':true">
|
||||||
<h3>{{email.from[0].name || email.from[0].address}}</h3>
|
<h3>{{email.from[0].name || email.from[0].address}}</h3>
|
||||||
<div class="head">
|
<div class="head">
|
||||||
<p class="subject">{{email.subject || 'No subject'}}</p>
|
<p class="subject">{{email.subject || 'No subject'}}</p>
|
||||||
@ -17,7 +17,7 @@
|
|||||||
</ul><!--/.mail-list-->
|
</ul><!--/.mail-list-->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer ng-class="{syncing: lastUpdateLbl === 'Syncing ...'}" ng-click="synchronize()">
|
<footer ng-class="{syncing: account.busy}" ng-click="synchronize()">
|
||||||
<span class="spinner"></span>
|
<span class="spinner"></span>
|
||||||
<span class="text">{{lastUpdateLbl}} {{lastUpdate | date:'shortTime'}}</span>
|
<span class="text">{{lastUpdateLbl}} {{lastUpdate | date:'shortTime'}}</span>
|
||||||
</footer>
|
</footer>
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<ul class="nav-primary">
|
<ul class="nav-primary">
|
||||||
<li ng-repeat="folder in folders" ng-switch="folder.count !== undefined">
|
<li ng-repeat="folder in account.folders" ng-switch="folder.count !== undefined">
|
||||||
<a href="#" ng-click="openFolder(folder); $event.preventDefault()">
|
<a href="#" ng-click="openFolder(folder); $event.preventDefault()">
|
||||||
{{folder.type}}
|
{{folder.type}}
|
||||||
<span class="label-wrapper" ng-switch-when="true"><span class="label label-light">{{folder.count}}</span></span>
|
<span class="label-wrapper" ng-switch-when="true"><span class="label label-light">{{folder.count}}</span></span>
|
||||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -21,8 +21,8 @@ define(function(require) {
|
|||||||
var hasChrome, hasIdentity;
|
var hasChrome, hasIdentity;
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
hasChrome = !!window.chrome;
|
hasChrome = !! window.chrome;
|
||||||
hasIdentity = !!window.chrome.identity;
|
hasIdentity = !! window.chrome.identity;
|
||||||
window.chrome = window.chrome || {};
|
window.chrome = window.chrome || {};
|
||||||
window.chrome.identity = window.chrome.identity || {};
|
window.chrome.identity = window.chrome.identity || {};
|
||||||
|
|
||||||
@ -66,15 +66,12 @@ define(function(require) {
|
|||||||
publicKey: 'b'
|
publicKey: 'b'
|
||||||
});
|
});
|
||||||
|
|
||||||
emailDaoMock.imapLogin.yields();
|
|
||||||
|
|
||||||
angular.module('logintest', []);
|
angular.module('logintest', []);
|
||||||
mocks.module('logintest');
|
mocks.module('logintest');
|
||||||
mocks.inject(function($controller, $rootScope, $location) {
|
mocks.inject(function($controller, $rootScope, $location) {
|
||||||
location = $location;
|
location = $location;
|
||||||
sinon.stub(location, 'path', function(path) {
|
sinon.stub(location, 'path', function(path) {
|
||||||
expect(path).to.equal('/login-existing');
|
expect(path).to.equal('/login-existing');
|
||||||
expect(emailDaoMock.imapLogin.calledOnce).to.be.true;
|
|
||||||
expect(startAppStub.calledOnce).to.be.true;
|
expect(startAppStub.calledOnce).to.be.true;
|
||||||
expect(checkForUpdateStub.calledOnce).to.be.true;
|
expect(checkForUpdateStub.calledOnce).to.be.true;
|
||||||
expect(fetchOAuthStub.calledOnce).to.be.true;
|
expect(fetchOAuthStub.calledOnce).to.be.true;
|
||||||
@ -103,15 +100,12 @@ define(function(require) {
|
|||||||
publicKey: 'b'
|
publicKey: 'b'
|
||||||
});
|
});
|
||||||
|
|
||||||
emailDaoMock.imapLogin.yields();
|
|
||||||
|
|
||||||
angular.module('logintest', []);
|
angular.module('logintest', []);
|
||||||
mocks.module('logintest');
|
mocks.module('logintest');
|
||||||
mocks.inject(function($controller, $rootScope, $location) {
|
mocks.inject(function($controller, $rootScope, $location) {
|
||||||
location = $location;
|
location = $location;
|
||||||
sinon.stub(location, 'path', function(path) {
|
sinon.stub(location, 'path', function(path) {
|
||||||
expect(path).to.equal('/login-new-device');
|
expect(path).to.equal('/login-new-device');
|
||||||
expect(emailDaoMock.imapLogin.calledOnce).to.be.true;
|
|
||||||
expect(startAppStub.calledOnce).to.be.true;
|
expect(startAppStub.calledOnce).to.be.true;
|
||||||
expect(checkForUpdateStub.calledOnce).to.be.true;
|
expect(checkForUpdateStub.calledOnce).to.be.true;
|
||||||
expect(fetchOAuthStub.calledOnce).to.be.true;
|
expect(fetchOAuthStub.calledOnce).to.be.true;
|
||||||
@ -138,15 +132,12 @@ define(function(require) {
|
|||||||
initStub = sinon.stub(appController, 'init');
|
initStub = sinon.stub(appController, 'init');
|
||||||
initStub.yields();
|
initStub.yields();
|
||||||
|
|
||||||
emailDaoMock.imapLogin.yields();
|
|
||||||
|
|
||||||
angular.module('logintest', []);
|
angular.module('logintest', []);
|
||||||
mocks.module('logintest');
|
mocks.module('logintest');
|
||||||
mocks.inject(function($controller, $rootScope, $location) {
|
mocks.inject(function($controller, $rootScope, $location) {
|
||||||
location = $location;
|
location = $location;
|
||||||
sinon.stub(location, 'path', function(path) {
|
sinon.stub(location, 'path', function(path) {
|
||||||
expect(path).to.equal('/login-initial');
|
expect(path).to.equal('/login-initial');
|
||||||
expect(emailDaoMock.imapLogin.calledOnce).to.be.true;
|
|
||||||
expect(startAppStub.calledOnce).to.be.true;
|
expect(startAppStub.calledOnce).to.be.true;
|
||||||
expect(checkForUpdateStub.calledOnce).to.be.true;
|
expect(checkForUpdateStub.calledOnce).to.be.true;
|
||||||
expect(fetchOAuthStub.calledOnce).to.be.true;
|
expect(fetchOAuthStub.calledOnce).to.be.true;
|
||||||
|
@ -31,7 +31,6 @@ function startTests() {
|
|||||||
require(
|
require(
|
||||||
[
|
[
|
||||||
'test/new-unit/email-dao-test',
|
'test/new-unit/email-dao-test',
|
||||||
'test/new-unit/email-dao-2-test',
|
|
||||||
'test/new-unit/app-controller-test',
|
'test/new-unit/app-controller-test',
|
||||||
'test/new-unit/pgp-test',
|
'test/new-unit/pgp-test',
|
||||||
'test/new-unit/rest-dao-test',
|
'test/new-unit/rest-dao-test',
|
||||||
|
@ -21,15 +21,21 @@ define(function(require) {
|
|||||||
origEmailDao = appController._emailDao;
|
origEmailDao = appController._emailDao;
|
||||||
|
|
||||||
emailDaoMock = sinon.createStubInstance(EmailDAO);
|
emailDaoMock = sinon.createStubInstance(EmailDAO);
|
||||||
|
emailDaoMock._account = {
|
||||||
|
folders: [{
|
||||||
|
type: 'Inbox',
|
||||||
|
count: 2,
|
||||||
|
path: 'INBOX'
|
||||||
|
}, {
|
||||||
|
type: 'Outbox',
|
||||||
|
count: 0,
|
||||||
|
path: 'OUTBOX'
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
outboxFolder = emailDaoMock._account.folders[1];
|
||||||
appController._emailDao = emailDaoMock;
|
appController._emailDao = emailDaoMock;
|
||||||
outboxBoMock = sinon.createStubInstance(OutboxBO);
|
outboxBoMock = sinon.createStubInstance(OutboxBO);
|
||||||
appController._outboxBo = outboxBoMock;
|
appController._outboxBo = outboxBoMock;
|
||||||
|
|
||||||
// for outbox checking
|
|
||||||
outboxFolder = {
|
|
||||||
type: 'Outbox'
|
|
||||||
};
|
|
||||||
emailDaoMock.imapListFolders.yields(null, [outboxFolder]);
|
|
||||||
outboxBoMock.startChecking.returns();
|
outboxBoMock.startChecking.returns();
|
||||||
|
|
||||||
angular.module('navigationtest', []);
|
angular.module('navigationtest', []);
|
||||||
@ -55,7 +61,7 @@ define(function(require) {
|
|||||||
it('should be well defined', function() {
|
it('should be well defined', function() {
|
||||||
expect(scope.state).to.exist;
|
expect(scope.state).to.exist;
|
||||||
expect(scope.state.nav.open).to.be.false;
|
expect(scope.state.nav.open).to.be.false;
|
||||||
expect(scope.folders).to.not.be.empty;
|
expect(scope.account.folders).to.not.be.empty;
|
||||||
|
|
||||||
expect(scope.onError).to.exist;
|
expect(scope.onError).to.exist;
|
||||||
expect(scope.openFolder).to.exist;
|
expect(scope.openFolder).to.exist;
|
||||||
@ -86,7 +92,6 @@ define(function(require) {
|
|||||||
it('should work', function() {
|
it('should work', function() {
|
||||||
var callback;
|
var callback;
|
||||||
|
|
||||||
expect(emailDaoMock.imapListFolders.callCount).to.equal(1);
|
|
||||||
expect(outboxBoMock.startChecking.callCount).to.equal(1);
|
expect(outboxBoMock.startChecking.callCount).to.equal(1);
|
||||||
|
|
||||||
outboxBoMock.startChecking.calledWith(sinon.match(function(cb) {
|
outboxBoMock.startChecking.calledWith(sinon.match(function(cb) {
|
||||||
|
@ -80,8 +80,8 @@ define(function(require) {
|
|||||||
dummyMails = [member, invited, notinvited];
|
dummyMails = [member, invited, notinvited];
|
||||||
|
|
||||||
emailDaoStub.list.yieldsAsync(null, dummyMails);
|
emailDaoStub.list.yieldsAsync(null, dummyMails);
|
||||||
emailDaoStub.encryptedSend.yieldsAsync();
|
emailDaoStub.sendEncrypted.yieldsAsync();
|
||||||
emailDaoStub.send.yieldsAsync();
|
emailDaoStub.sendPlaintext.yieldsAsync();
|
||||||
devicestorageStub.removeList.yieldsAsync();
|
devicestorageStub.removeList.yieldsAsync();
|
||||||
invitationDaoStub.check.withArgs(sinon.match(function(o) {
|
invitationDaoStub.check.withArgs(sinon.match(function(o) {
|
||||||
return o.recipient === 'invited@whiteout.io';
|
return o.recipient === 'invited@whiteout.io';
|
||||||
@ -102,8 +102,8 @@ define(function(require) {
|
|||||||
var check = _.after(dummyMails.length + 1, function() {
|
var check = _.after(dummyMails.length + 1, function() {
|
||||||
expect(unsentCount).to.equal(2);
|
expect(unsentCount).to.equal(2);
|
||||||
expect(emailDaoStub.list.callCount).to.equal(1);
|
expect(emailDaoStub.list.callCount).to.equal(1);
|
||||||
expect(emailDaoStub.encryptedSend.callCount).to.equal(1);
|
expect(emailDaoStub.sendEncrypted.callCount).to.equal(1);
|
||||||
expect(emailDaoStub.send.callCount).to.equal(1);
|
expect(emailDaoStub.sendPlaintext.callCount).to.equal(1);
|
||||||
expect(devicestorageStub.removeList.callCount).to.equal(1);
|
expect(devicestorageStub.removeList.callCount).to.equal(1);
|
||||||
expect(invitationDaoStub.check.callCount).to.equal(2);
|
expect(invitationDaoStub.check.callCount).to.equal(2);
|
||||||
expect(invitationDaoStub.invite.callCount).to.equal(1);
|
expect(invitationDaoStub.invite.callCount).to.equal(1);
|
||||||
|
Loading…
Reference in New Issue
Block a user