mirror of
https://github.com/moparisthebest/mail
synced 2025-01-30 22:50:17 -05:00
[WO-515] Fix folder handling
Use the first folder provided in well known folders Add migration script to fix clients with multiple folders for same category Handle folder deletion smarter Extract some magic number/strings to constants in emaildao
This commit is contained in:
parent
bb9a641b4e
commit
7adc095277
@ -11,7 +11,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"crypto-lib": "~0.2.1",
|
"crypto-lib": "~0.2.1",
|
||||||
"imap-client": "~0.3.7",
|
"imap-client": "https://github.com/whiteout-io/imap-client/tarball/dev/WO-515",
|
||||||
"mailreader": "~0.3.5",
|
"mailreader": "~0.3.5",
|
||||||
"pgpmailer": "~0.3.11",
|
"pgpmailer": "~0.3.11",
|
||||||
"pgpbuilder": "~0.3.7",
|
"pgpbuilder": "~0.3.7",
|
||||||
|
@ -92,9 +92,10 @@ define(function(require) {
|
|||||||
iconPath: '/img/icon.png',
|
iconPath: '/img/icon.png',
|
||||||
verificationUrl: '/verify/',
|
verificationUrl: '/verify/',
|
||||||
verificationUuidLength: 36,
|
verificationUuidLength: 36,
|
||||||
dbVersion: 4,
|
dbVersion: 5,
|
||||||
appVersion: appVersion,
|
appVersion: appVersion,
|
||||||
outboxMailboxPath: 'OUTBOX',
|
outboxMailboxPath: 'OUTBOX',
|
||||||
|
outboxMailboxName: 'Outbox',
|
||||||
outboxMailboxType: 'Outbox'
|
outboxMailboxType: 'Outbox'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -533,92 +533,25 @@ define(function(require) {
|
|||||||
this.cc = [{
|
this.cc = [{
|
||||||
address: 'john.doe@gmail.com'
|
address: 'john.doe@gmail.com'
|
||||||
}]; // list of receivers
|
}]; // list of receivers
|
||||||
if (attachments) {
|
this.attachments = attachments ? [{
|
||||||
// body structure with three attachments
|
"filename": "a.md",
|
||||||
this.bodystructure = {
|
"filesize": 123,
|
||||||
"1": {
|
"mimeType": "text/x-markdown",
|
||||||
"part": "1",
|
"part": "2",
|
||||||
"type": "text/plain",
|
"content": null
|
||||||
"parameters": {
|
}, {
|
||||||
"charset": "us-ascii"
|
"filename": "b.md",
|
||||||
},
|
"filesize": 456,
|
||||||
"encoding": "7bit",
|
"mimeType": "text/x-markdown",
|
||||||
"size": 9,
|
"part": "3",
|
||||||
"lines": 2
|
"content": null
|
||||||
},
|
}, {
|
||||||
"2": {
|
"filename": "c.md",
|
||||||
"part": "2",
|
"filesize": 789,
|
||||||
"type": "application/octet-stream",
|
"mimeType": "text/x-markdown",
|
||||||
"parameters": {
|
"part": "4",
|
||||||
"name": "a.md"
|
"content": null
|
||||||
},
|
}] : [];
|
||||||
"encoding": "7bit",
|
|
||||||
"size": 123,
|
|
||||||
"disposition": [{
|
|
||||||
"type": "attachment",
|
|
||||||
"filename": "a.md"
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
"3": {
|
|
||||||
"part": "3",
|
|
||||||
"type": "application/octet-stream",
|
|
||||||
"parameters": {
|
|
||||||
"name": "b.md"
|
|
||||||
},
|
|
||||||
"encoding": "7bit",
|
|
||||||
"size": 456,
|
|
||||||
"disposition": [{
|
|
||||||
"type": "attachment",
|
|
||||||
"filename": "b.md"
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
"4": {
|
|
||||||
"part": "4",
|
|
||||||
"type": "application/octet-stream",
|
|
||||||
"parameters": {
|
|
||||||
"name": "c.md"
|
|
||||||
},
|
|
||||||
"encoding": "7bit",
|
|
||||||
"size": 789,
|
|
||||||
"disposition": [{
|
|
||||||
"type": "attachment",
|
|
||||||
"filename": "c.md"
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
"type": "multipart/mixed"
|
|
||||||
};
|
|
||||||
this.attachments = [{
|
|
||||||
"filename": "a.md",
|
|
||||||
"filesize": 123,
|
|
||||||
"mimeType": "text/x-markdown",
|
|
||||||
"part": "2",
|
|
||||||
"content": null
|
|
||||||
}, {
|
|
||||||
"filename": "b.md",
|
|
||||||
"filesize": 456,
|
|
||||||
"mimeType": "text/x-markdown",
|
|
||||||
"part": "3",
|
|
||||||
"content": null
|
|
||||||
}, {
|
|
||||||
"filename": "c.md",
|
|
||||||
"filesize": 789,
|
|
||||||
"mimeType": "text/x-markdown",
|
|
||||||
"part": "4",
|
|
||||||
"content": null
|
|
||||||
}];
|
|
||||||
} else {
|
|
||||||
this.bodystructure = {
|
|
||||||
"part": "1",
|
|
||||||
"type": "text/plain",
|
|
||||||
"parameters": {
|
|
||||||
"charset": "us-ascii"
|
|
||||||
},
|
|
||||||
"encoding": "7bit",
|
|
||||||
"size": 9,
|
|
||||||
"lines": 2
|
|
||||||
};
|
|
||||||
this.attachments = [];
|
|
||||||
}
|
|
||||||
this.unread = unread;
|
this.unread = unread;
|
||||||
this.answered = answered;
|
this.answered = answered;
|
||||||
this.sentDate = new Date('Thu Sep 19 2013 20:41:23 GMT+0200 (CEST)');
|
this.sentDate = new Date('Thu Sep 19 2013 20:41:23 GMT+0200 (CEST)');
|
||||||
@ -639,13 +572,14 @@ define(function(require) {
|
|||||||
this.decrypted = true;
|
this.decrypted = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
var dummys = [new Email(true, true), new Email(true, false, true), new Email(false, true, true), new Email(false, true)];
|
var dummies = [],
|
||||||
|
i = 100;
|
||||||
for (var i = 0; i < 100; i++) {
|
while (i--) {
|
||||||
dummys.push(new Email(false));
|
// every second/third/fourth dummy mail with unread/attachments/answered
|
||||||
|
dummies.push(new Email((i % 2 === 0), (i % 3 === 0), (i % 5 === 0)));
|
||||||
}
|
}
|
||||||
|
|
||||||
return dummys;
|
return dummies;
|
||||||
}
|
}
|
||||||
|
|
||||||
return MailListCtrl;
|
return MailListCtrl;
|
||||||
|
@ -6,6 +6,38 @@ define(function(require) {
|
|||||||
config = require('js/app-config').config,
|
config = require('js/app-config').config,
|
||||||
str = require('js/app-config').string;
|
str = require('js/app-config').string;
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Constants
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
var FOLDER_DB_TYPE = 'folders';
|
||||||
|
|
||||||
|
var SYNC_TYPE_NEW = 'new';
|
||||||
|
var SYNC_TYPE_DELETED = 'deleted';
|
||||||
|
var SYNC_TYPE_MSGS = 'messages';
|
||||||
|
|
||||||
|
var FOLDER_TYPE_INBOX = 'Inbox';
|
||||||
|
var FOLDER_TYPE_SENT = 'Sent';
|
||||||
|
var FOLDER_TYPE_DRAFTS = 'Drafts';
|
||||||
|
var FOLDER_TYPE_TRASH = 'Trash';
|
||||||
|
|
||||||
|
var MSG_ATTR_UID = 'uid';
|
||||||
|
var MSG_PART_ATTR_CONTENT = 'content';
|
||||||
|
var MSG_PART_TYPE_ATTACHMENT = 'attachment';
|
||||||
|
var MSG_PART_TYPE_ENCRYPTED = 'encrypted';
|
||||||
|
var MSG_PART_TYPE_SIGNED = 'signed';
|
||||||
|
var MSG_PART_TYPE_TEXT = 'text';
|
||||||
|
var MSG_PART_TYPE_HTML = 'html';
|
||||||
|
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Email Dao
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* High-level data access object that orchestrates everything around the handling of encrypted mails:
|
* High-level data access object that orchestrates everything around the handling of encrypted mails:
|
||||||
* PGP de-/encryption, receiving via IMAP, sending via SMTP, MIME parsing, local db persistence
|
* PGP de-/encryption, receiving via IMAP, sending via SMTP, MIME parsing, local db persistence
|
||||||
@ -253,8 +285,8 @@ define(function(require) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var storedUids = _.pluck(storedMessages, 'uid'),
|
var storedUids = _.pluck(storedMessages, MSG_ATTR_UID),
|
||||||
memoryUids = _.pluck(folder.messages, 'uid'),
|
memoryUids = _.pluck(folder.messages, MSG_ATTR_UID),
|
||||||
newUids = _.difference(storedUids, memoryUids), // uids of messages that are not yet in memory
|
newUids = _.difference(storedUids, memoryUids), // uids of messages that are not yet in memory
|
||||||
removedUids = _.difference(memoryUids, storedUids); // uids of messages that are no longer stored on the disk
|
removedUids = _.difference(memoryUids, storedUids); // uids of messages that are no longer stored on the disk
|
||||||
|
|
||||||
@ -370,7 +402,7 @@ define(function(require) {
|
|||||||
// this enables us to already show the attachment clip in the message list ui
|
// this enables us to already show the attachment clip in the message list ui
|
||||||
messages.forEach(function(message) {
|
messages.forEach(function(message) {
|
||||||
message.attachments = message.bodyParts.filter(function(bodyPart) {
|
message.attachments = message.bodyParts.filter(function(bodyPart) {
|
||||||
return bodyPart.type === 'attachment';
|
return bodyPart.type === MSG_PART_TYPE_ATTACHMENT;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -401,7 +433,7 @@ define(function(require) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var body = _.pluck(filterBodyParts(parsedBodyParts, 'text'), 'content').join('\n'),
|
var body = _.pluck(filterBodyParts(parsedBodyParts, MSG_PART_TYPE_TEXT), MSG_PART_ATTR_CONTENT).join('\n'),
|
||||||
verificationUrlPrefix = config.cloudUrl + config.verificationUrl,
|
verificationUrlPrefix = config.cloudUrl + config.verificationUrl,
|
||||||
uuid = body.split(verificationUrlPrefix).pop().substr(0, config.verificationUuidLength),
|
uuid = body.split(verificationUrlPrefix).pop().substr(0, config.verificationUuidLength),
|
||||||
uuidRegex = /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/;
|
uuidRegex = /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/;
|
||||||
@ -645,10 +677,10 @@ define(function(require) {
|
|||||||
// we need to fetch the content for non-attachment body parts (encrypted, signed, text, html, resources referenced from the html)
|
// we need to fetch the content for non-attachment body parts (encrypted, signed, text, html, resources referenced from the html)
|
||||||
// but we spare the effort and fetch attachment content later upon explicit user request.
|
// but we spare the effort and fetch attachment content later upon explicit user request.
|
||||||
var contentParts = localMessage.bodyParts.filter(function(bodyPart) {
|
var contentParts = localMessage.bodyParts.filter(function(bodyPart) {
|
||||||
return bodyPart.type !== "attachment" || (bodyPart.type === "attachment" && bodyPart.id);
|
return bodyPart.type !== MSG_PART_TYPE_ATTACHMENT || (bodyPart.type === MSG_PART_TYPE_ATTACHMENT && bodyPart.id);
|
||||||
});
|
});
|
||||||
var attachmentParts = localMessage.bodyParts.filter(function(bodyPart) {
|
var attachmentParts = localMessage.bodyParts.filter(function(bodyPart) {
|
||||||
return bodyPart.type === "attachment" && !bodyPart.id;
|
return bodyPart.type === MSG_PART_TYPE_ATTACHMENT && !bodyPart.id;
|
||||||
});
|
});
|
||||||
|
|
||||||
// do we need to fetch content from the imap server?
|
// do we need to fetch content from the imap server?
|
||||||
@ -700,7 +732,7 @@ define(function(require) {
|
|||||||
function extractContent() {
|
function extractContent() {
|
||||||
if (message.encrypted) {
|
if (message.encrypted) {
|
||||||
// show the encrypted message
|
// show the encrypted message
|
||||||
message.body = filterBodyParts(message.bodyParts, 'encrypted')[0].content;
|
message.body = filterBodyParts(message.bodyParts, MSG_PART_TYPE_ENCRYPTED)[0].content;
|
||||||
return done();
|
return done();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -708,13 +740,13 @@ define(function(require) {
|
|||||||
|
|
||||||
if (message.signed) {
|
if (message.signed) {
|
||||||
// PGP/MIME signed
|
// PGP/MIME signed
|
||||||
var signedRoot = filterBodyParts(message.bodyParts, 'signed')[0]; // in case of a signed message, you only want to show the signed content and ignore the rest
|
var signedRoot = filterBodyParts(message.bodyParts, MSG_PART_TYPE_SIGNED)[0]; // in case of a signed message, you only want to show the signed content and ignore the rest
|
||||||
message.signedMessage = signedRoot.signedMessage;
|
message.signedMessage = signedRoot.signedMessage;
|
||||||
message.signature = signedRoot.signature;
|
message.signature = signedRoot.signature;
|
||||||
root = signedRoot.content;
|
root = signedRoot.content;
|
||||||
}
|
}
|
||||||
|
|
||||||
var body = _.pluck(filterBodyParts(root, 'text'), 'content').join('\n');
|
var body = _.pluck(filterBodyParts(root, MSG_PART_TYPE_TEXT), MSG_PART_ATTR_CONTENT).join('\n');
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* if the message is plain text and contains pgp/inline, we are only interested in the encrypted
|
* if the message is plain text and contains pgp/inline, we are only interested in the encrypted
|
||||||
@ -730,7 +762,7 @@ define(function(require) {
|
|||||||
|
|
||||||
// replace the bodyParts info with an artificial bodyPart of type "encrypted"
|
// replace the bodyParts info with an artificial bodyPart of type "encrypted"
|
||||||
message.bodyParts = [{
|
message.bodyParts = [{
|
||||||
type: 'encrypted',
|
type: MSG_PART_TYPE_ENCRYPTED,
|
||||||
content: pgpInlineMatch[0],
|
content: pgpInlineMatch[0],
|
||||||
_isPgpInline: true // used internally to avoid trying to parse non-MIME text with the mailreader
|
_isPgpInline: true // used internally to avoid trying to parse non-MIME text with the mailreader
|
||||||
}];
|
}];
|
||||||
@ -770,8 +802,8 @@ define(function(require) {
|
|||||||
function setBody() {
|
function setBody() {
|
||||||
message.body = body;
|
message.body = body;
|
||||||
if (!message.clearSignedMessage) {
|
if (!message.clearSignedMessage) {
|
||||||
message.attachments = filterBodyParts(root, 'attachment');
|
message.attachments = filterBodyParts(root, MSG_PART_TYPE_ATTACHMENT);
|
||||||
message.html = _.pluck(filterBodyParts(root, 'html'), 'content').join('\n');
|
message.html = _.pluck(filterBodyParts(root, MSG_PART_TYPE_HTML), MSG_PART_ATTR_CONTENT).join('\n');
|
||||||
inlineExternalImages(message);
|
inlineExternalImages(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -863,7 +895,7 @@ define(function(require) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// get the receiver's public key to check the message signature
|
// get the receiver's public key to check the message signature
|
||||||
var encryptedNode = filterBodyParts(message.bodyParts, 'encrypted')[0];
|
var encryptedNode = filterBodyParts(message.bodyParts, MSG_PART_TYPE_ENCRYPTED)[0];
|
||||||
var senderKey = senderPublicKey ? senderPublicKey.publicKey : undefined;
|
var senderKey = senderPublicKey ? senderPublicKey.publicKey : undefined;
|
||||||
self._pgp.decrypt(encryptedNode.content, senderKey, function(err, decrypted, signaturesValid) {
|
self._pgp.decrypt(encryptedNode.content, senderKey, function(err, decrypted, signaturesValid) {
|
||||||
if (err || !decrypted) {
|
if (err || !decrypted) {
|
||||||
@ -897,7 +929,7 @@ define(function(require) {
|
|||||||
if (!message.signed) {
|
if (!message.signed) {
|
||||||
// message had no signature in the ciphertext, so there's a little extra effort to be done here
|
// message had no signature in the ciphertext, so there's a little extra effort to be done here
|
||||||
// is there a signed MIME node?
|
// is there a signed MIME node?
|
||||||
var signedRoot = filterBodyParts(root, 'signed')[0];
|
var signedRoot = filterBodyParts(root, MSG_PART_TYPE_SIGNED)[0];
|
||||||
if (!signedRoot) {
|
if (!signedRoot) {
|
||||||
// no signed MIME node, obviously an unsigned PGP/MIME message
|
// no signed MIME node, obviously an unsigned PGP/MIME message
|
||||||
return setBody();
|
return setBody();
|
||||||
@ -927,9 +959,9 @@ define(function(require) {
|
|||||||
function setBody() {
|
function setBody() {
|
||||||
// we have successfully interpreted the descrypted message,
|
// we have successfully interpreted the descrypted message,
|
||||||
// so let's update the views on the message parts
|
// so let's update the views on the message parts
|
||||||
message.body = _.pluck(filterBodyParts(root, 'text'), 'content').join('\n');
|
message.body = _.pluck(filterBodyParts(root, MSG_PART_TYPE_TEXT), MSG_PART_ATTR_CONTENT).join('\n');
|
||||||
message.html = _.pluck(filterBodyParts(root, 'html'), 'content').join('\n');
|
message.html = _.pluck(filterBodyParts(root, MSG_PART_TYPE_HTML), MSG_PART_ATTR_CONTENT).join('\n');
|
||||||
message.attachments = _.reject(filterBodyParts(root, 'attachment'), function(attmt) {
|
message.attachments = _.reject(filterBodyParts(root, MSG_PART_TYPE_ATTACHMENT), function(attmt) {
|
||||||
// remove the pgp-signature from the attachments
|
// remove the pgp-signature from the attachments
|
||||||
return attmt.mimeType === "application/pgp-signature";
|
return attmt.mimeType === "application/pgp-signature";
|
||||||
});
|
});
|
||||||
@ -989,7 +1021,7 @@ define(function(require) {
|
|||||||
|
|
||||||
// upload the sent message to the sent folder if necessary
|
// upload the sent message to the sent folder if necessary
|
||||||
var sentFolder = _.findWhere(self._account.folders, {
|
var sentFolder = _.findWhere(self._account.folders, {
|
||||||
type: 'Sent'
|
type: FOLDER_TYPE_SENT
|
||||||
});
|
});
|
||||||
|
|
||||||
if (self.ignoreUploadOnSent || !sentFolder || !rfcText) {
|
if (self.ignoreUploadOnSent || !sentFolder || !rfcText) {
|
||||||
@ -1039,7 +1071,7 @@ define(function(require) {
|
|||||||
|
|
||||||
// upload the sent message to the sent folder if necessary
|
// upload the sent message to the sent folder if necessary
|
||||||
var sentFolder = _.findWhere(self._account.folders, {
|
var sentFolder = _.findWhere(self._account.folders, {
|
||||||
type: 'Sent'
|
type: FOLDER_TYPE_SENT
|
||||||
});
|
});
|
||||||
|
|
||||||
if (self.ignoreUploadOnSent || !sentFolder || !rfcText) {
|
if (self.ignoreUploadOnSent || !sentFolder || !rfcText) {
|
||||||
@ -1130,7 +1162,7 @@ define(function(require) {
|
|||||||
|
|
||||||
var uids, highestModseq, lastUid;
|
var uids, highestModseq, lastUid;
|
||||||
|
|
||||||
uids = _.pluck(folder.messages, 'uid').sort(function(a, b) {
|
uids = _.pluck(folder.messages, MSG_ATTR_UID).sort(function(a, b) {
|
||||||
return a - b;
|
return a - b;
|
||||||
});
|
});
|
||||||
lastUid = uids[uids.length - 1];
|
lastUid = uids[uids.length - 1];
|
||||||
@ -1153,7 +1185,7 @@ define(function(require) {
|
|||||||
|
|
||||||
// set up the imap client to listen for changes in the inbox
|
// set up the imap client to listen for changes in the inbox
|
||||||
var inbox = _.findWhere(self._account.folders, {
|
var inbox = _.findWhere(self._account.folders, {
|
||||||
type: 'Inbox'
|
type: FOLDER_TYPE_INBOX
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!inbox) {
|
if (!inbox) {
|
||||||
@ -1199,14 +1231,14 @@ define(function(require) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.type === 'new') {
|
if (options.type === SYNC_TYPE_NEW) {
|
||||||
// new messages available on imap, fetch from imap and store to disk and memory
|
// new messages available on imap, fetch from imap and store to disk and memory
|
||||||
self.fetchMessages({
|
self.fetchMessages({
|
||||||
folder: folder,
|
folder: folder,
|
||||||
firstUid: Math.min.apply(null, options.list),
|
firstUid: Math.min.apply(null, options.list),
|
||||||
lastUid: Math.max.apply(null, options.list)
|
lastUid: Math.max.apply(null, options.list)
|
||||||
}, self.onError.bind(self));
|
}, self.onError.bind(self));
|
||||||
} else if (options.type === 'deleted') {
|
} else if (options.type === SYNC_TYPE_DELETED) {
|
||||||
// messages have been deleted, remove from local storage and memory
|
// messages have been deleted, remove from local storage and memory
|
||||||
options.list.forEach(function(uid) {
|
options.list.forEach(function(uid) {
|
||||||
var message = _.findWhere(folder.messages, {
|
var message = _.findWhere(folder.messages, {
|
||||||
@ -1223,7 +1255,7 @@ define(function(require) {
|
|||||||
localOnly: true
|
localOnly: true
|
||||||
}, self.onError.bind(self));
|
}, self.onError.bind(self));
|
||||||
});
|
});
|
||||||
} else if (options.type === 'messages') {
|
} else if (options.type === SYNC_TYPE_MSGS) {
|
||||||
// NB! several possible reasons why this could be called.
|
// NB! several possible reasons why this could be called.
|
||||||
// if a message in the array has uid value and flag array, it had a possible flag update
|
// if a message in the array has uid value and flag array, it had a possible flag update
|
||||||
options.list.forEach(function(changedMsg) {
|
options.list.forEach(function(changedMsg) {
|
||||||
@ -1268,13 +1300,12 @@ define(function(require) {
|
|||||||
* @param {Function} callback Invoked when the folders are up to date
|
* @param {Function} callback Invoked when the folders are up to date
|
||||||
*/
|
*/
|
||||||
EmailDAO.prototype._initFoldersFromDisk = function(callback) {
|
EmailDAO.prototype._initFoldersFromDisk = function(callback) {
|
||||||
var self = this,
|
var self = this;
|
||||||
folderDbType = 'folders';
|
|
||||||
|
|
||||||
self.busy(); // start the spinner
|
self.busy(); // start the spinner
|
||||||
|
|
||||||
// fetch list from local cache
|
// fetch list from local cache
|
||||||
self._devicestorage.listItems(folderDbType, 0, null, function(err, stored) {
|
self._devicestorage.listItems(FOLDER_DB_TYPE, 0, null, function(err, stored) {
|
||||||
if (err) {
|
if (err) {
|
||||||
return done(err);
|
return done(err);
|
||||||
}
|
}
|
||||||
@ -1297,8 +1328,7 @@ define(function(require) {
|
|||||||
* @param {Function} callback Invoked when the folders are up to date
|
* @param {Function} callback Invoked when the folders are up to date
|
||||||
*/
|
*/
|
||||||
EmailDAO.prototype._initFoldersFromImap = function(callback) {
|
EmailDAO.prototype._initFoldersFromImap = function(callback) {
|
||||||
var self = this,
|
var self = this;
|
||||||
folderDbType = 'folders';
|
|
||||||
|
|
||||||
self.busy(); // start the spinner
|
self.busy(); // start the spinner
|
||||||
|
|
||||||
@ -1308,57 +1338,73 @@ define(function(require) {
|
|||||||
return done(err);
|
return done(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
// this array is dropped directly into the ui to create the folder list
|
// initialize the folders to something meaningful if that hasn't already happened
|
||||||
var folders = [];
|
self._account.folders = self._account.folders || [];
|
||||||
if (wellKnownFolders.inbox) {
|
|
||||||
folders.push(wellKnownFolders.inbox);
|
// smuggle the outbox into the well known folders, which is obv not present on imap...
|
||||||
}
|
wellKnownFolders[config.outboxMailboxType] = [{
|
||||||
if (wellKnownFolders.sent) {
|
name: config.outboxMailboxName,
|
||||||
folders.push(wellKnownFolders.sent);
|
type: config.outboxMailboxType,
|
||||||
}
|
|
||||||
folders.push({
|
|
||||||
type: 'Outbox',
|
|
||||||
path: config.outboxMailboxPath
|
path: config.outboxMailboxPath
|
||||||
});
|
}];
|
||||||
if (wellKnownFolders.drafts) {
|
|
||||||
folders.push(wellKnownFolders.drafts);
|
|
||||||
}
|
|
||||||
if (wellKnownFolders.trash) {
|
|
||||||
folders.push(wellKnownFolders.trash);
|
|
||||||
}
|
|
||||||
|
|
||||||
var foldersChanged = false; // indicates if are there any new/removed folders?
|
// indicates if we need to persist anything to disk
|
||||||
|
var foldersChanged = false;
|
||||||
|
|
||||||
// check for added folders
|
// the folders listed in the navigation pane
|
||||||
folders.forEach(function(folder) {
|
[FOLDER_TYPE_INBOX, FOLDER_TYPE_SENT, config.outboxMailboxType, FOLDER_TYPE_DRAFTS, FOLDER_TYPE_TRASH].forEach(function(mbxType) {
|
||||||
if (!_.findWhere(self._account.folders, {
|
var localFolderWithType, imapFolderWithPath;
|
||||||
path: folder.path
|
|
||||||
})) {
|
// check if there is a folder of this type locally available
|
||||||
// add the missing folder
|
localFolderWithType = _.findWhere(self._account.folders, {
|
||||||
self._account.folders.push(folder);
|
type: mbxType
|
||||||
|
});
|
||||||
|
|
||||||
|
if (localFolderWithType) {
|
||||||
|
// we have a local folder available, so let's check if this folder still exists on imap
|
||||||
|
|
||||||
|
imapFolderWithPath = _.findWhere(wellKnownFolders[mbxType], {
|
||||||
|
path: localFolderWithType.path
|
||||||
|
});
|
||||||
|
|
||||||
|
if (imapFolderWithPath) {
|
||||||
|
// folder present on imap, no need to update.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// folder not present on imap, so remove the folder and see if there are any updates for this folder type
|
||||||
|
self._account.folders.splice(self._account.folders.indexOf(localFolderWithType), 1);
|
||||||
foldersChanged = true;
|
foldersChanged = true;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// check for deleted folders
|
if (!wellKnownFolders[mbxType] || !wellKnownFolders[mbxType].length) {
|
||||||
self._account.folders.forEach(function(folder) {
|
// no imap folders of the respective mailbox type, so nothing to do here
|
||||||
if (!_.findWhere(folders, {
|
return;
|
||||||
path: folder.path
|
|
||||||
})) {
|
|
||||||
// remove the obsolete folder
|
|
||||||
self._account.folders.splice(self._account.folders.indexOf(folder), 1);
|
|
||||||
foldersChanged = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* we have no local folder of the type, so do something intelligent,
|
||||||
|
* i.e. take the first folder of the respective type
|
||||||
|
*/
|
||||||
|
self._account.folders.push(wellKnownFolders[mbxType][0]);
|
||||||
|
foldersChanged = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
// if folder have changed, we need to persist them to disk.
|
// if folders have not changed, can fill them with messages directly
|
||||||
if (!foldersChanged) {
|
if (!foldersChanged) {
|
||||||
return self._initMessagesFromDisk(done);
|
return self._initMessagesFromDisk(done);
|
||||||
}
|
}
|
||||||
|
|
||||||
// persist encrypted list in device storage
|
// persist encrypted list in device storage
|
||||||
// NB! persis the array we received from IMAP! do *not* persist self._account.folders with all the messages...
|
// note: the folders in the ui also include the messages array, so let's create a clean array here
|
||||||
self._devicestorage.storeList([folders], folderDbType, function(err) {
|
var folders = self._account.folders.map(function(folder) {
|
||||||
|
return {
|
||||||
|
name: folder.name,
|
||||||
|
path: folder.path,
|
||||||
|
type: folder.type
|
||||||
|
};
|
||||||
|
});
|
||||||
|
self._devicestorage.storeList([folders], FOLDER_DB_TYPE, function(err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
return done(err);
|
return done(err);
|
||||||
}
|
}
|
||||||
@ -1463,7 +1509,7 @@ define(function(require) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var trash = _.findWhere(this._account.folders, {
|
var trash = _.findWhere(this._account.folders, {
|
||||||
type: 'Trash'
|
type: FOLDER_TYPE_TRASH
|
||||||
});
|
});
|
||||||
|
|
||||||
// there's no known trash folder to move the mail to or we're in the trash folder, so we can purge the message
|
// there's no known trash folder to move the mail to or we're in the trash folder, so we can purge the message
|
||||||
|
@ -5,7 +5,8 @@ define(function(require) {
|
|||||||
updateV1 = require('js/util/update/update-v1'),
|
updateV1 = require('js/util/update/update-v1'),
|
||||||
updateV2 = require('js/util/update/update-v2'),
|
updateV2 = require('js/util/update/update-v2'),
|
||||||
updateV3 = require('js/util/update/update-v3'),
|
updateV3 = require('js/util/update/update-v3'),
|
||||||
updateV4 = require('js/util/update/update-v4');
|
updateV4 = require('js/util/update/update-v4'),
|
||||||
|
updateV5 = require('js/util/update/update-v5');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles database migration
|
* Handles database migration
|
||||||
@ -13,7 +14,7 @@ define(function(require) {
|
|||||||
var UpdateHandler = function(appConfigStorage, userStorage, auth) {
|
var UpdateHandler = function(appConfigStorage, userStorage, auth) {
|
||||||
this._appConfigStorage = appConfigStorage;
|
this._appConfigStorage = appConfigStorage;
|
||||||
this._userStorage = userStorage;
|
this._userStorage = userStorage;
|
||||||
this._updateScripts = [updateV1, updateV2, updateV3, updateV4];
|
this._updateScripts = [updateV1, updateV2, updateV3, updateV4, updateV5];
|
||||||
this._auth = auth;
|
this._auth = auth;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
56
src/js/util/update/update-v5.js
Normal file
56
src/js/util/update/update-v5.js
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
define(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var FOLDER_TYPE_INBOX = 'Inbox';
|
||||||
|
var FOLDER_TYPE_SENT = 'Sent';
|
||||||
|
var FOLDER_TYPE_DRAFTS = 'Drafts';
|
||||||
|
var FOLDER_TYPE_TRASH = 'Trash';
|
||||||
|
|
||||||
|
var FOLDER_DB_TYPE = 'folders';
|
||||||
|
var VERSION_DB_TYPE = 'dbVersion';
|
||||||
|
|
||||||
|
var POST_UPDATE_DB_VERSION = 5;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update handler for transition database version 4 -> 5
|
||||||
|
*
|
||||||
|
* Due to an overlooked issue, there may be multiple folders, e.g. for sent mails.
|
||||||
|
* This removes the "duplicate" folders.
|
||||||
|
*/
|
||||||
|
function update(options, callback) {
|
||||||
|
|
||||||
|
// remove the emails
|
||||||
|
options.userStorage.listItems(FOLDER_DB_TYPE, 0, null, function(err, stored) {
|
||||||
|
if (err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
var folders = stored[0] || [];
|
||||||
|
[FOLDER_TYPE_INBOX, FOLDER_TYPE_SENT, FOLDER_TYPE_DRAFTS, FOLDER_TYPE_TRASH].forEach(function(mbxType) {
|
||||||
|
var foldersForType = folders.filter(function(mbx) {
|
||||||
|
return mbx.type === mbxType;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (foldersForType.length <= 1) {
|
||||||
|
return; // nothing to do here
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove duplicate folders
|
||||||
|
for (var i = 1; i < foldersForType.length; i++) {
|
||||||
|
folders.splice(folders.indexOf(foldersForType[i]), 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
options.userStorage.storeList([folders], FOLDER_DB_TYPE, function(err) {
|
||||||
|
if (err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// update the database version to POST_UPDATE_DB_VERSION
|
||||||
|
options.appConfigStorage.storeList([POST_UPDATE_DB_VERSION], VERSION_DB_TYPE, callback);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return update;
|
||||||
|
});
|
@ -40,30 +40,35 @@ define(function(require) {
|
|||||||
asymKeySize = 2048;
|
asymKeySize = 2048;
|
||||||
|
|
||||||
inboxFolder = {
|
inboxFolder = {
|
||||||
|
name: 'Inbox',
|
||||||
type: 'Inbox',
|
type: 'Inbox',
|
||||||
path: 'INBOX',
|
path: 'INBOX',
|
||||||
messages: []
|
messages: []
|
||||||
};
|
};
|
||||||
|
|
||||||
sentFolder = {
|
sentFolder = {
|
||||||
|
name: 'Sent',
|
||||||
type: 'Sent',
|
type: 'Sent',
|
||||||
path: 'SENT',
|
path: 'SENT',
|
||||||
messages: []
|
messages: []
|
||||||
};
|
};
|
||||||
|
|
||||||
draftsFolder = {
|
draftsFolder = {
|
||||||
|
name: 'Drafts',
|
||||||
type: 'Drafts',
|
type: 'Drafts',
|
||||||
path: 'DRAFTS',
|
path: 'DRAFTS',
|
||||||
messages: []
|
messages: []
|
||||||
};
|
};
|
||||||
|
|
||||||
outboxFolder = {
|
outboxFolder = {
|
||||||
|
name: 'Outbox',
|
||||||
type: 'Outbox',
|
type: 'Outbox',
|
||||||
path: 'OUTBOX',
|
path: 'OUTBOX',
|
||||||
messages: []
|
messages: []
|
||||||
};
|
};
|
||||||
|
|
||||||
trashFolder = {
|
trashFolder = {
|
||||||
|
name: 'Trash',
|
||||||
type: 'Trash',
|
type: 'Trash',
|
||||||
path: 'TRASH',
|
path: 'TRASH',
|
||||||
messages: []
|
messages: []
|
||||||
@ -1969,18 +1974,76 @@ define(function(require) {
|
|||||||
it('should initialize from imap if online', function(done) {
|
it('should initialize from imap if online', function(done) {
|
||||||
account.folders = [];
|
account.folders = [];
|
||||||
imapClientStub.listWellKnownFolders.yieldsAsync(null, {
|
imapClientStub.listWellKnownFolders.yieldsAsync(null, {
|
||||||
inbox: inboxFolder,
|
Inbox: [inboxFolder],
|
||||||
sent: sentFolder,
|
Sent: [sentFolder],
|
||||||
drafts: draftsFolder,
|
Drafts: [draftsFolder],
|
||||||
trash: trashFolder
|
Trash: [trashFolder]
|
||||||
});
|
});
|
||||||
devicestorageStub.storeList.withArgs(sinon.match(function(arg) {
|
devicestorageStub.storeList.withArgs(sinon.match(function(arg) {
|
||||||
expect(arg[0][0]).to.deep.equal(inboxFolder);
|
expect(arg[0][0].name).to.deep.equal(inboxFolder.name);
|
||||||
expect(arg[0][1]).to.deep.equal(sentFolder);
|
expect(arg[0][0].path).to.deep.equal(inboxFolder.path);
|
||||||
|
expect(arg[0][0].type).to.deep.equal(inboxFolder.type);
|
||||||
|
expect(arg[0][1].name).to.deep.equal(sentFolder.name);
|
||||||
|
expect(arg[0][1].path).to.deep.equal(sentFolder.path);
|
||||||
|
expect(arg[0][1].type).to.deep.equal(sentFolder.type);
|
||||||
|
expect(arg[0][2].name).to.deep.equal(outboxFolder.name);
|
||||||
expect(arg[0][2].path).to.deep.equal(outboxFolder.path);
|
expect(arg[0][2].path).to.deep.equal(outboxFolder.path);
|
||||||
expect(arg[0][2].type).to.deep.equal(outboxFolder.type);
|
expect(arg[0][2].type).to.deep.equal(outboxFolder.type);
|
||||||
expect(arg[0][3]).to.deep.equal(draftsFolder);
|
expect(arg[0][3].name).to.deep.equal(draftsFolder.name);
|
||||||
expect(arg[0][4]).to.deep.equal(trashFolder);
|
expect(arg[0][3].path).to.deep.equal(draftsFolder.path);
|
||||||
|
expect(arg[0][3].type).to.deep.equal(draftsFolder.type);
|
||||||
|
expect(arg[0][4].name).to.deep.equal(trashFolder.name);
|
||||||
|
expect(arg[0][4].path).to.deep.equal(trashFolder.path);
|
||||||
|
expect(arg[0][4].type).to.deep.equal(trashFolder.type);
|
||||||
|
return true;
|
||||||
|
}), 'folders').yieldsAsync();
|
||||||
|
|
||||||
|
dao.refreshFolder.yieldsAsync();
|
||||||
|
|
||||||
|
dao._initFoldersFromImap(function(err) {
|
||||||
|
expect(err).to.not.exist;
|
||||||
|
expect(imapClientStub.listWellKnownFolders.calledOnce).to.be.true;
|
||||||
|
expect(devicestorageStub.storeList.calledOnce).to.be.true;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update folders from imap', function(done) {
|
||||||
|
account.folders = [inboxFolder, outboxFolder, trashFolder, {
|
||||||
|
name: 'foo',
|
||||||
|
type: 'Sent',
|
||||||
|
path: 'bar',
|
||||||
|
}];
|
||||||
|
|
||||||
|
imapClientStub.listWellKnownFolders.yieldsAsync(null, {
|
||||||
|
Inbox: [inboxFolder],
|
||||||
|
Sent: [sentFolder],
|
||||||
|
Drafts: [draftsFolder],
|
||||||
|
Trash: [trashFolder]
|
||||||
|
});
|
||||||
|
devicestorageStub.storeList.withArgs(sinon.match(function(arg) {
|
||||||
|
expect(arg[0]).to.deep.equal([{
|
||||||
|
name: inboxFolder.name,
|
||||||
|
path: inboxFolder.path,
|
||||||
|
type: inboxFolder.type
|
||||||
|
}, {
|
||||||
|
name: outboxFolder.name,
|
||||||
|
path: outboxFolder.path,
|
||||||
|
type: outboxFolder.type
|
||||||
|
}, {
|
||||||
|
name: trashFolder.name,
|
||||||
|
path: trashFolder.path,
|
||||||
|
type: trashFolder.type
|
||||||
|
}, {
|
||||||
|
name: sentFolder.name,
|
||||||
|
path: sentFolder.path,
|
||||||
|
type: sentFolder.type
|
||||||
|
}, {
|
||||||
|
name: draftsFolder.name,
|
||||||
|
path: draftsFolder.path,
|
||||||
|
type: draftsFolder.type
|
||||||
|
}]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}), 'folders').yieldsAsync();
|
}), 'folders').yieldsAsync();
|
||||||
|
|
||||||
|
@ -277,7 +277,7 @@ define(function(require) {
|
|||||||
var SMTP_DB_KEY = 'smtp';
|
var SMTP_DB_KEY = 'smtp';
|
||||||
var REALNAME_DB_KEY = 'realname';
|
var REALNAME_DB_KEY = 'realname';
|
||||||
var emailaddress = 'bla@blubb.io';
|
var emailaddress = 'bla@blubb.io';
|
||||||
|
|
||||||
var imap = config.gmail.imap,
|
var imap = config.gmail.imap,
|
||||||
smtp = config.gmail.smtp;
|
smtp = config.gmail.smtp;
|
||||||
|
|
||||||
@ -347,6 +347,128 @@ define(function(require) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('v4 -> v5', function() {
|
||||||
|
var FOLDER_TYPE_INBOX = 'Inbox';
|
||||||
|
var FOLDER_TYPE_SENT = 'Sent';
|
||||||
|
var FOLDER_TYPE_DRAFTS = 'Drafts';
|
||||||
|
var FOLDER_TYPE_TRASH = 'Trash';
|
||||||
|
|
||||||
|
var FOLDER_DB_TYPE = 'folders';
|
||||||
|
var VERSION_DB_TYPE = 'dbVersion';
|
||||||
|
|
||||||
|
var POST_UPDATE_DB_VERSION = 5;
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
cfg.dbVersion = 5; // app requires database version 4
|
||||||
|
appConfigStorageStub.listItems.withArgs(VERSION_DB_TYPE).yieldsAsync(null, [4]); // database version is 4
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function() {
|
||||||
|
// database version is only queried for version checking prior to the update script
|
||||||
|
// so no need to check this in case-specific tests
|
||||||
|
expect(appConfigStorageStub.listItems.calledOnce).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work', function(done) {
|
||||||
|
userStorageStub.listItems.withArgs(FOLDER_DB_TYPE).yieldsAsync(null, [
|
||||||
|
[{
|
||||||
|
name: 'inbox1',
|
||||||
|
type: FOLDER_TYPE_INBOX
|
||||||
|
}, {
|
||||||
|
name: 'inbox2',
|
||||||
|
type: FOLDER_TYPE_INBOX
|
||||||
|
}, {
|
||||||
|
name: 'sent1',
|
||||||
|
type: FOLDER_TYPE_SENT
|
||||||
|
}, {
|
||||||
|
name: 'sent2',
|
||||||
|
type: FOLDER_TYPE_SENT
|
||||||
|
}, {
|
||||||
|
name: 'drafts1',
|
||||||
|
type: FOLDER_TYPE_DRAFTS
|
||||||
|
}, {
|
||||||
|
name: 'drafts2',
|
||||||
|
type: FOLDER_TYPE_DRAFTS
|
||||||
|
}, {
|
||||||
|
name: 'trash1',
|
||||||
|
type: FOLDER_TYPE_TRASH
|
||||||
|
}, {
|
||||||
|
name: 'trash2',
|
||||||
|
type: FOLDER_TYPE_TRASH
|
||||||
|
}]
|
||||||
|
]);
|
||||||
|
|
||||||
|
userStorageStub.storeList.withArgs([
|
||||||
|
[{
|
||||||
|
name: 'inbox1',
|
||||||
|
type: FOLDER_TYPE_INBOX
|
||||||
|
}, {
|
||||||
|
name: 'sent1',
|
||||||
|
type: FOLDER_TYPE_SENT
|
||||||
|
}, {
|
||||||
|
name: 'drafts1',
|
||||||
|
type: FOLDER_TYPE_DRAFTS
|
||||||
|
}, {
|
||||||
|
name: 'trash1',
|
||||||
|
type: FOLDER_TYPE_TRASH
|
||||||
|
}]
|
||||||
|
], FOLDER_DB_TYPE).yieldsAsync();
|
||||||
|
|
||||||
|
appConfigStorageStub.storeList.withArgs([POST_UPDATE_DB_VERSION], VERSION_DB_TYPE).yieldsAsync();
|
||||||
|
|
||||||
|
updateHandler.update(function(error) {
|
||||||
|
expect(error).to.not.exist;
|
||||||
|
expect(userStorageStub.listItems.calledOnce).to.be.true;
|
||||||
|
expect(userStorageStub.storeList.calledOnce).to.be.true;
|
||||||
|
expect(appConfigStorageStub.storeList.calledOnce).to.be.true;
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail when persisting database version fails', function(done) {
|
||||||
|
userStorageStub.listItems.yieldsAsync(null, []);
|
||||||
|
userStorageStub.storeList.yieldsAsync();
|
||||||
|
appConfigStorageStub.storeList.yieldsAsync(new Error());
|
||||||
|
|
||||||
|
updateHandler.update(function(error) {
|
||||||
|
expect(error).to.exist;
|
||||||
|
expect(userStorageStub.listItems.calledOnce).to.be.true;
|
||||||
|
expect(userStorageStub.storeList.calledOnce).to.be.true;
|
||||||
|
expect(appConfigStorageStub.storeList.calledOnce).to.be.true;
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail when persisting folders fails', function(done) {
|
||||||
|
userStorageStub.listItems.yieldsAsync(null, []);
|
||||||
|
userStorageStub.storeList.yieldsAsync(new Error());
|
||||||
|
|
||||||
|
updateHandler.update(function(error) {
|
||||||
|
expect(error).to.exist;
|
||||||
|
expect(userStorageStub.listItems.calledOnce).to.be.true;
|
||||||
|
expect(userStorageStub.storeList.calledOnce).to.be.true;
|
||||||
|
expect(appConfigStorageStub.storeList.called).to.be.false;
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail when listing folders fails', function(done) {
|
||||||
|
userStorageStub.listItems.yieldsAsync(new Error());
|
||||||
|
|
||||||
|
updateHandler.update(function(error) {
|
||||||
|
expect(error).to.exist;
|
||||||
|
expect(userStorageStub.listItems.calledOnce).to.be.true;
|
||||||
|
expect(userStorageStub.storeList.called).to.be.false;
|
||||||
|
expect(appConfigStorageStub.storeList.called).to.be.false;
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
Loading…
Reference in New Issue
Block a user