From 04cf299e1e617cc2848392dfcd14059233fea500 Mon Sep 17 00:00:00 2001 From: Felix Hammerl Date: Thu, 16 Jan 2014 11:38:53 +0100 Subject: [PATCH 1/4] 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 2/4] 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 3/4] 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 4/4] 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);