1
0
mirror of https://github.com/moparisthebest/mail synced 2024-12-25 00:38:54 -05:00

Merge pull request #103 from whiteout-io/dev/WO-515

Dev/wo 515
This commit is contained in:
Tankred Hase 2014-08-05 19:01:35 +02:00
commit 04ed3dab68
9 changed files with 396 additions and 173 deletions

View File

@ -107,7 +107,7 @@ module.exports = function(grunt) {
}, },
js: { js: {
files: ['src/js/**/*.js'], files: ['src/js/**/*.js'],
tasks: ['copy:js'] tasks: ['copy:js', 'copy:integration']
}, },
lib: { lib: {
files: ['src/lib/**/*.js'], files: ['src/lib/**/*.js'],

View File

@ -11,7 +11,7 @@
}, },
"dependencies": { "dependencies": {
"crypto-lib": "~0.2.1", "crypto-lib": "~0.2.1",
"imap-client": "~0.3.7", "imap-client": "~0.4.0",
"mailreader": "~0.3.5", "mailreader": "~0.3.5",
"pgpmailer": "~0.3.11", "pgpmailer": "~0.3.11",
"pgpbuilder": "~0.3.7", "pgpbuilder": "~0.3.7",

View File

@ -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'
}; };

View File

@ -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;

View File

@ -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

View File

@ -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;
}; };

View 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;
});

View File

@ -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();

View File

@ -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();
});
});
});
}); });
}); });
}); });