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:
commit
86c0f04973
16
Gruntfile.js
16
Gruntfile.js
@ -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/'
|
||||
},
|
||||
|
@ -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": {
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
};
|
||||
|
||||
//
|
||||
|
@ -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;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
//
|
||||
|
@ -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;
|
||||
});
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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({});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user