mirror of
https://github.com/moparisthebest/mail
synced 2024-10-31 15:25:01 -04:00
commit
a730cad49d
@ -11,10 +11,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"crypto-lib": "https://github.com/whiteout-io/crypto-lib/tarball/v0.1.1",
|
||||
"imap-client": "https://github.com/whiteout-io/imap-client/tarball/v0.3.2",
|
||||
"mailreader": "https://github.com/whiteout-io/mailreader/tarball/v0.3.2",
|
||||
"pgpmailer": "https://github.com/whiteout-io/pgpmailer/tarball/v0.3.2",
|
||||
"pgpbuilder": "https://github.com/whiteout-io/pgpbuilder/tarball/v0.3.2",
|
||||
"imap-client": "https://github.com/whiteout-io/imap-client/tarball/v0.3.3",
|
||||
"mailreader": "https://github.com/whiteout-io/mailreader/tarball/v0.3.3",
|
||||
"pgpmailer": "https://github.com/whiteout-io/pgpmailer/tarball/v0.3.3",
|
||||
"pgpbuilder": "https://github.com/whiteout-io/pgpbuilder/tarball/v0.3.3",
|
||||
"requirejs": "2.1.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -46,15 +46,16 @@ define(function(require) {
|
||||
iconPath: '/img/icon.png',
|
||||
verificationUrl: '/verify/',
|
||||
verificationUuidLength: 36,
|
||||
dbVersion: 2,
|
||||
appVersion: appVersion
|
||||
dbVersion: 3,
|
||||
appVersion: appVersion,
|
||||
outboxMailboxPath: 'OUTBOX',
|
||||
outboxMailboxType: 'Outbox'
|
||||
};
|
||||
|
||||
/**
|
||||
* Strings are maintained here
|
||||
*/
|
||||
app.string = {
|
||||
subjectPrefix: '[whiteout] ',
|
||||
fallbackSubject: '(no subject)',
|
||||
invitationSubject: 'Invitation to a private conversation',
|
||||
invitationMessage: 'Hi,\n\nI use Whiteout Mail to send and receive encrypted email. I would like to exchange encrypted messages with you as well.\n\nPlease install the Whiteout Mail application. This application makes it easy to read and write messages securely with PGP encryption applied.\n\nGo to the Whiteout Networks homepage to learn more and to download the application: https://whiteout.io\n\n',
|
||||
@ -63,7 +64,7 @@ define(function(require) {
|
||||
cryptSuffix: '-----END PGP MESSAGE-----',
|
||||
signature: '\n\n\n--\nSent from Whiteout Mail - Email encryption for the rest of us\nhttps://whiteout.io\n\n',
|
||||
webSite: 'http://whiteout.io',
|
||||
verificationSubject: 'New public key uploaded',
|
||||
verificationSubject: '[whiteout] New public key uploaded',
|
||||
sendBtnClear: 'Send',
|
||||
sendBtnSecure: 'Send securely'
|
||||
};
|
||||
|
@ -15,7 +15,6 @@ define(function(require) {
|
||||
RestDAO = require('js/dao/rest-dao'),
|
||||
EmailDAO = require('js/dao/email-dao'),
|
||||
config = require('js/app-config').config,
|
||||
EmailSync = require('js/dao/email-sync'),
|
||||
KeychainDAO = require('js/dao/keychain-dao'),
|
||||
PublicKeyDAO = require('js/dao/publickey-dao'),
|
||||
LawnchairDAO = require('js/dao/lawnchair-dao'),
|
||||
@ -43,18 +42,18 @@ define(function(require) {
|
||||
function onDeviceReady() {
|
||||
console.log('Starting app.');
|
||||
|
||||
self.buildModules();
|
||||
self.buildModules(options);
|
||||
|
||||
// Handle offline and online gracefully
|
||||
window.addEventListener('online', self.onConnect.bind(self, options.onError));
|
||||
window.addEventListener('offline', self.onDisconnect.bind(self, options.onError));
|
||||
window.addEventListener('offline', self.onDisconnect.bind(self));
|
||||
|
||||
self._appConfigStore.init('app-config', callback);
|
||||
}
|
||||
};
|
||||
|
||||
self.buildModules = function() {
|
||||
var lawnchairDao, restDao, pubkeyDao, emailDao, emailSync, keychain, pgp, userStorage, pgpbuilder, oauth, appConfigStore;
|
||||
self.buildModules = function(options) {
|
||||
var lawnchairDao, restDao, pubkeyDao, emailDao, keychain, pgp, userStorage, pgpbuilder, oauth, appConfigStore;
|
||||
|
||||
// start the mailreader's worker thread
|
||||
mailreader.startWorker(config.workerPath + '/../lib/mailreader-parser-worker.js');
|
||||
@ -72,18 +71,19 @@ define(function(require) {
|
||||
self._keychain = keychain = new KeychainDAO(lawnchairDao, pubkeyDao);
|
||||
self._crypto = pgp = new PGP();
|
||||
self._pgpbuilder = pgpbuilder = new PgpBuilder();
|
||||
self._emailSync = emailSync = new EmailSync(keychain, userStorage, mailreader);
|
||||
self._emailDao = emailDao = new EmailDAO(keychain, pgp, userStorage, pgpbuilder, mailreader, emailSync);
|
||||
self._emailDao = emailDao = new EmailDAO(keychain, pgp, userStorage, pgpbuilder, mailreader);
|
||||
self._outboxBo = new OutboxBO(emailDao, keychain, userStorage);
|
||||
self._updateHandler = new UpdateHandler(appConfigStore, userStorage);
|
||||
|
||||
emailDao.onError = options.onError;
|
||||
};
|
||||
|
||||
self.isOnline = function() {
|
||||
return navigator.onLine;
|
||||
};
|
||||
|
||||
self.onDisconnect = function(callback) {
|
||||
self._emailDao.onDisconnect(null, callback);
|
||||
self.onDisconnect = function() {
|
||||
self._emailDao.onDisconnect();
|
||||
};
|
||||
|
||||
self.onConnect = function(callback) {
|
||||
|
@ -60,7 +60,7 @@ define(function(require) {
|
||||
allReaders = mail.from.concat(mail.to.concat(mail.cc.concat(mail.bcc))); // all the users that should be able to read the mail
|
||||
|
||||
mail.publicKeysArmored = []; // gather the public keys
|
||||
mail.id = util.UUID(); // the mail needs a random uuid for storage in the database
|
||||
mail.uid = mail.id = util.UUID(); // the mail needs a random id & uid for storage in the database
|
||||
|
||||
// do not encrypt mails with a bcc recipient, due to a possible privacy leak
|
||||
if (mail.bcc.length > 0) {
|
||||
@ -216,7 +216,7 @@ define(function(require) {
|
||||
|
||||
// removes the mail object from disk after successfully sending it
|
||||
function removeFromStorage(mail, done) {
|
||||
self._devicestorage.removeList(outboxDb + '_' + mail.id, function(err) {
|
||||
self._devicestorage.removeList(outboxDb + '_' + mail.uid, function(err) {
|
||||
if (err) {
|
||||
self._outboxBusy = false;
|
||||
callback(err);
|
||||
|
@ -6,7 +6,7 @@ define(function(require) {
|
||||
appController = require('js/app-controller'),
|
||||
IScroll = require('iscroll'),
|
||||
notification = require('js/util/notification'),
|
||||
emailDao, outboxBo, emailSync;
|
||||
emailDao, outboxBo;
|
||||
|
||||
var MailListCtrl = function($scope, $timeout) {
|
||||
//
|
||||
@ -15,7 +15,6 @@ define(function(require) {
|
||||
|
||||
emailDao = appController._emailDao;
|
||||
outboxBo = appController._outboxBo;
|
||||
emailSync = appController._emailSync;
|
||||
|
||||
//
|
||||
// scope functions
|
||||
@ -23,7 +22,7 @@ define(function(require) {
|
||||
|
||||
$scope.getBody = function(email) {
|
||||
emailDao.getBody({
|
||||
folder: currentFolder().path,
|
||||
folder: currentFolder(),
|
||||
message: email
|
||||
}, function(err) {
|
||||
if (err && err.code !== 42) {
|
||||
@ -35,7 +34,7 @@ define(function(require) {
|
||||
$scope.$digest();
|
||||
|
||||
// automatically decrypt if it's the selected email
|
||||
if (email === $scope.state.mailList.selected) {
|
||||
if (email === currentMessage()) {
|
||||
emailDao.decryptBody({
|
||||
message: email
|
||||
}, $scope.onError);
|
||||
@ -66,49 +65,24 @@ define(function(require) {
|
||||
return;
|
||||
}
|
||||
|
||||
email.unread = false;
|
||||
$scope.synchronize();
|
||||
$scope.toggleUnread(email);
|
||||
};
|
||||
|
||||
/**
|
||||
* Mark an email as unread or read, respectively
|
||||
*/
|
||||
$scope.toggleUnread = function(email) {
|
||||
email.unread = !email.unread;
|
||||
$scope.synchronize();
|
||||
};
|
||||
|
||||
/**
|
||||
* Synchronize the selected imap folder to local storage
|
||||
*/
|
||||
$scope.synchronize = function(options) {
|
||||
updateStatus('Syncing ...');
|
||||
|
||||
options = options || {};
|
||||
options.folder = options.folder || currentFolder().path;
|
||||
|
||||
// let email dao handle sync transparently
|
||||
if (currentFolder().type === 'Outbox') {
|
||||
emailDao.syncOutbox({
|
||||
folder: currentFolder().path
|
||||
}, done);
|
||||
} else {
|
||||
emailDao.sync({
|
||||
folder: options.folder || currentFolder().path
|
||||
}, done);
|
||||
}
|
||||
|
||||
|
||||
function done(err) {
|
||||
if (err && err.code === 409) {
|
||||
// sync still busy
|
||||
return;
|
||||
}
|
||||
$scope.toggleUnread = function(message) {
|
||||
updateStatus('Updating unread flag...');
|
||||
|
||||
message.unread = !message.unread;
|
||||
emailDao.setFlags({
|
||||
folder: currentFolder(),
|
||||
message: message
|
||||
}, function(err) {
|
||||
if (err && err.code === 42) {
|
||||
// offline
|
||||
updateStatus('Offline mode');
|
||||
$scope.$apply();
|
||||
// offline, restore
|
||||
message.unread = !message.unread;
|
||||
updateStatus('Unable to mark unread flag in offline mode!');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -118,59 +92,46 @@ define(function(require) {
|
||||
return;
|
||||
}
|
||||
|
||||
// display last update
|
||||
updateStatus('Last update: ', new Date());
|
||||
|
||||
// do not change the selection if we just updated another folder in the background
|
||||
if (currentFolder().path === options.folder) {
|
||||
selectFirstMessage();
|
||||
}
|
||||
|
||||
updateStatus('Flag updated!');
|
||||
$scope.$apply();
|
||||
|
||||
// fetch visible bodies at the end of a successful sync
|
||||
$scope.loadVisibleBodies();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete an email by moving it to the trash folder or purging it.
|
||||
* Delete a message
|
||||
*/
|
||||
$scope.remove = function(email) {
|
||||
if (!email) {
|
||||
$scope.remove = function(message) {
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentFolder().type === 'Outbox') {
|
||||
$scope.onError({
|
||||
errMsg: 'Deleting messages from the outbox is not yet supported.'
|
||||
updateStatus('Deleting message...');
|
||||
remove();
|
||||
|
||||
function remove() {
|
||||
emailDao.deleteMessage({
|
||||
folder: currentFolder(),
|
||||
message: message
|
||||
}, function(err) {
|
||||
if (err) {
|
||||
// show errors where appropriate
|
||||
if (err.code === 42) {
|
||||
$scope.select(message);
|
||||
updateStatus('Unable to delete message in offline mode!');
|
||||
return;
|
||||
}
|
||||
updateStatus('Error during delete!');
|
||||
$scope.onError(err);
|
||||
}
|
||||
updateStatus('Message deleted!');
|
||||
$scope.$apply();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
removeAndShowNext();
|
||||
$scope.synchronize();
|
||||
|
||||
function removeAndShowNext() {
|
||||
var index = currentFolder().messages.indexOf(email);
|
||||
// show the next mail
|
||||
if (currentFolder().messages.length > 1) {
|
||||
// if we're about to delete the last entry of the array, show the previous (i.e. the one below in the list),
|
||||
// otherwise show the next one (i.e. the one above in the list)
|
||||
$scope.select(_.last(currentFolder().messages) === email ? currentFolder().messages[index - 1] : currentFolder().messages[index + 1]);
|
||||
} else {
|
||||
// if we have only one email in the array, show nothing
|
||||
$scope.select();
|
||||
$scope.state.mailList.selected = undefined;
|
||||
}
|
||||
currentFolder().messages.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
// share local scope functions with root state
|
||||
$scope.state.mailList = {
|
||||
remove: $scope.remove,
|
||||
synchronize: $scope.synchronize
|
||||
remove: $scope.remove
|
||||
};
|
||||
|
||||
//
|
||||
@ -185,81 +146,79 @@ define(function(require) {
|
||||
return;
|
||||
}
|
||||
|
||||
// development... display dummy mail objects
|
||||
// in development, display dummy mail objects
|
||||
if (!window.chrome || !chrome.identity) {
|
||||
updateStatus('Last update: ', new Date());
|
||||
currentFolder().messages = createDummyMails();
|
||||
selectFirstMessage();
|
||||
return;
|
||||
}
|
||||
|
||||
// production... in chrome packaged app
|
||||
|
||||
// unselect selection from old folder
|
||||
$scope.select();
|
||||
// display and select first
|
||||
selectFirstMessage();
|
||||
|
||||
$scope.synchronize();
|
||||
openCurrentFolder();
|
||||
});
|
||||
|
||||
$scope.$watchCollection('state.nav.currentFolder.messages', selectFirstMessage);
|
||||
|
||||
function selectFirstMessage(messages) {
|
||||
if (!messages) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Shows the next message based on the uid of the currently selected element
|
||||
if (messages.indexOf(currentMessage()) === -1) {
|
||||
// wait until after first $digest() so $scope.filteredMessages is set
|
||||
$timeout(function() {
|
||||
$scope.select($scope.filteredMessages ? $scope.filteredMessages[0] : undefined);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync current folder when client comes back online
|
||||
*/
|
||||
$scope.$watch('account.online', function(isOnline) {
|
||||
if (isOnline) {
|
||||
$scope.synchronize();
|
||||
updateStatus('Online');
|
||||
openCurrentFolder();
|
||||
} else {
|
||||
updateStatus('Offline mode');
|
||||
}
|
||||
}, true);
|
||||
|
||||
//
|
||||
// helper functions
|
||||
// Helper Functions
|
||||
//
|
||||
|
||||
function openCurrentFolder() {
|
||||
emailDao.openFolder({
|
||||
folder: currentFolder()
|
||||
}, function(error) {
|
||||
if (error && error.code === 42) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.onError(error);
|
||||
});
|
||||
}
|
||||
|
||||
function updateStatus(lbl, time) {
|
||||
$scope.lastUpdateLbl = lbl;
|
||||
$scope.lastUpdate = (time) ? time : '';
|
||||
}
|
||||
|
||||
function selectFirstMessage() {
|
||||
// wait until after first $digest() so $scope.filteredMessages is set
|
||||
$timeout(function() {
|
||||
var emails = $scope.filteredMessages;
|
||||
|
||||
if (!emails || emails.length < 1) {
|
||||
$scope.select();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$scope.state.mailList.selected) {
|
||||
// select first message
|
||||
$scope.select(emails[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function currentFolder() {
|
||||
return $scope.state.nav.currentFolder;
|
||||
}
|
||||
|
||||
if (!emailDao || !emailSync) {
|
||||
return; // development mode
|
||||
function currentMessage() {
|
||||
return $scope.state.mailList.selected;
|
||||
}
|
||||
|
||||
emailDao.onNeedsSync = function(error, folder) {
|
||||
if (error) {
|
||||
$scope.onError(error);
|
||||
return;
|
||||
}
|
||||
//
|
||||
// Notification API
|
||||
//
|
||||
|
||||
$scope.synchronize({
|
||||
folder: folder
|
||||
});
|
||||
};
|
||||
|
||||
emailSync.onIncomingMessage = function(msgs) {
|
||||
(emailDao || {}).onIncomingMessage = function(msgs) {
|
||||
var popupId, popupTitle, popupMessage, unreadMsgs;
|
||||
|
||||
unreadMsgs = msgs.filter(function(msg) {
|
||||
@ -299,6 +258,81 @@ define(function(require) {
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Directives
|
||||
//
|
||||
|
||||
var ngModule = angular.module('mail-list', []);
|
||||
|
||||
ngModule.directive('ngIscroll', function($timeout) {
|
||||
return {
|
||||
link: function(scope, elm, attrs) {
|
||||
var model = attrs.ngIscroll,
|
||||
listEl = elm[0],
|
||||
myScroll;
|
||||
|
||||
/*
|
||||
* iterates over the mails in the mail list and loads their bodies if they are visible in the viewport
|
||||
*/
|
||||
scope.loadVisibleBodies = function() {
|
||||
var listBorder = listEl.getBoundingClientRect(),
|
||||
top = listBorder.top,
|
||||
bottom = listBorder.bottom,
|
||||
listItems = listEl.children[0].children,
|
||||
inViewport = false,
|
||||
listItem, message,
|
||||
isPartiallyVisibleTop, isPartiallyVisibleBottom, isVisible;
|
||||
|
||||
for (var i = 0, len = listItems.length; i < len; i++) {
|
||||
// the n-th list item (the dom representation of an email) corresponds to
|
||||
// the n-th message model in the filteredMessages array
|
||||
listItem = listItems.item(i).getBoundingClientRect();
|
||||
|
||||
if (!scope.filteredMessages || scope.filteredMessages.length <= i) {
|
||||
// stop if i get larger than the size of filtered messages
|
||||
break;
|
||||
}
|
||||
message = scope.filteredMessages[i];
|
||||
|
||||
|
||||
isPartiallyVisibleTop = listItem.top < top && listItem.bottom > top; // a portion of the list item is visible on the top
|
||||
isPartiallyVisibleBottom = listItem.top < bottom && listItem.bottom > bottom; // a portion of the list item is visible on the bottom
|
||||
isVisible = listItem.top >= top && listItem.bottom <= bottom; // the list item is visible as a whole
|
||||
|
||||
if (isPartiallyVisibleTop || isVisible || isPartiallyVisibleBottom) {
|
||||
// we are now iterating over visible elements
|
||||
inViewport = true;
|
||||
// load mail body of visible
|
||||
scope.getBody(message);
|
||||
} else if (inViewport) {
|
||||
// we are leaving the viewport, so stop iterating over the items
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// activate iscroll
|
||||
myScroll = new IScroll(listEl, {
|
||||
mouseWheel: true,
|
||||
scrollbars: true,
|
||||
fadeScrollbars: true
|
||||
});
|
||||
myScroll.on('scrollEnd', scope.loadVisibleBodies);
|
||||
|
||||
// refresh iScroll when model length changes
|
||||
scope.$watchCollection(model, function() {
|
||||
$timeout(function() {
|
||||
myScroll.refresh();
|
||||
});
|
||||
// load the visible message bodies, when the list is re-initialized and when scrolling stopped
|
||||
scope.loadVisibleBodies();
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Helper for development mode
|
||||
|
||||
function createDummyMails() {
|
||||
var uid = 0;
|
||||
|
||||
@ -428,78 +462,5 @@ define(function(require) {
|
||||
return dummys;
|
||||
}
|
||||
|
||||
//
|
||||
// Directives
|
||||
//
|
||||
|
||||
var ngModule = angular.module('mail-list', []);
|
||||
|
||||
ngModule.directive('ngIscroll', function($timeout) {
|
||||
return {
|
||||
link: function(scope, elm, attrs) {
|
||||
var model = attrs.ngIscroll,
|
||||
listEl = elm[0],
|
||||
myScroll;
|
||||
|
||||
/*
|
||||
* iterates over the mails in the mail list and loads their bodies if they are visible in the viewport
|
||||
*/
|
||||
scope.loadVisibleBodies = function() {
|
||||
var listBorder = listEl.getBoundingClientRect(),
|
||||
top = listBorder.top,
|
||||
bottom = listBorder.bottom,
|
||||
listItems = listEl.children[0].children,
|
||||
inViewport = false,
|
||||
listItem, message,
|
||||
isPartiallyVisibleTop, isPartiallyVisibleBottom, isVisible;
|
||||
|
||||
for (var i = 0, len = listItems.length; i < len; i++) {
|
||||
// the n-th list item (the dom representation of an email) corresponds to
|
||||
// the n-th message model in the filteredMessages array
|
||||
listItem = listItems.item(i).getBoundingClientRect();
|
||||
|
||||
if (!scope.filteredMessages || scope.filteredMessages.length <= i) {
|
||||
// stop if i get larger than the size of filtered messages
|
||||
break;
|
||||
}
|
||||
message = scope.filteredMessages[i];
|
||||
|
||||
|
||||
isPartiallyVisibleTop = listItem.top < top && listItem.bottom > top; // a portion of the list item is visible on the top
|
||||
isPartiallyVisibleBottom = listItem.top < bottom && listItem.bottom > bottom; // a portion of the list item is visible on the bottom
|
||||
isVisible = listItem.top >= top && listItem.bottom <= bottom; // the list item is visible as a whole
|
||||
|
||||
if (isPartiallyVisibleTop || isVisible || isPartiallyVisibleBottom) {
|
||||
// we are now iterating over visible elements
|
||||
inViewport = true;
|
||||
// load mail body of visible
|
||||
scope.getBody(message);
|
||||
} else if (inViewport) {
|
||||
// we are leaving the viewport, so stop iterating over the items
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// activate iscroll
|
||||
myScroll = new IScroll(listEl, {
|
||||
mouseWheel: true,
|
||||
scrollbars: true,
|
||||
fadeScrollbars: true
|
||||
});
|
||||
myScroll.on('scrollEnd', scope.loadVisibleBodies);
|
||||
|
||||
// refresh iScroll when model length changes
|
||||
scope.$watchCollection(model, function() {
|
||||
$timeout(function() {
|
||||
myScroll.refresh();
|
||||
});
|
||||
// load the visible message bodies, when the list is re-initialized and when scrolling stopped
|
||||
scope.loadVisibleBodies();
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
return MailListCtrl;
|
||||
});
|
@ -2,8 +2,8 @@ define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var angular = require('angular'),
|
||||
str = require('js/app-config').string,
|
||||
appController = require('js/app-controller'),
|
||||
config = require('js/app-config').config,
|
||||
notification = require('js/util/notification'),
|
||||
_ = require('underscore'),
|
||||
emailDao, outboxBo;
|
||||
@ -38,19 +38,16 @@ define(function(require) {
|
||||
return;
|
||||
}
|
||||
|
||||
// update the outbox mail count. this should normally happen during the delta sync
|
||||
// problem is that the outbox continuously retries in the background, whereas the delta sync only runs
|
||||
// when the outbox is currently viewed...
|
||||
// update the outbox mail count
|
||||
var outbox = _.findWhere($scope.account.folders, {
|
||||
type: 'Outbox'
|
||||
type: config.outboxMailboxType
|
||||
});
|
||||
|
||||
if (outbox === $scope.state.nav.currentFolder) {
|
||||
$scope.state.mailList.synchronize();
|
||||
} else {
|
||||
outbox.count = count;
|
||||
$scope.$apply();
|
||||
}
|
||||
|
||||
emailDao.refreshFolder({
|
||||
folder: outbox
|
||||
}, $scope.onError);
|
||||
};
|
||||
|
||||
//
|
||||
@ -58,7 +55,7 @@ define(function(require) {
|
||||
//
|
||||
|
||||
// init folders
|
||||
initFolders();
|
||||
initializeFolders();
|
||||
|
||||
// select inbox as the current folder on init
|
||||
if ($scope.account.folders && $scope.account.folders.length > 0) {
|
||||
@ -82,8 +79,13 @@ define(function(require) {
|
||||
// helper functions
|
||||
//
|
||||
|
||||
function initFolders() {
|
||||
if (window.chrome && chrome.identity) {
|
||||
function initializeFolders() {
|
||||
// create dummy folder in dev environment only
|
||||
if (!window.chrome || !chrome.identity) {
|
||||
createDummyFolders();
|
||||
return;
|
||||
}
|
||||
|
||||
// get pointer to account/folder/message tree on root scope
|
||||
$scope.$root.account = emailDao._account;
|
||||
|
||||
@ -92,10 +94,19 @@ define(function(require) {
|
||||
// start checking outbox periodically
|
||||
outboxBo.startChecking($scope.onOutboxUpdate);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
function sentNotification(email) {
|
||||
notification.create({
|
||||
id: 'o' + email.id,
|
||||
title: 'Message sent',
|
||||
message: email.subject
|
||||
}, function() {});
|
||||
}
|
||||
|
||||
|
||||
// attach dummy folders for development
|
||||
function createDummyFolders() {
|
||||
$scope.$root.account = {};
|
||||
$scope.account.folders = [{
|
||||
type: 'Inbox',
|
||||
@ -106,9 +117,9 @@ define(function(require) {
|
||||
count: 0,
|
||||
path: 'SENT'
|
||||
}, {
|
||||
type: 'Outbox',
|
||||
type: config.outboxMailboxType,
|
||||
count: 0,
|
||||
path: 'OUTBOX'
|
||||
path: config.outboxMailboxPath
|
||||
}, {
|
||||
type: 'Drafts',
|
||||
count: 0,
|
||||
@ -119,14 +130,6 @@ define(function(require) {
|
||||
path: 'TRASH'
|
||||
}];
|
||||
}
|
||||
|
||||
function sentNotification(email) {
|
||||
notification.create({
|
||||
id: 'o' + email.id,
|
||||
title: 'Message sent',
|
||||
message: email.subject.replace(str.subjectPrefix, '')
|
||||
}, function() {});
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
@ -165,12 +168,6 @@ define(function(require) {
|
||||
scope.state.writer.write(scope.state.mailList.selected);
|
||||
scope.$apply();
|
||||
|
||||
} else if (modifier && e.keyCode === 83 && scope.state.lightbox !== 'write' && scope.state.mailList.synchronize) {
|
||||
// s -> sync folder
|
||||
e.preventDefault();
|
||||
scope.state.mailList.synchronize();
|
||||
scope.$apply();
|
||||
|
||||
} else if (e.keyCode === 27 && scope.state.lightbox !== undefined) {
|
||||
// escape -> close current lightbox
|
||||
e.preventDefault();
|
||||
|
@ -117,7 +117,7 @@ define(function(require) {
|
||||
var email = $scope.state.mailList.selected;
|
||||
|
||||
emailDao.getAttachment({
|
||||
folder: folder.path,
|
||||
folder: folder,
|
||||
uid: email.uid,
|
||||
attachment: attachment
|
||||
}, function(err) {
|
||||
|
@ -326,51 +326,44 @@ define(function(require) {
|
||||
return;
|
||||
}
|
||||
|
||||
// helper flag to remember if we need to sync back to imap
|
||||
// in case the replyTo.answered changed
|
||||
var needsSync = false;
|
||||
// if we need to synchronize replyTo.answered = true to imap,
|
||||
// let's do that. otherwise, we're done
|
||||
if (!$scope.replyTo || $scope.replyTo.answered) {
|
||||
return;
|
||||
}
|
||||
|
||||
// mark replyTo as answered, if necessary
|
||||
if ($scope.replyTo && !$scope.replyTo.answered) {
|
||||
$scope.replyTo.answered = true;
|
||||
// update the ui
|
||||
$scope.$apply();
|
||||
needsSync = true;
|
||||
}
|
||||
|
||||
// if we need to synchronize replyTo.answered, let's do that.
|
||||
// otherwise, we're done
|
||||
if (!needsSync) {
|
||||
return;
|
||||
}
|
||||
|
||||
emailDao.sync({
|
||||
folder: $scope.state.nav.currentFolder.path
|
||||
emailDao.setFlags({
|
||||
folder: currentFolder(),
|
||||
message: $scope.replyTo
|
||||
}, function(err) {
|
||||
if (err && err.code === 42) {
|
||||
// offline
|
||||
$scope.onError();
|
||||
if (err && err.code !== 42) {
|
||||
$scope.onError(err);
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.onError(err);
|
||||
// offline or no error, let's apply the ui changes
|
||||
$scope.$apply();
|
||||
});
|
||||
});
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
//
|
||||
// Helpers
|
||||
//
|
||||
|
||||
function currentFolder() {
|
||||
return $scope.state.nav.currentFolder;
|
||||
}
|
||||
|
||||
/*
|
||||
* Visitor to filter out objects without an address property, i.e. empty addresses
|
||||
*/
|
||||
function filterEmptyAddresses(addr) {
|
||||
return !!addr.address;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
//
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,848 +0,0 @@
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var _ = require('underscore'),
|
||||
config = require('js/app-config').config,
|
||||
str = require('js/app-config').string;
|
||||
|
||||
var EmailSync = function(keychain, devicestorage, mailreader) {
|
||||
this._keychain = keychain;
|
||||
this._devicestorage = devicestorage;
|
||||
this._mailreader = mailreader;
|
||||
};
|
||||
|
||||
EmailSync.prototype.init = function(options, callback) {
|
||||
this._account = options.account;
|
||||
|
||||
callback();
|
||||
};
|
||||
|
||||
EmailSync.prototype.onConnect = function(options, callback) {
|
||||
this._imapClient = options.imapClient;
|
||||
|
||||
callback();
|
||||
};
|
||||
|
||||
EmailSync.prototype.onDisconnect = function(options, callback) {
|
||||
this._imapClient = undefined;
|
||||
|
||||
callback();
|
||||
};
|
||||
|
||||
/**
|
||||
* Syncs outbox content from disk to memory, not vice-versa
|
||||
*/
|
||||
EmailSync.prototype.syncOutbox = function(options, callback) {
|
||||
var self = this;
|
||||
|
||||
// check busy status
|
||||
if (self._account.busy) {
|
||||
callback({
|
||||
errMsg: 'Sync aborted: Previous sync still in progress',
|
||||
code: 409
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// make sure two syncs for the same folder don't interfere
|
||||
self._account.busy = true;
|
||||
|
||||
var folder = _.findWhere(self._account.folders, {
|
||||
path: options.folder
|
||||
});
|
||||
|
||||
folder.messages = folder.messages || [];
|
||||
|
||||
self._localListMessages({
|
||||
folder: folder.path
|
||||
}, function(err, storedMessages) {
|
||||
if (err) {
|
||||
self._account.busy = false;
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// calculate the diffs between memory and disk
|
||||
var storedIds = _.pluck(storedMessages, 'id'),
|
||||
inMemoryIds = _.pluck(folder.messages, 'id'),
|
||||
newIds = _.difference(storedIds, inMemoryIds),
|
||||
removedIds = _.difference(inMemoryIds, storedIds);
|
||||
|
||||
// which messages are new on the disk that are not yet in memory?
|
||||
var newMessages = _.filter(storedMessages, function(msg) {
|
||||
return _.contains(newIds, msg.id);
|
||||
});
|
||||
|
||||
// which messages are no longer on disk, i.e. have been sent
|
||||
var removedMessages = _.filter(folder.messages, function(msg) {
|
||||
return _.contains(removedIds, msg.id);
|
||||
});
|
||||
|
||||
// add the new messages to memory
|
||||
newMessages.forEach(function(newMessage) {
|
||||
folder.messages.push(newMessage);
|
||||
});
|
||||
|
||||
// remove the sent messages from memory
|
||||
removedMessages.forEach(function(removedMessage) {
|
||||
var index = folder.messages.indexOf(removedMessage);
|
||||
folder.messages.splice(index, 1);
|
||||
});
|
||||
|
||||
// update the folder count and we're done.
|
||||
folder.count = folder.messages.length;
|
||||
self._account.busy = false;
|
||||
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
EmailSync.prototype.sync = function(options, callback) {
|
||||
/*
|
||||
* Here's how delta sync works:
|
||||
*
|
||||
* First, we sync the messages between memory and local storage, based on their uid
|
||||
* delta1: storage > memory => we deleted messages, remove from remote and memory
|
||||
* delta2: memory > storage => we added messages, push to remote <<< not supported yet
|
||||
*
|
||||
* Second, we check the delta for the flags
|
||||
* deltaF2: memory > storage => we changed flags, sync them to the remote and memory
|
||||
*
|
||||
* Third, we go on to sync between imap and memory, again based on uid
|
||||
* 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
|
||||
*
|
||||
* Fourth, we pull changes in the flags downstream
|
||||
* deltaF4: imap > memory => we changed flags directly on the remote, sync them to the storage and memory
|
||||
*/
|
||||
|
||||
var self = this;
|
||||
|
||||
// validate options
|
||||
if (!options.folder) {
|
||||
callback({
|
||||
errMsg: 'Invalid options!'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// check busy status
|
||||
if (self._account.busy) {
|
||||
callback({
|
||||
errMsg: 'Sync aborted: Previous sync still in progress',
|
||||
code: 409
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// make sure two syncs for the same folder don't interfere
|
||||
self._account.busy = true;
|
||||
|
||||
var folder = _.findWhere(self._account.folders, {
|
||||
path: options.folder
|
||||
});
|
||||
|
||||
/*
|
||||
* if the folder is not initialized with the messages from the memory, we need to fill it first, otherwise the delta sync obviously breaks.
|
||||
* initial filling from local storage is an exception from the normal sync. after reading from local storage, do imap sync
|
||||
*/
|
||||
var isFolderInitialized = !! folder.messages;
|
||||
if (!isFolderInitialized) {
|
||||
initFolderMessages();
|
||||
return;
|
||||
}
|
||||
|
||||
doLocalDelta();
|
||||
|
||||
/*
|
||||
* pre-fill the memory with the messages stored on the hard disk
|
||||
*/
|
||||
function initFolderMessages() {
|
||||
folder.messages = [];
|
||||
self._localListMessages({
|
||||
folder: folder.path
|
||||
}, function(err, storedMessages) {
|
||||
if (err) {
|
||||
self._account.busy = false;
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
storedMessages.forEach(function(storedMessage) {
|
||||
// remove the body parts to not load unnecessary data to memory
|
||||
delete storedMessage.bodyParts;
|
||||
|
||||
folder.messages.push(storedMessage);
|
||||
});
|
||||
|
||||
callback();
|
||||
doImapDelta();
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* compares the messages in memory to the messages on the disk
|
||||
*/
|
||||
function doLocalDelta() {
|
||||
self._localListMessages({
|
||||
folder: folder.path
|
||||
}, function(err, storedMessages) {
|
||||
if (err) {
|
||||
self._account.busy = false;
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
doDelta1();
|
||||
|
||||
/*
|
||||
* delta1:
|
||||
* storage contains messages that are not present in memory => we deleted messages from the memory, so remove the messages from the remote and the disk
|
||||
*/
|
||||
function doDelta1() {
|
||||
var inMemoryUids = _.pluck(folder.messages, 'uid'),
|
||||
storedMessageUids = _.pluck(storedMessages, 'uid'),
|
||||
delta1 = _.difference(storedMessageUids, inMemoryUids); // delta1 contains only uids
|
||||
|
||||
// if we're we are done here
|
||||
if (_.isEmpty(delta1)) {
|
||||
doDeltaF2();
|
||||
return;
|
||||
}
|
||||
|
||||
var after = _.after(delta1.length, function() {
|
||||
doDeltaF2();
|
||||
});
|
||||
|
||||
// delta1 contains uids of messages on the disk
|
||||
delta1.forEach(function(inMemoryUid) {
|
||||
var deleteMe = {
|
||||
folder: folder.path,
|
||||
uid: inMemoryUid
|
||||
};
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* deltaF2:
|
||||
* memory contains messages that have flags other than those in storage => we changed flags, sync them to the remote and memory
|
||||
*/
|
||||
function doDeltaF2() {
|
||||
var deltaF2 = checkFlags(folder.messages, storedMessages); // deltaF2 contains the message objects, we need those to sync the flags
|
||||
|
||||
if (_.isEmpty(deltaF2)) {
|
||||
callback();
|
||||
doImapDelta();
|
||||
return;
|
||||
}
|
||||
|
||||
var after = _.after(deltaF2.length, function() {
|
||||
callback();
|
||||
doImapDelta();
|
||||
});
|
||||
|
||||
// deltaF2 contains references to the in-memory messages
|
||||
deltaF2.forEach(function(inMemoryMessage) {
|
||||
self._imapMark({
|
||||
folder: folder.path,
|
||||
uid: inMemoryMessage.uid,
|
||||
unread: inMemoryMessage.unread,
|
||||
answered: inMemoryMessage.answered
|
||||
}, function(err) {
|
||||
if (err) {
|
||||
self._account.busy = false;
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
var storedMessage = _.findWhere(storedMessages, {
|
||||
uid: inMemoryMessage.uid
|
||||
});
|
||||
|
||||
storedMessage.unread = inMemoryMessage.unread;
|
||||
storedMessage.answered = inMemoryMessage.answered;
|
||||
|
||||
self._localStoreMessages({
|
||||
folder: folder.path,
|
||||
emails: [storedMessage]
|
||||
}, function(err) {
|
||||
if (err) {
|
||||
self._account.busy = false;
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
after();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* compare the messages on the imap server to the in memory messages
|
||||
*/
|
||||
function doImapDelta() {
|
||||
self._imapSearch({
|
||||
folder: folder.path
|
||||
}, function(err, inImapUids) {
|
||||
if (err) {
|
||||
self._account.busy = false;
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
doDelta3();
|
||||
|
||||
/*
|
||||
* delta3:
|
||||
* memory contains messages that are not present on the imap => we deleted messages directly from the remote, remove from memory and storage
|
||||
*/
|
||||
function doDelta3() {
|
||||
var inMemoryUids = _.pluck(folder.messages, 'uid'),
|
||||
delta3 = _.difference(inMemoryUids, inImapUids);
|
||||
|
||||
if (_.isEmpty(delta3)) {
|
||||
doDelta4();
|
||||
return;
|
||||
}
|
||||
|
||||
var after = _.after(delta3.length, function() {
|
||||
doDelta4();
|
||||
});
|
||||
|
||||
// delta3 contains uids of the in-memory messages that have been deleted from the remote
|
||||
delta3.forEach(function(inMemoryUid) {
|
||||
// remove from local storage
|
||||
self._localDeleteMessage({
|
||||
folder: folder.path,
|
||||
uid: inMemoryUid
|
||||
}, function(err) {
|
||||
if (err) {
|
||||
self._account.busy = false;
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// remove from memory
|
||||
var inMemoryMessage = _.findWhere(folder.messages, function(msg) {
|
||||
return msg.uid === inMemoryUid;
|
||||
});
|
||||
folder.messages.splice(folder.messages.indexOf(inMemoryMessage), 1);
|
||||
|
||||
after();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* delta4:
|
||||
* imap contains messages that are not present in memory => we have new messages available, fetch downstream to memory and storage
|
||||
*/
|
||||
function doDelta4() {
|
||||
var inMemoryUids = _.pluck(folder.messages, 'uid'),
|
||||
delta4 = _.difference(inImapUids, inMemoryUids);
|
||||
|
||||
// eliminate uids smaller than the biggest local uid, i.e. just fetch everything
|
||||
// that came in AFTER the most recent email we have in memory. Keep in mind that
|
||||
// uids are strictly ascending, so there can't be a NEW mail in the mailbox with a
|
||||
// uid smaller than anything we've encountered before.
|
||||
if (!_.isEmpty(inMemoryUids)) {
|
||||
var maxInMemoryUid = Math.max.apply(null, inMemoryUids); // apply works with separate arguments rather than an array
|
||||
|
||||
// eliminate everything prior to maxInMemoryUid, i.e. everything that was already synced
|
||||
delta4 = _.filter(delta4, function(uid) {
|
||||
return uid > maxInMemoryUid;
|
||||
});
|
||||
}
|
||||
|
||||
// no delta, we're done here
|
||||
if (_.isEmpty(delta4)) {
|
||||
doDeltaF4();
|
||||
return;
|
||||
}
|
||||
|
||||
// list the messages starting from the lowest new uid to the highest new uid
|
||||
self._imapListMessages({
|
||||
folder: folder.path,
|
||||
firstUid: Math.min.apply(null, delta4),
|
||||
lastUid: Math.max.apply(null, delta4)
|
||||
}, function(err, messages) {
|
||||
if (err) {
|
||||
self._account.busy = false;
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// if there are verification messages in the synced messages, handle it
|
||||
var verificationMessages = _.filter(messages, function(message) {
|
||||
return message.subject === (str.subjectPrefix + str.verificationSubject);
|
||||
});
|
||||
|
||||
// if there are verification messages, continue after we've tried to verify
|
||||
if (verificationMessages.length > 0) {
|
||||
var after = _.after(verificationMessages.length, storeHeaders);
|
||||
|
||||
verificationMessages.forEach(function(verificationMessage) {
|
||||
handleVerification(verificationMessage, function(err, isValid) {
|
||||
// if it was NOT a valid verification mail, do nothing
|
||||
if (!isValid) {
|
||||
after();
|
||||
return;
|
||||
}
|
||||
|
||||
// if an error occurred and the mail was a valid verification mail, display the error, but
|
||||
// keep the mail in the list so the user can see it and verify manually
|
||||
if (err) {
|
||||
callback(err);
|
||||
after();
|
||||
return;
|
||||
}
|
||||
|
||||
// if verification worked, we remove the mail from the list.
|
||||
messages.splice(messages.indexOf(verificationMessage), 1);
|
||||
after();
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// no verification messages, just proceed as usual
|
||||
storeHeaders();
|
||||
|
||||
function storeHeaders() {
|
||||
// no delta, we're done here
|
||||
if (_.isEmpty(messages)) {
|
||||
doDeltaF4();
|
||||
return;
|
||||
}
|
||||
|
||||
// persist the encrypted message to the local storage
|
||||
self._localStoreMessages({
|
||||
folder: folder.path,
|
||||
emails: messages
|
||||
}, function(err) {
|
||||
if (err) {
|
||||
self._account.busy = false;
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// this enables us to already show the attachment clip in the message list ui
|
||||
messages.forEach(function(message) {
|
||||
message.attachments = message.bodyParts.filter(function(bodyPart) {
|
||||
return bodyPart.type === 'attachment';
|
||||
});
|
||||
});
|
||||
|
||||
// if persisting worked, add them to the messages array
|
||||
folder.messages = folder.messages.concat(messages);
|
||||
self.onIncomingMessage(messages);
|
||||
doDeltaF4();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* deltaF4: imap > memory => we changed flags directly on the remote, sync them to the storage and memory
|
||||
*/
|
||||
function doDeltaF4() {
|
||||
var answeredUids, unreadUids,
|
||||
deltaF4 = [];
|
||||
|
||||
getUnreadUids();
|
||||
|
||||
// find all the relevant unread mails
|
||||
function getUnreadUids() {
|
||||
self._imapSearch({
|
||||
folder: folder.path,
|
||||
unread: true
|
||||
}, function(err, uids) {
|
||||
if (err) {
|
||||
self._account.busy = false;
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// we're done here, let's get all the answered mails
|
||||
unreadUids = uids;
|
||||
getAnsweredUids();
|
||||
});
|
||||
}
|
||||
|
||||
// find all the relevant answered mails
|
||||
function getAnsweredUids() {
|
||||
// find all the relevant answered mails
|
||||
self._imapSearch({
|
||||
folder: folder.path,
|
||||
answered: true
|
||||
}, function(err, uids) {
|
||||
if (err) {
|
||||
self._account.busy = false;
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// we're done here, let's update what we have in memory and persist that!
|
||||
answeredUids = uids;
|
||||
updateFlags();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function updateFlags() {
|
||||
folder.messages.forEach(function(msg) {
|
||||
// if the message's uid is among the uids that should be unread,
|
||||
// AND the message is not unread, we clearly have to change that
|
||||
var shouldBeUnread = _.contains(unreadUids, msg.uid);
|
||||
if (msg.unread === shouldBeUnread) {
|
||||
// everything is in order, we're good here
|
||||
return;
|
||||
}
|
||||
|
||||
msg.unread = shouldBeUnread;
|
||||
deltaF4.push(msg);
|
||||
});
|
||||
|
||||
folder.messages.forEach(function(msg) {
|
||||
// if the message's uid is among the uids that should be answered,
|
||||
// AND the message is not answered, we clearly have to change that
|
||||
var shouldBeAnswered = _.contains(answeredUids, msg.uid);
|
||||
if (msg.answered === shouldBeAnswered) {
|
||||
// everything is in order, we're good here
|
||||
return;
|
||||
}
|
||||
|
||||
msg.answered = shouldBeAnswered;
|
||||
deltaF4.push(msg);
|
||||
});
|
||||
|
||||
// maybe a mail had BOTH flags wrong, so let's create
|
||||
// a duplicate-free version of deltaF4
|
||||
deltaF4 = _.uniq(deltaF4);
|
||||
|
||||
// everything up to date? fine, we're done!
|
||||
if (_.isEmpty(deltaF4)) {
|
||||
finishSync();
|
||||
return;
|
||||
}
|
||||
|
||||
var after = _.after(deltaF4.length, function() {
|
||||
// we're doing updating everything
|
||||
finishSync();
|
||||
});
|
||||
|
||||
// alright, so let's sync the corrected messages
|
||||
deltaF4.forEach(function(inMemoryMessage) {
|
||||
// do a short round trip to the database to avoid re-encrypting,
|
||||
// instead use the encrypted object in the storage
|
||||
self._localListMessages({
|
||||
folder: folder.path,
|
||||
uid: inMemoryMessage.uid
|
||||
}, function(err, storedMessages) {
|
||||
if (err) {
|
||||
self._account.busy = false;
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
var storedMessage = storedMessages[0];
|
||||
storedMessage.unread = inMemoryMessage.unread;
|
||||
storedMessage.answered = inMemoryMessage.answered;
|
||||
|
||||
// persist the modified object
|
||||
self._localStoreMessages({
|
||||
folder: folder.path,
|
||||
emails: [storedMessage]
|
||||
}, function(err) {
|
||||
if (err) {
|
||||
self._account.busy = false;
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// and we're done.
|
||||
after();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function finishSync() {
|
||||
// whereas normal folders show the unread messages count only,
|
||||
// the outbox shows the total count
|
||||
// after all the tags are up to date, let's adjust the unread mail count
|
||||
folder.count = _.filter(folder.messages, function(msg) {
|
||||
return msg.unread === true;
|
||||
}).length;
|
||||
|
||||
// allow the next sync to take place
|
||||
self._account.busy = false;
|
||||
callback();
|
||||
}
|
||||
|
||||
/*
|
||||
* checks if there are some flags that have changed in a and b
|
||||
*/
|
||||
function checkFlags(a, b) {
|
||||
var i, aI, bI,
|
||||
delta = [];
|
||||
|
||||
// find the delta
|
||||
for (i = a.length - 1; i >= 0; i--) {
|
||||
aI = a[i];
|
||||
bI = _.findWhere(b, {
|
||||
uid: aI.uid
|
||||
});
|
||||
if (bI && (aI.unread !== bI.unread || aI.answered !== bI.answered)) {
|
||||
delta.push(aI);
|
||||
}
|
||||
}
|
||||
|
||||
return delta;
|
||||
}
|
||||
|
||||
function handleVerification(message, localCallback) {
|
||||
self._getBodyParts({
|
||||
folder: options.folder,
|
||||
uid: message.uid,
|
||||
bodyParts: message.bodyParts
|
||||
}, function(error, parsedBodyParts) {
|
||||
// we could not stream the text to determine if the verification was valid or not
|
||||
// so handle it as if it were valid
|
||||
if (error) {
|
||||
localCallback(error, true);
|
||||
return;
|
||||
}
|
||||
|
||||
var body = _.pluck(self.filterBodyParts(parsedBodyParts, 'text'), 'content').join('\n'),
|
||||
verificationUrlPrefix = config.cloudUrl + config.verificationUrl,
|
||||
uuid = body.split(verificationUrlPrefix).pop().substr(0, config.verificationUuidLength),
|
||||
isValidUuid = new RegExp('[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}').test(uuid);
|
||||
|
||||
// there's no valid uuid in the message, so forget about it
|
||||
if (!isValidUuid) {
|
||||
localCallback(null, false);
|
||||
return;
|
||||
}
|
||||
|
||||
// there's a valid uuid in the message, so try to verify it
|
||||
self._keychain.verifyPublicKey(uuid, function(err) {
|
||||
if (err) {
|
||||
localCallback({
|
||||
errMsg: 'Verifying your public key failed: ' + err.errMsg
|
||||
}, true);
|
||||
return;
|
||||
}
|
||||
|
||||
// public key has been verified, delete the message
|
||||
self._imapDeleteMessage({
|
||||
folder: options.folder,
|
||||
uid: message.uid
|
||||
}, function() {
|
||||
// if we could successfully not delete the message or not doesn't matter.
|
||||
// just don't show it in whiteout and keep quiet about it
|
||||
localCallback(null, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Internal APIs
|
||||
//
|
||||
|
||||
// Local Storage API
|
||||
|
||||
EmailSync.prototype._localListMessages = function(options, callback) {
|
||||
var dbType = 'email_' + options.folder;
|
||||
if (typeof options.uid !== 'undefined') {
|
||||
dbType = dbType + '_' + options.uid;
|
||||
}
|
||||
this._devicestorage.listItems(dbType, 0, null, callback);
|
||||
};
|
||||
|
||||
EmailSync.prototype._localStoreMessages = function(options, callback) {
|
||||
var dbType = 'email_' + options.folder;
|
||||
this._devicestorage.storeList(options.emails, dbType, callback);
|
||||
};
|
||||
|
||||
EmailSync.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
|
||||
|
||||
/**
|
||||
* Mark imap messages as un-/read or un-/answered
|
||||
*/
|
||||
EmailSync.prototype._imapMark = function(options, callback) {
|
||||
if (!this._account.online) {
|
||||
callback({
|
||||
errMsg: 'Client is currently offline!',
|
||||
code: 42
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
options.path = options.folder;
|
||||
this._imapClient.updateFlags(options, callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the relevant messages corresponding to the search terms in the options
|
||||
* @param {String} options.folder The folder's path
|
||||
* @param {Boolean} options.answered (optional) Mails with or without the \Answered flag set.
|
||||
* @param {Boolean} options.unread (optional) Mails with or without the \Seen flag set.
|
||||
* @param {Function} callback(error, uids) invoked with the uids of messages matching the search terms, or an error object if an error occurred
|
||||
*/
|
||||
EmailSync.prototype._imapSearch = function(options, callback) {
|
||||
if (!this._account.online) {
|
||||
callback({
|
||||
errMsg: 'Client is currently offline!',
|
||||
code: 42
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
options.path = options.folder;
|
||||
this._imapClient.search(options, callback);
|
||||
};
|
||||
|
||||
EmailSync.prototype._imapDeleteMessage = function(options, callback) {
|
||||
if (!this._account.online) {
|
||||
callback({
|
||||
errMsg: 'Client is currently offline!',
|
||||
code: 42
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var trash = _.findWhere(this._account.folders, {
|
||||
type: 'Trash'
|
||||
});
|
||||
|
||||
// there's no known trash folder to move the mail to or we're in the trash folder,
|
||||
// so we can purge the message
|
||||
if (!trash || options.folder === trash.path) {
|
||||
this._imapClient.deleteMessage({
|
||||
path: options.folder,
|
||||
uid: options.uid
|
||||
}, callback);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this._imapClient.moveMessage({
|
||||
path: options.folder,
|
||||
destination: trash.path,
|
||||
uid: options.uid
|
||||
}, callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get an email messsage without the body
|
||||
* @param {String} options.folder The folder
|
||||
* @param {Number} options.firstUid The lower bound of the uid (inclusive)
|
||||
* @param {Number} options.lastUid The upper bound of the uid range (inclusive)
|
||||
* @param {Function} callback (error, messages) The callback when the imap client is done fetching message metadata
|
||||
*/
|
||||
EmailSync.prototype._imapListMessages = function(options, callback) {
|
||||
var self = this;
|
||||
|
||||
if (!this._account.online) {
|
||||
callback({
|
||||
errMsg: 'Client is currently offline!',
|
||||
code: 42
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
options.path = options.folder;
|
||||
self._imapClient.listMessages(options, callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* Stream an email messsage's body
|
||||
* @param {String} options.folder The folder
|
||||
* @param {String} options.uid the message's uid
|
||||
* @param {Object} options.bodyParts The message, as retrieved by _imapListMessages
|
||||
* @param {Function} callback (error, message) The callback when the imap client is done streaming message text content
|
||||
*/
|
||||
EmailSync.prototype._getBodyParts = function(options, callback) {
|
||||
var self = this;
|
||||
|
||||
if (!this._account.online) {
|
||||
callback({
|
||||
errMsg: 'Client is currently offline!',
|
||||
code: 42
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
options.path = options.folder;
|
||||
self._imapClient.getBodyParts(options, function(err) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
// interpret the raw content of the email
|
||||
self._mailreader.parse(options, callback);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function that recursively traverses the body parts tree. Looks for bodyParts that match the provided type and aggregates them
|
||||
* @param {[type]} bodyParts The bodyParts array
|
||||
* @param {[type]} type The type to look up
|
||||
* @param {undefined} result Leave undefined, only used for recursion
|
||||
*/
|
||||
EmailSync.prototype.filterBodyParts = function(bodyParts, type, result) {
|
||||
var self = this;
|
||||
|
||||
result = result || [];
|
||||
bodyParts.forEach(function(part) {
|
||||
if (part.type === type) {
|
||||
result.push(part);
|
||||
} else if (Array.isArray(part.content)) {
|
||||
self.filterBodyParts(part.content, type, result);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
return EmailSync;
|
||||
});
|
@ -3,7 +3,8 @@ define(function(require) {
|
||||
|
||||
var cfg = require('js/app-config').config,
|
||||
updateV1 = require('js/util/update/update-v1'),
|
||||
updateV2 = require('js/util/update/update-v2');
|
||||
updateV2 = require('js/util/update/update-v2'),
|
||||
updateV3 = require('js/util/update/update-v3');
|
||||
|
||||
/**
|
||||
* Handles database migration
|
||||
@ -11,7 +12,7 @@ define(function(require) {
|
||||
var UpdateHandler = function(appConfigStorage, userStorage) {
|
||||
this._appConfigStorage = appConfigStorage;
|
||||
this._userStorage = userStorage;
|
||||
this._updateScripts = [updateV1, updateV2];
|
||||
this._updateScripts = [updateV1, updateV2, updateV3];
|
||||
};
|
||||
|
||||
/**
|
||||
|
28
src/js/util/update/update-v3.js
Normal file
28
src/js/util/update/update-v3.js
Normal file
@ -0,0 +1,28 @@
|
||||
define(function() {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Update handler for transition database version 2 -> 3
|
||||
*
|
||||
* In database version 3, we introduced new flags to the messages, also
|
||||
* the outbox uses artificial uids
|
||||
*/
|
||||
function updateV2(options, callback) {
|
||||
var emailDbType = 'email_',
|
||||
versionDbType = 'dbVersion',
|
||||
postUpdateDbVersion = 3;
|
||||
|
||||
// remove the emails
|
||||
options.userStorage.removeList(emailDbType, function(err) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// update the database version to postUpdateDbVersion
|
||||
options.appConfigStorage.storeList([postUpdateDbVersion], versionDbType, callback);
|
||||
});
|
||||
}
|
||||
|
||||
return updateV2;
|
||||
});
|
@ -23,7 +23,7 @@
|
||||
</ul><!--/.mail-list-->
|
||||
</div>
|
||||
|
||||
<footer ng-class="{syncing: account.loggingIn || account.busy}" ng-click="synchronize()">
|
||||
<footer ng-class="{syncing: account.loggingIn || account.busy}">
|
||||
<span class="spinner"></span>
|
||||
<span class="text" ng-switch="account.online">
|
||||
<span ng-switch-when="false">
|
||||
|
@ -29,7 +29,9 @@ define(function(require) {
|
||||
|
||||
describe('buildModules', function() {
|
||||
it('should work', function() {
|
||||
controller.buildModules();
|
||||
controller.buildModules({
|
||||
onError: function() {}
|
||||
});
|
||||
expect(controller._appConfigStore).to.exist;
|
||||
expect(controller._auth).to.exist;
|
||||
expect(controller._userStorage).to.exist;
|
||||
@ -55,13 +57,10 @@ define(function(require) {
|
||||
});
|
||||
|
||||
describe('onDisconnect', function() {
|
||||
it('should work', function(done) {
|
||||
emailDaoStub.onDisconnect.yields();
|
||||
it('should work', function() {
|
||||
controller.onDisconnect();
|
||||
|
||||
controller.onDisconnect(function(err) {
|
||||
expect(err).to.not.exist;
|
||||
done();
|
||||
});
|
||||
expect(emailDaoStub.onDisconnect.calledOnce).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -6,7 +6,6 @@ define(function(require) {
|
||||
mocks = require('angularMocks'),
|
||||
MailListCtrl = require('js/controller/mail-list'),
|
||||
EmailDAO = require('js/dao/email-dao'),
|
||||
EmailSync = require('js/dao/email-sync'),
|
||||
DeviceStorageDAO = require('js/dao/devicestorage-dao'),
|
||||
KeychainDAO = require('js/dao/keychain-dao'),
|
||||
appController = require('js/app-controller'),
|
||||
@ -15,7 +14,7 @@ define(function(require) {
|
||||
chai.Assertion.includeStack = true;
|
||||
|
||||
describe('Mail List controller unit test', function() {
|
||||
var scope, ctrl, origEmailDao, origEmailSync, emailDaoMock, emailSyncMock, keychainMock, deviceStorageMock,
|
||||
var scope, ctrl, origEmailDao, emailDaoMock, keychainMock, deviceStorageMock,
|
||||
emailAddress, notificationClickedHandler, emails,
|
||||
hasChrome, hasSocket, hasRuntime, hasIdentity;
|
||||
|
||||
@ -54,11 +53,8 @@ define(function(require) {
|
||||
};
|
||||
|
||||
origEmailDao = appController._emailDao;
|
||||
origEmailSync = appController._emailSync;
|
||||
emailDaoMock = sinon.createStubInstance(EmailDAO);
|
||||
emailSyncMock = sinon.createStubInstance(EmailSync);
|
||||
appController._emailDao = emailDaoMock;
|
||||
appController._emailSync = emailSyncMock;
|
||||
emailAddress = 'fred@foo.com';
|
||||
emailDaoMock._account = {
|
||||
emailAddress: emailAddress,
|
||||
@ -106,13 +102,11 @@ define(function(require) {
|
||||
|
||||
// restore the module
|
||||
appController._emailDao = origEmailDao;
|
||||
appController._emailSync = origEmailDao;
|
||||
});
|
||||
|
||||
describe('scope variables', function() {
|
||||
it('should be set correctly', function() {
|
||||
expect(scope.select).to.exist;
|
||||
expect(scope.synchronize).to.exist;
|
||||
expect(scope.remove).to.exist;
|
||||
expect(scope.state.mailList).to.exist;
|
||||
});
|
||||
@ -142,7 +136,7 @@ define(function(require) {
|
||||
done();
|
||||
});
|
||||
|
||||
emailSyncMock.onIncomingMessage([mail]);
|
||||
emailDaoMock.onIncomingMessage([mail]);
|
||||
});
|
||||
|
||||
it('should succeed for multiple mails', function(done) {
|
||||
@ -178,7 +172,7 @@ define(function(require) {
|
||||
done();
|
||||
});
|
||||
|
||||
emailSyncMock.onIncomingMessage(mails);
|
||||
emailDaoMock.onIncomingMessage(mails);
|
||||
});
|
||||
|
||||
it('should focus mail when clicked', function() {
|
||||
@ -216,33 +210,6 @@ define(function(require) {
|
||||
});
|
||||
});
|
||||
|
||||
describe('synchronize', function() {
|
||||
it('should do imap sync and display mails', function(done) {
|
||||
scope._stopWatchTask();
|
||||
|
||||
emailDaoMock.sync.yieldsAsync();
|
||||
|
||||
var currentFolder = {
|
||||
type: 'Inbox',
|
||||
messages: emails
|
||||
};
|
||||
scope.folders = [currentFolder];
|
||||
scope.state.nav = {
|
||||
currentFolder: currentFolder
|
||||
};
|
||||
|
||||
var loadVisibleBodiesStub = sinon.stub(scope, 'loadVisibleBodies', function() {
|
||||
expect(scope.state.nav.currentFolder.messages).to.deep.equal(emails);
|
||||
expect(loadVisibleBodiesStub.calledOnce).to.be.true;
|
||||
loadVisibleBodiesStub.restore();
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
scope.synchronize();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBody', function() {
|
||||
it('should get the mail content', function() {
|
||||
scope.state.nav = {
|
||||
@ -257,13 +224,10 @@ define(function(require) {
|
||||
});
|
||||
|
||||
describe('select', function() {
|
||||
it('should decrypt, focus mark an unread mail as read', function() {
|
||||
var mail, synchronizeMock;
|
||||
|
||||
mail = {
|
||||
it('should decrypt, focus, and mark an unread mail as read', function() {
|
||||
var mail = {
|
||||
unread: true
|
||||
};
|
||||
synchronizeMock = sinon.stub(scope, 'synchronize');
|
||||
scope.state = {
|
||||
nav: {
|
||||
currentFolder: {
|
||||
@ -279,19 +243,13 @@ define(function(require) {
|
||||
scope.select(mail);
|
||||
|
||||
expect(emailDaoMock.decryptBody.calledOnce).to.be.true;
|
||||
expect(synchronizeMock.calledOnce).to.be.true;
|
||||
expect(scope.state.mailList.selected).to.equal(mail);
|
||||
|
||||
scope.synchronize.restore();
|
||||
});
|
||||
|
||||
it('should decrypt and focus a read mail', function() {
|
||||
var mail, synchronizeMock;
|
||||
|
||||
mail = {
|
||||
var mail = {
|
||||
unread: false
|
||||
};
|
||||
synchronizeMock = sinon.stub(scope, 'synchronize');
|
||||
scope.state = {
|
||||
mailList: {},
|
||||
read: {
|
||||
@ -307,54 +265,13 @@ define(function(require) {
|
||||
scope.select(mail);
|
||||
|
||||
expect(emailDaoMock.decryptBody.calledOnce).to.be.true;
|
||||
expect(synchronizeMock.called).to.be.false;
|
||||
expect(scope.state.mailList.selected).to.equal(mail);
|
||||
|
||||
scope.synchronize.restore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', function() {
|
||||
it('should not delete without a selected mail', function() {
|
||||
scope.remove();
|
||||
|
||||
expect(emailDaoMock.sync.called).to.be.false;
|
||||
});
|
||||
|
||||
it('should not delete from the outbox', function(done) {
|
||||
var currentFolder, mail;
|
||||
|
||||
scope._stopWatchTask();
|
||||
|
||||
scope.account = {};
|
||||
mail = {
|
||||
uid: 123,
|
||||
from: [{
|
||||
address: 'asd'
|
||||
}],
|
||||
subject: '[whiteout] asdasd',
|
||||
unread: true
|
||||
};
|
||||
currentFolder = {
|
||||
type: 'Outbox',
|
||||
path: 'OUTBOX',
|
||||
messages: [mail]
|
||||
};
|
||||
|
||||
scope.emails = [mail];
|
||||
scope.account.folders = [currentFolder];
|
||||
scope.state.nav = {
|
||||
currentFolder: currentFolder
|
||||
};
|
||||
|
||||
scope.onError = function(err) {
|
||||
expect(err).to.exist; // would normally display the notification
|
||||
expect(emailDaoMock.sync.called).to.be.false;
|
||||
done();
|
||||
};
|
||||
|
||||
scope.remove(mail);
|
||||
|
||||
});
|
||||
|
||||
it('should delete the selected mail', function() {
|
||||
@ -381,11 +298,11 @@ define(function(require) {
|
||||
scope.state.nav = {
|
||||
currentFolder: currentFolder
|
||||
};
|
||||
emailDaoMock.sync.yields();
|
||||
emailDaoMock.deleteMessage.yields();
|
||||
|
||||
scope.remove(mail);
|
||||
|
||||
expect(emailDaoMock.sync.calledOnce).to.be.true;
|
||||
expect(emailDaoMock.deleteMessage.calledOnce).to.be.true;
|
||||
expect(scope.state.mailList.selected).to.not.exist;
|
||||
});
|
||||
});
|
||||
|
@ -31,7 +31,6 @@ function startTests() {
|
||||
'test/new-unit/oauth-test',
|
||||
'test/new-unit/auth-test',
|
||||
'test/new-unit/email-dao-test',
|
||||
'test/new-unit/email-sync-test',
|
||||
'test/new-unit/app-controller-test',
|
||||
'test/new-unit/pgp-test',
|
||||
'test/new-unit/rest-dao-test',
|
||||
|
@ -213,6 +213,59 @@ define(function(require) {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('v2 -> v3', function() {
|
||||
var emailDbType = 'email_';
|
||||
|
||||
beforeEach(function() {
|
||||
cfg.dbVersion = 3; // app requires database version 2
|
||||
appConfigStorageStub.listItems.withArgs(versionDbType).yieldsAsync(null, [2]); // database version is 0
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
// database version is only queried for version checking prior to the update script
|
||||
// so no need to check this in case-specific tests
|
||||
expect(appConfigStorageStub.listItems.calledOnce).to.be.true;
|
||||
});
|
||||
|
||||
it('should work', function(done) {
|
||||
userStorageStub.removeList.withArgs(emailDbType).yieldsAsync();
|
||||
appConfigStorageStub.storeList.withArgs([3], versionDbType).yieldsAsync();
|
||||
|
||||
updateHandler.update(function(error) {
|
||||
expect(error).to.not.exist;
|
||||
expect(userStorageStub.removeList.calledOnce).to.be.true;
|
||||
expect(appConfigStorageStub.storeList.calledOnce).to.be.true;
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail when persisting database version fails', function(done) {
|
||||
userStorageStub.removeList.yieldsAsync();
|
||||
appConfigStorageStub.storeList.yieldsAsync({});
|
||||
|
||||
updateHandler.update(function(error) {
|
||||
expect(error).to.exist;
|
||||
expect(userStorageStub.removeList.calledOnce).to.be.true;
|
||||
expect(appConfigStorageStub.storeList.calledOnce).to.be.true;
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail when wiping emails from database fails', function(done) {
|
||||
userStorageStub.removeList.yieldsAsync({});
|
||||
|
||||
updateHandler.update(function(error) {
|
||||
expect(error).to.exist;
|
||||
expect(userStorageStub.removeList.calledOnce).to.be.true;
|
||||
expect(appConfigStorageStub.storeList.called).to.be.false;
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -338,7 +338,7 @@ define(function(require) {
|
||||
|
||||
return true;
|
||||
})).yields();
|
||||
emailDaoMock.sync.yields();
|
||||
emailDaoMock.setFlags.yields();
|
||||
|
||||
scope.onError = function(err) {
|
||||
expect(err).to.not.exist;
|
||||
@ -347,7 +347,7 @@ define(function(require) {
|
||||
scope.sendToOutbox();
|
||||
|
||||
expect(outboxMock.put.calledOnce).to.be.true;
|
||||
expect(emailDaoMock.sync.calledOnce).to.be.true;
|
||||
expect(emailDaoMock.setFlags.calledOnce).to.be.true;
|
||||
expect(scope.state.lightbox).to.be.undefined;
|
||||
expect(scope.replyTo.answered).to.be.true;
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user