1
0
mirror of https://github.com/moparisthebest/mail synced 2024-11-26 02:42:17 -05:00

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/node-shims/src/*.js',
'imap-client/node_modules/inbox/node_modules/utf7/src/utf7.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/inbox/node_modules/xoauth2/src/xoauth2.js',
'imap-client/node_modules/mimelib/src/mimelib.js', 'mailreader/src/*.js',
'imap-client/node_modules/mimelib/node_modules/addressparser/src/addressparser.js', 'mailreader/node_modules/mailparser/src/*.js',
'imap-client/node_modules/mimelib/node_modules/encoding/src/encoding.js', 'mailreader/node_modules/mailparser/node_modules/encoding/src/encoding.js',
'imap-client/node_modules/mimelib/node_modules/encoding/node_modules/iconv-lite/src/*.js', 'mailreader/node_modules/mailparser/node_modules/mimelib/src/mimelib.js',
'imap-client/node_modules/mailparser/src/*.js', 'mailreader/node_modules/mailparser/node_modules/mimelib/node_modules/addressparser/src/addressparser.js',
'imap-client/node_modules/mime/src/mime.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/src/*.js',
'pgpmailer/node_modules/simplesmtp/src/*', 'pgpmailer/node_modules/simplesmtp/src/*',
'pgpmailer/node_modules/mailbuilder/src/*.js' 'pgpbuilder/src/*.js',
'pgpbuilder/node_modules/mailbuilder/src/*.js'
], ],
dest: 'src/lib/' dest: 'src/lib/'
}, },

View File

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

View File

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

View File

@ -2,10 +2,11 @@ define(function(require) {
'use strict'; 'use strict';
var _ = require('underscore'), var _ = require('underscore'),
util = require('cryptoLib/util'),
str = require('js/app-config').string, str = require('js/app-config').string,
config = require('js/app-config').config, config = require('js/app-config').config,
InvitationDAO = require('js/dao/invitation-dao'), InvitationDAO = require('js/dao/invitation-dao'),
dbType = 'email_OUTBOX'; outboxDb = 'email_OUTBOX';
/** /**
* High level business object that orchestrates the local 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. * Semaphore-esque flag to avoid 'concurrent' calls to _processOutbox when the timeout fires, but a call is still in process.
* @private */ * @private */
this._outboxBusy = false; 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 * Put a email dto in the outbox for sending when ready
* The public callback "onSent" can be set by the caller to get notified. * @param {Object} mail The Email DTO
* @param {Function} callback Invoked when the object was encrypted and persisted to disk
*/ */
OutboxBO.prototype._onSent = function(message) { OutboxBO.prototype.put = function(mail, callback) {
if (typeof this.onSent === 'function') { var self = this,
this.onSent(message); 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) { OutboxBO.prototype._processOutbox = function(callback) {
var self = this, var self = this,
emails; unsentMails = 0;
// if a _processOutbox call is still in progress when a new timeout kicks // also, if a _processOutbox call is still in progress, ignore it.
// in, since sending mails might take time, ignore it. otherwise, mails
// could get sent multiple times
if (self._outboxBusy) { if (self._outboxBusy) {
return; return;
} }
checkStorage(); self._outboxBusy = true;
function checkStorage() { // get pending mails from the outbox
self._outboxBusy = true; 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 // if we're not online, don't even bother sending mails.
self._emailDao.listForOutbox(function(err, pending) { 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) { if (err) {
self._outboxBusy = false; self._outboxBusy = false;
callback(err); callback(err);
return; return;
} }
// update outbox folder count // stores the newly encrypted mail object to disk in case something funky
emails = pending; // 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 send(mail, done);
self.pendingEmails.length = 0; //fastest way to empty an array
pending.forEach(function(i) {
self.pendingEmails.push(i);
}); });
});
}
// we're not online, don't even bother sending mails // send the encrypted message
if (!self._emailDao._account.online) { function send(mail, done) {
self._emailDao.sendEncrypted({
email: mail
}, function(err) {
if (err) {
self._outboxBusy = false; self._outboxBusy = false;
callback(null, self.pendingEmails.length); if (err.code === 42) {
// offline try again later
done();
} else {
self._outboxBusy = false;
callback(err);
}
return; return;
} }
// sending pending mails // remove the pending mail from the storage
processMails(); removeFromStorage(mail, done);
// fire sent notification
if (typeof self.onSent === 'function') {
self.onSent(mail);
}
}); });
} }
// process the next pending mail // removes the mail object from disk after successfully sending it
function processMails() { function removeFromStorage(mail, done) {
if (emails.length === 0) { self._devicestorage.removeList(outboxDb + '_' + mail.id, function(err) {
// in the navigation controller, this updates the folder count if (err) {
self._outboxBusy = false; self._outboxBusy = false;
callback(null, self.pendingEmails.length); callback(err);
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);
return; return;
} }
sendEncrypted(email); done();
}); });
}
};
// find out if there are unregistered users /**
email.to.forEach(function(recipient) { * Sends an invitation mail to an array of users that have no public key available yet
self._keychain.getReceiverPublicKey(recipient.address, function(err, key) { * @param {Array} recipients Array of objects with information on the sender (name, address)
if (err) { * @param {Function} callback Invoked when the mail was sent
self._outboxBusy = false; */
callback(err); OutboxBO.prototype._invite = function(options, callback) {
return; var self = this,
} sender = options.sender;
if (!key) { var after = _.after(options.recipients.length, callback);
unregisteredUsers.push(recipient);
}
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 // let's invite the recipient and send him a mail to inform him to join whiteout
function invite(addresses) { function invite(recipient, done) {
var sender = self._emailDao._account.emailAddress; self._invitationDao.invite({
recipient: recipient.address,
var invitationFinished = _.after(addresses.length, function() { sender: sender.address
// after all of the invitations are checked and sent (if necessary), }, function(err, status) {
processMails(); if (err) {
}); callback(err);
return;
// check which of the adresses has pending invitations }
addresses.forEach(function(recipient) { if (status !== InvitationDAO.INVITE_SUCCESS) {
var recipientAddress = recipient.address; callback({
errMsg: 'Could not successfully invite ' + recipient
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);
}); });
return;
}
});
});
// send an invitation to the unregistered user, aka the recipient
function sendInvitationMail(recipient, sender) {
var invitationMail = { var invitationMail = {
from: [sender], from: [sender],
to: [recipient], to: [recipient],
@ -239,10 +337,9 @@ define(function(require) {
email: invitationMail email: invitationMail
}, function(err) { }, function(err) {
if (err) { if (err) {
self._outboxBusy = false;
if (err.code === 42) { if (err.code === 42) {
// offline try again later // offline try again later
callback(); done();
} else { } else {
callback(err); callback(err);
} }
@ -250,63 +347,12 @@ define(function(require) {
} }
// fire sent notification // fire sent notification
self._onSent(invitationMail); if (typeof self.onSent === 'function') {
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);
} }
return;
}
// the email has been sent, remove from pending mails done();
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!'
}); });
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) { $scope.getBody = function(email) {
// don't stream message content of outbox messages...
if (getFolder().type === 'Outbox') {
return;
}
emailDao.getBody({ emailDao.getBody({
folder: getFolder().path, folder: getFolder().path,
message: email message: email
@ -78,7 +73,7 @@ define(function(require) {
// automatically decrypt if it's the selected email // automatically decrypt if it's the selected email
if (email === $scope.state.mailList.selected) { if (email === $scope.state.mailList.selected) {
emailDao.decryptMessageContent({ emailDao.decryptBody({
message: email message: email
}, $scope.onError); }, $scope.onError);
} }
@ -98,12 +93,9 @@ define(function(require) {
$scope.state.mailList.selected = email; $scope.state.mailList.selected = email;
$scope.state.read.toggle(true); $scope.state.read.toggle(true);
// if we're in the outbox, don't decrypt as usual emailDao.decryptBody({
if (getFolder().type !== 'Outbox') { message: email
emailDao.decryptMessageContent({ }, $scope.onError);
message: email
}, $scope.onError);
}
// if the email is unread, please sync the new state. // if the email is unread, please sync the new state.
// otherweise forget about it. // otherweise forget about it.
@ -127,19 +119,21 @@ define(function(require) {
* Synchronize the selected imap folder to local storage * Synchronize the selected imap folder to local storage
*/ */
$scope.synchronize = function(callback) { $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 ...'); updateStatus('Syncing ...');
// let email dao handle sync transparently // let email dao handle sync transparently
emailDao.sync({ if ($scope.state.nav.currentFolder.type === 'Outbox') {
folder: getFolder().path emailDao.syncOutbox({
}, function(err) { folder: getFolder().path
}, done);
} else {
emailDao.sync({
folder: getFolder().path
}, done);
}
function done(err) {
if (err && err.code === 409) { if (err && err.code === 409) {
// sync still busy // sync still busy
return; return;
@ -167,7 +161,7 @@ define(function(require) {
if (callback) { if (callback) {
callback(); callback();
} }
}); }
}; };
/** /**
@ -178,17 +172,7 @@ define(function(require) {
return; return;
} }
var index, currentFolder, outboxFolder; if (getFolder().type === 'Outbox') {
currentFolder = getFolder();
// trashFolder = _.findWhere($scope.folders, {
// type: 'Trash'
// });
outboxFolder = _.findWhere($scope.account.folders, {
type: 'Outbox'
});
if (currentFolder === outboxFolder) {
$scope.onError({ $scope.onError({
errMsg: 'Deleting messages from the outbox is not yet supported.' errMsg: 'Deleting messages from the outbox is not yet supported.'
}); });
@ -199,7 +183,7 @@ define(function(require) {
$scope.synchronize(); $scope.synchronize();
function removeAndShowNext() { function removeAndShowNext() {
index = getFolder().messages.indexOf(email); var index = getFolder().messages.indexOf(email);
// show the next mail // show the next mail
if (getFolder().messages.length > 1) { if (getFolder().messages.length > 1) {
// if we're about to delete the last entry of the array, show the previous (i.e. the one below in the list), // if we're about to delete the last entry of the array, show the previous (i.e. the one below in the list),
@ -242,13 +226,6 @@ define(function(require) {
// production... in chrome packaged app // 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 // unselect selection from old folder
$scope.select(); $scope.select();
// display and select first // display and select first

View File

@ -42,51 +42,24 @@ define(function(require) {
}; };
$scope.onOutboxUpdate = function(err, count) { $scope.onOutboxUpdate = function(err, count) {
var outbox, mail;
if (err) { if (err) {
$scope.onError(err); $scope.onError(err);
return; 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' 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 (outbox === $scope.state.nav.currentFolder) {
if ($scope.state.nav.currentFolder !== outbox || !$scope.state.mailList.selected) { $scope.state.mailList.synchronize();
$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;
} else { } else {
if (outbox.messages.length) { outbox.count = count;
// there are more mails to show, select another one in the list $scope.$apply();
$scope.state.mailList.selected = outbox.messages[0];
} else {
// no mails to show, don't select anything...
$scope.state.mailList.selected = undefined;
}
} }
$scope.$apply();
}; };
// //

View File

@ -6,7 +6,7 @@ define(function(require) {
aes = require('cryptoLib/aes-cbc'), aes = require('cryptoLib/aes-cbc'),
util = require('cryptoLib/util'), util = require('cryptoLib/util'),
str = require('js/app-config').string, str = require('js/app-config').string,
crypto, emailDao; crypto, emailDao, outbox;
// //
// Controller // Controller
@ -14,7 +14,8 @@ define(function(require) {
var WriteCtrl = function($scope, $filter) { var WriteCtrl = function($scope, $filter) {
crypto = appController._crypto; crypto = appController._crypto;
emailDao = appController._emailDao; emailDao = appController._emailDao,
outbox = appController._outboxBo;
// set default value so that the popover height is correct on init // set default value so that the popover height is correct on init
$scope.keyId = 'XXXXXXXX'; $scope.keyId = 'XXXXXXXX';
@ -211,83 +212,63 @@ define(function(require) {
// build email model for smtp-client // build email model for smtp-client
email = { email = {
to: [], from: [{
cc: [], address: emailDao._account.emailAddress
bcc: [], }],
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 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 // persist the email to disk for later sending
email.receiverKeys = []; // gather public keys for emailDao._encrypt outbox.put(email, function(err) {
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) {
if (err) { if (err) {
$scope.onError(err); $scope.onError(err);
return; 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(); $scope.state.writer.close();
// update the ui the scope
$scope.$apply(); $scope.$apply();
$scope.emptyOutbox($scope.onOutboxUpdate);
markAnswered(); // if we need to synchronize replyTo.answered, let's do that.
}); // otherwise, we're done
}; if (!needsSync) {
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();
return; 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, str = require('js/app-config').string,
config = require('js/app-config').config; config = require('js/app-config').config;
var EmailDAO = function(keychain, crypto, devicestorage) { var EmailDAO = function(keychain, crypto, devicestorage, pgpbuilder, mailreader) {
var self = this; this._keychain = keychain;
this._crypto = crypto;
self._keychain = keychain; this._devicestorage = devicestorage;
self._crypto = crypto; this._pgpbuilder = pgpbuilder;
self._devicestorage = devicestorage; this._mailreader = mailreader;
}; };
// //
@ -74,10 +74,6 @@ define(function(require) {
self._imapClient = options.imapClient; self._imapClient = options.imapClient;
self._pgpMailer = options.pgpMailer; 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 // delegation-esque pattern to mitigate between node-style events and plain js
self._imapClient.onIncomingMessage = function(message) { self._imapClient.onIncomingMessage = function(message) {
@ -137,9 +133,16 @@ define(function(require) {
passphrase: options.passphrase, passphrase: options.passphrase,
privateKeyArmored: options.keypair.privateKey.encryptedKey, privateKeyArmored: options.keypair.privateKey.encryptedKey,
publicKeyArmored: options.keypair.publicKey.publicKey publicKeyArmored: options.keypair.publicKey.publicKey
}, callback); }, function(err) {
// set decrypted privateKey to pgpMailer if (err) {
self._pgpMailer._privateKey = self._crypto._privateKey; callback(err);
return;
}
// set decrypted privateKey to pgpMailer
self._pgpbuilder._privateKey = self._crypto._privateKey;
callback();
});
return; return;
} }
@ -182,11 +185,88 @@ define(function(require) {
encryptedKey: generatedKeypair.privateKeyArmored 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) { EmailDAO.prototype.sync = function(options, callback) {
/* /*
* Here's how delta sync works: * 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 * deltaF4: imap > memory => we changed flags directly on the remote, sync them to the storage and memory
*/ */
var self = this, var self = this;
folder, isFolderInitialized;
// validate options // validate options
if (!options.folder) { if (!options.folder) {
@ -226,16 +305,18 @@ define(function(require) {
return; return;
} }
// not busy -> set busy // make sure two syncs for the same folder don't interfere
self._account.busy = true; self._account.busy = true;
folder = _.findWhere(self._account.folders, { var folder = _.findWhere(self._account.folders, {
path: options.folder 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) { if (!isFolderInitialized) {
initFolderMessages(); initFolderMessages();
return; return;
@ -293,6 +374,7 @@ define(function(require) {
storedMessageUids = _.pluck(storedMessages, 'uid'), storedMessageUids = _.pluck(storedMessages, 'uid'),
delta1 = _.difference(storedMessageUids, inMemoryUids); // delta1 contains only uids delta1 = _.difference(storedMessageUids, inMemoryUids); // delta1 contains only uids
// if we're we are done here
if (_.isEmpty(delta1)) { if (_.isEmpty(delta1)) {
doDeltaF2(); doDeltaF2();
return; 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 * 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, var self = this,
message = options.message; message = options.message;
@ -900,7 +985,7 @@ define(function(require) {
decrypted = decrypted || err.errMsg || 'Error occurred during decryption'; decrypted = decrypted || err.errMsg || 'Error occurred during decryption';
// this is a very primitive detection if we have PGP/MIME or PGP/INLINE // 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.body = decrypted;
message.decrypted = true; message.decrypted = true;
message.decryptingBody = false; message.decryptingBody = false;
@ -911,7 +996,7 @@ define(function(require) {
// parse the decrypted MIME message // parse the decrypted MIME message
self._imapParseMessageBlock({ self._imapParseMessageBlock({
message: message, message: message,
block: decrypted raw: decrypted
}, function(error) { }, function(error) {
if (error) { if (error) {
message.decryptingBody = false; 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) { EmailDAO.prototype.getAttachment = function(options, callback) {
if (!this._account.online) { if (!this._account.online) {
callback({ callback({
@ -980,8 +1032,7 @@ define(function(require) {
}; };
EmailDAO.prototype.sendEncrypted = function(options, callback) { EmailDAO.prototype.sendEncrypted = function(options, callback) {
var self = this, var self = this;
email = options.email;
if (!this._account.online) { if (!this._account.online) {
callback({ callback({
@ -991,35 +1042,16 @@ define(function(require) {
return; return;
} }
// validate the email input // add whiteout tag to subject
if (!email.to || !email.from || !email.to[0].address || !email.from[0].address || !Array.isArray(email.receiverKeys)) { options.email.subject = str.subjectPrefix + options.email.subject;
callback({
errMsg: 'Invalid email object!'
});
return;
}
// get own public key so send message can be read // mime encode, sign, encrypt and send email via smtp
self._crypto.exportKeys(function(err, ownKeys) { self._pgpMailer.send({
if (err) { encrypt: true,
callback(err); cleartextMessage: str.message,
return; mail: options.email,
} publicKeysArmored: options.email.publicKeysArmored
}, callback);
// 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);
});
}; };
EmailDAO.prototype.sendPlaintext = function(options, callback) { EmailDAO.prototype.sendPlaintext = function(options, callback) {
@ -1040,6 +1072,14 @@ define(function(require) {
}, callback); }, 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 // Internal API
// //
@ -1080,9 +1120,28 @@ define(function(require) {
this._pgpMailer.login(); this._pgpMailer.login();
}; };
// IMAP API // 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 * Login the imap client
*/ */
@ -1161,7 +1220,7 @@ define(function(require) {
}; };
EmailDAO.prototype._imapParseMessageBlock = function(options, callback) { 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; return EmailDAO;
}); });

View File

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

View File

@ -5,18 +5,20 @@ define(function(require) {
KeychainDAO = require('js/dao/keychain-dao'), KeychainDAO = require('js/dao/keychain-dao'),
ImapClient = require('imap-client'), ImapClient = require('imap-client'),
PgpMailer = require('pgpmailer'), PgpMailer = require('pgpmailer'),
PgpBuilder = require('pgpbuilder'),
PGP = require('js/crypto/pgp'), PGP = require('js/crypto/pgp'),
DeviceStorageDAO = require('js/dao/devicestorage-dao'), DeviceStorageDAO = require('js/dao/devicestorage-dao'),
mailreader = require('mailreader'),
str = require('js/app-config').string, str = require('js/app-config').string,
expect = chai.expect; expect = chai.expect;
chai.Assertion.includeStack = true; chai.Assertion.includeStack = true;
describe('Email DAO unit tests', function() { 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, var emailAddress, passphrase, asymKeySize, mockkeyId, dummyEncryptedMail,
dummyDecryptedMail, dummyLegacyDecryptedMail, mockKeyPair, account, publicKey, verificationMail, verificationUuid, dummyDecryptedMail, mockKeyPair, account, verificationMail, verificationUuid,
corruptedVerificationMail, corruptedVerificationUuid, corruptedVerificationMail, corruptedVerificationUuid,
nonWhitelistedMail; 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', 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, unread: false,
answered: 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 = { nonWhitelistedMail = {
uid: 1234, uid: 1234,
@ -122,15 +109,15 @@ define(function(require) {
asymKeySize: asymKeySize, asymKeySize: asymKeySize,
busy: false 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); keychainStub = sinon.createStubInstance(KeychainDAO);
imapClientStub = sinon.createStubInstance(ImapClient); imapClientStub = sinon.createStubInstance(ImapClient);
pgpMailerStub = sinon.createStubInstance(PgpMailer); pgpMailerStub = sinon.createStubInstance(PgpMailer);
pgpBuilderStub = sinon.createStubInstance(PgpBuilder);
pgpStub = sinon.createStubInstance(PGP); pgpStub = sinon.createStubInstance(PGP);
devicestorageStub = sinon.createStubInstance(DeviceStorageDAO); devicestorageStub = sinon.createStubInstance(DeviceStorageDAO);
dao = new EmailDAO(keychainStub, pgpStub, devicestorageStub); dao = new EmailDAO(keychainStub, pgpStub, devicestorageStub, pgpBuilderStub, mailreader);
dao._account = account; dao._account = account;
expect(dao._keychain).to.equal(keychainStub); expect(dao._keychain).to.equal(keychainStub);
@ -369,14 +356,15 @@ define(function(require) {
describe('unlock', function() { describe('unlock', function() {
it('should unlock', function(done) { it('should unlock', function(done) {
var importMatcher = sinon.match(function(o) { dao._pgpMailer = {
expect(o.passphrase).to.equal(passphrase); _pgpbuilder: {}
expect(o.privateKeyArmored).to.equal(mockKeyPair.privateKey.encryptedKey); };
expect(o.publicKeyArmored).to.equal(mockKeyPair.publicKey.publicKey);
return true;
});
pgpStub.importKeys.withArgs(importMatcher).yields(); pgpStub.importKeys.withArgs({
passphrase: passphrase,
privateKeyArmored: mockKeyPair.privateKey.encryptedKey,
publicKeyArmored: mockKeyPair.publicKey.publicKey
}).yields();
dao.unlock({ dao.unlock({
passphrase: passphrase, passphrase: passphrase,
@ -391,33 +379,29 @@ define(function(require) {
}); });
it('should generate a keypair and unlock', function(done) { it('should generate a keypair and unlock', function(done) {
var genKeysMatcher, persistKeysMatcher, importMatcher, keypair; var keypair;
dao._pgpMailer = {
_pgpbuilder: {}
};
keypair = { keypair = {
keyId: 123, keyId: 123,
publicKeyArmored: mockKeyPair.publicKey.publicKey, publicKeyArmored: mockKeyPair.publicKey.publicKey,
privateKeyArmored: mockKeyPair.privateKey.encryptedKey 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({
pgpStub.importKeys.withArgs(importMatcher).yields(); passphrase: passphrase,
privateKeyArmored: mockKeyPair.privateKey.encryptedKey,
publicKeyArmored: mockKeyPair.publicKey.publicKey
}).yields();
keychainStub.putUserKeyPair.withArgs().yields(); keychainStub.putUserKeyPair.withArgs().yields();
dao.unlock({ dao.unlock({
@ -495,14 +479,12 @@ define(function(require) {
describe('_imapParseMessageBlock', function() { describe('_imapParseMessageBlock', function() {
it('should parse a message', function(done) { it('should parse a message', function(done) {
imapClientStub.parseDecryptedMessageBlock.yields(null, {}); var parseRfc = sinon.stub(mailreader, 'parseRfc').withArgs({}).yields();
dao._imapParseMessageBlock(function(err, msg) { dao._imapParseMessageBlock({}, function() {
expect(err).to.not.exist; expect(parseRfc.calledOnce).to.be.true;
expect(msg).to.exist;
done(); 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() { it('should not do anything when the message is not encrypted', function() {
var message = { var message = {
encrypted: false encrypted: false
}; };
dao.decryptMessageContent({ dao.decryptBody({
message: message message: message
}); });
@ -1204,7 +1186,7 @@ define(function(require) {
decrypted: true decrypted: true
}; };
dao.decryptMessageContent({ dao.decryptBody({
message: message message: message
}); });
@ -1223,20 +1205,20 @@ define(function(require) {
body: '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----' body: '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----'
}; };
mimeBody = 'Content-Transfer-Encoding: Content-Type:'; mimeBody = 'Content-Type: asdasdasd';
parsedBody = 'body? yes.'; parsedBody = 'body? yes.';
keychainStub.getReceiverPublicKey.withArgs(message.from[0].address).yieldsAsync(null, mockKeyPair.publicKey); keychainStub.getReceiverPublicKey.withArgs(message.from[0].address).yieldsAsync(null, mockKeyPair.publicKey);
pgpStub.decrypt.withArgs(message.body, mockKeyPair.publicKey.publicKey).yieldsAsync(null, mimeBody); pgpStub.decrypt.withArgs(message.body, mockKeyPair.publicKey.publicKey).yieldsAsync(null, mimeBody);
parseStub = sinon.stub(dao, '_imapParseMessageBlock', function(o, cb) { parseStub = sinon.stub(dao, '_imapParseMessageBlock', function(o, cb) {
expect(o.message).to.equal(message); expect(o.message).to.equal(message);
expect(o.block).to.equal(mimeBody); expect(o.raw).to.equal(mimeBody);
o.message.body = parsedBody; o.message.body = parsedBody;
cb(null, o.message); cb(null, o.message);
}); });
dao.decryptMessageContent({ dao.decryptBody({
message: message message: message
}, function(error, msg) { }, function(error, msg) {
expect(error).to.not.exist; expect(error).to.not.exist;
@ -1274,7 +1256,7 @@ define(function(require) {
pgpStub.decrypt.withArgs(message.body, mockKeyPair.publicKey.publicKey).yieldsAsync(null, plaintextBody); pgpStub.decrypt.withArgs(message.body, mockKeyPair.publicKey.publicKey).yieldsAsync(null, plaintextBody);
parseStub = sinon.stub(dao, '_imapParseMessageBlock'); parseStub = sinon.stub(dao, '_imapParseMessageBlock');
dao.decryptMessageContent({ dao.decryptBody({
message: message message: message
}, function(error, msg) { }, function(error, msg) {
expect(error).to.not.exist; expect(error).to.not.exist;
@ -1314,7 +1296,7 @@ define(function(require) {
}); });
parseStub = sinon.stub(dao, '_imapParseMessageBlock'); parseStub = sinon.stub(dao, '_imapParseMessageBlock');
dao.decryptMessageContent({ dao.decryptBody({
message: message message: message
}, function(error, msg) { }, function(error, msg) {
expect(error).to.not.exist; expect(error).to.not.exist;
@ -1347,7 +1329,7 @@ define(function(require) {
keychainStub.getReceiverPublicKey.yields({}); keychainStub.getReceiverPublicKey.yields({});
parseStub = sinon.stub(dao, '_imapParseMessageBlock'); parseStub = sinon.stub(dao, '_imapParseMessageBlock');
dao.decryptMessageContent({ dao.decryptBody({
message: message message: message
}, function(error, msg) { }, function(error, msg) {
expect(error).to.exist; 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() { describe('sendPlaintext', function() {
it('should work', function(done) { it('should work', function(done) {
pgpMailerStub.send.withArgs({ pgpMailerStub.send.withArgs({
@ -2616,16 +2578,14 @@ define(function(require) {
describe('sendEncrypted', function() { describe('sendEncrypted', function() {
it('should work', function(done) { it('should work', function(done) {
pgpStub.exportKeys.yields(null, { var publicKeys = ["PUBLIC KEY"];
privateKeyArmored: mockKeyPair.privateKey.encryptedKey, dummyDecryptedMail.publicKeysArmored = publicKeys;
publicKeyArmored: mockKeyPair.publicKey.publicKey
});
pgpMailerStub.send.withArgs({ pgpMailerStub.send.withArgs({
encrypt: true, encrypt: true,
cleartextMessage: str.message, cleartextMessage: str.message,
mail: dummyDecryptedMail, mail: dummyDecryptedMail,
publicKeysArmored: dummyDecryptedMail.receiverKeys publicKeysArmored: publicKeys
}).yields(); }).yields();
dao.sendEncrypted({ dao.sendEncrypted({
@ -2633,17 +2593,14 @@ define(function(require) {
}, function(err) { }, function(err) {
expect(err).to.not.exist; expect(err).to.not.exist;
expect(pgpStub.exportKeys.calledOnce).to.be.true;
expect(pgpMailerStub.send.calledOnce).to.be.true; expect(pgpMailerStub.send.calledOnce).to.be.true;
done(); done();
}); });
}); });
it('should not work when pgpmailer fails', function(done) { it('should not work when pgpmailer fails', function(done) {
pgpStub.exportKeys.yields(null, { var publicKeys = ["PUBLIC KEY"];
privateKeyArmored: mockKeyPair.privateKey.encryptedKey, dummyDecryptedMail.publicKeysArmored = publicKeys;
publicKeyArmored: mockKeyPair.publicKey.publicKey
});
pgpMailerStub.send.yields({}); pgpMailerStub.send.yields({});
dao.sendEncrypted({ dao.sendEncrypted({
@ -2651,176 +2608,53 @@ define(function(require) {
}, function(err) { }, function(err) {
expect(err).to.exist; expect(err).to.exist;
expect(pgpStub.exportKeys.calledOnce).to.be.true;
expect(pgpMailerStub.send.calledOnce).to.be.true; expect(pgpMailerStub.send.calledOnce).to.be.true;
done(); done();
}); });
}); });
it('should not work when sender key export fails', function(done) { });
pgpStub.exportKeys.yields({});
dao.sendEncrypted({ describe('encrypt', function() {
email: dummyDecryptedMail 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) { }, 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(err).to.not.exist;
expect(localListStub.calledOnce).to.be.true;
expect(pgpStub.exportKeys.calledOnce).to.be.true; expect(dao._account.folders[0].messages.length).to.equal(1);
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;
done(); 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() { describe('getBody', function() {
@ -260,7 +242,7 @@ define(function(require) {
scope.select(mail); scope.select(mail);
expect(emailDaoMock.decryptMessageContent.calledOnce).to.be.true; expect(emailDaoMock.decryptBody.calledOnce).to.be.true;
expect(synchronizeMock.calledOnce).to.be.true; expect(synchronizeMock.calledOnce).to.be.true;
expect(scope.state.mailList.selected).to.equal(mail); expect(scope.state.mailList.selected).to.equal(mail);
@ -288,7 +270,7 @@ define(function(require) {
scope.select(mail); scope.select(mail);
expect(emailDaoMock.decryptMessageContent.calledOnce).to.be.true; expect(emailDaoMock.decryptBody.calledOnce).to.be.true;
expect(synchronizeMock.called).to.be.false; expect(synchronizeMock.called).to.be.false;
expect(scope.state.mailList.selected).to.equal(mail); expect(scope.state.mailList.selected).to.equal(mail);

View File

@ -2,13 +2,14 @@ define(function(require) {
'use strict'; 'use strict';
var expect = chai.expect, var expect = chai.expect,
_ = require('underscore'),
OutboxBO = require('js/bo/outbox'), OutboxBO = require('js/bo/outbox'),
KeychainDAO = require('js/dao/keychain-dao'), KeychainDAO = require('js/dao/keychain-dao'),
EmailDAO = require('js/dao/email-dao'), EmailDAO = require('js/dao/email-dao'),
DeviceStorageDAO = require('js/dao/devicestorage-dao'), DeviceStorageDAO = require('js/dao/devicestorage-dao'),
InvitationDAO = require('js/dao/invitation-dao'); InvitationDAO = require('js/dao/invitation-dao');
chai.Assertion.includeStack = true;
describe('Outbox Business Object unit test', function() { describe('Outbox Business Object unit test', function() {
var outbox, emailDaoStub, devicestorageStub, invitationDaoStub, keychainStub, var outbox, emailDaoStub, devicestorageStub, invitationDaoStub, keychainStub,
dummyUser = 'spiderpig@springfield.com'; dummyUser = 'spiderpig@springfield.com';
@ -26,24 +27,10 @@ define(function(require) {
keychainStub = sinon.createStubInstance(KeychainDAO); keychainStub = sinon.createStubInstance(KeychainDAO);
invitationDaoStub = sinon.createStubInstance(InvitationDAO); invitationDaoStub = sinon.createStubInstance(InvitationDAO);
outbox = new OutboxBO(emailDaoStub, keychainStub, devicestorageStub, invitationDaoStub); outbox = new OutboxBO(emailDaoStub, keychainStub, devicestorageStub, invitationDaoStub);
outbox.init();
}); });
afterEach(function() {}); 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() { describe('start/stop checking', function() {
it('should work', function() { it('should work', function() {
function onOutboxUpdate(err) { function onOutboxUpdate(err) {
@ -58,77 +45,171 @@ define(function(require) {
}); });
}); });
describe('process outbox', function() { describe('put', function() {
it('should send to registered users and update pending mails', function(done) { it('should encrypt and store a mail', function(done) {
var member, invited, notinvited, dummyMails, unsentCount; var mail, senderKey, receiverKey;
member = { senderKey = {
id: '123', publicKey: 'SENDER PUBLIC KEY'
to: [{ };
receiverKey = {
publicKey: 'RECEIVER PUBLIC KEY'
};
mail = {
from: [{
name: 'member', name: 'member',
address: 'member@whiteout.io' 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 = { invited = {
id: '456', id: '34',
from: from,
to: [{ to: [{
name: 'invited', name: 'invited',
address: 'invited@whiteout.io' address: 'invited'
}],
publicKeysArmored: [],
unregisteredUsers: [{
name: 'invited',
address: 'invited'
}] }]
}; };
notinvited = { notinvited = {
id: '789', id: '56',
from: from,
to: [{ to: [{
name: 'notinvited', 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(); 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() { emailDaoStub.reEncrypt.withArgs({
expect(outbox._outboxBusy).to.be.false; mail: newlyjoined,
publicKeysArmored: [newlyjoinedKey.publicKey]
}).yieldsAsync(null, newlyjoined);
expect(unsentCount).to.equal(2); emailDaoStub.sendEncrypted.withArgs({
expect(emailDaoStub.listForOutbox.callCount).to.equal(1); email: newlyjoined
expect(emailDaoStub.sendEncrypted.callCount).to.equal(1); }).yieldsAsync();
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);
expect(outbox.pendingEmails.length).to.equal(2); emailDaoStub.sendEncrypted.withArgs({
expect(outbox.pendingEmails).to.contain(invited); email: member
expect(outbox.pendingEmails).to.contain(notinvited); }).yieldsAsync();
done();
}); 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) { function onOutboxUpdate(err, count) {
expect(err).to.not.exist; expect(err).to.not.exist;
expect(count).to.exist; expect(count).to.equal(2);
unsentCount = count;
check(); 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); outbox._processOutbox(onOutboxUpdate);
@ -136,31 +217,16 @@ define(function(require) {
it('should not process outbox in offline mode', function(done) { it('should not process outbox in offline mode', function(done) {
emailDaoStub._account.online = false; emailDaoStub._account.online = false;
emailDaoStub.listForOutbox.yieldsAsync(null, [{ devicestorageStub.listItems.yieldsAsync(null, [{}]);
id: '123',
to: [{
name: 'member',
address: 'member@whiteout.io'
}]
}]);
outbox._processOutbox(function(err, count) { outbox._processOutbox(function(err, count) {
expect(err).to.not.exist; expect(err).to.not.exist;
expect(count).to.equal(1); 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; expect(outbox._outboxBusy).to.be.false;
done(); 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'), mocks = require('angularMocks'),
WriteCtrl = require('js/controller/write'), WriteCtrl = require('js/controller/write'),
EmailDAO = require('js/dao/email-dao'), EmailDAO = require('js/dao/email-dao'),
OutboxBO = require('js/bo/outbox'),
KeychainDAO = require('js/dao/keychain-dao'), KeychainDAO = require('js/dao/keychain-dao'),
appController = require('js/app-controller'); appController = require('js/app-controller');
describe('Write controller unit test', function() { describe('Write controller unit test', function() {
var ctrl, scope, origEmailDao, emailDaoMock, keychainMock, emailAddress; var ctrl, scope,
origEmailDao, origOutbox,
emailDaoMock, keychainMock, outboxMock, emailAddress;
beforeEach(function() { 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; origEmailDao = appController._emailDao;
origOutbox = appController._outboxBo;
outboxMock = sinon.createStubInstance(OutboxBO);
appController._outboxBo = outboxMock;
emailDaoMock = sinon.createStubInstance(EmailDAO); emailDaoMock = sinon.createStubInstance(EmailDAO);
appController._emailDao = emailDaoMock; appController._emailDao = emailDaoMock;
@ -37,8 +47,9 @@ define(function(require) {
}); });
afterEach(function() { afterEach(function() {
// restore the module // restore the app controller
appController._emailDao = origEmailDao; appController._emailDao = origEmailDao;
appController._outboxBo = origOutbox;
}); });
describe('scope variables', function() { describe('scope variables', function() {
@ -259,127 +270,38 @@ define(function(require) {
}); });
describe('send to outbox', function() { describe('send to outbox', function() {
it('should work when offline', function(done) { it('should work', function() {
var re = { scope.from = [{
from: [{ address: 'pity@dafool'
address: 'pity@dafool' }];
}], scope.to = [{
subject: 'Ermahgerd!', address: 'pity@dafool'
sentDate: new Date(), }];
body: 'so much body!' scope.cc = [];
}; scope.bcc = [];
scope.subject = 'Ermahgerd!';
scope.body = 'wow. much body! very text!';
scope.attachments = [];
scope.state.nav = { scope.state.nav = {
currentFolder: 'currentFolder' currentFolder: 'currentFolder'
}; };
scope.emptyOutbox = function() {}; scope.replyTo = {};
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(); outboxMock.put.yields();
};
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();
emailDaoMock.sync.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) { scope.onError = function(err) {
expect(err).to.exist; 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();
emailDaoMock.sync.yields({});
scope.state.writer.write(re);
scope.sendToOutbox(); scope.sendToOutbox();
});
it('should not work and not close the write view', function(done) { expect(outboxMock.put.calledOnce).to.be.true;
scope.state.writer.write(); expect(emailDaoMock.sync.calledOnce).to.be.true;
scope.to = [{ expect(scope.state.writer.open).to.be.false;
address: 'pity@dafool.de', expect(scope.replyTo.answered).to.be.true;
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();
}); });
}); });
}); });