1
0
mirror of https://github.com/moparisthebest/mail synced 2024-11-29 12:22:22 -05:00

integrate new email-dao into controllers and first attempt at starting app

This commit is contained in:
Tankred Hase 2013-12-03 19:21:50 +01:00
parent 7542cf8589
commit 58ed8928e6
16 changed files with 2325 additions and 3972 deletions

View File

@ -222,7 +222,9 @@ define(function(require) {
asymKeySize: config.asymKeySize asymKeySize: config.asymKeySize
}; };
self._emailDao.init(account, callback); self._emailDao.init({
account: account
}, callback);
} }
}; };

View File

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

View File

@ -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);
}); });
} }

View File

@ -45,15 +45,7 @@ define(function(require) {
return; return;
} }
// login to imap backend redirect(availableKeys);
appController._emailDao.imapLogin(function(err) {
if (err) {
$scope.onError(err);
return;
}
redirect(availableKeys);
});
}); });
}); });
} }

View File

@ -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() { // sort emails
updateStatus('Last update: ', new Date()); displayEmails(getFolder().messages);
if (callback) {
callback(); // 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,21 +201,9 @@ 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
@ -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'

View File

@ -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.account.folders[0]);
$scope.openFolder($scope.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) { // start checking outbox periodically
f.count = 0; outboxBo.startChecking($scope.onOutboxUpdate);
}); // make function available globally for write controller
$scope.emptyOutbox = outboxBo._processOutbox.bind(outboxBo);
// 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();
});
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'
}]); }];
} }
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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