Merge pull request #25 from whiteout-io/dev/offline-bug

Dev/offline bug
This commit is contained in:
Tankred Hase 2014-02-25 20:50:53 +01:00
commit 86c0f04973
13 changed files with 746 additions and 980 deletions

View File

@ -133,15 +133,17 @@ module.exports = function(grunt) {
'imap-client/node_modules/inbox/node_modules/node-shims/src/*.js',
'imap-client/node_modules/inbox/node_modules/utf7/src/utf7.js',
'imap-client/node_modules/inbox/node_modules/xoauth2/src/xoauth2.js',
'imap-client/node_modules/mimelib/src/mimelib.js',
'imap-client/node_modules/mimelib/node_modules/addressparser/src/addressparser.js',
'imap-client/node_modules/mimelib/node_modules/encoding/src/encoding.js',
'imap-client/node_modules/mimelib/node_modules/encoding/node_modules/iconv-lite/src/*.js',
'imap-client/node_modules/mailparser/src/*.js',
'imap-client/node_modules/mime/src/mime.js',
'mailreader/src/*.js',
'mailreader/node_modules/mailparser/src/*.js',
'mailreader/node_modules/mailparser/node_modules/encoding/src/encoding.js',
'mailreader/node_modules/mailparser/node_modules/mimelib/src/mimelib.js',
'mailreader/node_modules/mailparser/node_modules/mimelib/node_modules/addressparser/src/addressparser.js',
'mailreader/node_modules/mailparser/node_modules/encoding/node_modules/iconv-lite/src/*.js',
'mailreader/node_modules/mailparser/node_modules/mime/src/mime.js',
'pgpmailer/src/*.js',
'pgpmailer/node_modules/simplesmtp/src/*',
'pgpmailer/node_modules/mailbuilder/src/*.js'
'pgpbuilder/src/*.js',
'pgpbuilder/node_modules/mailbuilder/src/*.js'
],
dest: 'src/lib/'
},

View File

@ -12,7 +12,9 @@
"dependencies": {
"crypto-lib": "https://github.com/whiteout-io/crypto-lib/tarball/master",
"imap-client": "https://github.com/whiteout-io/imap-client/tarball/master",
"mailreader": "https://github.com/whiteout-io/mailreader/tarball/master",
"pgpmailer": "https://github.com/whiteout-io/pgpmailer/tarball/master",
"pgpbuilder": "https://github.com/whiteout-io/pgpbuilder/tarball/master",
"requirejs": "2.1.10"
},
"devDependencies": {
@ -33,4 +35,4 @@
"grunt-contrib-copy": "~0.4.1",
"grunt-contrib-compress": "~0.5.2"
}
}
}

View File

