From 04cf299e1e617cc2848392dfcd14059233fea500 Mon Sep 17 00:00:00 2001 From: Felix Hammerl Date: Thu, 16 Jan 2014 11:38:53 +0100 Subject: [PATCH 01/14] adapt gruntfile, package.json, dummy mails --- Gruntfile.js | 5 ++--- package.json | 2 +- src/js/controller/mail-list.js | 11 +++++++++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 56e7405..1ed96a0 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -137,13 +137,12 @@ module.exports = function(grunt) { 'imap-client/node_modules/mimelib/node_modules/addressparser/src/addressparser.js', 'imap-client/node_modules/mimelib/node_modules/encoding/src/encoding.js', 'imap-client/node_modules/mimelib/node_modules/encoding/node_modules/iconv-lite/src/*.js', - 'imap-client/node_modules/mimelib/node_modules/encoding/node_modules/mime/src/*.js', 'imap-client/node_modules/mailparser/src/*.js', - 'imap-client/node_modules/mailparser/node_modules/mime/src/mime.js', + 'imap-client/node_modules/mime/src/mime.js', 'smtp-client/src/*.js', 'smtp-client/node_modules/mailcomposer/src/*', 'smtp-client/node_modules/nodemailer/src/*', - 'smtp-client/node_modules/nodemailer/node_modules/simplesmtp/src/*', + 'smtp-client/node_modules/nodemailer/node_modules/simplesmtp/src/*' ], dest: 'src/lib/' }, diff --git a/package.json b/package.json index dd1c789..751964b 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ }, "dependencies": { "crypto-lib": "https://github.com/whiteout-io/crypto-lib/tarball/master", - "imap-client": "https://github.com/whiteout-io/imap-client/tarball/master", + "imap-client": "https://github.com/whiteout-io/imap-client/tarball/dev/attachments", "smtp-client": "https://github.com/whiteout-io/smtp-client/tarball/master", "requirejs": "2.1.8" }, diff --git a/src/js/controller/mail-list.js b/src/js/controller/mail-list.js index 073f4b5..855a4ce 100644 --- a/src/js/controller/mail-list.js +++ b/src/js/controller/mail-list.js @@ -277,7 +277,14 @@ define(function(require) { this.to = [{ address: 'max.musterman@gmail.com' }]; // list of receivers - this.attachments = (attachments) ? [true] : undefined; + if (attachments) { + // body structure with three attachments + this.bodystructure = {"1": {"part": "1","type": "text/plain","parameters": {"charset": "us-ascii"},"encoding": "7bit","size": 9,"lines": 2},"2": {"part": "2","type": "application/octet-stream","parameters": {"name": "a.md"},"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.answered = answered; this.html = html; @@ -286,7 +293,7 @@ define(function(require) { this.body = 'Here are a few pointers to help you get started with Whiteout Mail.\n\n# Write encrypted message\n- You can compose a message by clicking on the compose button on the upper right (keyboard shortcut is "n" for a new message or "r" to reply).\n- When typing the recipient\'s email address, secure recipients are marked with a blue label and insecure recipients are red.\n- When sending an email to insecure recipients, the default behavior for Whiteout Mail is to invite them to the service and only send the message content in an encrypted form, once they have joined.\n\n# Advanced features\n- To verify a recipient\'s PGP key, you can hover over the blue label containing their email address and their key fingerprint will be displayed.\n- To view your own key fingerprint, open the account view in the navigation bar on the left. You can compare these with your correspondants over a second channel such as a phonecall.\n\nWe hope this helped you to get started with Whiteout Mail.\n\nYour Whiteout Networks team'; // plaintext body }; - var dummys = [new Email(true, true), new Email(true, false, true, true), new Email(false, true, true), new Email(false), new Email(false), new Email(false), new Email(false), new Email(false), new Email(false), new Email(false), new Email(false), new Email(false), new Email(false), new Email(false), new Email(false), new Email(false), new Email(false), new Email(false)]; + var dummys = [new Email(true, true), new Email(true, false, true, true), new Email(false, true, true), new Email(false, true), new Email(false), new Email(false), new Email(false), new Email(false), new Email(false), new Email(false), new Email(false), new Email(false), new Email(false), new Email(false), new Email(false), new Email(false), new Email(false), new Email(false)]; return dummys; } From c40c6b8f50835198fe9996a0e9d1281c3843d57b Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Thu, 16 Jan 2014 15:37:08 +0100 Subject: [PATCH 02/14] download attachment ui implemented (work in progress) --- src/js/controller/read.js | 37 ++++++++++++++++++++++++++++++++++++- src/js/dao/email-dao.js | 12 ++++++++++++ src/tpl/read.html | 14 +++++++++++++- 3 files changed, 61 insertions(+), 2 deletions(-) diff --git a/src/js/controller/read.js b/src/js/controller/read.js index 90aaac8..e14ce25 100644 --- a/src/js/controller/read.js +++ b/src/js/controller/read.js @@ -2,14 +2,17 @@ define(function(require) { 'use strict'; var appController = require('js/app-controller'), + download = require('js/util/download'), angular = require('angular'), - crypto, keychain; + emailDao, crypto, keychain; // // Controller // var ReadCtrl = function($scope) { + + emailDao = appController._emailDao; crypto = appController._crypto; keychain = appController._keychain; @@ -74,6 +77,38 @@ define(function(require) { $scope.$apply(); }); } + + $scope.download = function(attachment) { + // download file to disk if content is available + if (attachment.content) { + saveToDisk(attachment); + return; + } + + var folder = $scope.state.nav.currentFolder; + var email = $scope.state.mailList.selected; + + emailDao.getAttachment({ + path: folder.path, + uid: email.uid, + attachment: attachment + }, function(err) { + if (err) { + $scope.onError(err); + return; + } + + saveToDisk(attachment); + }); + + function saveToDisk(attachment) { + download.createDownload({ + content: attachment.content, + filename: attachment.filename, + contentType: attachment.mimeType + }, $scope.onError); + } + }; }; // diff --git a/src/js/dao/email-dao.js b/src/js/dao/email-dao.js index fbf2198..6486b60 100644 --- a/src/js/dao/email-dao.js +++ b/src/js/dao/email-dao.js @@ -862,6 +862,18 @@ define(function(require) { }, callback); }; + EmailDAO.prototype.getAttachment = function(options, callback) { + if (!this._account.online) { + callback({ + errMsg: 'Client is currently offline!', + code: 42 + }); + return; + } + + this._imapClient.getAttachment(options, callback); + }; + EmailDAO.prototype.sendEncrypted = function(options, callback) { var self = this, email = options.email; diff --git a/src/tpl/read.html b/src/tpl/read.html index ca2dbf9..1b0f7ff 100644 --- a/src/tpl/read.html +++ b/src/tpl/read.html @@ -21,7 +21,19 @@ -
+
+
+
+ + + +
+
+
+
+
+
+
From 0d1f0000de4edfb79399a2e2d38ba3bef50b2da0 Mon Sep 17 00:00:00 2001 From: Felix Hammerl Date: Sat, 18 Jan 2014 11:42:28 +0100 Subject: [PATCH 03/14] add pgp parsing capability --- src/js/dao/email-dao.js | 27 ++++++-- test/new-unit/email-dao-test.js | 119 ++++++++++++++++++++++++++++++-- 2 files changed, 132 insertions(+), 14 deletions(-) diff --git a/src/js/dao/email-dao.js b/src/js/dao/email-dao.js index 6486b60..1c6b85f 100644 --- a/src/js/dao/email-dao.js +++ b/src/js/dao/email-dao.js @@ -798,7 +798,8 @@ define(function(require) { if (!senderPubkey) { // this should only happen if a mail from another channel is in the inbox - setBodyAndContinue('Public key for sender not found!'); + email.body = 'Public key for sender not found!'; + localCallback(null, email); return; } @@ -810,7 +811,20 @@ define(function(require) { // set encrypted flag email.encrypted = true; - setBodyAndContinue(decrypted); + + // does our message block even need to be parsed? + if (decrypted.indexOf('Content-Type: multipart/signed') === -1) { + // decrypted message is plain text and not a well-formed email + email.body = decrypted; + localCallback(null, email); + return; + } + + // parse decrypted message + self._imapParseMessageBlock({ + message: email, + block: decrypted + }, localCallback); }); }); @@ -821,11 +835,6 @@ define(function(require) { // parse email body for encrypted message block email.body = email.body.substring(start, end); } - - function setBodyAndContinue(text) { - email.body = text; - localCallback(null, email); - } } }; @@ -1074,6 +1083,10 @@ define(function(require) { }, callback); }; + EmailDAO.prototype._imapParseMessageBlock = function(options, callback) { + this._imapClient.parseDecryptedMessageBlock(options, callback); + }; + /** * Get an email messsage including the email body from imap * @param {String} options.messageId The diff --git a/test/new-unit/email-dao-test.js b/test/new-unit/email-dao-test.js index 28bfb33..4609471 100644 --- a/test/new-unit/email-dao-test.js +++ b/test/new-unit/email-dao-test.js @@ -14,7 +14,7 @@ define(function(require) { var dao, keychainStub, imapClientStub, smtpClientStub, pgpStub, devicestorageStub; var emailAddress, passphrase, asymKeySize, mockkeyId, dummyEncryptedMail, - dummyDecryptedMail, mockKeyPair, account, publicKey, verificationMail, verificationUuid, + dummyDecryptedMail, dummyLegacyDecryptedMail, mockKeyPair, account, publicKey, verificationMail, verificationUuid, corruptedVerificationMail, corruptedVerificationUuid, nonWhitelistedMail; @@ -65,6 +65,20 @@ define(function(require) { answered: false }; dummyDecryptedMail = { + uid: 1234, + from: [{ + address: 'asd@asd.de' + }], + to: [{ + 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', + unread: false, + answered: false, + receiverKeys: ['-----BEGIN PGP PUBLIC KEY-----\nasd\n-----END PGP PUBLIC KEY-----'] + }; + dummyLegacyDecryptedMail = { uid: 1234, from: [{ address: 'asd@asd.de' @@ -471,6 +485,19 @@ define(function(require) { }); }); + describe('_imapParseMessageBlock', function() { + it('should parse a message', function(done) { + imapClientStub.parseDecryptedMessageBlock.yields(null, {}); + + dao._imapParseMessageBlock(function(err, msg) { + expect(err).to.not.exist; + expect(msg).to.exist; + done(); + }); + + }); + }); + describe('_imapLogin', function() { it('should fail when disconnected', function(done) { dao.onDisconnect(null, function(err) { @@ -823,7 +850,7 @@ define(function(require) { describe('sync', function() { it('should work initially', function(done) { - var folder, localListStub, invocations, imapSearchStub; + var folder, localListStub, invocations, imapSearchStub, imapParseStub; invocations = 0; folder = 'FOLDAAAA'; @@ -852,6 +879,13 @@ define(function(require) { answered: true }).yields(null, []); + imapParseStub = sinon.stub(dao, '_imapParseMessageBlock'); + imapParseStub.withArgs({ + message: dummyEncryptedMail, + block: dummyDecryptedMail.body + }).yields(null, dummyDecryptedMail); + + dao.sync({ folder: folder }, function(err) { @@ -870,6 +904,7 @@ define(function(require) { expect(pgpStub.decrypt.calledOnce).to.be.true; expect(imapSearchStub.calledThrice).to.be.true; expect(dao._account.folders[0].count).to.equal(1); + expect(imapParseStub.calledOnce).to.be.true; done(); }); @@ -903,7 +938,7 @@ define(function(require) { }); it('should initially sync downstream when storage is empty', function(done) { - var folder, localListStub, localStoreStub, invocations, imapSearchStub, imapGetStub; + var folder, localListStub, localStoreStub, invocations, imapSearchStub, imapGetStub, imapParseStub; invocations = 0; folder = 'FOLDAAAA'; @@ -912,8 +947,8 @@ define(function(require) { path: folder }]; - dummyEncryptedMail.unread = true; - dummyEncryptedMail.answered = true; + dummyDecryptedMail.unread = true; + dummyDecryptedMail.answered = true; localListStub = sinon.stub(dao, '_localListMessages').withArgs({ folder: folder @@ -938,6 +973,9 @@ define(function(require) { answered: true }).yields(null, [dummyEncryptedMail.uid]); + imapParseStub = sinon.stub(dao, '_imapParseMessageBlock'); + imapParseStub.yields(null, dummyDecryptedMail); + localStoreStub = sinon.stub(dao, '_localStoreMessages').yields(); dao.sync({ @@ -960,6 +998,7 @@ define(function(require) { expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true; expect(pgpStub.decrypt.calledOnce).to.be.true; expect(dao._account.folders[0].count).to.equal(1); + expect(imapParseStub.calledOnce).to.be.true; done(); }); @@ -1162,7 +1201,7 @@ define(function(require) { }); }); - it('should error whilte removing messages from local', function(done) { + it('should error while removing messages from local', function(done) { var invocations, folder, localListStub, imapSearchStub, localDeleteStub, imapDeleteStub; invocations = 0; @@ -1316,7 +1355,7 @@ define(function(require) { }); }); - it('should fetch messages downstream from the remote', function(done) { + it('should fetch legacy messages downstream from the remote', function(done) { var invocations, folder, localListStub, imapSearchStub, imapGetStub, localStoreStub; invocations = 0; @@ -1348,10 +1387,75 @@ define(function(require) { uid: dummyEncryptedMail.uid }).yields(null, dummyEncryptedMail); + localStoreStub = sinon.stub(dao, '_localStoreMessages').yields(); + keychainStub.getReceiverPublicKey.withArgs(dummyEncryptedMail.from[0].address).yields(null, mockKeyPair); + pgpStub.decrypt.withArgs(dummyEncryptedMail.body, mockKeyPair.publicKey).yields(null, dummyLegacyDecryptedMail.body); + + + dao.sync({ + folder: folder + }, function(err) { + expect(err).to.not.exist; + + if (invocations === 0) { + expect(dao._account.busy).to.be.true; + invocations++; + return; + } + + expect(dao._account.busy).to.be.false; + expect(dao._account.folders[0].messages).to.not.be.empty; + expect(localListStub.calledOnce).to.be.true; + expect(imapSearchStub.calledThrice).to.be.true; + expect(imapGetStub.calledOnce).to.be.true; + expect(localStoreStub.calledOnce).to.be.true; + expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true; + expect(pgpStub.decrypt.calledOnce).to.be.true; + done(); + }); + }); + + it('should fetch valid pgp messages downstream from the remote', function(done) { + var invocations, folder, localListStub, imapSearchStub, imapGetStub, localStoreStub, imapParseStub; + + invocations = 0; + folder = 'FOLDAAAA'; + dao._account.folders = [{ + type: 'Folder', + path: folder, + messages: [] + }]; + + localListStub = sinon.stub(dao, '_localListMessages').withArgs({ + folder: folder + }).yields(null, []); + imapSearchStub = sinon.stub(dao, '_imapSearch'); + imapSearchStub.withArgs({ + folder: folder + }).yields(null, [dummyEncryptedMail.uid]); + imapSearchStub.withArgs({ + folder: folder, + unread: true + }).yields(null, []); + imapSearchStub.withArgs({ + folder: folder, + answered: true + }).yields(null, []); + + imapGetStub = sinon.stub(dao, '_imapGetMessage').withArgs({ + folder: folder, + uid: dummyEncryptedMail.uid + }).yields(null, dummyEncryptedMail); + localStoreStub = sinon.stub(dao, '_localStoreMessages').yields(); keychainStub.getReceiverPublicKey.withArgs(dummyEncryptedMail.from[0].address).yields(null, mockKeyPair); pgpStub.decrypt.withArgs(dummyEncryptedMail.body, mockKeyPair.publicKey).yields(null, dummyDecryptedMail.body); + imapParseStub = sinon.stub(dao, '_imapParseMessageBlock'); + imapParseStub.withArgs({ + message: dummyEncryptedMail, + block: dummyDecryptedMail.body + }).yields(null, dummyDecryptedMail); dao.sync({ folder: folder @@ -1372,6 +1476,7 @@ define(function(require) { expect(localStoreStub.calledOnce).to.be.true; expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true; expect(pgpStub.decrypt.calledOnce).to.be.true; + expect(imapParseStub.calledOnce).to.be.true; done(); }); }); From b234ec57f564578a062849d3be497d479f5119b5 Mon Sep 17 00:00:00 2001 From: Felix Hammerl Date: Sat, 18 Jan 2014 13:14:41 +0100 Subject: [PATCH 04/14] disarm plain text detection to include unsigned messages --- src/js/dao/email-dao.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/js/dao/email-dao.js b/src/js/dao/email-dao.js index 1c6b85f..7526074 100644 --- a/src/js/dao/email-dao.js +++ b/src/js/dao/email-dao.js @@ -813,7 +813,10 @@ define(function(require) { email.encrypted = true; // does our message block even need to be parsed? - if (decrypted.indexOf('Content-Type: multipart/signed') === -1) { + // this is a very primitive detection if we have a mime node or plain text + // taking this out breaks compatibility to clients < 0.5 + if (decrypted.indexOf('Content-Transfer-Encoding:') === -1 && + decrypted.indexOf('Content-Type:') === -1) { // decrypted message is plain text and not a well-formed email email.body = decrypted; localCallback(null, email); From 8b71a57360ee7f583c6bcbefb2edca41e6bae81c Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Mon, 3 Feb 2014 22:07:39 +0100 Subject: [PATCH 05/14] integrate pgpmailer and remove smtp-client dependency --- Gruntfile.js | 7 ++--- package.json | 2 +- src/js/app-config.js | 2 +- src/js/app-controller.js | 15 ++++++---- src/js/controller/write.js | 2 +- src/js/dao/email-dao.js | 58 ++++++++++++++++++++++---------------- 6 files changed, 48 insertions(+), 38 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index b91ab7b..010714c 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -139,10 +139,9 @@ module.exports = function(grunt) { 'imap-client/node_modules/mimelib/node_modules/encoding/node_modules/iconv-lite/src/*.js', 'imap-client/node_modules/mailparser/src/*.js', 'imap-client/node_modules/mime/src/mime.js', - 'smtp-client/src/*.js', - 'smtp-client/node_modules/mailcomposer/src/*', - 'smtp-client/node_modules/nodemailer/src/*', - 'smtp-client/node_modules/nodemailer/node_modules/simplesmtp/src/*' + 'pgpmailer/src/*.js', + 'pgpmailer/node_modules/simplesmtp/src/*', + 'pgpmailer/node_modules/mailbuilder/src/*.js' ], dest: 'src/lib/' }, diff --git a/package.json b/package.json index 85beac5..1c3c174 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "dependencies": { "crypto-lib": "https://github.com/whiteout-io/crypto-lib/tarball/master", "imap-client": "https://github.com/whiteout-io/imap-client/tarball/dev/attachments", - "smtp-client": "https://github.com/whiteout-io/smtp-client/tarball/master", + "pgpmailer": "https://github.com/whiteout-io/pgpmailer/tarball/master", "requirejs": "2.1.10" }, "devDependencies": { diff --git a/src/js/app-config.js b/src/js/app-config.js index be38b38..7d992df 100644 --- a/src/js/app-config.js +++ b/src/js/app-config.js @@ -54,7 +54,7 @@ define(function(require) { fallbackSubject: '(no subject)', invitationSubject: 'Invitation to a private conversation', invitationMessage: 'I would like to invite you to a private conversation!\n\nIn order to read my encrypted message please install the Whiteout Mail application. This application is used to read and write messages securely with strong encryption applied.\n\nGo to the Whiteout Networks homepage to learn more and to download the application: https://whiteout.io', - message: 'this is a private conversation. To read my encrypted message below, simply open it in Whiteout Mail.\n\nOpen Whiteout Mail: https://chrome.google.com/webstore/detail/jjgghafhamholjigjoghcfcekhkonijg', + message: 'Hi,\n\nthis is a private conversation. To read my encrypted message below, simply open it in Whiteout Mail.\n\nOpen Whiteout Mail: https://chrome.google.com/webstore/detail/jjgghafhamholjigjoghcfcekhkonijg', cryptPrefix: '-----BEGIN PGP MESSAGE-----', cryptSuffix: '-----END PGP MESSAGE-----', signature: 'Sent securely from Whiteout Mail', diff --git a/src/js/app-controller.js b/src/js/app-controller.js index 40f469c..c4940b0 100644 --- a/src/js/app-controller.js +++ b/src/js/app-controller.js @@ -6,7 +6,7 @@ define(function(require) { var $ = require('jquery'), ImapClient = require('imap-client'), - SmtpClient = require('smtp-client'), + PgpMailer = require('pgpmailer'), EmailDAO = require('js/dao/email-dao'), RestDAO = require('js/dao/rest-dao'), PublicKeyDAO = require('js/dao/publickey-dao'), @@ -89,7 +89,7 @@ define(function(require) { }); function initClients(oauth, certificate) { - var auth, imapOptions, imapClient, smtpOptions, smtpClient; + var auth, imapOptions, imapClient, smtpOptions, pgpMailer; auth = { XOAuth2: { @@ -106,15 +106,18 @@ define(function(require) { ca: [certificate] }; smtpOptions = { - secure: config.gmail.smtp.secure, + secureConnection: config.gmail.smtp.secure, port: config.gmail.smtp.port, host: config.gmail.smtp.host, auth: auth, - ca: [certificate] + tls: { + ca: [certificate] + }, + onError: console.error }; imapClient = new ImapClient(imapOptions); - smtpClient = new SmtpClient(smtpOptions); + pgpMailer = new PgpMailer(smtpOptions); imapClient.onError = function(err) { console.log('IMAP error.', err); @@ -126,7 +129,7 @@ define(function(require) { // connect to clients self._emailDao.onConnect({ imapClient: imapClient, - smtpClient: smtpClient + pgpMailer: pgpMailer }, callback); } }; diff --git a/src/js/controller/write.js b/src/js/controller/write.js index a870023..b570f21 100644 --- a/src/js/controller/write.js +++ b/src/js/controller/write.js @@ -79,7 +79,7 @@ define(function(require) { // only display non html mails in reply part if (!re.html) { - body += re.body.split('\n').join('\n> '); + body += re.body.trim().split('\n').join('\n> '); $scope.body = body; } } diff --git a/src/js/dao/email-dao.js b/src/js/dao/email-dao.js index 04c1def..f673f81 100644 --- a/src/js/dao/email-dao.js +++ b/src/js/dao/email-dao.js @@ -73,7 +73,11 @@ define(function(require) { var self = this; self._imapClient = options.imapClient; - self._smtpClient = options.smtpClient; + self._pgpMailer = options.pgpMailer; + // set private key + if (self._crypto && self._crypto._privateKey) { + self._pgpMailer._privateKey = self._crypto._privateKey; + } // delegation-esque pattern to mitigate between node-style events and plain js self._imapClient.onIncomingMessage = function(message) { @@ -116,7 +120,7 @@ define(function(require) { // set status to online this._account.online = false; this._imapClient = undefined; - this._smtpClient = undefined; + self._pgpMailer = undefined; callback(); }; @@ -131,6 +135,8 @@ define(function(require) { privateKeyArmored: options.keypair.privateKey.encryptedKey, publicKeyArmored: options.keypair.publicKey.publicKey }, callback); + // set decrypted privateKey to pgpMailer + self._pgpMailer._privateKey = self._crypto._privateKey; return; } @@ -913,17 +919,26 @@ define(function(require) { return; } - // public key found... encrypt and send - self._encrypt({ - email: email, - keys: email.receiverKeys // this Array is set in writer controller - }, function(err, email) { + // get own public key so send message can be read + self._crypto.exportKeys(function(err, ownKeys) { if (err) { callback(err); return; } - self._smtpClient.send(email, callback); + // add own public key to receiver list + email.receiverKeys.push(ownKeys.publicKeyArmored); + + // add whiteout tag to subject + email.subject = str.subjectPrefix + email.subject; + + // mime encode, sign, encrypt and send email via smtp + self._pgpMailer.send({ + encrypt: true, + cleartextMessage: str.message + '\n\n' + str.signature + '\n\n', + mail: email, + publicKeysArmored: email.receiverKeys + }, callback); }); }; @@ -936,7 +951,13 @@ define(function(require) { return; } - this._smtpClient.send(options.email, callback); + // add whiteout tag to subject + options.email.subject = str.subjectPrefix + options.email.subject; + + // mime encode, sign and send email via smtp + this._pgpMailer.send({ + mail: options.email + }, callback); }; // @@ -967,23 +988,12 @@ define(function(require) { return; } - // bundle encrypted email together for sending - frameEncryptedMessage(options.email, ct); + // replace plaintext body with pgp message + options.email.body = ct; + callback(null, options.email); }); }); - - function frameEncryptedMessage(email, ct) { - var greeting, - message = str.message + '\n\n\n', - signature = '\n\n' + str.signature + '\n\n'; - - greeting = 'Hi,\n\n'; - - // build encrypted text body - email.body = greeting + message + ct + signature; - email.subject = str.subjectPrefix + email.subject; - } }; // Local Storage API @@ -1237,8 +1247,6 @@ define(function(require) { }); mails.forEach(function(mail) { - mail.body = str.cryptPrefix + mail.body.split(str.cryptPrefix)[1].split(str.cryptSuffix)[0] + str.cryptSuffix; - mail.subject = mail.subject.split(str.subjectPrefix)[1]; self._crypto.decrypt(mail.body, ownKeys.publicKeyArmored, function(err, decrypted) { mail.body = err ? err.errMsg : decrypted; after(); From 1237fe684acbb89df925f4d8595aeb5bd35ac3da Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Tue, 4 Feb 2014 18:58:41 +0100 Subject: [PATCH 06/14] remove sent securely by whiteout mail in encrypted message --- src/js/dao/email-dao.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/dao/email-dao.js b/src/js/dao/email-dao.js index f673f81..c713b05 100644 --- a/src/js/dao/email-dao.js +++ b/src/js/dao/email-dao.js @@ -935,7 +935,7 @@ define(function(require) { // mime encode, sign, encrypt and send email via smtp self._pgpMailer.send({ encrypt: true, - cleartextMessage: str.message + '\n\n' + str.signature + '\n\n', + cleartextMessage: str.message + '\n\n', mail: email, publicKeysArmored: email.receiverKeys }, callback); From bb5b63558e6f7e6f610460b4bad192ad8316d38b Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Tue, 4 Feb 2014 21:04:48 +0100 Subject: [PATCH 07/14] add file input and filereader logic to writer --- src/js/controller/write.js | 29 +++++++++++++++++++++++++++++ src/tpl/write.html | 4 ++++ 2 files changed, 33 insertions(+) diff --git a/src/js/controller/write.js b/src/js/controller/write.js index b570f21..d5b6eac 100644 --- a/src/js/controller/write.js +++ b/src/js/controller/write.js @@ -56,6 +56,7 @@ define(function(require) { $scope.subject = ''; $scope.body = ''; $scope.ciphertextPreview = ''; + $scope.attachments = []; } function fillFields(re) { @@ -239,6 +240,11 @@ define(function(require) { }); } + // add attachment to email object + if ($scope.attachments.length > 0) { + email.attachments = $scope.attachments; + } + // persist the email locally for later smtp transmission emailDao.store(email, function(err) { if (err) { @@ -440,5 +446,28 @@ define(function(require) { }; }); + ngModule.directive('attachment', function() { + return function(scope, elm) { + elm.bind('change', function(e) { + for (var i = 0; i < e.target.files.length; i++) { + addAttachment(e.target.files.item(i)); + } + }); + + function addAttachment(file) { + var reader = new FileReader(); + reader.onload = function(e) { + scope.attachments.push({ + fileName: file.name, + contentType: file.type, + uint8Array: new Uint8Array(e.target.result) + }); + scope.$apply(); + }; + reader.readAsArrayBuffer(file); + } + }; + }); + return WriteCtrl; }); \ No newline at end of file diff --git a/src/tpl/write.html b/src/tpl/write.html index 0ea80a1..ff06bdc 100644 --- a/src/tpl/write.html +++ b/src/tpl/write.html @@ -30,6 +30,10 @@
+
+ +
+

From 2709b42c22e4caee61737cac8a86ce328ed22a9e Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Thu, 6 Feb 2014 00:34:31 +0100 Subject: [PATCH 08/14] cleanup message strings --- src/js/app-config.js | 2 +- src/js/dao/email-dao.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/js/app-config.js b/src/js/app-config.js index 7d992df..8c66a00 100644 --- a/src/js/app-config.js +++ b/src/js/app-config.js @@ -54,7 +54,7 @@ define(function(require) { fallbackSubject: '(no subject)', invitationSubject: 'Invitation to a private conversation', invitationMessage: 'I would like to invite you to a private conversation!\n\nIn order to read my encrypted message please install the Whiteout Mail application. This application is used to read and write messages securely with strong encryption applied.\n\nGo to the Whiteout Networks homepage to learn more and to download the application: https://whiteout.io', - message: 'Hi,\n\nthis is a private conversation. To read my encrypted message below, simply open it in Whiteout Mail.\n\nOpen Whiteout Mail: https://chrome.google.com/webstore/detail/jjgghafhamholjigjoghcfcekhkonijg', + message: 'Hi,\n\nthis is a private conversation. To read my encrypted message below, simply open it in Whiteout Mail.\n\nOpen Whiteout Mail: https://chrome.google.com/webstore/detail/jjgghafhamholjigjoghcfcekhkonijg\n\n', cryptPrefix: '-----BEGIN PGP MESSAGE-----', cryptSuffix: '-----END PGP MESSAGE-----', signature: 'Sent securely from Whiteout Mail', diff --git a/src/js/dao/email-dao.js b/src/js/dao/email-dao.js index c713b05..640429b 100644 --- a/src/js/dao/email-dao.js +++ b/src/js/dao/email-dao.js @@ -935,7 +935,7 @@ define(function(require) { // mime encode, sign, encrypt and send email via smtp self._pgpMailer.send({ encrypt: true, - cleartextMessage: str.message + '\n\n', + cleartextMessage: str.message, mail: email, publicKeysArmored: email.receiverKeys }, callback); From 651401735891dff462f4a471c25c7e55c2edb878 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Thu, 6 Feb 2014 00:41:08 +0100 Subject: [PATCH 09/14] implement attachment ui for writer --- src/js/controller/write.js | 20 +++++++++-- src/sass/views/_write.scss | 73 ++++++++++++++++++++++++++++++++++---- src/tpl/write.html | 13 ++++--- 3 files changed, 93 insertions(+), 13 deletions(-) diff --git a/src/js/controller/write.js b/src/js/controller/write.js index d5b6eac..bd754e8 100644 --- a/src/js/controller/write.js +++ b/src/js/controller/write.js @@ -183,6 +183,14 @@ define(function(require) { } }; + // + // Editing attachments + // + + $scope.remove = function(attachment) { + $scope.attachments.splice($scope.attachments.indexOf(attachment), 1); + }; + // // Editing email body // @@ -446,9 +454,9 @@ define(function(require) { }; }); - ngModule.directive('attachment', function() { + ngModule.directive('attachmentInput', function() { return function(scope, elm) { - elm.bind('change', function(e) { + elm.on('change', function(e) { for (var i = 0; i < e.target.files.length; i++) { addAttachment(e.target.files.item(i)); } @@ -469,5 +477,13 @@ define(function(require) { }; }); + ngModule.directive('attachmentBtn', function() { + return function(scope, elm) { + elm.on('click', function() { + document.querySelector('#attachment-input').click(); + }); + }; + }); + return WriteCtrl; }); \ No newline at end of file diff --git a/src/sass/views/_write.scss b/src/sass/views/_write.scss index b373585..cb3a02e 100644 --- a/src/sass/views/_write.scss +++ b/src/sass/views/_write.scss @@ -38,7 +38,7 @@ } .subject-box { - margin: 20px 0; + margin: 20px 0 7px 0; width: inherit; border: 1px; border-style: solid; @@ -56,17 +56,76 @@ font-weight: bold; width: 100%; } + } + + .attachments-box { + position: relative; + margin: 0 0 5px 0; + width: inherit; + border: 1px; + border-style: solid; + border-color: $color-grey-lighter; + min-height: em(44); + + .attachment { + line-height: 41px; + border-radius: 15px; + vertical-align: middle; + margin: 5px 0 5px 5px; + padding: 5px 5px 5px 10px; + border: 1px; + border-style: solid; + border-color: $color-grey-lighter; + cursor: default; + + span { + font-size: 14px; + color: $color-grey-input; + vertical-align: middle; + } + + .close { + margin-left: 5px; + + &:hover, + &:focus { + color: darken($color-grey, 10%); + cursor: pointer; + } + } + + &:hover, + &:focus { + background-color: darken($color-white, 2%); + } + } + + input[type=file] { + visibility: hidden; + width: 0; + height: 0; + } .btn-attachment { - float: right; + position: absolute; + top: 0; + right: 0; padding: em(7.5) em(7.5) em(4) em(7.5); margin: em(5); outline: none; - //color: $color-grey-lightest; - //background-color: $color-blue; - color: $btn-disabled-color; - background-color: $btn-disabled-back-color; - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAYAAADgzO9IAAAAJUlEQVQIW2NkQABJIPM5lCvJCGMgC4LYIAkUlTAFMB0gjRQaBQCw8go5lVnl5wAAAABJRU5ErkJggg==); + color: $color-grey-lightest; + background-color: $color-blue; + + &:hover, + &:focus { + background-color: lighten($color-blue, 10%); + } + + &:active, + &.active { + top: 1px; + right: -1px; + } } } diff --git a/src/tpl/write.html b/src/tpl/write.html index ff06bdc..f573998 100644 --- a/src/tpl/write.html +++ b/src/tpl/write.html @@ -25,13 +25,18 @@
-
- + + + {{attachment.fileName}} + + + +
From 34f0551d6fa5187a494c3f27973a31da9a75c103 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Thu, 6 Feb 2014 01:04:50 +0100 Subject: [PATCH 10/14] implement attachment read css --- src/sass/views/_read.scss | 36 ++++++++++++++++++++++++++++++++++-- src/tpl/read.html | 11 ++++++----- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/src/sass/views/_read.scss b/src/sass/views/_read.scss index 1ca564f..8146069 100644 --- a/src/sass/views/_read.scss +++ b/src/sass/views/_read.scss @@ -5,6 +5,8 @@ color: $color-grey-dark; .headers { + margin-bottom: 1em; + p { margin: 0px; padding: 0px; @@ -36,15 +38,45 @@ } } + .attachments { + width: inherit; + border: 1px; + border-style: solid; + border-color: $color-grey-lighter; + min-height: em(44); + + .attachment { + line-height: 42px; + border-radius: 15px; + vertical-align: middle; + margin: 5px 0 5px 5px; + padding: 5px 10px 5px 10px; + border: 1px; + border-style: solid; + border-color: $color-grey-lighter; + + span { + font-size: 14px; + color: $color-grey-input; + vertical-align: middle; + } + + &:hover, + &:focus { + background-color: darken($color-white, 3%); + cursor: pointer; + } + } + } + .seperator-line { height: 1px; color: $color-grey-lighter; background-color: $color-grey-lighter; - margin-top: 1em; - margin-bottom: 1.75em; } .body { + margin-top: 1.75em; cursor: text; padding-bottom: 200px; line-height: 1.5em; diff --git a/src/tpl/read.html b/src/tpl/read.html index 1b0f7ff..2f0184a 100644 --- a/src/tpl/read.html +++ b/src/tpl/read.html @@ -24,15 +24,16 @@
- - - -
+ + + {{attachment.filename}} + +
-
+
From 20f36285b6789c79be33ee3572c18a08e2f95034 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Thu, 6 Feb 2014 09:13:31 +0100 Subject: [PATCH 11/14] fix overflow of multiple attachments --- src/sass/views/_read.scss | 6 ++- src/sass/views/_write.scss | 88 +++++++++++++++++++------------------- src/tpl/write.html | 26 ++++++----- 3 files changed, 64 insertions(+), 56 deletions(-) diff --git a/src/sass/views/_read.scss b/src/sass/views/_read.scss index 8146069..1e387fb 100644 --- a/src/sass/views/_read.scss +++ b/src/sass/views/_read.scss @@ -39,6 +39,7 @@ } .attachments { + position: relative; width: inherit; border: 1px; border-style: solid; @@ -46,14 +47,15 @@ min-height: em(44); .attachment { - line-height: 42px; - border-radius: 15px; + height: 32px; + border-radius: 16px; vertical-align: middle; margin: 5px 0 5px 5px; padding: 5px 10px 5px 10px; border: 1px; border-style: solid; border-color: $color-grey-lighter; + display: inline-block; span { font-size: 14px; diff --git a/src/sass/views/_write.scss b/src/sass/views/_write.scss index cb3a02e..6c5ad96 100644 --- a/src/sass/views/_write.scss +++ b/src/sass/views/_write.scss @@ -38,6 +38,7 @@ } .subject-box { + position: relative; margin: 20px 0 7px 0; width: inherit; border: 1px; @@ -56,49 +57,6 @@ font-weight: bold; width: 100%; } - } - - .attachments-box { - position: relative; - margin: 0 0 5px 0; - width: inherit; - border: 1px; - border-style: solid; - border-color: $color-grey-lighter; - min-height: em(44); - - .attachment { - line-height: 41px; - border-radius: 15px; - vertical-align: middle; - margin: 5px 0 5px 5px; - padding: 5px 5px 5px 10px; - border: 1px; - border-style: solid; - border-color: $color-grey-lighter; - cursor: default; - - span { - font-size: 14px; - color: $color-grey-input; - vertical-align: middle; - } - - .close { - margin-left: 5px; - - &:hover, - &:focus { - color: darken($color-grey, 10%); - cursor: pointer; - } - } - - &:hover, - &:focus { - background-color: darken($color-white, 2%); - } - } input[type=file] { visibility: hidden; @@ -129,6 +87,50 @@ } } + .attachments-box { + position: relative; + margin: 0 0 5px 0; + width: inherit; + border: 1px; + border-style: solid; + border-color: $color-grey-lighter; + min-height: em(44); + + .attachment { + height: 32px; + border-radius: 16px; + vertical-align: middle; + margin: 5px 0 5px 5px; + padding: 5px 5px 5px 10px; + border: 1px; + border-style: solid; + border-color: $color-grey-lighter; + cursor: default; + display: inline-block; + + span { + font-size: 14px; + color: $color-grey-input; + vertical-align: middle; + } + + .close { + margin-left: 5px; + + &:hover, + &:focus { + color: darken($color-grey, 10%); + cursor: pointer; + } + } + + &:hover, + &:focus { + background-color: darken($color-white, 2%); + } + } + } + .body { line-height: 1.5em; cursor: text; diff --git a/src/tpl/write.html b/src/tpl/write.html index f573998..8c0270e 100644 --- a/src/tpl/write.html +++ b/src/tpl/write.html @@ -24,20 +24,24 @@
-
-
- -
- - - {{attachment.fileName}} - - +
-
+ + + +
+
+
+ + + {{attachment.fileName}} + + +
+
+

From 6cd57fa0f627f739f5ca7f8ddba4f5f2c44034a4 Mon Sep 17 00:00:00 2001 From: Felix Hammerl Date: Thu, 6 Feb 2014 11:55:24 +0100 Subject: [PATCH 12/14] fix tests --- package.json | 2 +- src/js/bo/outbox.js | 2 +- src/js/controller/write.js | 2 +- src/js/dao/email-dao.js | 86 +++---- test/new-unit/add-account-ctrl-test.js | 11 +- test/new-unit/email-dao-test.js | 306 +++++++++++++++++-------- test/new-unit/outbox-bo-test.js | 8 +- test/new-unit/write-ctrl-test.js | 16 +- 8 files changed, 278 insertions(+), 155 deletions(-) diff --git a/package.json b/package.json index 1c3c174..bd25924 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "dependencies": { "crypto-lib": "https://github.com/whiteout-io/crypto-lib/tarball/master", "imap-client": "https://github.com/whiteout-io/imap-client/tarball/dev/attachments", - "pgpmailer": "https://github.com/whiteout-io/pgpmailer/tarball/master", + "pgpmailer": "https://github.com/whiteout-io/pgpmailer/tarball/dev/attachments", "requirejs": "2.1.10" }, "devDependencies": { diff --git a/src/js/bo/outbox.js b/src/js/bo/outbox.js index 6fdae00..3a0d054 100644 --- a/src/js/bo/outbox.js +++ b/src/js/bo/outbox.js @@ -96,7 +96,7 @@ define(function(require) { self._outboxBusy = true; // get last item from outbox - self._emailDao.list(function(err, pending) { + self._emailDao.listForOutbox(function(err, pending) { if (err) { self._outboxBusy = false; callback(err); diff --git a/src/js/controller/write.js b/src/js/controller/write.js index bd754e8..4a5f84b 100644 --- a/src/js/controller/write.js +++ b/src/js/controller/write.js @@ -254,7 +254,7 @@ define(function(require) { } // persist the email locally for later smtp transmission - emailDao.store(email, function(err) { + emailDao.storeForOutbox(email, function(err) { if (err) { $scope.onError(err); return; diff --git a/src/js/dao/email-dao.js b/src/js/dao/email-dao.js index 640429b..141b239 100644 --- a/src/js/dao/email-dao.js +++ b/src/js/dao/email-dao.js @@ -86,6 +86,9 @@ define(function(require) { } }; + // connect the pgpmailer + self._pgpmailerLogin(); + // connect to newly created imap client self._imapLogin(function(err) { if (err) { @@ -964,38 +967,6 @@ define(function(require) { // Internal API // - // Encryption API - - EmailDAO.prototype._encrypt = function(options, callback) { - var self = this, - pt = options.email.body; - - options.keys = options.keys || []; - - // get own public key so send message can be read - self._crypto.exportKeys(function(err, ownKeys) { - if (err) { - callback(err); - return; - } - - // add own public key to receiver list - options.keys.push(ownKeys.publicKeyArmored); - // encrypt the email - self._crypto.encrypt(pt, options.keys, function(err, ct) { - if (err) { - callback(err); - return; - } - - // replace plaintext body with pgp message - options.email.body = ct; - - callback(null, options.email); - }); - }); - }; - // Local Storage API EmailDAO.prototype._localListMessages = function(options, callback) { @@ -1023,6 +994,16 @@ define(function(require) { }; + // PGP Mailer API + + /** + * Login the smtp client + */ + EmailDAO.prototype._pgpmailerLogin = function() { + this._pgpMailer.login(); + }; + + // IMAP API /** @@ -1199,29 +1180,48 @@ define(function(require) { } }; - // to be removed and solved with IMAP! - EmailDAO.prototype.store = function(email, callback) { + /** + * Persists an email object for the outbox, encrypted with the user's public key + * @param {Object} email The email object + * @param {Function} callback(error) Invoked when the email was encrypted and persisted, contains information in case of an error + */ + EmailDAO.prototype.storeForOutbox = function(email, callback) { var self = this, - dbType = 'email_OUTBOX'; + dbType = 'email_OUTBOX', + plaintext = email.body; + // give the email a random identifier (used for storage) email.id = util.UUID(); - // encrypt - self._encrypt({ - email: email - }, function(err, email) { + // get own public key so send message can be read + self._crypto.exportKeys(function(err, ownKeys) { if (err) { callback(err); return; } - // store to local storage - self._devicestorage.storeList([email], dbType, callback); + // encrypt the email with the user's public key + self._crypto.encrypt(plaintext, [ownKeys.publicKeyArmored], function(err, ciphertext) { + if (err) { + callback(err); + return; + } + + // replace plaintext body with pgp message + email.body = ciphertext; + + // store to local storage + self._devicestorage.storeList([email], dbType, callback); + + }); }); }; - // to be removed and solved with IMAP! - EmailDAO.prototype.list = function(callback) { + /** + * Reads and decrypts persisted email objects for the outbox + * @param {Function} callback(error, emails) Invoked when the email was encrypted and persisted, contains information in case of an error + */ + EmailDAO.prototype.listForOutbox = function(callback) { var self = this, dbType = 'email_OUTBOX'; diff --git a/test/new-unit/add-account-ctrl-test.js b/test/new-unit/add-account-ctrl-test.js index 6e406b3..3f2ce4f 100644 --- a/test/new-unit/add-account-ctrl-test.js +++ b/test/new-unit/add-account-ctrl-test.js @@ -50,13 +50,20 @@ define(function(require) { mocks.module('addaccounttest'); mocks.inject(function($controller, $rootScope, $location) { location = $location; + scope = $rootScope.$new(); + scope.state = {}; + sinon.stub(location, 'path', function(path) { expect(path).to.equal('/login'); expect(fetchOAuthTokenStub.calledOnce).to.be.true; + + location.path.restore(); + scope.$apply.restore(); done(); }); - scope = $rootScope.$new(); - scope.state = {}; + + sinon.stub(scope, '$apply', function() {}); + ctrl = $controller(AddAccountCtrl, { $location: location, $scope: scope diff --git a/test/new-unit/email-dao-test.js b/test/new-unit/email-dao-test.js index 4609471..b4ebd94 100644 --- a/test/new-unit/email-dao-test.js +++ b/test/new-unit/email-dao-test.js @@ -4,14 +4,15 @@ define(function(require) { var EmailDAO = require('js/dao/email-dao'), KeychainDAO = require('js/dao/keychain-dao'), ImapClient = require('imap-client'), - SmtpClient = require('smtp-client'), + PgpMailer = require('pgpmailer'), PGP = require('js/crypto/pgp'), DeviceStorageDAO = require('js/dao/devicestorage-dao'), + str = require('js/app-config').string, expect = chai.expect; describe('Email DAO unit tests', function() { - var dao, keychainStub, imapClientStub, smtpClientStub, pgpStub, devicestorageStub; + var dao, keychainStub, imapClientStub, pgpMailerStub, pgpStub, devicestorageStub; var emailAddress, passphrase, asymKeySize, mockkeyId, dummyEncryptedMail, dummyDecryptedMail, dummyLegacyDecryptedMail, mockKeyPair, account, publicKey, verificationMail, verificationUuid, @@ -124,7 +125,7 @@ define(function(require) { keychainStub = sinon.createStubInstance(KeychainDAO); imapClientStub = sinon.createStubInstance(ImapClient); - smtpClientStub = sinon.createStubInstance(SmtpClient); + pgpMailerStub = sinon.createStubInstance(PgpMailer); pgpStub = sinon.createStubInstance(PGP); devicestorageStub = sinon.createStubInstance(DeviceStorageDAO); @@ -144,7 +145,7 @@ define(function(require) { dao.onConnect({ imapClient: imapClientStub, - smtpClient: smtpClientStub + pgpMailer: pgpMailerStub }, function(err) { expect(err).to.not.exist; expect(dao._account.online).to.be.true; @@ -289,11 +290,12 @@ define(function(require) { }); describe('onConnect', function() { - var imapLoginStub, imapListFoldersStub; + var imapLoginStub, imapListFoldersStub, pgpmailerLoginStub; beforeEach(function(done) { // imap login imapLoginStub = sinon.stub(dao, '_imapLogin'); + pgpmailerLoginStub = sinon.stub(dao, '_pgpmailerLogin'); imapListFoldersStub = sinon.stub(dao, '_imapListFolders'); dao.onDisconnect(null, function(err) { @@ -307,17 +309,20 @@ define(function(require) { afterEach(function() { imapLoginStub.restore(); + pgpmailerLoginStub.restore(); imapListFoldersStub.restore(); }); it('should fail due to error in imap login', function(done) { imapLoginStub.yields({}); + pgpmailerLoginStub.returns(); dao.onConnect({ imapClient: imapClientStub, - smtpClient: smtpClientStub + pgpMailer: pgpMailerStub }, function(err) { expect(err).to.exist; + expect(pgpmailerLoginStub.calledOnce).to.be.true; expect(imapLoginStub.calledOnce).to.be.true; expect(dao._account.online).to.be.false; done(); @@ -327,10 +332,11 @@ define(function(require) { it('should work when folder already initiated', function(done) { dao._account.folders = []; imapLoginStub.yields(); + pgpmailerLoginStub.returns(); dao.onConnect({ imapClient: imapClientStub, - smtpClient: smtpClientStub + pgpMailer: pgpMailerStub }, function(err) { expect(err).to.not.exist; expect(dao._account.online).to.be.true; @@ -343,11 +349,12 @@ define(function(require) { it('should work when folder not yet initiated', function(done) { var folders = []; imapLoginStub.yields(); + pgpmailerLoginStub.returns(); imapListFoldersStub.yields(null, folders); dao.onConnect({ imapClient: imapClientStub, - smtpClient: smtpClientStub + pgpMailer: pgpMailerStub }, function(err) { expect(err).to.not.exist; expect(dao._account.online).to.be.true; @@ -498,6 +505,20 @@ define(function(require) { }); }); + describe('_pgpmailerLogin', function() { + it('should work', function() { + // called once in onConnect + expect(pgpMailerStub.login.calledOnce).to.be.true; + + pgpMailerStub.login.returns(); + + dao._pgpmailerLogin(); + + expect(pgpMailerStub.login.calledTwice).to.be.true; + }); + }); + + describe('_imapLogin', function() { it('should fail when disconnected', function(done) { dao.onDisconnect(null, function(err) { @@ -2459,13 +2480,27 @@ define(function(require) { describe('sendPlaintext', function() { it('should work', function(done) { - smtpClientStub.send.withArgs(dummyEncryptedMail).yields(); + pgpMailerStub.send.withArgs({ + mail: dummyEncryptedMail + }).yields(); dao.sendPlaintext({ email: dummyEncryptedMail }, function(err) { expect(err).to.not.exist; - expect(smtpClientStub.send.calledOnce).to.be.true; + expect(pgpMailerStub.send.calledOnce).to.be.true; + done(); + }); + }); + + it('should not work when pgpmailer fails', function(done) { + pgpMailerStub.send.yields({}); + + dao.sendPlaintext({ + email: dummyEncryptedMail + }, function(err) { + expect(err).to.exist; + expect(pgpMailerStub.send.calledOnce).to.be.true; done(); }); }); @@ -2473,101 +2508,76 @@ define(function(require) { describe('sendEncrypted', function() { it('should work', function(done) { - var encryptStub = sinon.stub(dao, '_encrypt').yields(null, {}); - - smtpClientStub.send.yields(); - - dao.sendEncrypted({ - email: dummyDecryptedMail - }, function(err) { - expect(err).to.not.exist; - - expect(encryptStub.calledOnce).to.be.true; - expect(smtpClientStub.send.calledOnce).to.be.true; - - done(); - }); - }); - it('should not work when encryption fails', function(done) { - var encryptStub = sinon.stub(dao, '_encrypt').yields({}); - - dao.sendEncrypted({ - email: dummyDecryptedMail - }, function(err) { - expect(err).to.exist; - - expect(encryptStub.calledOnce).to.be.true; - expect(smtpClientStub.send.called).to.be.false; - - done(); - }); - }); - it('should not work without recipients', function(done) { - var encryptStub = sinon.stub(dao, '_encrypt'); - delete dummyDecryptedMail.to; - - dao.sendEncrypted({ - email: dummyDecryptedMail - }, function(err) { - expect(err).to.exist; - - expect(encryptStub.called).to.be.false; - expect(smtpClientStub.send.called).to.be.false; - - done(); - }); - }); - it('should not work with without sender', function(done) { - var encryptStub = sinon.stub(dao, '_encrypt'); - delete dummyDecryptedMail.from; - - dao.sendEncrypted({ - email: dummyDecryptedMail - }, function(err) { - expect(err).to.exist; - - expect(encryptStub.called).to.be.false; - expect(smtpClientStub.send.called).to.be.false; - - done(); - }); - }); - }); - - describe('_encrypt', function() { - it('should work without attachments', function(done) { - var ct = 'OMGSOENCRYPTED'; - pgpStub.exportKeys.yields(null, { privateKeyArmored: mockKeyPair.privateKey.encryptedKey, publicKeyArmored: mockKeyPair.publicKey.publicKey }); - pgpStub.encrypt.yields(null, ct); - dao._encrypt({ + pgpMailerStub.send.withArgs({ + encrypt: true, + cleartextMessage: str.message, + mail: dummyDecryptedMail, + publicKeysArmored: dummyDecryptedMail.receiverKeys + }).yields(); + + dao.sendEncrypted({ email: dummyDecryptedMail }, function(err) { expect(err).to.not.exist; expect(pgpStub.exportKeys.calledOnce).to.be.true; - expect(pgpStub.encrypt.calledOnce).to.be.true; - expect(dummyDecryptedMail.body).to.contain(ct); + expect(pgpMailerStub.send.calledOnce).to.be.true; + + done(); + }); + }); + it('should not work when pgpmailer fails', function(done) { + pgpStub.exportKeys.yields(null, { + privateKeyArmored: mockKeyPair.privateKey.encryptedKey, + publicKeyArmored: mockKeyPair.publicKey.publicKey + }); + pgpMailerStub.send.yields({}); + + dao.sendEncrypted({ + email: dummyDecryptedMail + }, function(err) { + expect(err).to.exist; + + expect(pgpStub.exportKeys.calledOnce).to.be.true; + expect(pgpMailerStub.send.calledOnce).to.be.true; + + done(); + }); + }); + it('should not work when sender key export fails', function(done) { + pgpStub.exportKeys.yields({}); + + dao.sendEncrypted({ + email: dummyDecryptedMail + }, function(err) { + expect(err).to.exist; + + expect(pgpStub.exportKeys.calledOnce).to.be.true; + expect(pgpMailerStub.send.calledOnce).to.be.false; done(); }); }); }); - describe('store', function() { + describe('storeForOutbox', function() { it('should work', function(done) { - pgpStub.exportKeys.yields(null, { - publicKeyArmored: 'omgsocrypto' - }); - pgpStub.encrypt.yields(null, 'asdfasfd'); - devicestorageStub.storeList.yields(); + var key = 'omgsocrypto'; - dao.store(dummyDecryptedMail, function(err) { + pgpStub.exportKeys.yields(null, { + publicKeyArmored: key + }); + pgpStub.encrypt.withArgs(dummyDecryptedMail.body, [key]).yields(null, 'asdfasfd'); + devicestorageStub.storeList.withArgs([dummyDecryptedMail], 'email_OUTBOX').yields(); + + dao.storeForOutbox(dummyDecryptedMail, function(err) { expect(err).to.not.exist; + expect(pgpStub.exportKeys.calledOnce).to.be.true; expect(pgpStub.encrypt.calledOnce).to.be.true; expect(devicestorageStub.storeList.calledOnce).to.be.true; @@ -2575,17 +2585,72 @@ define(function(require) { done(); }); }); + + it('should work when store fails', function(done) { + var key = 'omgsocrypto'; + + pgpStub.exportKeys.yields(null, { + publicKeyArmored: key + }); + pgpStub.encrypt.yields(null, 'asdfasfd'); + devicestorageStub.storeList.yields({}); + + dao.storeForOutbox(dummyDecryptedMail, function(err) { + expect(err).to.exist; + + expect(pgpStub.exportKeys.calledOnce).to.be.true; + expect(pgpStub.encrypt.calledOnce).to.be.true; + expect(devicestorageStub.storeList.calledOnce).to.be.true; + + done(); + }); + }); + + it('should work when encryption fails', function(done) { + var key = 'omgsocrypto'; + + pgpStub.exportKeys.yields(null, { + publicKeyArmored: key + }); + pgpStub.encrypt.yields({}); + + dao.storeForOutbox(dummyDecryptedMail, function(err) { + expect(err).to.exist; + + expect(pgpStub.exportKeys.calledOnce).to.be.true; + expect(pgpStub.encrypt.calledOnce).to.be.true; + expect(devicestorageStub.storeList.called).to.be.false; + + done(); + }); + }); + + it('should work when key export fails', function(done) { + pgpStub.exportKeys.yields({}); + + dao.storeForOutbox(dummyDecryptedMail, function(err) { + expect(err).to.exist; + + expect(pgpStub.exportKeys.calledOnce).to.be.true; + expect(pgpStub.encrypt.called).to.be.false; + expect(devicestorageStub.storeList.called).to.be.false; + + done(); + }); + }); }); - describe('list', function() { + describe('listForOutbox', function() { it('should work', function(done) { - devicestorageStub.listItems.yields(null, [dummyEncryptedMail]); - pgpStub.exportKeys.yields(null, { - publicKeyArmored: 'omgsocrypto' - }); - pgpStub.decrypt.yields(null, dummyDecryptedMail.body); + var key = 'omgsocrypto'; - dao.list(function(err, mails) { + devicestorageStub.listItems.withArgs('email_OUTBOX', 0, null).yields(null, [dummyEncryptedMail]); + pgpStub.exportKeys.yields(null, { + publicKeyArmored: key + }); + pgpStub.decrypt.withArgs(dummyEncryptedMail.body, key).yields(null, dummyDecryptedMail.body); + + dao.listForOutbox(function(err, mails) { expect(err).to.not.exist; expect(devicestorageStub.listItems.calledOnce).to.be.true; @@ -2593,12 +2658,63 @@ define(function(require) { expect(pgpStub.decrypt.calledOnce).to.be.true; expect(mails.length).to.equal(1); expect(mails[0].body).to.equal(dummyDecryptedMail.body); - expect(mails[0].subject).to.equal(dummyDecryptedMail.subject); + + done(); + }); + }); + + it('should not work when decryption fails', function(done) { + var key = 'omgsocrypto', + errMsg = 'THIS IS AN ERROR!'; + + devicestorageStub.listItems.yields(null, [dummyEncryptedMail]); + pgpStub.exportKeys.yields(null, { + publicKeyArmored: key + }); + pgpStub.decrypt.yields({errMsg: errMsg}); + + dao.listForOutbox(function(err, mails) { + expect(err).to.not.exist; + expect(mails[0].body).to.equal(errMsg); + + expect(devicestorageStub.listItems.calledOnce).to.be.true; + expect(pgpStub.exportKeys.calledOnce).to.be.true; + expect(pgpStub.decrypt.calledOnce).to.be.true; + + done(); + }); + }); + + it('should not work when key export fails', function(done) { + devicestorageStub.listItems.yields(null, [dummyEncryptedMail]); + pgpStub.exportKeys.yields({}); + + dao.listForOutbox(function(err, mails) { + expect(err).to.exist; + expect(mails).to.not.exist; + + expect(devicestorageStub.listItems.calledOnce).to.be.true; + expect(pgpStub.exportKeys.calledOnce).to.be.true; + expect(pgpStub.decrypt.called).to.be.false; + + done(); + }); + }); + + it('should not work when list from storage fails', function(done) { + devicestorageStub.listItems.yields({}); + + dao.listForOutbox(function(err, mails) { + expect(err).to.exist; + expect(mails).to.not.exist; + + expect(devicestorageStub.listItems.calledOnce).to.be.true; + expect(pgpStub.exportKeys.called).to.be.false; + expect(pgpStub.decrypt.called).to.be.false; done(); }); }); }); - }); }); \ No newline at end of file diff --git a/test/new-unit/outbox-bo-test.js b/test/new-unit/outbox-bo-test.js index 21eb46a..46c950c 100644 --- a/test/new-unit/outbox-bo-test.js +++ b/test/new-unit/outbox-bo-test.js @@ -85,7 +85,7 @@ define(function(require) { }; dummyMails = [member, invited, notinvited]; - emailDaoStub.list.yieldsAsync(null, dummyMails); + emailDaoStub.listForOutbox.yieldsAsync(null, dummyMails); emailDaoStub.sendEncrypted.withArgs(sinon.match(function(opts) { return typeof opts.email !== 'undefined' && opts.email.to.address === member.to.address; })).yieldsAsync(); @@ -111,7 +111,7 @@ define(function(require) { expect(outbox._outboxBusy).to.be.false; expect(unsentCount).to.equal(2); - expect(emailDaoStub.list.callCount).to.equal(1); + expect(emailDaoStub.listForOutbox.callCount).to.equal(1); expect(emailDaoStub.sendEncrypted.callCount).to.equal(1); expect(emailDaoStub.sendPlaintext.callCount).to.equal(1); expect(devicestorageStub.removeList.callCount).to.equal(1); @@ -136,7 +136,7 @@ define(function(require) { it('should not process outbox in offline mode', function(done) { emailDaoStub._account.online = false; - emailDaoStub.list.yieldsAsync(null, [{ + emailDaoStub.listForOutbox.yieldsAsync(null, [{ id: '123', to: [{ name: 'member', @@ -147,7 +147,7 @@ define(function(require) { outbox._processOutbox(function(err, count) { expect(err).to.not.exist; expect(count).to.equal(1); - expect(emailDaoStub.list.callCount).to.equal(1); + expect(emailDaoStub.listForOutbox.callCount).to.equal(1); expect(outbox._outboxBusy).to.be.false; done(); }); diff --git a/test/new-unit/write-ctrl-test.js b/test/new-unit/write-ctrl-test.js index 15f9cf6..746ffa9 100644 --- a/test/new-unit/write-ctrl-test.js +++ b/test/new-unit/write-ctrl-test.js @@ -277,13 +277,13 @@ define(function(require) { scope.onError = function(err) { expect(err).to.not.exist; expect(scope.state.writer.open).to.be.false; - expect(emailDaoMock.store.calledOnce).to.be.true; + expect(emailDaoMock.storeForOutbox.calledOnce).to.be.true; expect(emailDaoMock.sync.calledOnce).to.be.true; done(); }; - emailDaoMock.store.yields(); + emailDaoMock.storeForOutbox.yields(); emailDaoMock.sync.yields({ code: 42 }); @@ -310,13 +310,13 @@ define(function(require) { scope.onError = function(err) { expect(err).to.not.exist; expect(scope.state.writer.open).to.be.false; - expect(emailDaoMock.store.calledOnce).to.be.true; + expect(emailDaoMock.storeForOutbox.calledOnce).to.be.true; expect(emailDaoMock.sync.calledOnce).to.be.true; done(); }; - emailDaoMock.store.yields(); + emailDaoMock.storeForOutbox.yields(); emailDaoMock.sync.yields(); scope.state.writer.write(re); @@ -341,13 +341,13 @@ define(function(require) { scope.onError = function(err) { expect(err).to.exist; expect(scope.state.writer.open).to.be.false; - expect(emailDaoMock.store.calledOnce).to.be.true; + expect(emailDaoMock.storeForOutbox.calledOnce).to.be.true; expect(emailDaoMock.sync.calledOnce).to.be.true; done(); }; - emailDaoMock.store.yields(); + emailDaoMock.storeForOutbox.yields(); emailDaoMock.sync.yields({}); scope.state.writer.write(re); @@ -367,7 +367,7 @@ define(function(require) { scope.subject = 'yaddablabla'; scope.toKey = 'Public Key'; - emailDaoMock.store.withArgs(sinon.match(function(mail) { + emailDaoMock.storeForOutbox.withArgs(sinon.match(function(mail) { return mail.from[0].address === emailAddress && mail.to.length === 1 && mail.receiverKeys.length === 1; })).yields({ errMsg: 'snafu' @@ -376,7 +376,7 @@ define(function(require) { scope.onError = function(err) { expect(err).to.exist; expect(scope.state.writer.open).to.be.true; - expect(emailDaoMock.store.calledOnce).to.be.true; + expect(emailDaoMock.storeForOutbox.calledOnce).to.be.true; done(); }; scope.sendToOutbox(); From 7c76156adf222f686b8a63ef99636924f6b3fdc8 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Thu, 6 Feb 2014 19:19:00 +0100 Subject: [PATCH 13/14] finish integrating and end review --- package.json | 2 +- src/js/controller/write.js | 6 +++--- src/js/dao/email-dao.js | 6 ++---- src/tpl/write.html | 2 +- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index bd25924..1c3c174 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "dependencies": { "crypto-lib": "https://github.com/whiteout-io/crypto-lib/tarball/master", "imap-client": "https://github.com/whiteout-io/imap-client/tarball/dev/attachments", - "pgpmailer": "https://github.com/whiteout-io/pgpmailer/tarball/dev/attachments", + "pgpmailer": "https://github.com/whiteout-io/pgpmailer/tarball/master", "requirejs": "2.1.10" }, "devDependencies": { diff --git a/src/js/controller/write.js b/src/js/controller/write.js index 4a5f84b..2ac820a 100644 --- a/src/js/controller/write.js +++ b/src/js/controller/write.js @@ -466,9 +466,9 @@ define(function(require) { var reader = new FileReader(); reader.onload = function(e) { scope.attachments.push({ - fileName: file.name, - contentType: file.type, - uint8Array: new Uint8Array(e.target.result) + filename: file.name, + mimeType: file.type, + content: new Uint8Array(e.target.result) }); scope.$apply(); }; diff --git a/src/js/dao/email-dao.js b/src/js/dao/email-dao.js index 141b239..8e8552b 100644 --- a/src/js/dao/email-dao.js +++ b/src/js/dao/email-dao.js @@ -995,14 +995,14 @@ define(function(require) { // PGP Mailer API - + /** * Login the smtp client */ EmailDAO.prototype._pgpmailerLogin = function() { this._pgpMailer.login(); }; - + // IMAP API @@ -1212,7 +1212,6 @@ define(function(require) { // store to local storage self._devicestorage.storeList([email], dbType, callback); - }); }); }; @@ -1253,7 +1252,6 @@ define(function(require) { }); mail.encrypted = true; }); - }); }); }; diff --git a/src/tpl/write.html b/src/tpl/write.html index 8c0270e..d901543 100644 --- a/src/tpl/write.html +++ b/src/tpl/write.html @@ -36,7 +36,7 @@
- {{attachment.fileName}} + {{attachment.filename}}
From 401438c873c455dbdc7734351d83e54460f14521 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Thu, 6 Feb 2014 20:10:20 +0100 Subject: [PATCH 14/14] change imap-client dep to master branch --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1c3c174..3d6e426 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ }, "dependencies": { "crypto-lib": "https://github.com/whiteout-io/crypto-lib/tarball/master", - "imap-client": "https://github.com/whiteout-io/imap-client/tarball/dev/attachments", + "imap-client": "https://github.com/whiteout-io/imap-client/tarball/master", "pgpmailer": "https://github.com/whiteout-io/pgpmailer/tarball/master", "requirejs": "2.1.10" },