1
0
mirror of https://github.com/moparisthebest/mail synced 2024-11-25 18:32:20 -05:00

[WO-286] adapt to changes in data model for use of signed msgs and html

This commit is contained in:
Felix Hammerl 2014-05-06 17:23:08 +02:00
parent f44db9d1bd
commit a7efdf1125
10 changed files with 500 additions and 420 deletions

View File

@ -11,8 +11,8 @@
},
"dependencies": {
"crypto-lib": "https://github.com/whiteout-io/crypto-lib/tarball/v0.1.1",
"imap-client": "https://github.com/whiteout-io/imap-client/tarball/v0.2.6",
"mailreader": "https://github.com/whiteout-io/mailreader/tarball/v0.2.2",
"imap-client": "https://github.com/whiteout-io/imap-client/tarball/dev/WO-286",
"mailreader": "https://github.com/whiteout-io/mailreader/tarball/dev/wo-286",
"pgpmailer": "https://github.com/whiteout-io/pgpmailer/tarball/v0.2.2",
"pgpbuilder": "https://github.com/whiteout-io/pgpbuilder/tarball/v0.2.3",
"requirejs": "2.1.10"

View File

@ -46,7 +46,7 @@ define(function(require) {
iconPath: '/img/icon.png',
verificationUrl: '/verify/',
verificationUuidLength: 36,
dbVersion: 1,
dbVersion: 2,
appVersion: appVersion
};

View File

@ -72,7 +72,7 @@ define(function(require) {
self._keychain = keychain = new KeychainDAO(lawnchairDao, pubkeyDao);
self._crypto = pgp = new PGP();
self._pgpbuilder = pgpbuilder = new PgpBuilder();
self._emailSync = emailSync = new EmailSync(keychain, userStorage);
self._emailSync = emailSync = new EmailSync(keychain, userStorage, mailreader);
self._emailDao = emailDao = new EmailDAO(keychain, pgp, userStorage, pgpbuilder, mailreader, emailSync);
self._outboxBo = new OutboxBO(emailDao, keychain, userStorage);
self._updateHandler = new UpdateHandler(appConfigStore, userStorage);
@ -129,7 +129,7 @@ define(function(require) {
};
pgpMailer = new PgpMailer(smtpOptions, self._pgpbuilder);
imapClient = new ImapClient(imapOptions, mailreader);
imapClient = new ImapClient(imapOptions);
imapClient.onError = onImapError;
// connect to clients

View File

@ -274,123 +274,121 @@ define(function(require) {
message = options.message,
folder = options.folder;
if (message.loadingBody) {
return;
}
// the message already has a body, so no need to become active here
if (message.body) {
// the message either already has a body or is fetching it right now, so no need to become active here
if (message.loadingBody || typeof message.body !== 'undefined') {
return;
}
message.loadingBody = true;
// the mail does not have its content in memory
readFromDevice();
/*
* read this before inspecting the method!
*
* you will wonder about the round trip to the disk where we load the persisted object. there are two reasons for this behavior:
* 1) if you work with a message that was loaded from the disk, we strip the message.bodyParts array,
* because it is not really necessary to keep everything in memory
* 2) the message in memory is polluted by angular. angular tracks ordering of a list by adding a property
* to the model. this property is auto generated and must not be persisted.
*/
// if possible, read the message body from the device
function readFromDevice() {
retrieveContent();
function retrieveContent() {
// load the local message from memory
self._emailSync._localListMessages({
folder: folder,
uid: message.uid
}, function(err, localMessages) {
var localMessage;
if (err) {
message.loadingBody = false;
callback(err);
if (err || localMessages.length === 0) {
done(err);
return;
}
localMessage = localMessages[0];
var localMessage = localMessages[0];
if (!localMessage.body) {
streamFromImap();
// do we need to fetch content from the imap server?
var needsFetch = false;
localMessage.bodyParts.forEach(function(part) {
needsFetch = (typeof part.content === 'undefined');
});
if (!needsFetch) {
// if we have all the content we need,
// we can extract the content
message.bodyParts = localMessage.bodyParts;
extractContent();
return;
}
// attach the body to the mail object
message.body = localMessage.body;
handleEncryptedContent();
});
}
// if reading the message body from the device was unsuccessful,
// stream the message from the imap server
function streamFromImap() {
self._emailSync._imapStreamText({
folder: folder,
message: message
}, function(error) {
if (error) {
message.loadingBody = false;
callback(error);
return;
}
message.loadingBody = false;
// do not write the object from the object used by angular to the disk, instead
// do a short round trip and write back the unpolluted object
self._emailSync._localListMessages({
// get the raw content from the imap server
self._emailSync._getBodyParts({
folder: folder,
uid: message.uid
}, function(error, storedMessages) {
if (error) {
callback(error);
uid: localMessage.uid,
bodyParts: localMessage.bodyParts
}, function(err, parsedBodyParts) {
if (err) {
done(err);
return;
}
storedMessages[0].body = message.body;
message.bodyParts = parsedBodyParts;
localMessage.bodyParts = parsedBodyParts;
// persist it to disk
self._emailSync._localStoreMessages({
folder: folder,
emails: storedMessages
emails: [localMessage]
}, function(error) {
if (error) {
callback(error);
done(error);
return;
}
handleEncryptedContent();
// extract the content
extractContent();
});
});
});
}
function handleEncryptedContent() {
// normally, the imap-client should already have set the message.encrypted flag. problem: if we have pgp/inline,
// we can't reliably determine if the message is encrypted before we have inspected the payload...
message.encrypted = containsArmoredCiphertext(message);
// cleans the message body from everything but the ciphertext
function extractContent() {
if (message.encrypted) {
message.decrypted = false;
extractCiphertext();
// show the encrypted message
message.body = self._emailSync.filterBodyParts(message.bodyParts, 'encrypted')[0].content;
done();
return;
}
// for unencrypted messages, this is the array where the body parts are located
var root = message.bodyParts;
if (message.signed) {
var signedPart = self._emailSync.filterBodyParts(message.bodyParts, 'signed')[0];
message.message = signedPart.message;
message.signature = signedPart.signature;
// TODO check integrity
// in case of a signed message, you only want to show the signed content and ignore the rest
root = signedPart.content;
}
message.attachments = self._emailSync.filterBodyParts(root, 'attachment');
message.body = _.pluck(self._emailSync.filterBodyParts(root, 'text'), 'content').join('\n');
message.html = _.pluck(self._emailSync.filterBodyParts(root, 'html'), 'content').join('\n');
done();
}
function done(err) {
message.loadingBody = false;
callback(null, message);
callback(err, err ? undefined : message);
}
function containsArmoredCiphertext() {
return message.body.indexOf(str.cryptPrefix) !== -1 && message.body.indexOf(str.cryptSuffix) !== -1;
}
function extractCiphertext() {
var start = message.body.indexOf(str.cryptPrefix),
end = message.body.indexOf(str.cryptSuffix) + str.cryptSuffix.length;
// parse message body for encrypted message block
message.body = message.body.substring(start, end);
}
};
EmailDAO.prototype.decryptBody = function(options, callback) {
var self = this,
message = options.message;
// the message has no body, is not encrypted or has already been decrypted
// the message is decrypting has no body, is not encrypted or has already been decrypted
if (message.decryptingBody || !message.body || !message.encrypted || message.decrypted) {
return;
}
@ -408,61 +406,56 @@ define(function(require) {
if (!senderPublicKey) {
// this should only happen if a mail from another channel is in the inbox
message.body = 'Public key for sender not found!';
message.decryptingBody = false;
callback(null, message);
done();
return;
}
// get the receiver's public key to check the message signature
self._crypto.decrypt(message.body, senderPublicKey.publicKey, function(err, decrypted) {
// if an error occurs during decryption, display the error message as the message content
decrypted = decrypted || err.errMsg || 'Error occurred during decryption';
// this is a very primitive detection if we have PGP/MIME or PGP/INLINE
if (!self._mailreader.isRfc(decrypted)) {
message.body = decrypted;
message.decrypted = true;
message.decryptingBody = false;
callback(null, message);
var encryptedNode = self._emailSync.filterBodyParts(message.bodyParts, 'encrypted')[0];
self._crypto.decrypt(encryptedNode.content, senderPublicKey.publicKey, function(err, decrypted) {
if (err || !decrypted) {
err = err || {
errMsg: 'Error occurred during decryption'
};
done(err);
return;
}
// parse the decrypted MIME message
self._imapParseMessageBlock({
message: message,
raw: decrypted
}, function(error) {
// the mailparser works on the .raw property
encryptedNode.raw = decrypted;
// parse the decrpyted raw content in the mailparser
self._mailreader.parse({
bodyParts: [encryptedNode]
}, function(error, parsedBodyParts) {
if (error) {
message.decryptingBody = false;
callback(error);
done(error);
return;
}
message.decrypted = true;
// we have successfully interpreted the descrypted message,
// so let's update the views on the message parts
// remove the pgp-signature from the attachments
message.attachments = _.reject(message.attachments, function(attmt) {
message.body = _.pluck(self._emailSync.filterBodyParts(parsedBodyParts, 'text'), 'content').join('\n');
message.html = _.pluck(self._emailSync.filterBodyParts(parsedBodyParts, 'html'), 'content').join('\n');
message.attachments = _.reject(self._emailSync.filterBodyParts(parsedBodyParts, 'attachment'), function(attmt) {
// remove the pgp-signature from the attachments
return attmt.mimeType === "application/pgp-signature";
});
message.decrypted = true;
// we're done here!
message.decryptingBody = false;
callback(null, message);
done();
});
});
});
};
EmailDAO.prototype.getAttachment = function(options, callback) {
if (!this._account.online) {
callback({
errMsg: 'Client is currently offline!',
code: 42
});
return;
function done(err) {
message.decryptingBody = false;
callback(err, err ? undefined : message);
}
this._imapClient.getAttachment(options, callback);
};
EmailDAO.prototype.sendEncrypted = function(options, callback) {
@ -541,10 +534,6 @@ define(function(require) {
this._imapClient.logout(callback);
};
EmailDAO.prototype._imapParseMessageBlock = function(options, callback) {
this._mailreader.parseRfc(options, callback);
};
/**
* List the folders in the user's IMAP mailbox.
*/

View File

@ -5,9 +5,10 @@ define(function(require) {
config = require('js/app-config').config,
str = require('js/app-config').string;
var EmailSync = function(keychain, devicestorage) {
var EmailSync = function(keychain, devicestorage, mailreader) {
this._keychain = keychain;
this._devicestorage = devicestorage;
this._mailreader = mailreader;
};
EmailSync.prototype.init = function(options, callback) {
@ -168,8 +169,8 @@ define(function(require) {
}
storedMessages.forEach(function(storedMessage) {
// remove the body to not load unnecessary data to memory
delete storedMessage.body;
// remove the body parts to not load unnecessary data to memory
delete storedMessage.bodyParts;
folder.messages.push(storedMessage);
});
@ -619,10 +620,11 @@ define(function(require) {
}
function handleVerification(message, localCallback) {
self._imapStreamText({
self._getBodyParts({
folder: options.folder,
message: message
}, function(error) {
uid: message.uid,
bodyParts: message.bodyParts
}, function(error, parsedBodyParts) {
// we could not stream the text to determine if the verification was valid or not
// so handle it as if it were valid
if (error) {
@ -630,8 +632,9 @@ define(function(require) {
return;
}
var verificationUrlPrefix = config.cloudUrl + config.verificationUrl,
uuid = message.body.split(verificationUrlPrefix).pop().substr(0, config.verificationUuidLength),
var body = _.pluck(self.filterBodyParts(parsedBodyParts, 'text'), 'content').join('\n'),
verificationUrlPrefix = config.cloudUrl + config.verificationUrl,
uuid = body.split(verificationUrlPrefix).pop().substr(0, config.verificationUuidLength),
isValidUuid = new RegExp('[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}').test(uuid);
// there's no valid uuid in the message, so forget about it
@ -707,12 +710,8 @@ define(function(require) {
return;
}
this._imapClient.updateFlags({
path: options.folder,
uid: options.uid,
unread: options.unread,
answered: options.answered
}, callback);
options.path = options.folder;
this._imapClient.updateFlags(options, callback);
};
/**
@ -731,18 +730,8 @@ define(function(require) {
return;
}
var o = {
path: options.folder
};
if (typeof options.answered !== 'undefined') {
o.answered = options.answered;
}
if (typeof options.unread !== 'undefined') {
o.unread = options.unread;
}
this._imapClient.search(o, callback);
options.path = options.folder;
this._imapClient.search(options, callback);
};
EmailSync.prototype._imapDeleteMessage = function(options, callback) {
@ -754,10 +743,8 @@ define(function(require) {
return;
}
this._imapClient.deleteMessage({
path: options.folder,
uid: options.uid
}, callback);
options.path = options.folder;
this._imapClient.deleteMessage(options, callback);
};
/**
@ -778,20 +765,18 @@ define(function(require) {
return;
}
self._imapClient.listMessagesByUid({
path: options.folder,
firstUid: options.firstUid,
lastUid: options.lastUid
}, callback);
options.path = options.folder;
self._imapClient.listMessages(options, callback);
};
/**
* Stream an email messsage's body
* @param {String} options.folder The folder
* @param {Object} options.message The message, as retrieved by _imapListMessages
* @param {String} options.uid the message's uid
* @param {Object} options.bodyParts The message, as retrieved by _imapListMessages
* @param {Function} callback (error, message) The callback when the imap client is done streaming message text content
*/
EmailSync.prototype._imapStreamText = function(options, callback) {
EmailSync.prototype._getBodyParts = function(options, callback) {
var self = this;
if (!this._account.online) {
@ -802,11 +787,37 @@ define(function(require) {
return;
}
self._imapClient.getBody({
path: options.folder,
message: options.message
}, callback);
options.path = options.folder;
self._imapClient.getBodyParts(options, function(err) {
if (err) {
callback(err);
return;
}
// interpret the raw content of the email
self._mailreader.parse(options, callback);
});
};
/**
* Helper function that recursively traverses the body parts tree. Looks for bodyParts that match the provided type and aggregates them
* @param {[type]} bodyParts The bodyParts array
* @param {[type]} type The type to look up
* @param {undefined} result Leave undefined, only used for recursion
*/
EmailSync.prototype.filterBodyParts = function(bodyParts, type, result) {
var self = this;
result = result || [];
bodyParts.forEach(function(part) {
if (part.type === type) {
result.push(part);
} else if (Array.isArray(part.content)) {
self.filterBodyParts(part.content, type, result);
}
});
return result;
};
return EmailSync;
});

View File

@ -2,7 +2,8 @@ define(function(require) {
'use strict';
var cfg = require('js/app-config').config,
updateV1 = require('js/util/update/update-v1');
updateV1 = require('js/util/update/update-v1'),
updateV2 = require('js/util/update/update-v2');
/**
* Handles database migration
@ -10,7 +11,7 @@ define(function(require) {
var UpdateHandler = function(appConfigStorage, userStorage) {
this._appConfigStorage = appConfigStorage;
this._userStorage = userStorage;
this._updateScripts = [updateV1];
this._updateScripts = [updateV1, updateV2];
};
/**

View File

@ -0,0 +1,28 @@
define(function() {
'use strict';
/**
* Update handler for transition database version 1 -> 2
*
* In database version 2, the stored email objects have to be purged, because the
* new data model stores information about the email structure in the property 'bodyParts'.
*/
function updateV2(options, callback) {
var emailDbType = 'email_',
versionDbType = 'dbVersion',
postUpdateDbVersion = 2;
// remove the emails
options.userStorage.removeList(emailDbType, function(err) {
if (err) {
callback(err);
return;
}
// update the database version to postUpdateDbVersion
options.appConfigStorage.storeList([postUpdateDbVersion], versionDbType, callback);
});
}
return updateV2;
});

View File

@ -447,17 +447,6 @@ define(function(require) {
});
});
describe('_imapParseMessageBlock', function() {
it('should parse a message', function(done) {
var parseRfc = sinon.stub(mailreader, 'parseRfc').withArgs({}).yields();
dao._imapParseMessageBlock({}, function() {
expect(parseRfc.calledOnce).to.be.true;
done();
});
});
});
describe('_imapLogin', function() {
it('should fail when disconnected', function(done) {
dao.onDisconnect(null, function(err) {
@ -616,6 +605,10 @@ define(function(require) {
describe('getBody', function() {
var folder = 'asdasdasdasdasd',
uid = 1234,
localListStub, localStoreStub, imapGetStub;
it('should not do anything if the message already has content', function() {
var message = {
body: 'bender is great!'
@ -629,11 +622,9 @@ define(function(require) {
});
it('should read an unencrypted body from the device', function(done) {
var message, uid, folder, body, localListStub;
var message, body;
folder = 'asdasdasdasdasd';
body = 'bender is great! bender is great!';
uid = 1234;
message = {
uid: uid
};
@ -642,10 +633,12 @@ define(function(require) {
folder: folder,
uid: uid
}).yieldsAsync(null, [{
body: body
bodyParts: [{
type: 'text',
content: body
}]
}]);
dao.getBody({
message: message,
folder: folder
@ -653,8 +646,7 @@ define(function(require) {
expect(err).to.not.exist;
expect(msg).to.equal(message);
expect(msg.body).to.not.be.empty;
expect(msg.encrypted).to.be.false;
expect(msg.body).to.equal(body);
expect(msg.loadingBody).to.be.false;
expect(localListStub.calledOnce).to.be.true;
@ -665,20 +657,26 @@ define(function(require) {
});
it('should read an encrypted body from the device', function(done) {
var message, uid, folder, body, localListStub;
var message, ct, pt;
folder = 'asdasdasdasdasd';
body = '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----';
uid = 1234;
pt = 'bender is great!';
ct = '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----';
message = {
uid: uid
uid: uid,
encrypted: true
};
localListStub = sinon.stub(emailSync, '_localListMessages').withArgs({
folder: folder,
uid: uid
}).yieldsAsync(null, [{
body: body
bodyParts: [{
type: 'text',
content: pt
}, {
type: 'encrypted',
content: ct
}]
}]);
dao.getBody({
@ -688,9 +686,8 @@ define(function(require) {
expect(err).to.not.exist;
expect(msg).to.equal(message);
expect(msg.body).to.not.be.empty;
expect(msg.body).to.equal(ct);
expect(msg.encrypted).to.be.true;
expect(msg.decrypted).to.be.false;
expect(message.loadingBody).to.be.false;
expect(localListStub.calledOnce).to.be.true;
@ -700,14 +697,17 @@ define(function(require) {
expect(message.loadingBody).to.be.true;
});
it('should stream an unencrypted body from imap', function(done) {
var message, uid, folder, body, localListStub, localStoreStub, imapStreamStub;
it('should stream from imap and set plain text body', function(done) {
var message, body;
folder = 'asdasdasdasdasd';
body = 'bender is great! bender is great!';
uid = 1234;
message = {
uid: uid
uid: uid,
bodyParts: [{
type: 'text'
}]
};
localListStub = sinon.stub(emailSync, '_localListMessages').withArgs({
@ -720,16 +720,14 @@ define(function(require) {
emails: [message]
}).yieldsAsync();
imapStreamStub = sinon.stub(emailSync, '_imapStreamText', function(opts, cb) {
expect(opts).to.deep.equal({
folder: folder,
message: message
});
message.body = body;
cb();
});
imapGetStub = sinon.stub(emailSync, '_getBodyParts').withArgs({
folder: folder,
uid: message.uid,
bodyParts: message.bodyParts
}).yieldsAsync(null, [{
type: 'text',
content: body
}]);
dao.getBody({
message: message,
@ -738,12 +736,11 @@ define(function(require) {
expect(err).to.not.exist;
expect(msg).to.equal(message);
expect(msg.body).to.not.be.empty;
expect(msg.encrypted).to.be.false;
expect(msg.body).to.equal(body);
expect(msg.loadingBody).to.be.false;
expect(localListStub.calledTwice).to.be.true;
expect(imapStreamStub.calledOnce).to.be.true;
expect(localListStub.calledOnce).to.be.true;
expect(imapGetStub.calledOnce).to.be.true;
expect(localStoreStub.calledOnce).to.be.true;
done();
@ -751,14 +748,19 @@ define(function(require) {
expect(message.loadingBody).to.be.true;
});
it('should stream an encrypted body from imap', function(done) {
var message, uid, folder, body, localListStub, localStoreStub, imapStreamStub;
it('should stream from imap and set encrypted body', function(done) {
var message, ct, pt;
folder = 'asdasdasdasdasd';
body = '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----';
uid = 1234;
pt = 'bender is great';
ct = '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----';
message = {
uid: uid
uid: uid,
encrypted: true,
bodyParts: [{
type: 'text'
}, {
type: 'encrypted'
}]
};
localListStub = sinon.stub(emailSync, '_localListMessages').withArgs({
@ -771,15 +773,17 @@ define(function(require) {
emails: [message]
}).yieldsAsync();
imapStreamStub = sinon.stub(emailSync, '_imapStreamText', function(opts, cb) {
expect(opts).to.deep.equal({
folder: folder,
message: message
});
message.body = body;
cb();
});
imapGetStub = sinon.stub(emailSync, '_getBodyParts').withArgs({
folder: folder,
uid: message.uid,
bodyParts: message.bodyParts
}).yieldsAsync(null, [{
type: 'text',
content: pt
}, {
type: 'encrypted',
content: ct
}]);
dao.getBody({
@ -789,13 +793,12 @@ define(function(require) {
expect(err).to.not.exist;
expect(msg).to.equal(message);
expect(msg.body).to.not.be.empty;
expect(msg.body).to.equal(ct);
expect(msg.encrypted).to.be.true;
expect(msg.decrypted).to.be.false;
expect(msg.loadingBody).to.be.false;
expect(localListStub.calledTwice).to.be.true;
expect(imapStreamStub.calledOnce).to.be.true;
expect(localListStub.calledOnce).to.be.true;
expect(imapGetStub.calledOnce).to.be.true;
expect(localStoreStub.calledOnce).to.be.true;
done();
@ -804,22 +807,19 @@ define(function(require) {
});
it('fail to stream from imap due to error when persisting', function(done) {
var message, uid, folder, body, localListStub, localStoreStub, imapStreamStub;
folder = 'asdasdasdasdasd';
body = 'THIS IS THE BODY';
uid = 1234;
message = {
uid: uid
var message = {
uid: uid,
bodyParts: [{
type: 'text'
}]
};
localListStub = sinon.stub(emailSync, '_localListMessages').yieldsAsync(null, [message]);
localStoreStub = sinon.stub(emailSync, '_localStoreMessages').yieldsAsync({});
imapStreamStub = sinon.stub(emailSync, '_imapStreamText', function(opts, cb) {
message.body = body;
cb();
});
imapGetStub = sinon.stub(emailSync, '_getBodyParts').yieldsAsync(null, [{
type: 'text',
content: 'bender is great! bender is great!'
}]);
dao.getBody({
message: message,
@ -827,8 +827,8 @@ define(function(require) {
}, function(err, msg) {
expect(err).to.exist;
expect(msg).to.not.exist;
expect(localListStub.calledTwice).to.be.true;
expect(imapStreamStub.calledOnce).to.be.true;
expect(localListStub.calledOnce).to.be.true;
expect(imapGetStub.calledOnce).to.be.true;
expect(localStoreStub.calledOnce).to.be.true;
expect(message.loadingBody).to.be.false;
@ -838,21 +838,15 @@ define(function(require) {
});
it('fail to stream from imap due to stream error', function(done) {
var message, uid, folder, body, localListStub, localStoreStub, imapStreamStub;
folder = 'asdasdasdasdasd';
uid = 1234;
message = {
uid: uid
var message = {
uid: uid,
bodyParts: [{
type: 'text'
}]
};
localListStub = sinon.stub(emailSync, '_localListMessages').yields(null, [{}]);
imapStreamStub = sinon.stub(emailSync, '_imapStreamText', function(opts, cb) {
message.body = body;
cb({});
});
localListStub = sinon.stub(emailSync, '_localListMessages').yields(null, [message]);
imapGetStub = sinon.stub(emailSync, '_getBodyParts').yieldsAsync({});
localStoreStub = sinon.stub(emailSync, '_localStoreMessages');
dao.getBody({
@ -862,7 +856,7 @@ define(function(require) {
expect(err).to.exist;
expect(msg).to.not.exist;
expect(localListStub.calledOnce).to.be.true;
expect(imapStreamStub.calledOnce).to.be.true;
expect(imapGetStub.calledOnce).to.be.true;
expect(localStoreStub.called).to.be.false;
expect(message.loadingBody).to.be.false;
@ -873,55 +867,89 @@ define(function(require) {
});
describe('decryptBody', function() {
it('should not do anything when the message is not encrypted', function() {
it('should do nothing when the message is not encrypted', function() {
var message = {
encrypted: false
encrypted: false,
decrypted: true,
body: 'asd'
};
dao.decryptBody({
message: message
});
// should do nothing
});
it('should not do anything when the message is already decrypted', function() {
it('should do nothing when the message is already decrypted', function() {
var message = {
encrypted: true,
decrypted: true
decrypted: true,
body: 'asd'
};
dao.decryptBody({
message: message
});
});
// should do nothing
it('should do nothing when the message has no body', function() {
var message = {
encrypted: true,
decrypted: false,
body: ''
};
dao.decryptBody({
message: message
});
});
it('should do nothing when the message is decrypting', function() {
var message = {
encrypted: true,
decrypted: false,
body: 'asd',
decryptingBody: true
};
dao.decryptBody({
message: message
});
});
it('decrypt a pgp/mime message', function(done) {
var message, parsedBody, mimeBody, parseStub;
var message, ct, pt, parsed, parseStub;
pt = 'bender is great';
ct = '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----';
parsed = 'bender! bender! bender!';
message = {
from: [{
address: 'asdasdasd'
}],
body: ct,
encrypted: true,
decrypted: false,
body: '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----'
bodyParts: [{
type: 'encrypted',
content: ct
}]
};
mimeBody = 'Content-Type: asdasdasd';
parsedBody = 'body? yes.';
keychainStub.getReceiverPublicKey.withArgs(message.from[0].address).yieldsAsync(null, mockKeyPair.publicKey);
pgpStub.decrypt.withArgs(message.body, mockKeyPair.publicKey.publicKey).yieldsAsync(null, mimeBody);
parseStub = sinon.stub(dao, '_imapParseMessageBlock', function(o, cb) {
expect(o.message).to.equal(message);
expect(o.raw).to.equal(mimeBody);
o.message.body = parsedBody;
cb(null, o.message);
});
pgpStub.decrypt.withArgs(ct, mockKeyPair.publicKey.publicKey).yieldsAsync(null, pt);
parseStub = sinon.stub(mailreader, 'parse').withArgs({
bodyParts: [{
type: 'encrypted',
content: ct,
raw: pt
}]
}).yieldsAsync(null, [{
type: 'encrypted',
content: [{
type: 'text',
content: parsed
}]
}]);
dao.decryptBody({
message: message
@ -929,110 +957,71 @@ define(function(require) {
expect(error).to.not.exist;
expect(msg).to.equal(message);
expect(msg.decrypted).to.be.true;
expect(msg.body).to.equal(parsedBody);
expect(msg.decryptingBody).to.be.false;
expect(message.decrypted).to.be.true;
expect(message.body).to.equal(parsed);
expect(message.decryptingBody).to.be.false;
expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true;
expect(pgpStub.decrypt.calledOnce).to.be.true;
expect(parseStub.calledOnce).to.be.true;
mailreader.parse.restore();
done();
});
expect(message.decryptingBody).to.be.true;
});
it('decrypt a pgp/inline message', function(done) {
var message, plaintextBody, parseStub;
message = {
from: [{
address: 'asdasdasd'
}],
encrypted: true,
decrypted: false,
body: '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----'
};
plaintextBody = 'body? yes.';
keychainStub.getReceiverPublicKey.withArgs(message.from[0].address).yieldsAsync(null, mockKeyPair.publicKey);
pgpStub.decrypt.withArgs(message.body, mockKeyPair.publicKey.publicKey).yieldsAsync(null, plaintextBody);
parseStub = sinon.stub(dao, '_imapParseMessageBlock');
dao.decryptBody({
message: message
}, function(error, msg) {
expect(error).to.not.exist;
expect(msg).to.equal(message);
expect(msg.decrypted).to.be.true;
expect(msg.body).to.equal(plaintextBody);
expect(msg.decryptingBody).to.be.false;
expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true;
expect(pgpStub.decrypt.calledOnce).to.be.true;
expect(parseStub.called).to.be.false;
done();
});
expect(message.decryptingBody).to.be.true;
});
it('should fail during decryption message', function(done) {
var message, plaintextBody, parseStub, errMsg;
message = {
var message = {
from: [{
address: 'asdasdasd'
}],
body: 'asdjafuad',
encrypted: true,
decrypted: false,
body: '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----'
bodyParts: [{
type: 'encrypted',
content: '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----'
}]
};
plaintextBody = 'body? yes.';
errMsg = 'yaddayadda';
keychainStub.getReceiverPublicKey.withArgs(message.from[0].address).yields(null, mockKeyPair.publicKey);
var parseStub = sinon.spy(mailreader, 'parse');
keychainStub.getReceiverPublicKey.yields(null, mockKeyPair.publicKey);
pgpStub.decrypt.yields({
errMsg: errMsg
errMsg: 'asd'
});
parseStub = sinon.stub(dao, '_imapParseMessageBlock');
dao.decryptBody({
message: message
}, function(error, msg) {
expect(error).to.not.exist;
expect(msg).to.equal(message);
expect(msg.decrypted).to.be.true;
expect(msg.body).to.equal(errMsg);
expect(msg.decryptingBody).to.be.false;
expect(error).to.exist;
expect(msg).to.not.exist;
expect(message.decryptingBody).to.be.false;
expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true;
expect(pgpStub.decrypt.calledOnce).to.be.true;
expect(parseStub.called).to.be.false;
mailreader.parse.restore();
done();
});
});
it('should fail during key export', function(done) {
var message, parseStub;
message = {
var message = {
from: [{
address: 'asdasdasd'
}],
encrypted: true,
decrypted: false,
body: '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----'
body: 'asdjafuad',
bodyParts: [{
type: 'encrypted',
content: '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----'
}]
};
var parseStub = sinon.spy(mailreader, 'parse');
keychainStub.getReceiverPublicKey.yields({});
parseStub = sinon.stub(dao, '_imapParseMessageBlock');
dao.decryptBody({
message: message
@ -1041,13 +1030,13 @@ define(function(require) {
expect(msg).to.not.exist;
expect(message.decrypted).to.be.false;
expect(message.decryptingBody).to.be.false;
expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true;
expect(pgpStub.decrypt.called).to.be.false;
expect(parseStub.called).to.be.false;
mailreader.parse.restore();
done();
});
});

View File

@ -3,6 +3,7 @@ define(function(require) {
var EmailSync = require('js/dao/email-sync'),
KeychainDAO = require('js/dao/keychain-dao'),
mailreader = require('mailreader'),
ImapClient = require('imap-client'),
DeviceStorageDAO = require('js/dao/devicestorage-dao'),
expect = chai.expect;
@ -13,8 +14,7 @@ define(function(require) {
var emailSync, keychainStub, imapClientStub, devicestorageStub;
var emailAddress, mockkeyId, dummyEncryptedMail,
dummyDecryptedMail, mockKeyPair, account, verificationMail, verificationUuid,
corruptedVerificationMail, corruptedVerificationUuid,
dummyDecryptedMail, mockKeyPair, account, verificationMail, verificationUuid, corruptedVerificationUuid,
nonWhitelistedMail;
beforeEach(function(done) {
@ -29,7 +29,9 @@ define(function(require) {
address: 'qwe@qwe.de'
}],
subject: 'qweasd',
body: '-----BEGIN PGP MESSAGE-----\nasd\n-----END PGP MESSAGE-----',
bodyParts: [{
type: 'encrypted'
}],
unread: false,
answered: false
};
@ -43,24 +45,13 @@ define(function(require) {
address: 'safewithme.testuser@gmail.com'
}], // list of receivers
subject: "[whiteout] New public key uploaded", // Subject line
body: 'yadda yadda bla blabla foo bar https://keys.whiteout.io/verify/' + verificationUuid, // plaintext body
bodyParts: [{
type: 'text'
}],
unread: false,
answered: false
};
corruptedVerificationUuid = 'OMFG_FUCKING_BASTARD_UUID_FROM_HELL!';
corruptedVerificationMail = {
from: [{
name: 'Whiteout Test',
address: 'whiteout.test@t-online.de'
}], // sender address
to: [{
address: 'safewithme.testuser@gmail.com'
}], // list of receivers
subject: "[whiteout] New public key uploaded", // Subject line
body: 'yadda yadda bla blabla foo bar https://keys.whiteout.io/verify/' + corruptedVerificationUuid, // plaintext body
unread: false,
answered: false
};
dummyDecryptedMail = {
uid: 1234,
from: [{
@ -70,7 +61,9 @@ define(function(require) {
address: 'qwe@qwe.de'
}],
subject: 'qweasd',
body: 'Content-Type: multipart/signed;\r\n boundary="Apple-Mail=_1D8756C0-F347-4D7A-A8DB-7869CBF14FD2";\r\n protocol="application/pgp-signature";\r\n micalg=pgp-sha512\r\n\r\n\r\n--Apple-Mail=_1D8756C0-F347-4D7A-A8DB-7869CBF14FD2\r\nContent-Type: multipart/mixed;\r\n boundary="Apple-Mail=_8ED7DC84-6AD9-4A08-8327-80B62D6BCBFA"\r\n\r\n\r\n--Apple-Mail=_8ED7DC84-6AD9-4A08-8327-80B62D6BCBFA\r\nContent-Transfer-Encoding: 7bit\r\nContent-Type: text/plain;\r\n charset=us-ascii\r\n\r\nasdasd \r\n--Apple-Mail=_8ED7DC84-6AD9-4A08-8327-80B62D6BCBFA\r\nContent-Disposition: attachment;\r\n filename=dummy.txt\r\nContent-Type: text/plain;\r\n name="dummy.txt"\r\nContent-Transfer-Encoding: 7bit\r\n\r\noaudbcoaurbvosuabvlasdjbfalwubjvawvb\r\n--Apple-Mail=_8ED7DC84-6AD9-4A08-8327-80B62D6BCBFA--\r\n\r\n--Apple-Mail=_1D8756C0-F347-4D7A-A8DB-7869CBF14FD2\r\nContent-Transfer-Encoding: 7bit\r\nContent-Disposition: attachment;\r\n filename=signature.asc\r\nContent-Type: application/pgp-signature;\r\n name=signature.asc\r\nContent-Description: Message signed with OpenPGP using GPGMail\r\n\r\n-----BEGIN PGP SIGNATURE-----\r\nComment: GPGTools - https://gpgtools.org\r\n\r\niQEcBAEBCgAGBQJS2kO1AAoJEDzmUwH7XO/cP+YH/2PSBxX1ZZd83Uf9qBGDY807\r\niHOdgPFXm64YjSnohO7XsPcnmihqP1ipS2aaCXFC3/Vgb9nc4isQFS+i1VdPwfuR\r\n1Pd2l3dC4/nD4xO9h/W6JW7Yd24NS5TJD5cA7LYwQ8LF+rOzByMatiTMmecAUCe8\r\nEEalEjuogojk4IacA8dg/bfLqQu9E+0GYUJBcI97dx/0jZ0qMOxbWOQLsJ3DnUnV\r\nOad7pAIbHEO6T0EBsH7TyTj4RRHkP6SKE0mm6ZYUC7KCk2Z3MtkASTxUrnqW5qZ5\r\noaXUO9GEc8KZcmbCdhZY2Y5h+dmucaO0jpbeSKkvtYyD4KZrSvt7NTb/0dSLh4Y=\r\n=G8km\r\n-----END PGP SIGNATURE-----\r\n\r\n--Apple-Mail=_1D8756C0-F347-4D7A-A8DB-7869CBF14FD2--\r\n',
bodyParts: [{
type: 'text'
}],
unread: false,
answered: false,
};
@ -83,7 +76,9 @@ define(function(require) {
address: 'qwe@qwe.de'
}],
subject: 'qweasd',
body: 'asd'
bodyParts: [{
type: 'text'
}],
};
mockKeyPair = {
publicKey: {
@ -106,10 +101,11 @@ define(function(require) {
imapClientStub = sinon.createStubInstance(ImapClient);
devicestorageStub = sinon.createStubInstance(DeviceStorageDAO);
emailSync = new EmailSync(keychainStub, devicestorageStub);
emailSync = new EmailSync(keychainStub, devicestorageStub, mailreader);
expect(emailSync._keychain).to.equal(keychainStub);
expect(emailSync._devicestorage).to.equal(devicestorageStub);
expect(emailSync._mailreader).to.equal(mailreader);
// init
emailSync.init({
@ -154,6 +150,8 @@ define(function(require) {
describe('_imapSearch', function() {
var path = 'FOLDAAAA';
it('should fail when disconnected', function(done) {
// this is set in the emailDao.onDisconnect
emailSync._account.online = false;
@ -164,10 +162,9 @@ define(function(require) {
});
});
it('should work', function(done) {
var path = 'FOLDAAAA';
it('should list all uids', function(done) {
imapClientStub.search.withArgs({
folder: path,
path: path
}).yields();
@ -175,10 +172,10 @@ define(function(require) {
folder: path
}, done);
});
it('should work', function(done) {
var path = 'FOLDAAAA';
it('should list answered uids', function(done) {
imapClientStub.search.withArgs({
folder: path,
path: path,
answered: true
}).yields();
@ -188,10 +185,10 @@ define(function(require) {
answered: true
}, done);
});
it('should work', function(done) {
var path = 'FOLDAAAA';
it('should list unread uids', function(done) {
imapClientStub.search.withArgs({
folder: path,
path: path,
unread: true
}).yields();
@ -204,6 +201,9 @@ define(function(require) {
});
describe('_imapDeleteMessage', function() {
var path = 'FOLDAAAA',
uid = 1337;
it('should fail when disconnected', function(done) {
// this is set in the emailDao.onDisconnect
emailSync._account.online = false;
@ -215,11 +215,9 @@ define(function(require) {
});
it('should work', function(done) {
var path = 'FOLDAAAA',
uid = 1337;
imapClientStub.deleteMessage.withArgs({
path: path,
folder: path,
uid: uid
}).yields();
@ -231,13 +229,15 @@ define(function(require) {
});
describe('_imapListMessages', function() {
it('should work', function(done) {
var path = 'FOLDAAAA',
firstUid = 1337,
lastUid = 1339;
var path = 'FOLDAAAA',
firstUid = 1337,
lastUid = 1339;
imapClientStub.listMessagesByUid.withArgs({
it('should work', function(done) {
imapClientStub.listMessages.withArgs({
path: path,
folder: path,
firstUid: firstUid,
lastUid: lastUid
}).yields(null, []);
@ -250,18 +250,14 @@ define(function(require) {
expect(err).to.not.exist;
expect(msgs).to.exist;
expect(imapClientStub.listMessagesByUid.calledOnce).to.be.true;
expect(imapClientStub.listMessages.calledOnce).to.be.true;
done();
});
});
it('should not work when listMessagesByUid fails', function(done) {
var path = 'FOLDAAAA',
firstUid = 1337,
lastUid = 1339;
imapClientStub.listMessagesByUid.yields({});
it('should not work when listMessages fails', function(done) {
imapClientStub.listMessages.yields({});
emailSync._imapListMessages({
folder: path,
@ -271,7 +267,7 @@ define(function(require) {
expect(err).to.exist;
expect(msgs).to.not.exist;
expect(imapClientStub.listMessagesByUid.calledOnce).to.be.true;
expect(imapClientStub.listMessages.calledOnce).to.be.true;
done();
});
@ -288,42 +284,50 @@ define(function(require) {
});
});
describe('_imapStreamText', function() {
describe('_getBodyParts', function() {
var path = 'FOLDAAAA',
parseStub;
it('should work', function(done) {
var path = 'FOLDAAAA';
imapClientStub.getBody.withArgs({
path: path,
message: {}
}).yields(null, {});
emailSync._imapStreamText({
var o = {
folder: path,
message: {}
}, function(err, msg) {
uid: 123,
bodyParts: []
};
imapClientStub.getBodyParts.withArgs(o).yields(null, {});
parseStub = sinon.stub(mailreader, 'parse').withArgs(o).yields(null, []);
emailSync._getBodyParts(o, function(err, parts) {
expect(err).to.not.exist;
expect(msg).to.exist;
expect(parts).to.exist;
expect(imapClientStub.getBody.calledOnce).to.be.true;
expect(imapClientStub.getBodyParts.calledOnce).to.be.true;
expect(parseStub.calledOnce).to.be.true;
mailreader.parse.restore();
done();
});
});
it('should not work when getBody fails', function(done) {
var path = 'FOLDAAAA';
imapClientStub.getBody.yields({});
emailSync._imapStreamText({
var o = {
folder: path,
message: {}
}, function(err, msg) {
uid: 123,
bodyParts: []
};
imapClientStub.getBodyParts.yields({});
parseStub = sinon.spy(mailreader, 'parse');
emailSync._getBodyParts(o, function(err, msg) {
expect(err).to.exist;
expect(msg).to.not.exist;
expect(imapClientStub.getBody.calledOnce).to.be.true;
expect(imapClientStub.getBodyParts.calledOnce).to.be.true;
expect(parseStub.called).to.be.false;
mailreader.parse.restore();
done();
});
});
@ -332,7 +336,7 @@ define(function(require) {
// this is set in the emailDao.onDisconnect
emailSync._account.online = false;
emailSync._imapStreamText({}, function(err) {
emailSync._getBodyParts({}, function(err) {
expect(err.code).to.equal(42);
done();
});
@ -950,13 +954,12 @@ define(function(require) {
}).yields(null, []);
imapListMessagesStub = sinon.stub(emailSync, '_imapListMessages').yields(null, [verificationMail]);
imapGetStub = sinon.stub(emailSync, '_imapStreamText').yields(null);
imapGetStub = sinon.stub(emailSync, '_getBodyParts').yields(null, [{
type: 'text',
content: 'yadda yadda bla blabla foo bar https://keys.whiteout.io/verify/' + verificationUuid,
}]);
keychainStub.verifyPublicKey.withArgs(verificationUuid).yields();
localStoreStub = sinon.stub(emailSync, '_localStoreMessages');
imapDeleteStub = sinon.stub(emailSync, '_imapDeleteMessage').withArgs({
folder: folder,
uid: verificationMail.uid
@ -1012,7 +1015,10 @@ define(function(require) {
answered: true
}).yields(null, []);
imapListMessagesStub = sinon.stub(emailSync, '_imapListMessages').yields(null, [verificationMail]);
imapGetStub = sinon.stub(emailSync, '_imapStreamText').yields(null);
imapGetStub = sinon.stub(emailSync, '_getBodyParts').yields(null, [{
type: 'text',
content: 'yadda yadda bla blabla foo bar https://keys.whiteout.io/verify/' + verificationUuid,
}]);
keychainStub.verifyPublicKey.withArgs(verificationUuid).yields();
imapDeleteStub = sinon.stub(emailSync, '_imapDeleteMessage').yields({});
@ -1073,7 +1079,10 @@ define(function(require) {
answered: true
}).yields(null, []);
imapListMessagesStub = sinon.stub(emailSync, '_imapListMessages').yields(null, [verificationMail]);
imapGetStub = sinon.stub(emailSync, '_imapStreamText').yields(null);
imapGetStub = sinon.stub(emailSync, '_getBodyParts').yields(null, [{
type: 'text',
content: 'yadda yadda bla blabla foo bar https://keys.whiteout.io/verify/' + verificationUuid,
}]);
keychainStub.verifyPublicKey.withArgs(verificationUuid).yields({
errMsg: 'fubar'
});
@ -1134,7 +1143,7 @@ define(function(require) {
imapSearchStub = sinon.stub(emailSync, '_imapSearch');
imapSearchStub.withArgs({
folder: folder
}).yields(null, [corruptedVerificationMail.uid]);
}).yields(null, [verificationMail.uid]);
imapSearchStub.withArgs({
folder: folder,
unread: true
@ -1146,12 +1155,15 @@ define(function(require) {
localStoreStub = sinon.stub(emailSync, '_localStoreMessages').withArgs({
folder: folder,
emails: [corruptedVerificationMail]
emails: [verificationMail]
}).yields();
imapListMessagesStub = sinon.stub(emailSync, '_imapListMessages').yields(null, [corruptedVerificationMail]);
imapGetStub = sinon.stub(emailSync, '_imapStreamText').yields(null);
imapListMessagesStub = sinon.stub(emailSync, '_imapListMessages').yields(null, [verificationMail]);
imapGetStub = sinon.stub(emailSync, '_getBodyParts').yields(null, [{
type: 'text',
content: 'yadda yadda bla blabla foo bar https://keys.whiteout.io/verify/' + corruptedVerificationUuid,
}]);
keychainStub.verifyPublicKey.withArgs(corruptedVerificationUuid).yields({
errMsg: 'fubar'
});
@ -1602,19 +1614,16 @@ define(function(require) {
describe('mark', function() {
it('should work', function(done) {
imapClientStub.updateFlags.withArgs({
path: 'asdf',
uid: 1,
unread: false,
answered: false
}).yields();
emailSync._imapMark({
var o = {
folder: 'asdf',
uid: 1,
unread: false,
answered: false
}, function(err) {
};
imapClientStub.updateFlags.withArgs(o).yields();
emailSync._imapMark(o, function(err) {
expect(imapClientStub.updateFlags.calledOnce).to.be.true;
expect(err).to.not.exist;
done();

View File

@ -6,11 +6,11 @@ define(function(require) {
UpdateHandler = require('js/util/update/update-handler'),
expect = chai.expect;
chai.Assertion.includeStack = true;
describe('UpdateHandler', function() {
var updateHandler, appConfigStorageStub, userStorageStub, origDbVersion;
chai.Assertion.includeStack = true;
beforeEach(function() {
origDbVersion = cfg.dbVersion;
appConfigStorageStub = sinon.createStubInstance(DeviceStorageDAO);
@ -160,6 +160,59 @@ define(function(require) {
});
});
});
describe('v1 -> v2', function() {
var emailDbType = 'email_';
beforeEach(function() {
cfg.dbVersion = 2; // app requires database version 2
appConfigStorageStub.listItems.withArgs(versionDbType).yieldsAsync(null, [1]); // database version is 0
});
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.removeList.withArgs(emailDbType).yieldsAsync();
appConfigStorageStub.storeList.withArgs([2], versionDbType).yieldsAsync();
updateHandler.update(function(error) {
expect(error).to.not.exist;
expect(userStorageStub.removeList.calledOnce).to.be.true;
expect(appConfigStorageStub.storeList.calledOnce).to.be.true;
done();
});
});
it('should fail when persisting database version fails', function(done) {
userStorageStub.removeList.yieldsAsync();
appConfigStorageStub.storeList.yieldsAsync({});
updateHandler.update(function(error) {
expect(error).to.exist;
expect(userStorageStub.removeList.calledOnce).to.be.true;
expect(appConfigStorageStub.storeList.calledOnce).to.be.true;
done();
});
});
it('should fail when wiping emails from database fails', function(done) {
userStorageStub.removeList.yieldsAsync({});
updateHandler.update(function(error) {
expect(error).to.exist;
expect(userStorageStub.removeList.calledOnce).to.be.true;
expect(appConfigStorageStub.storeList.called).to.be.false;
done();
});
});
});
});
});
});