@ -6,6 +6,7 @@ define(function(require) {
var $ = require('jquery'),
ImapClient = require('imap-client'),
mailreader = require('mailreader'),
PgpMailer = require('pgpmailer'),
EmailDAO = require('js/dao/email-dao'),
RestDAO = require('js/dao/rest-dao'),
@ -16,6 +17,7 @@ define(function(require) {
InvitationDAO = require('js/dao/invitation-dao'),
OutboxBO = require('js/bo/outbox'),
PGP = require('js/crypto/pgp'),
PgpBuilder = require('pgpbuilder'),
config = require('js/app-config').config;
var self = {};
@ -116,8 +118,8 @@ define(function(require) {
onError: console.error
};
imapClient = new ImapClient(imapOptions);
pgpMailer = new PgpMailer(smtpOptions);
imapClient = new ImapClient(imapOptions, mailreader);
pgpMailer = new PgpMailer(smtpOptions, self._pgpbuilder);
imapClient.onError = function(err) {
console.log('IMAP error.', err);
@ -341,7 +343,7 @@ define(function(require) {
self.buildModules = function() {
var lawnchairDao, restDao, pubkeyDao, invitationDao,
emailDao, keychain, pgp, userStorage;
emailDao, keychain, pgp, userStorage, pgpbuilder;
// init objects and inject dependencies
restDao = new RestDAO();
@ -354,7 +356,8 @@ define(function(require) {
self._keychain = keychain;
pgp = new PGP();
self._crypto = pgp;
self._emailDao = emailDao = new EmailDAO(keychain, pgp, userStorage);
self._pgpbuilder = pgpbuilder = new PgpBuilder();
self._emailDao = emailDao = new EmailDAO(keychain, pgp, userStorage, pgpbuilder, mailreader);
self._outboxBo = new OutboxBO(emailDao, keychain, userStorage, invitationDao);
};
@ -385,9 +388,6 @@ define(function(require) {
return;
}
// init outbox
self._outboxBo.init();
callback(null, keypair);
});
});

View File

@ -2,10 +2,11 @@ define(function(require) {
'use strict';
var _ = require('underscore'),
util = require('cryptoLib/util'),
str = require('js/app-config').string,
config = require('js/app-config').config,
InvitationDAO = require('js/dao/invitation-dao'),
dbType = 'email_OUTBOX';
outboxDb = 'email_OUTBOX';
/**
* High level business object that orchestrates the local outbox.
@ -30,18 +31,6 @@ define(function(require) {
* Semaphore-esque flag to avoid 'concurrent' calls to _processOutbox when the timeout fires, but a call is still in process.
* @private */
this._outboxBusy = false;
/**
* Pending, unsent emails stored in the outbox. Updated on each call to _processOutbox
* @public */
this.pendingEmails = [];
};
OutboxBO.prototype.init = function() {
var outboxFolder = _.findWhere(this._emailDao._account.folders, {
type: 'Outbox'
});
outboxFolder.messages = this.pendingEmails;
};
/**
@ -66,12 +55,60 @@ define(function(require) {
};
/**
* Private Api which is called whenever a message has been sent
* The public callback "onSent" can be set by the caller to get notified.
* Put a email dto in the outbox for sending when ready
* @param {Object} mail The Email DTO
* @param {Function} callback Invoked when the object was encrypted and persisted to disk
*/
OutboxBO.prototype._onSent = function(message) {
if (typeof this.onSent === 'function') {
this.onSent(message);
OutboxBO.prototype.put = function(mail, callback) {
var self = this,
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.unregisteredUsers = []; // gather the recipients for which no public key is available
mail.id = util.UUID(); // the mail needs a random uuid for storage in the database
checkRecipients(allReaders);
// check if there are unregistered recipients
function checkRecipients(recipients) {
var after = _.after(recipients.length, function() {
encryptAndPersist();
});
// find out if there are unregistered users
recipients.forEach(function(recipient) {
self._keychain.getReceiverPublicKey(recipient.address, function(err, key) {
if (err) {
callback(err);
return;
}
// if a public key is available, add the recipient's key to the armored public keys,
// otherwise remember the recipient as unregistered for later sending
if (key) {
mail.publicKeysArmored.push(key.publicKey);
} else {
mail.unregisteredUsers.push(recipient);
}
after();
});
});
}
// encrypts the body and attachments and persists the mail object
function encryptAndPersist() {
self._emailDao.encrypt({
mail: mail,
publicKeysArmored: mail.publicKeysArmored
}, function(err) {
if (err) {
callback(err);
return;
}
self._devicestorage.storeList([mail], outboxDb, callback);
});
}
};
@ -81,152 +118,213 @@ define(function(require) {
*/
OutboxBO.prototype._processOutbox = function(callback) {
var self = this,
emails;
unsentMails = 0;
// if a _processOutbox call is still in progress when a new timeout kicks
// in, since sending mails might take time, ignore it. otherwise, mails
// could get sent multiple times
// also, if a _processOutbox call is still in progress, ignore it.
if (self._outboxBusy) {
return;
}
checkStorage();
self._outboxBusy = true;
function checkStorage() {
self._outboxBusy = true;
// get pending mails from the outbox
self._devicestorage.listItems(outboxDb, 0, null, function(err, pendingMails) {
// error, we're done here
if (err) {
self._outboxBusy = false;
callback(err);
return;
}
// get last item from outbox
self._emailDao.listForOutbox(function(err, pending) {
// if we're not online, don't even bother sending mails.
if (!self._emailDao._account.online || _.isEmpty(pendingMails)) {
self._outboxBusy = false;
callback(null, pendingMails.length);
return;
}
// we're done after all the mails have been handled
// update the outbox count...
var after = _.after(pendingMails.length, function() {
self._outboxBusy = false;
callback(null, unsentMails);
});
// send pending mails if possible
pendingMails.forEach(function(mail) {
handleMail(mail, after);
});
});
// if we can send the mail, do that. otherwise check if there are users that need to be invited
function handleMail(mail, done) {
// no unregistered users, go straight to send
if (mail.unregisteredUsers.length === 0) {
send(mail, done);
return;
}
var after = _.after(mail.unregisteredUsers.length, function() {
// invite unregistered users if necessary
if (mail.unregisteredUsers.length > 0) {
unsentMails++;
self._invite({
sender: mail.from[0],
recipients: mail.unregisteredUsers
}, done);
return;
}
// there are public keys available for the missing users,
// so let's re-encrypt the mail for them and send it
reencryptAndSend(mail, done);
});
// find out if the unregistered users have registered in the meantime
mail.unregisteredUsers.forEach(function(recipient) {
self._keychain.getReceiverPublicKey(recipient.address, function(err, key) {
var index;
if (err) {
self._outboxBusy = false;
callback(err);
return;
}
if (key) {
// remove the newly joined users from the unregistered users
index = mail.unregisteredUsers.indexOf(recipient);
mail.unregisteredUsers.splice(index, 1);
mail.publicKeysArmored.push(key.publicKey);
}
after();
});
});
}
// all the recipients have public keys available, so let's re-encrypt the mail
// to make it available for them, too
function reencryptAndSend(mail, done) {
self._emailDao.reEncrypt({
mail: mail,
publicKeysArmored: mail.publicKeysArmored
}, function(err) {
if (err) {
self._outboxBusy = false;
callback(err);
return;
}
// update outbox folder count
emails = pending;
// stores the newly encrypted mail object to disk in case something funky
// happens during sending and we need do re-send the mail later.
// avoids doing the encryption twice...
self._devicestorage.storeList([mail], outboxDb, function(err) {
if (err) {
self._outboxBusy = false;
callback(err);
return;
}
// fill all the pending mails into the pending mails array
self.pendingEmails.length = 0; //fastest way to empty an array
pending.forEach(function(i) {
self.pendingEmails.push(i);
send(mail, done);
});
});
}
// we're not online, don't even bother sending mails
if (!self._emailDao._account.online) {
// send the encrypted message
function send(mail, done) {
self._emailDao.sendEncrypted({
email: mail
}, function(err) {
if (err) {
self._outboxBusy = false;
callback(null, self.pendingEmails.length);
if (err.code === 42) {
// offline try again later
done();
} else {
self._outboxBusy = false;
callback(err);
}
return;
}
// sending pending mails
processMails();
// remove the pending mail from the storage
removeFromStorage(mail, done);
// fire sent notification
if (typeof self.onSent === 'function') {
self.onSent(mail);
}
});
}
// process the next pending mail
function processMails() {
if (emails.length === 0) {
// in the navigation controller, this updates the folder count
self._outboxBusy = false;
callback(null, self.pendingEmails.length);
return;
}
// in the navigation controller, this updates the folder count
callback(null, self.pendingEmails.length);
var email = emails.shift();
checkReceivers(email);
}
// check whether there are unregistered receivers, i.e. receivers without a public key
function checkReceivers(email) {
var unregisteredUsers, receiverChecked;
unregisteredUsers = [];
receiverChecked = _.after(email.to.length, function() {
// invite unregistered users if necessary
if (unregisteredUsers.length > 0) {
invite(unregisteredUsers);
// removes the mail object from disk after successfully sending it
function removeFromStorage(mail, done) {
self._devicestorage.removeList(outboxDb + '_' + mail.id, function(err) {
if (err) {
self._outboxBusy = false;
callback(err);
return;
}
sendEncrypted(email);
done();
});
}
};
// find out if there are unregistered users
email.to.forEach(function(recipient) {
self._keychain.getReceiverPublicKey(recipient.address, function(err, key) {
if (err) {
self._outboxBusy = false;
callback(err);
return;
}
/**
* Sends an invitation mail to an array of users that have no public key available yet
* @param {Array} recipients Array of objects with information on the sender (name, address)
* @param {Function} callback Invoked when the mail was sent
*/
OutboxBO.prototype._invite = function(options, callback) {
var self = this,
sender = options.sender;
if (!key) {
unregisteredUsers.push(recipient);
}
var after = _.after(options.recipients.length, callback);
receiverChecked();
});
options.recipients.forEach(function(recipient) {
checkInvitationStatus(recipient, after);
});
// checks the invitation status. if an invitation is pending, we do not need to resend the invitation mail
function checkInvitationStatus(recipient, done) {
self._invitationDao.check({
recipient: recipient.address,
sender: sender.address
}, function(err, status) {
if (err) {
callback(err);
return;
}
if (status === InvitationDAO.INVITE_PENDING) {
// the recipient is already invited, we're done here.
done();
return;
}
invite(recipient, done);
});
}
// invite the unregistered receivers, if necessary
function invite(addresses) {
var sender = self._emailDao._account.emailAddress;
var invitationFinished = _.after(addresses.length, function() {
// after all of the invitations are checked and sent (if necessary),
processMails();
});
// check which of the adresses has pending invitations
addresses.forEach(function(recipient) {
var recipientAddress = recipient.address;
self._invitationDao.check({
recipient: recipientAddress,
sender: sender
}, function(err, status) {
if (err) {
self._outboxBusy = false;
callback(err);
return;
}
if (status === InvitationDAO.INVITE_PENDING) {
// the recipient is already invited, we're done here.
invitationFinished();
return;
}
// the recipient is not yet invited, so let's do that
self._invitationDao.invite({
recipient: recipientAddress,
sender: sender
}, function(err, status) {
if (err) {
self._outboxBusy = false;
callback(err);
return;
}
if (status !== InvitationDAO.INVITE_SUCCESS) {
self._outboxBusy = false;
callback({
errMsg: 'could not successfully invite ' + recipientAddress
});
return;
}
sendInvitationMail(recipient, sender);
// let's invite the recipient and send him a mail to inform him to join whiteout
function invite(recipient, done) {
self._invitationDao.invite({
recipient: recipient.address,
sender: sender.address
}, function(err, status) {
if (err) {
callback(err);
return;
}
if (status !== InvitationDAO.INVITE_SUCCESS) {
callback({
errMsg: 'Could not successfully invite ' + recipient
});
return;
}
});
});
// send an invitation to the unregistered user, aka the recipient
function sendInvitationMail(recipient, sender) {
var invitationMail = {
from: [sender],
to: [recipient],
@ -239,10 +337,9 @@ define(function(require) {
email: invitationMail
}, function(err) {
if (err) {
self._outboxBusy = false;
if (err.code === 42) {
// offline try again later
callback();
done();
} else {
callback(err);
}
@ -250,63 +347,12 @@ define(function(require) {
}
// fire sent notification
self._onSent(invitationMail);
invitationFinished();
});
}
}
function sendEncrypted(email) {
self._emailDao.sendEncrypted({
email: email
}, function(err) {
if (err) {
self._outboxBusy = false;
if (err.code === 42) {
// offline try again later
callback();
} else {
callback(err);
if (typeof self.onSent === 'function') {
self.onSent(invitationMail);
}
return;
}
// the email has been sent, remove from pending mails
removeFromPendingMails(email);
// fire sent notification
self._onSent(email);
removeFromStorage(email.id);
});
}
// update the member so that the outbox can visualize
function removeFromPendingMails(email) {
var i = self.pendingEmails.indexOf(email);
self.pendingEmails.splice(i, 1);
}
function removeFromStorage(id) {
if (!id) {
self._outboxBusy = false;
callback({
errMsg: 'Cannot remove email from storage without a valid id!'
done();
});
return;
}
// delete email from local storage
var key = dbType + '_' + id;
self._devicestorage.removeList(key, function(err) {
if (err) {
self._outboxBusy = false;
callback(err);
return;
}
processMails();
});
}
};

View File

@ -59,11 +59,6 @@ define(function(require) {
//
$scope.getBody = function(email) {
// don't stream message content of outbox messages...
if (getFolder().type === 'Outbox') {
return;
}
emailDao.getBody({
folder: getFolder().path,
message: email
@ -78,7 +73,7 @@ define(function(require) {
// automatically decrypt if it's the selected email
if (email === $scope.state.mailList.selected) {
emailDao.decryptMessageContent({
emailDao.decryptBody({
message: email
}, $scope.onError);
}
@ -98,12 +93,9 @@ define(function(require) {
$scope.state.mailList.selected = email;
$scope.state.read.toggle(true);
// if we're in the outbox, don't decrypt as usual
if (getFolder().type !== 'Outbox') {
emailDao.decryptMessageContent({
message: email
}, $scope.onError);
}
emailDao.decryptBody({
message: email
}, $scope.onError);
// if the email is unread, please sync the new state.
// otherweise forget about it.
@ -127,19 +119,21 @@ define(function(require) {
* Synchronize the selected imap folder to local storage
*/
$scope.synchronize = function(callback) {
// if we're in the outbox, don't do an imap sync
if (getFolder().type === 'Outbox') {
updateStatus('Last update: ', new Date());
selectFirstMessage(outboxBo.pendingEmails);
return;
}
updateStatus('Syncing ...');
// let email dao handle sync transparently
emailDao.sync({
folder: getFolder().path
}, function(err) {
if ($scope.state.nav.currentFolder.type === 'Outbox') {
emailDao.syncOutbox({
folder: getFolder().path
}, done);
} else {
emailDao.sync({
folder: getFolder().path
}, done);
}
function done(err) {
if (err && err.code === 409) {
// sync still busy
return;
@ -167,7 +161,7 @@ define(function(require) {
if (callback) {
callback();
}
});
}
};
/**
@ -178,17 +172,7 @@ define(function(require) {
return;
}
var index, currentFolder, outboxFolder;
currentFolder = getFolder();
// trashFolder = _.findWhere($scope.folders, {
// type: 'Trash'
// });
outboxFolder = _.findWhere($scope.account.folders, {
type: 'Outbox'
});
if (currentFolder === outboxFolder) {
if (getFolder().type === 'Outbox') {
$scope.onError({
errMsg: 'Deleting messages from the outbox is not yet supported.'
});
@ -199,7 +183,7 @@ define(function(require) {
$scope.synchronize();
function removeAndShowNext() {
index = getFolder().messages.indexOf(email);
var index = getFolder().messages.indexOf(email);
// show the next mail
if (getFolder().messages.length > 1) {
// if we're about to delete the last entry of the array, show the previous (i.e. the one below in the list),
@ -242,13 +226,6 @@ define(function(require) {
// production... in chrome packaged app
// if we're in the outbox, read directly from there.
if (getFolder().type === 'Outbox') {
updateStatus('Last update: ', new Date());
selectFirstMessage(outboxBo.pendingEmails);
return;
}
// unselect selection from old folder
$scope.select();
// display and select first

View File

@ -42,51 +42,24 @@ define(function(require) {
};
$scope.onOutboxUpdate = function(err, count) {
var outbox, mail;
if (err) {
$scope.onError(err);
return;
}
outbox = _.findWhere($scope.account.folders, {
// 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...
var outbox = _.findWhere($scope.account.folders, {
type: 'Outbox'
});
// update the outbox mail count
outbox.count = count;
// if we're NOT viewing the outbox or we're not looking at any mail now, we're done
if ($scope.state.nav.currentFolder !== outbox || !$scope.state.mailList.selected) {
$scope.$apply();
return;
}
// so we're currently in the outbox.
// if the currently selected mail is still among the pending mails, re-select it, since the object has changed.
// however, if the mail is NOT in the outbox anymore, select another pending mail
//
// this is a workaround due to the fact that the outbox loads pending messages from the indexedDB,
// where object identity is broken when you read an object twice, which happens upon the next retry
// to send the pending messages...
mail = _.findWhere(outbox.messages, {
id: $scope.state.mailList.selected.id
});
if (mail) {
// select the 'new old' mail
$scope.state.mailList.selected = mail;
if (outbox === $scope.state.nav.currentFolder) {
$scope.state.mailList.synchronize();
} else {
if (outbox.messages.length) {
// there are more mails to show, select another one in the list
$scope.state.mailList.selected = outbox.messages[0];
} else {
// no mails to show, don't select anything...
$scope.state.mailList.selected = undefined;
}
outbox.count = count;
$scope.$apply();
}
$scope.$apply();
};
//

View File

@ -6,7 +6,7 @@ define(function(require) {
aes = require('cryptoLib/aes-cbc'),
util = require('cryptoLib/util'),
str = require('js/app-config').string,
crypto, emailDao;
crypto, emailDao, outbox;
//
// Controller
@ -14,7 +14,8 @@ define(function(require) {
var WriteCtrl = function($scope, $filter) {
crypto = appController._crypto;
emailDao = appController._emailDao;
emailDao = appController._emailDao,
outbox = appController._outboxBo;
// set default value so that the popover height is correct on init
$scope.keyId = 'XXXXXXXX';
@ -211,83 +212,63 @@ define(function(require) {
// build email model for smtp-client
email = {
to: [],
cc: [],
bcc: [],
from: [{
address: emailDao._account.emailAddress
}],
to: $scope.to.filter(filterEmptyAddresses),
cc: $scope.cc.filter(filterEmptyAddresses),
bcc: $scope.bcc.filter(filterEmptyAddresses),
subject: $scope.subject.trim() ? $scope.subject.trim() : str.fallbackSubject, // Subject line, or the fallback subject, if nothing valid was entered
body: $scope.body.trim() // use parsed plaintext body
body: $scope.body.trim(), // use parsed plaintext body
attachments: $scope.attachments
};
email.from = [{
name: '',
address: emailDao._account.emailAddress
}];
// validate recipients and gather public keys
email.receiverKeys = []; // gather public keys for emailDao._encrypt
appendReceivers($scope.to, email.to);
appendReceivers($scope.cc, email.cc);
appendReceivers($scope.bcc, email.bcc);
function appendReceivers(srcField, destField) {
srcField.forEach(function(recipient) {
// validate address
if (!util.validateEmailAddress(recipient.address)) {
return;
}
// append address to email model
destField.push({
address: recipient.address
});
// add public key to list of recipient keys
if (recipient.key && recipient.key.publicKey) {
email.receiverKeys.push(recipient.key.publicKey);
}
});
}
// add attachment to email object
if ($scope.attachments.length > 0) {
email.attachments = $scope.attachments;
}
// persist the email locally for later smtp transmission
emailDao.storeForOutbox(email, function(err) {
// persist the email to disk for later sending
outbox.put(email, function(err) {
if (err) {
$scope.onError(err);
return;
}
// helper flag to remember if we need to sync back to imap
// in case the replyTo.answered changed
var needsSync = false;
// mark replyTo as answered, if necessary
if ($scope.replyTo && !$scope.replyTo.answered) {
$scope.replyTo.answered = true;
needsSync = true;
}
// close the writer
$scope.state.writer.close();
// update the ui the scope
$scope.$apply();
$scope.emptyOutbox($scope.onOutboxUpdate);
markAnswered();
});
};
function markAnswered() {
// mark replyTo as answered
if (!$scope.replyTo) {
return;
}
// mark list object
$scope.replyTo.answered = true;
emailDao.sync({
folder: $scope.state.nav.currentFolder.path
}, function(err) {
if (err && err.code === 42) {
// offline
$scope.onError();
// if we need to synchronize replyTo.answered, let's do that.
// otherwise, we're done
if (!needsSync) {
return;
}
$scope.onError(err);
emailDao.sync({
folder: $scope.state.nav.currentFolder.path
}, function(err) {
if (err && err.code === 42) {
// offline
$scope.onError();
return;
}
$scope.onError(err);
});
});
}
function filterEmptyAddresses(addr) {
return !!addr.address;
}
};
};
//

View File

@ -6,12 +6,12 @@ define(function(require) {
str = require('js/app-config').string,
config = require('js/app-config').config;
var EmailDAO = function(keychain, crypto, devicestorage) {
var self = this;
self._keychain = keychain;
self._crypto = crypto;
self._devicestorage = devicestorage;
var EmailDAO = function(keychain, crypto, devicestorage, pgpbuilder, mailreader) {
this._keychain = keychain;
this._crypto = crypto;
this._devicestorage = devicestorage;
this._pgpbuilder = pgpbuilder;
this._mailreader = mailreader;
};
//
@ -74,10 +74,6 @@ define(function(require) {
self._imapClient = options.imapClient;
self._pgpMailer = options.pgpMailer;
// set private key
if (self._crypto && self._crypto._privateKey) {
self._pgpMailer._privateKey = self._crypto._privateKey;
}
// delegation-esque pattern to mitigate between node-style events and plain js
self._imapClient.onIncomingMessage = function(message) {
@ -137,9 +133,16 @@ define(function(require) {
passphrase: options.passphrase,
privateKeyArmored: options.keypair.privateKey.encryptedKey,
publicKeyArmored: options.keypair.publicKey.publicKey
}, callback);
// set decrypted privateKey to pgpMailer
self._pgpMailer._privateKey = self._crypto._privateKey;
}, function(err) {
if (err) {
callback(err);
return;
}
// set decrypted privateKey to pgpMailer
self._pgpbuilder._privateKey = self._crypto._privateKey;
callback();
});
return;
}
@ -182,11 +185,88 @@ define(function(require) {
encryptedKey: generatedKeypair.privateKeyArmored
}
};
self._keychain.putUserKeyPair(newKeypair, callback);
self._keychain.putUserKeyPair(newKeypair, function(err) {
if (err) {
callback(err);
return;
}
// set decrypted privateKey to pgpMailer
self._pgpbuilder._privateKey = self._crypto._privateKey;
callback();
});
});
}
};
/**
* Syncs outbox content from disk to memory, not vice-versa
*/
EmailDAO.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();
});
};
EmailDAO.prototype.sync = function(options, callback) {
/*
* Here's how delta sync works:
@ -206,8 +286,7 @@ define(function(require) {
* deltaF4: imap > memory => we changed flags directly on the remote, sync them to the storage and memory
*/
var self = this,
folder, isFolderInitialized;
var self = this;
// validate options
if (!options.folder) {
@ -226,16 +305,18 @@ define(function(require) {
return;
}
// not busy -> set busy
// make sure two syncs for the same folder don't interfere
self._account.busy = true;
folder = _.findWhere(self._account.folders, {
var 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 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;
@ -293,6 +374,7 @@ define(function(require) {
storedMessageUids = _.pluck(storedMessages, 'uid'),
delta1 = _.difference(storedMessageUids, inMemoryUids); // delta1 contains only uids
// if we're we are done here
if (_.isEmpty(delta1)) {
doDeltaF2();
return;
@ -664,19 +746,22 @@ define(function(require) {
});
}
function finishSync() {
// 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();
}
}
}
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
*/
@ -867,7 +952,7 @@ define(function(require) {
};
EmailDAO.prototype.decryptMessageContent = function(options, callback) {
EmailDAO.prototype.decryptBody = function(options, callback) {
var self = this,
message = options.message;
@ -900,7 +985,7 @@ define(function(require) {
decrypted = decrypted || err.errMsg || 'Error occurred during decryption';
// this is a very primitive detection if we have PGP/MIME or PGP/INLINE
if (decrypted.indexOf('Content-Transfer-Encoding:') === -1 && decrypted.indexOf('Content-Type:') === -1) {
if (!self._mailreader.isRfc(decrypted)) {
message.body = decrypted;
message.decrypted = true;
message.decryptingBody = false;
@ -911,7 +996,7 @@ define(function(require) {
// parse the decrypted MIME message
self._imapParseMessageBlock({
message: message,
block: decrypted
raw: decrypted
}, function(error) {
if (error) {
message.decryptingBody = false;
@ -934,39 +1019,6 @@ define(function(require) {
});
};
EmailDAO.prototype._imapMark = function(options, callback) {
if (!this._account.online) {
callback({
errMsg: 'Client is currently offline!',
code: 42
});
return;
}
this._imapClient.updateFlags({
path: options.folder,
uid: options.uid,
unread: options.unread,
answered: options.answered
}, callback);
};
EmailDAO.prototype.move = function(options, callback) {
if (!this._account.online) {
callback({
errMsg: 'Client is currently offline!',
code: 42
});
return;
}
this._imapClient.moveMessage({
path: options.folder,
uid: options.uid,
destination: options.destination
}, callback);
};
EmailDAO.prototype.getAttachment = function(options, callback) {
if (!this._account.online) {
callback({
@ -980,8 +1032,7 @@ define(function(require) {
};
EmailDAO.prototype.sendEncrypted = function(options, callback) {
var self = this,
email = options.email;
var self = this;
if (!this._account.online) {
callback({
@ -991,35 +1042,16 @@ define(function(require) {
return;
}
// validate the email input
if (!email.to || !email.from || !email.to[0].address || !email.from[0].address || !Array.isArray(email.receiverKeys)) {
callback({
errMsg: 'Invalid email object!'
});
return;
}
// add whiteout tag to subject
options.email.subject = str.subjectPrefix + options.email.subject;
// 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
email.receiverKeys.push(ownKeys.publicKeyArmored);
// add whiteout tag to subject
email.subject = str.subjectPrefix + email.subject;
// mime encode, sign, encrypt and send email via smtp
self._pgpMailer.send({
encrypt: true,
cleartextMessage: str.message,
mail: email,
publicKeysArmored: email.receiverKeys
}, callback);
});
// mime encode, sign, encrypt and send email via smtp
self._pgpMailer.send({
encrypt: true,
cleartextMessage: str.message,
mail: options.email,
publicKeysArmored: options.email.publicKeysArmored
}, callback);
};
EmailDAO.prototype.sendPlaintext = function(options, callback) {
@ -1040,6 +1072,14 @@ define(function(require) {
}, callback);
};
EmailDAO.prototype.encrypt = function(options, callback) {
this._pgpbuilder.encrypt(options, callback);
};
EmailDAO.prototype.reEncrypt = function(options, callback) {
this._pgpbuilder.reEncrypt(options, callback);
};
//
// Internal API
//
@ -1080,9 +1120,28 @@ define(function(require) {
this._pgpMailer.login();
};
// IMAP API
/**
* Mark imap messages as un-/read or un-/answered
*/
EmailDAO.prototype._imapMark = function(options, callback) {
if (!this._account.online) {
callback({
errMsg: 'Client is currently offline!',
code: 42
});
return;
}
this._imapClient.updateFlags({
path: options.folder,
uid: options.uid,
unread: options.unread,
answered: options.answered
}, callback);
};
/**
* Login the imap client
*/
@ -1161,7 +1220,7 @@ define(function(require) {
};
EmailDAO.prototype._imapParseMessageBlock = function(options, callback) {
this._imapClient.parseDecryptedMessageBlock(options, callback);
this._mailreader.parseRfc(options, callback);
};
/**
@ -1277,81 +1336,5 @@ define(function(require) {
}
};
/**
* Persists an email object for the outbox, encrypted with the user's public key
* @param {Object} email The email object
* @param {Function} callback(error) Invoked when the email was encrypted and persisted, contains information in case of an error
*/
EmailDAO.prototype.storeForOutbox = function(email, callback) {
var self = this,
dbType = 'email_OUTBOX',
plaintext = email.body;
// give the email a random identifier (used for storage)
email.id = util.UUID();
// get own public key so send message can be read
self._crypto.exportKeys(function(err, ownKeys) {
if (err) {
callback(err);
return;
}
// encrypt the email with the user's public key
self._crypto.encrypt(plaintext, [ownKeys.publicKeyArmored], function(err, ciphertext) {
if (err) {
callback(err);
return;
}
// replace plaintext body with pgp message
email.body = ciphertext;
// store to local storage
self._devicestorage.storeList([email], dbType, callback);
});
});
};
/**
* Reads and decrypts persisted email objects for the outbox
* @param {Function} callback(error, emails) Invoked when the email was encrypted and persisted, contains information in case of an error
*/
EmailDAO.prototype.listForOutbox = 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) {
self._crypto.decrypt(mail.body, ownKeys.publicKeyArmored, function(err, decrypted) {
mail.body = err ? err.errMsg : decrypted;
after();
});
mail.encrypted = true;
});
});
});
};
return EmailDAO;
});

View File

@ -261,13 +261,11 @@ define(function(require) {
emailDaoStub.init.yields(null, {});
onConnectStub.yields();
outboxStub.init.returns();
controller.init({}, function(err, keypair) {
expect(err).to.not.exist;
expect(keypair).to.exist;
expect(onConnectStub.calledOnce).to.be.true;
expect(outboxStub.init.calledOnce).to.be.true;
done();
});
});

View File

@ -5,18 +5,20 @@ define(function(require) {
KeychainDAO = require('js/dao/keychain-dao'),
ImapClient = require('imap-client'),
PgpMailer = require('pgpmailer'),
PgpBuilder = require('pgpbuilder'),
PGP = require('js/crypto/pgp'),
DeviceStorageDAO = require('js/dao/devicestorage-dao'),
mailreader = require('mailreader'),
str = require('js/app-config').string,
expect = chai.expect;
chai.Assertion.includeStack = true;
describe('Email DAO unit tests', function() {
var dao, keychainStub, imapClientStub, pgpMailerStub, pgpStub, devicestorageStub;
var dao, keychainStub, imapClientStub, pgpMailerStub, pgpBuilderStub, pgpStub, devicestorageStub;
var emailAddress, passphrase, asymKeySize, mockkeyId, dummyEncryptedMail,
dummyDecryptedMail, dummyLegacyDecryptedMail, mockKeyPair, account, publicKey, verificationMail, verificationUuid,
dummyDecryptedMail, mockKeyPair, account, verificationMail, verificationUuid,
corruptedVerificationMail, corruptedVerificationUuid,
nonWhitelistedMail;
@ -78,21 +80,6 @@ define(function(require) {
body: 'Content-Type: multipart/signed;\r\n boundary="Apple-Mail=_1D8756C0-F347-4D7A-A8DB-7869CBF14FD2";\r\n protocol="application/pgp-signature";\r\n micalg=pgp-sha512\r\n\r\n\r\n--Apple-Mail=_1D8756C0-F347-4D7A-A8DB-7869CBF14FD2\r\nContent-Type: multipart/mixed;\r\n boundary="Apple-Mail=_8ED7DC84-6AD9-4A08-8327-80B62D6BCBFA"\r\n\r\n\r\n--Apple-Mail=_8ED7DC84-6AD9-4A08-8327-80B62D6BCBFA\r\nContent-Transfer-Encoding: 7bit\r\nContent-Type: text/plain;\r\n charset=us-ascii\r\n\r\nasdasd \r\n--Apple-Mail=_8ED7DC84-6AD9-4A08-8327-80B62D6BCBFA\r\nContent-Disposition: attachment;\r\n filename=dummy.txt\r\nContent-Type: text/plain;\r\n name="dummy.txt"\r\nContent-Transfer-Encoding: 7bit\r\n\r\noaudbcoaurbvosuabvlasdjbfalwubjvawvb\r\n--Apple-Mail=_8ED7DC84-6AD9-4A08-8327-80B62D6BCBFA--\r\n\r\n--Apple-Mail=_1D8756C0-F347-4D7A-A8DB-7869CBF14FD2\r\nContent-Transfer-Encoding: 7bit\r\nContent-Disposition: attachment;\r\n filename=signature.asc\r\nContent-Type: application/pgp-signature;\r\n name=signature.asc\r\nContent-Description: Message signed with OpenPGP using GPGMail\r\n\r\n-----BEGIN PGP SIGNATURE-----\r\nComment: GPGTools - https://gpgtools.org\r\n\r\niQEcBAEBCgAGBQJS2kO1AAoJEDzmUwH7XO/cP+YH/2PSBxX1ZZd83Uf9qBGDY807\r\niHOdgPFXm64YjSnohO7XsPcnmihqP1ipS2aaCXFC3/Vgb9nc4isQFS+i1VdPwfuR\r\n1Pd2l3dC4/nD4xO9h/W6JW7Yd24NS5TJD5cA7LYwQ8LF+rOzByMatiTMmecAUCe8\r\nEEalEjuogojk4IacA8dg/bfLqQu9E+0GYUJBcI97dx/0jZ0qMOxbWOQLsJ3DnUnV\r\nOad7pAIbHEO6T0EBsH7TyTj4RRHkP6SKE0mm6ZYUC7KCk2Z3MtkASTxUrnqW5qZ5\r\noaXUO9GEc8KZcmbCdhZY2Y5h+dmucaO0jpbeSKkvtYyD4KZrSvt7NTb/0dSLh4Y=\r\n=G8km\r\n-----END PGP SIGNATURE-----\r\n\r\n--Apple-Mail=_1D8756C0-F347-4D7A-A8DB-7869CBF14FD2--\r\n',
unread: false,
answered: false,
receiverKeys: ['-----BEGIN PGP PUBLIC KEY-----\nasd\n-----END PGP PUBLIC KEY-----']
};
dummyLegacyDecryptedMail = {
uid: 1234,
from: [{
address: 'asd@asd.de'
}],
to: [{
address: 'qwe@qwe.de'
}],
subject: 'qweasd',
body: 'asd',
unread: false,
answered: false,
receiverKeys: ['-----BEGIN PGP PUBLIC KEY-----\nasd\n-----END PGP PUBLIC KEY-----']
};
nonWhitelistedMail = {
uid: 1234,
@ -122,15 +109,15 @@ define(function(require) {
asymKeySize: asymKeySize,
busy: false
};
publicKey = "-----BEGIN PUBLIC KEY-----\r\n" + "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCxy+Te5dyeWd7g0P+8LNO7fZDQ\r\n" + "g96xTb1J6pYE/pPTMlqhB6BRItIYjZ1US5q2vk5Zk/5KasBHAc9RbCqvh9v4XFEY\r\n" + "JVmTXC4p8ft1LYuNWIaDk+R3dyYXmRNct/JC4tks2+8fD3aOvpt0WNn3R75/FGBt\r\n" + "h4BgojAXDE+PRQtcVQIDAQAB\r\n" + "-----END PUBLIC KEY-----";
keychainStub = sinon.createStubInstance(KeychainDAO);
imapClientStub = sinon.createStubInstance(ImapClient);
pgpMailerStub = sinon.createStubInstance(PgpMailer);
pgpBuilderStub = sinon.createStubInstance(PgpBuilder);
pgpStub = sinon.createStubInstance(PGP);
devicestorageStub = sinon.createStubInstance(DeviceStorageDAO);
dao = new EmailDAO(keychainStub, pgpStub, devicestorageStub);
dao = new EmailDAO(keychainStub, pgpStub, devicestorageStub, pgpBuilderStub, mailreader);
dao._account = account;
expect(dao._keychain).to.equal(keychainStub);
@ -369,14 +356,15 @@ define(function(require) {
describe('unlock', function() {
it('should unlock', function(done) {
var importMatcher = sinon.match(function(o) {
expect(o.passphrase).to.equal(passphrase);
expect(o.privateKeyArmored).to.equal(mockKeyPair.privateKey.encryptedKey);
expect(o.publicKeyArmored).to.equal(mockKeyPair.publicKey.publicKey);
return true;
});
dao._pgpMailer = {
_pgpbuilder: {}
};
pgpStub.importKeys.withArgs(importMatcher).yields();
pgpStub.importKeys.withArgs({
passphrase: passphrase,
privateKeyArmored: mockKeyPair.privateKey.encryptedKey,
publicKeyArmored: mockKeyPair.publicKey.publicKey
}).yields();
dao.unlock({
passphrase: passphrase,
@ -391,33 +379,29 @@ define(function(require) {
});
it('should generate a keypair and unlock', function(done) {
var genKeysMatcher, persistKeysMatcher, importMatcher, keypair;
var keypair;
dao._pgpMailer = {
_pgpbuilder: {}
};
keypair = {
keyId: 123,
publicKeyArmored: mockKeyPair.publicKey.publicKey,
privateKeyArmored: mockKeyPair.privateKey.encryptedKey
};
genKeysMatcher = sinon.match(function(o) {
expect(o.emailAddress).to.equal(emailAddress);
expect(o.keySize).to.equal(asymKeySize);
expect(o.passphrase).to.equal(passphrase);
return true;
});
importMatcher = sinon.match(function(o) {
expect(o.passphrase).to.equal(passphrase);
expect(o.privateKeyArmored).to.equal(mockKeyPair.privateKey.encryptedKey);
expect(o.publicKeyArmored).to.equal(mockKeyPair.publicKey.publicKey);
return true;
});
persistKeysMatcher = sinon.match(function(o) {
expect(o).to.deep.equal(mockKeyPair);
return true;
});
pgpStub.generateKeys.withArgs({
emailAddress: emailAddress,
keySize: asymKeySize,
passphrase: passphrase
}).yields(null, keypair);
pgpStub.generateKeys.withArgs(genKeysMatcher).yields(null, keypair);
pgpStub.importKeys.withArgs(importMatcher).yields();
pgpStub.importKeys.withArgs({
passphrase: passphrase,
privateKeyArmored: mockKeyPair.privateKey.encryptedKey,
publicKeyArmored: mockKeyPair.publicKey.publicKey
}).yields();
keychainStub.putUserKeyPair.withArgs().yields();
dao.unlock({
@ -495,14 +479,12 @@ define(function(require) {
describe('_imapParseMessageBlock', function() {
it('should parse a message', function(done) {
imapClientStub.parseDecryptedMessageBlock.yields(null, {});
var parseRfc = sinon.stub(mailreader, 'parseRfc').withArgs({}).yields();
dao._imapParseMessageBlock(function(err, msg) {
expect(err).to.not.exist;
expect(msg).to.exist;
dao._imapParseMessageBlock({}, function() {
expect(parseRfc.calledOnce).to.be.true;
done();
});
});
});
@ -1185,13 +1167,13 @@ define(function(require) {
});
});
describe('decryptMessageContent', function() {
describe('decryptBody', function() {
it('should not do anything when the message is not encrypted', function() {
var message = {
encrypted: false
};
dao.decryptMessageContent({
dao.decryptBody({
message: message
});
@ -1204,7 +1186,7 @@ define(function(require) {
decrypted: true
};
dao.decryptMessageContent({
dao.decryptBody({
message: message
});
@ -1223,20 +1205,20 @@ define(function(require) {
body: '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----'
};
mimeBody = 'Content-Transfer-Encoding: Content-Type:';
mimeBody = 'Content-Type: asdasdasd';
parsedBody = 'body? yes.';
keychainStub.getReceiverPublicKey.withArgs(message.from[0].address).yieldsAsync(null, mockKeyPair.publicKey);
pgpStub.decrypt.withArgs(message.body, mockKeyPair.publicKey.publicKey).yieldsAsync(null, mimeBody);
parseStub = sinon.stub(dao, '_imapParseMessageBlock', function(o, cb) {
expect(o.message).to.equal(message);
expect(o.block).to.equal(mimeBody);
expect(o.raw).to.equal(mimeBody);
o.message.body = parsedBody;
cb(null, o.message);
});
dao.decryptMessageContent({
dao.decryptBody({
message: message
}, function(error, msg) {
expect(error).to.not.exist;
@ -1274,7 +1256,7 @@ define(function(require) {
pgpStub.decrypt.withArgs(message.body, mockKeyPair.publicKey.publicKey).yieldsAsync(null, plaintextBody);
parseStub = sinon.stub(dao, '_imapParseMessageBlock');
dao.decryptMessageContent({
dao.decryptBody({
message: message
}, function(error, msg) {
expect(error).to.not.exist;
@ -1314,7 +1296,7 @@ define(function(require) {
});
parseStub = sinon.stub(dao, '_imapParseMessageBlock');
dao.decryptMessageContent({
dao.decryptBody({
message: message
}, function(error, msg) {
expect(error).to.not.exist;
@ -1347,7 +1329,7 @@ define(function(require) {
keychainStub.getReceiverPublicKey.yields({});
parseStub = sinon.stub(dao, '_imapParseMessageBlock');
dao.decryptMessageContent({
dao.decryptBody({
message: message
}, function(error, msg) {
expect(error).to.exist;
@ -2566,26 +2548,6 @@ define(function(require) {
});
});
describe('move', function() {
it('should work', function(done) {
imapClientStub.moveMessage.withArgs({
path: 'asdf',
uid: 1,
destination: 'asdasd'
}).yields();
dao.move({
folder: 'asdf',
uid: 1,
destination: 'asdasd'
}, function(err) {
expect(imapClientStub.moveMessage.calledOnce).to.be.true;
expect(err).to.not.exist;
done();
});
});
});
describe('sendPlaintext', function() {
it('should work', function(done) {
pgpMailerStub.send.withArgs({
@ -2616,16 +2578,14 @@ define(function(require) {
describe('sendEncrypted', function() {
it('should work', function(done) {
pgpStub.exportKeys.yields(null, {
privateKeyArmored: mockKeyPair.privateKey.encryptedKey,
publicKeyArmored: mockKeyPair.publicKey.publicKey
});
var publicKeys = ["PUBLIC KEY"];
dummyDecryptedMail.publicKeysArmored = publicKeys;
pgpMailerStub.send.withArgs({
encrypt: true,
cleartextMessage: str.message,
mail: dummyDecryptedMail,
publicKeysArmored: dummyDecryptedMail.receiverKeys
publicKeysArmored: publicKeys
}).yields();
dao.sendEncrypted({
@ -2633,17 +2593,14 @@ define(function(require) {
}, function(err) {
expect(err).to.not.exist;
expect(pgpStub.exportKeys.calledOnce).to.be.true;
expect(pgpMailerStub.send.calledOnce).to.be.true;
done();
});
});
it('should not work when pgpmailer fails', function(done) {
pgpStub.exportKeys.yields(null, {
privateKeyArmored: mockKeyPair.privateKey.encryptedKey,
publicKeyArmored: mockKeyPair.publicKey.publicKey
});
var publicKeys = ["PUBLIC KEY"];
dummyDecryptedMail.publicKeysArmored = publicKeys;
pgpMailerStub.send.yields({});
dao.sendEncrypted({
@ -2651,176 +2608,53 @@ define(function(require) {
}, function(err) {
expect(err).to.exist;
expect(pgpStub.exportKeys.calledOnce).to.be.true;
expect(pgpMailerStub.send.calledOnce).to.be.true;
done();
});
});
it('should not work when sender key export fails', function(done) {
pgpStub.exportKeys.yields({});
});
dao.sendEncrypted({
email: dummyDecryptedMail
describe('encrypt', function() {
it('should encrypt', function(done) {
pgpBuilderStub.encrypt.yields();
dao.encrypt({}, function() {
expect(pgpBuilderStub.encrypt.calledOnce).to.be.true;
done();
});
});
});
describe('reEncrypt', function() {
it('should re-encrypt', function(done) {
pgpBuilderStub.reEncrypt.yields();
dao.reEncrypt({}, function() {
expect(pgpBuilderStub.reEncrypt.calledOnce).to.be.true;
done();
});
});
});
describe('syncOutbox', function() {
it('should sync the outbox', function(done) {
var folder = 'FOLDAAAA';
dao._account.folders = [{
type: 'Folder',
path: folder
}];
var localListStub = sinon.stub(dao, '_localListMessages').withArgs({
folder: folder
}).yields(null, [dummyEncryptedMail]);
dao.syncOutbox({
folder: folder
}, function(err) {
expect(err).to.exist;
expect(pgpStub.exportKeys.calledOnce).to.be.true;
expect(pgpMailerStub.send.calledOnce).to.be.false;
done();
});
});
});
describe('storeForOutbox', function() {
it('should work', function(done) {
var key = 'omgsocrypto';
pgpStub.exportKeys.yields(null, {
publicKeyArmored: key
});
pgpStub.encrypt.withArgs(dummyDecryptedMail.body, [key]).yields(null, 'asdfasfd');
devicestorageStub.storeList.withArgs([dummyDecryptedMail], 'email_OUTBOX').yields();
dao.storeForOutbox(dummyDecryptedMail, function(err) {
expect(err).to.not.exist;
expect(pgpStub.exportKeys.calledOnce).to.be.true;
expect(pgpStub.encrypt.calledOnce).to.be.true;
expect(devicestorageStub.storeList.calledOnce).to.be.true;
done();
});
});
it('should work when store fails', function(done) {
var key = 'omgsocrypto';
pgpStub.exportKeys.yields(null, {
publicKeyArmored: key
});
pgpStub.encrypt.yields(null, 'asdfasfd');
devicestorageStub.storeList.yields({});
dao.storeForOutbox(dummyDecryptedMail, function(err) {
expect(err).to.exist;
expect(pgpStub.exportKeys.calledOnce).to.be.true;
expect(pgpStub.encrypt.calledOnce).to.be.true;
expect(devicestorageStub.storeList.calledOnce).to.be.true;
done();
});
});
it('should work when encryption fails', function(done) {
var key = 'omgsocrypto';
pgpStub.exportKeys.yields(null, {
publicKeyArmored: key
});
pgpStub.encrypt.yields({});
dao.storeForOutbox(dummyDecryptedMail, function(err) {
expect(err).to.exist;
expect(pgpStub.exportKeys.calledOnce).to.be.true;
expect(pgpStub.encrypt.calledOnce).to.be.true;
expect(devicestorageStub.storeList.called).to.be.false;
done();
});
});
it('should work when key export fails', function(done) {
pgpStub.exportKeys.yields({});
dao.storeForOutbox(dummyDecryptedMail, function(err) {
expect(err).to.exist;
expect(pgpStub.exportKeys.calledOnce).to.be.true;
expect(pgpStub.encrypt.called).to.be.false;
expect(devicestorageStub.storeList.called).to.be.false;
done();
});
});
});
describe('listForOutbox', function() {
it('should work', function(done) {
var key = 'omgsocrypto';
devicestorageStub.listItems.withArgs('email_OUTBOX', 0, null).yields(null, [dummyEncryptedMail]);
pgpStub.exportKeys.yields(null, {
publicKeyArmored: key
});
pgpStub.decrypt.withArgs(dummyEncryptedMail.body, key).yields(null, dummyDecryptedMail.body);
dao.listForOutbox(function(err, mails) {
expect(err).to.not.exist;
expect(devicestorageStub.listItems.calledOnce).to.be.true;
expect(pgpStub.exportKeys.calledOnce).to.be.true;
expect(pgpStub.decrypt.calledOnce).to.be.true;
expect(mails.length).to.equal(1);
expect(mails[0].body).to.equal(dummyDecryptedMail.body);
done();
});
});
it('should not work when decryption fails', function(done) {
var key = 'omgsocrypto',
errMsg = 'THIS IS AN ERROR!';
devicestorageStub.listItems.yields(null, [dummyEncryptedMail]);
pgpStub.exportKeys.yields(null, {
publicKeyArmored: key
});
pgpStub.decrypt.yields({
errMsg: errMsg
});
dao.listForOutbox(function(err, mails) {
expect(err).to.not.exist;
expect(mails[0].body).to.equal(errMsg);
expect(devicestorageStub.listItems.calledOnce).to.be.true;
expect(pgpStub.exportKeys.calledOnce).to.be.true;
expect(pgpStub.decrypt.calledOnce).to.be.true;
done();
});
});
it('should not work when key export fails', function(done) {
devicestorageStub.listItems.yields(null, [dummyEncryptedMail]);
pgpStub.exportKeys.yields({});
dao.listForOutbox(function(err, mails) {
expect(err).to.exist;
expect(mails).to.not.exist;
expect(devicestorageStub.listItems.calledOnce).to.be.true;
expect(pgpStub.exportKeys.calledOnce).to.be.true;
expect(pgpStub.decrypt.called).to.be.false;
done();
});
});
it('should not work when list from storage fails', function(done) {
devicestorageStub.listItems.yields({});
dao.listForOutbox(function(err, mails) {
expect(err).to.exist;
expect(mails).to.not.exist;
expect(devicestorageStub.listItems.calledOnce).to.be.true;
expect(pgpStub.exportKeys.called).to.be.false;
expect(pgpStub.decrypt.called).to.be.false;
expect(localListStub.calledOnce).to.be.true;
expect(dao._account.folders[0].messages.length).to.equal(1);
done();
});

View File

@ -205,24 +205,6 @@ define(function(require) {
});
});
it('should read directly from outbox instead of doing a full imap sync', function() {
scope._stopWatchTask();
var currentFolder = {
type: 'Outbox'
};
scope.folders = [currentFolder];
scope.state.nav = {
currentFolder: currentFolder
};
scope.state.mailList.selected = undefined;
scope.synchronize();
// emails array is also used as the outbox's pending mail
expect(scope.state.mailList.selected).to.deep.equal(emails[emails.length - 1]);
});
});
describe('getBody', function() {
@ -260,7 +242,7 @@ define(function(require) {
scope.select(mail);
expect(emailDaoMock.decryptMessageContent.calledOnce).to.be.true;
expect(emailDaoMock.decryptBody.calledOnce).to.be.true;
expect(synchronizeMock.calledOnce).to.be.true;
expect(scope.state.mailList.selected).to.equal(mail);
@ -288,7 +270,7 @@ define(function(require) {
scope.select(mail);
expect(emailDaoMock.decryptMessageContent.calledOnce).to.be.true;
expect(emailDaoMock.decryptBody.calledOnce).to.be.true;
expect(synchronizeMock.called).to.be.false;
expect(scope.state.mailList.selected).to.equal(mail);

View File

@ -2,13 +2,14 @@ define(function(require) {
'use strict';
var expect = chai.expect,
_ = require('underscore'),
OutboxBO = require('js/bo/outbox'),
KeychainDAO = require('js/dao/keychain-dao'),
EmailDAO = require('js/dao/email-dao'),
DeviceStorageDAO = require('js/dao/devicestorage-dao'),
InvitationDAO = require('js/dao/invitation-dao');
chai.Assertion.includeStack = true;
describe('Outbox Business Object unit test', function() {
var outbox, emailDaoStub, devicestorageStub, invitationDaoStub, keychainStub,
dummyUser = 'spiderpig@springfield.com';
@ -26,24 +27,10 @@ define(function(require) {
keychainStub = sinon.createStubInstance(KeychainDAO);
invitationDaoStub = sinon.createStubInstance(InvitationDAO);
outbox = new OutboxBO(emailDaoStub, keychainStub, devicestorageStub, invitationDaoStub);
outbox.init();
});
afterEach(function() {});
describe('init', function() {
it('should work', function() {
expect(outbox).to.exist;
expect(outbox._emailDao).to.equal(emailDaoStub);
expect(outbox._keychain).to.equal(keychainStub);
expect(outbox._devicestorage).to.equal(devicestorageStub);
expect(outbox._invitationDao).to.equal(invitationDaoStub);
expect(outbox._outboxBusy).to.be.false;
expect(outbox.pendingEmails).to.be.empty;
expect(emailDaoStub._account.folders[0].messages).to.equal(outbox.pendingEmails);
});
});
describe('start/stop checking', function() {
it('should work', function() {
function onOutboxUpdate(err) {
@ -58,77 +45,171 @@ define(function(require) {
});
});
describe('process outbox', function() {
it('should send to registered users and update pending mails', function(done) {
var member, invited, notinvited, dummyMails, unsentCount;
describe('put', function() {
it('should encrypt and store a mail', function(done) {
var mail, senderKey, receiverKey;
member = {
id: '123',
to: [{
senderKey = {
publicKey: 'SENDER PUBLIC KEY'
};
receiverKey = {
publicKey: 'RECEIVER PUBLIC KEY'
};
mail = {
from: [{
name: 'member',
address: 'member@whiteout.io'
}]
}],
to: [{
name: 'member',
address: 'member'
}, {
name: 'notamember',
address: 'notamember'
}],
cc: [],
bcc: []
};
keychainStub.getReceiverPublicKey.withArgs(mail.from[0].address).yieldsAsync(null, senderKey);
keychainStub.getReceiverPublicKey.withArgs(mail.to[0].address).yieldsAsync(null, receiverKey);
keychainStub.getReceiverPublicKey.withArgs(mail.to[1].address).yieldsAsync();
emailDaoStub.encrypt.withArgs({
mail: mail,
publicKeysArmored: [senderKey.publicKey, receiverKey.publicKey]
}).yieldsAsync();
devicestorageStub.storeList.withArgs([mail]).yieldsAsync();
outbox.put(mail, function(error) {
expect(error).to.not.exist;
expect(mail.publicKeysArmored.length).to.equal(2);
expect(mail.unregisteredUsers.length).to.equal(1);
done();
});
});
});
describe('process outbox', function() {
it('should send to registered users and update pending mails', function(done) {
var from, member, invited, notinvited, newlyjoined, dummyMails, newlyjoinedKey;
from = [{
name: 'member',
address: 'member@whiteout.io'
}];
member = {
id: '12',
from: from,
to: [{
name: 'member',
address: 'member'
}],
publicKeysArmored: ['ARMORED KEY OF MEMBER'],
unregisteredUsers: []
};
invited = {
id: '456',
id: '34',
from: from,
to: [{
name: 'invited',
address: 'invited@whiteout.io'
address: 'invited'
}],
publicKeysArmored: [],
unregisteredUsers: [{
name: 'invited',
address: 'invited'
}]
};
notinvited = {
id: '789',
id: '56',
from: from,
to: [{
name: 'notinvited',
address: 'notinvited@whiteout.io'
address: 'notinvited'
}],
publicKeysArmored: [],
unregisteredUsers: [{
name: 'notinvited',
address: 'notinvited'
}]
};
dummyMails = [member, invited, notinvited];
newlyjoined = {
id: '78',
from: from,
to: [{
name: 'newlyjoined',
address: 'newlyjoined'
}],
publicKeysArmored: [],
unregisteredUsers: [{
name: 'newlyjoined',
address: 'newlyjoined'
}]
};
newlyjoinedKey = {
publicKey: 'THIS IS THE NEWLY JOINED PUBLIC KEY!'
};
dummyMails = [member, invited, notinvited, newlyjoined];
devicestorageStub.listItems.yieldsAsync(null, dummyMails);
keychainStub.getReceiverPublicKey.withArgs(invited.unregisteredUsers[0].address).yieldsAsync();
keychainStub.getReceiverPublicKey.withArgs(notinvited.unregisteredUsers[0].address).yieldsAsync();
keychainStub.getReceiverPublicKey.withArgs(newlyjoined.unregisteredUsers[0].address).yieldsAsync(null, newlyjoinedKey);
invitationDaoStub.check.withArgs({
recipient: invited.to[0].address,
sender: invited.from[0].address
}).yieldsAsync(null, InvitationDAO.INVITE_PENDING);
invitationDaoStub.check.withArgs({
recipient: notinvited.to[0].address,
sender: notinvited.from[0].address
}).yieldsAsync(null, InvitationDAO.INVITE_MISSING);
invitationDaoStub.invite.withArgs({
recipient: notinvited.to[0].address,
sender: notinvited.from[0].address
}).yieldsAsync(null, InvitationDAO.INVITE_SUCCESS);
emailDaoStub.listForOutbox.yieldsAsync(null, dummyMails);
emailDaoStub.sendEncrypted.withArgs(sinon.match(function(opts) {
return typeof opts.email !== 'undefined' && opts.email.to.address === member.to.address;
})).yieldsAsync();
emailDaoStub.sendPlaintext.yieldsAsync();
devicestorageStub.removeList.yieldsAsync();
invitationDaoStub.check.withArgs(sinon.match(function(o) {
return o.recipient === 'invited@whiteout.io';
})).yieldsAsync(null, InvitationDAO.INVITE_PENDING);
invitationDaoStub.check.withArgs(sinon.match(function(o) {
return o.recipient === 'notinvited@whiteout.io';
})).yieldsAsync(null, InvitationDAO.INVITE_MISSING);
invitationDaoStub.invite.withArgs(sinon.match(function(o) {
return o.recipient === 'notinvited@whiteout.io';
})).yieldsAsync(null, InvitationDAO.INVITE_SUCCESS);
keychainStub.getReceiverPublicKey.withArgs(sinon.match(function(o) {
return o === 'member@whiteout.io';
})).yieldsAsync(null, 'this is not the key you are looking for...');
keychainStub.getReceiverPublicKey.withArgs(sinon.match(function(o) {
return o === 'invited@whiteout.io' || o === 'notinvited@whiteout.io';
})).yieldsAsync();
var check = _.after(dummyMails.length + 1, function() {
expect(outbox._outboxBusy).to.be.false;
emailDaoStub.reEncrypt.withArgs({
mail: newlyjoined,
publicKeysArmored: [newlyjoinedKey.publicKey]
}).yieldsAsync(null, newlyjoined);
expect(unsentCount).to.equal(2);
expect(emailDaoStub.listForOutbox.callCount).to.equal(1);
expect(emailDaoStub.sendEncrypted.callCount).to.equal(1);
expect(emailDaoStub.sendPlaintext.callCount).to.equal(1);
expect(devicestorageStub.removeList.callCount).to.equal(1);
expect(invitationDaoStub.check.callCount).to.equal(2);
expect(invitationDaoStub.invite.callCount).to.equal(1);
emailDaoStub.sendEncrypted.withArgs({
email: newlyjoined
}).yieldsAsync();
expect(outbox.pendingEmails.length).to.equal(2);
expect(outbox.pendingEmails).to.contain(invited);
expect(outbox.pendingEmails).to.contain(notinvited);
done();
});
emailDaoStub.sendEncrypted.withArgs({
email: member
}).yieldsAsync();
devicestorageStub.storeList.withArgs([newlyjoined]).yieldsAsync();
devicestorageStub.removeList.withArgs('email_OUTBOX_' + member.id).yieldsAsync();
devicestorageStub.removeList.withArgs('email_OUTBOX_' + newlyjoined.id).yieldsAsync();
function onOutboxUpdate(err, count) {
expect(err).to.not.exist;
expect(count).to.exist;
unsentCount = count;
check();
expect(count).to.equal(2);
expect(outbox._outboxBusy).to.be.false;
expect(emailDaoStub.sendEncrypted.calledTwice).to.be.true;
expect(emailDaoStub.reEncrypt.calledOnce).to.be.true;
expect(emailDaoStub.sendPlaintext.calledOnce).to.be.true;
expect(devicestorageStub.listItems.calledOnce).to.be.true;
expect(keychainStub.getReceiverPublicKey.calledThrice).to.be.true;
expect(invitationDaoStub.check.calledTwice).to.be.true;
done();
}
outbox._processOutbox(onOutboxUpdate);
@ -136,31 +217,16 @@ define(function(require) {
it('should not process outbox in offline mode', function(done) {
emailDaoStub._account.online = false;
emailDaoStub.listForOutbox.yieldsAsync(null, [{
id: '123',
to: [{
name: 'member',
address: 'member@whiteout.io'
}]
}]);
devicestorageStub.listItems.yieldsAsync(null, [{}]);
outbox._processOutbox(function(err, count) {
expect(err).to.not.exist;
expect(count).to.equal(1);
expect(emailDaoStub.listForOutbox.callCount).to.equal(1);
expect(devicestorageStub.listItems.callCount).to.equal(1);
expect(outbox._outboxBusy).to.be.false;
done();
});
});
it('should fire notification', function(done) {
outbox.onSent = function(email) {
expect(email).to.exist;
done();
};
outbox._onSent({});
});
});
});
});

View File

@ -6,14 +6,24 @@ define(function(require) {
mocks = require('angularMocks'),
WriteCtrl = require('js/controller/write'),
EmailDAO = require('js/dao/email-dao'),
OutboxBO = require('js/bo/outbox'),
KeychainDAO = require('js/dao/keychain-dao'),
appController = require('js/app-controller');
describe('Write controller unit test', function() {
var ctrl, scope, origEmailDao, emailDaoMock, keychainMock, emailAddress;
var ctrl, scope,
origEmailDao, origOutbox,
emailDaoMock, keychainMock, outboxMock, emailAddress;
beforeEach(function() {
// the app controller is a singleton, we need to remember the
// outbox and email dao to restore it after the tests
origEmailDao = appController._emailDao;
origOutbox = appController._outboxBo;
outboxMock = sinon.createStubInstance(OutboxBO);
appController._outboxBo = outboxMock;
emailDaoMock = sinon.createStubInstance(EmailDAO);
appController._emailDao = emailDaoMock;
@ -37,8 +47,9 @@ define(function(require) {
});
afterEach(function() {
// restore the module
// restore the app controller
appController._emailDao = origEmailDao;
appController._outboxBo = origOutbox;
});
describe('scope variables', function() {
@ -259,127 +270,38 @@ define(function(require) {
});
describe('send to outbox', function() {
it('should work when offline', function(done) {
var re = {
from: [{
address: 'pity@dafool'
}],
subject: 'Ermahgerd!',
sentDate: new Date(),
body: 'so much body!'
};
it('should work', function() {
scope.from = [{
address: 'pity@dafool'
}];
scope.to = [{
address: 'pity@dafool'
}];
scope.cc = [];
scope.bcc = [];
scope.subject = 'Ermahgerd!';
scope.body = 'wow. much body! very text!';
scope.attachments = [];
scope.state.nav = {
currentFolder: 'currentFolder'
};
scope.emptyOutbox = function() {};
scope.onError = function(err) {
expect(err).to.not.exist;
expect(scope.state.writer.open).to.be.false;
expect(emailDaoMock.storeForOutbox.calledOnce).to.be.true;
expect(emailDaoMock.sync.calledOnce).to.be.true;
scope.replyTo = {};
done();
};
emailDaoMock.storeForOutbox.yields();
emailDaoMock.sync.yields({
code: 42
});
scope.state.writer.write(re);
scope.sendToOutbox();
});
it('should work', function(done) {
var re = {
from: [{
address: 'pity@dafool'
}],
subject: 'Ermahgerd!',
sentDate: new Date(),
body: 'so much body!'
};
scope.state.nav = {
currentFolder: 'currentFolder'
};
scope.emptyOutbox = function() {};
scope.onError = function(err) {
expect(err).to.not.exist;
expect(scope.state.writer.open).to.be.false;
expect(emailDaoMock.storeForOutbox.calledOnce).to.be.true;
expect(emailDaoMock.sync.calledOnce).to.be.true;
done();
};
emailDaoMock.storeForOutbox.yields();
outboxMock.put.yields();
emailDaoMock.sync.yields();
scope.state.writer.write(re);
scope.sendToOutbox();
});
it('should fail', function(done) {
var re = {
from: [{
address: 'pity@dafool'
}],
subject: 'Ermahgerd!',
sentDate: new Date(),
body: 'so much body!'
};
scope.state.nav = {
currentFolder: 'currentFolder'
};
scope.emptyOutbox = function() {};
scope.onError = function(err) {
expect(err).to.exist;
expect(scope.state.writer.open).to.be.false;
expect(emailDaoMock.storeForOutbox.calledOnce).to.be.true;
expect(emailDaoMock.sync.calledOnce).to.be.true;
done();
expect(err).to.not.exist;
};
emailDaoMock.storeForOutbox.yields();
emailDaoMock.sync.yields({});
scope.state.writer.write(re);
scope.sendToOutbox();
});
it('should not work and not close the write view', function(done) {
scope.state.writer.write();
expect(outboxMock.put.calledOnce).to.be.true;
expect(emailDaoMock.sync.calledOnce).to.be.true;
scope.to = [{
address: 'pity@dafool.de',
key: {
publicKey: '----- PGP Stuff -----'
}
}];
scope.body = 'asd';
scope.subject = 'yaddablabla';
scope.toKey = 'Public Key';
emailDaoMock.storeForOutbox.withArgs(sinon.match(function(mail) {
return mail.from[0].address === emailAddress && mail.to.length === 1 && mail.receiverKeys.length === 1;
})).yields({
errMsg: 'snafu'
});
scope.onError = function(err) {
expect(err).to.exist;
expect(scope.state.writer.open).to.be.true;
expect(emailDaoMock.storeForOutbox.calledOnce).to.be.true;
done();
};
scope.sendToOutbox();
expect(scope.state.writer.open).to.be.false;
expect(scope.replyTo.answered).to.be.true;
});
});
});