mirror of
https://github.com/moparisthebest/mail
synced 2024-12-25 00:38:54 -05:00
commit
04ed3dab68
@ -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'],
|
||||||
|
@ -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",
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
@ -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