From 93ddfb1c994f2b08cb4dc56c87d1f26f0664fc5b Mon Sep 17 00:00:00 2001 From: Felix Hammerl Date: Wed, 20 Nov 2013 19:13:39 +0100 Subject: [PATCH] [WO-18] introduce invitation email functionality --- src/js/app-config.js | 2 + src/js/bo/outbox.js | 113 ++++++++++++++++++++++++++++++-- src/js/dao/email-dao.js | 2 - test/new-unit/outbox-bo-test.js | 65 ++++++++++++++---- 4 files changed, 159 insertions(+), 23 deletions(-) diff --git a/src/js/app-config.js b/src/js/app-config.js index abd043c..baebd09 100644 --- a/src/js/app-config.js +++ b/src/js/app-config.js @@ -38,6 +38,8 @@ define([], function() { */ app.string = { subjectPrefix: '[whiteout] ', + invitationSubject: 'Invitation to your secure eMail experience', + invitationMessage: 'I would like to invite you to a private conversation. To read my encrypted messages, simply install Whiteout Mail for Chrome. The app is really easy to use and automatically encrypts sent emails, so that only the two of us can read them: https://chrome.google.com/webstore/detail/jjgghafhamholjigjoghcfcekhkonijg', message: 'this is a private conversation. To read my encrypted message below, simply install Whiteout Mail for Chrome. The app is really easy to use and automatically encrypts sent emails, so that only the two of us can read them: https://chrome.google.com/webstore/detail/jjgghafhamholjigjoghcfcekhkonijg', cryptPrefix: '-----BEGIN PGP MESSAGE-----', cryptSuffix: '-----END PGP MESSAGE-----', diff --git a/src/js/bo/outbox.js b/src/js/bo/outbox.js index 63fd5fe..4a76d5b 100644 --- a/src/js/bo/outbox.js +++ b/src/js/bo/outbox.js @@ -1,7 +1,10 @@ define(function(require) { 'use strict'; - var config = require('js/app-config').config, + var _ = require('underscore'), + str = require('js/app-config').string, + config = require('js/app-config').config, + InvitationDAO = require('js/dao/invitation-dao'), dbType = 'email_OUTBOX'; var OutboxBO = function(emailDao, invitationDao) { @@ -12,7 +15,7 @@ define(function(require) { OutboxBO.prototype.startChecking = function(callback) { // start periodic checking of outbox - this._intervalId = setInterval(this._emptyOutbox.bind(this, callback), config.checkOutboxInterval); + this._intervalId = setInterval(this._processOutbox.bind(this, callback), config.checkOutboxInterval); }; OutboxBO.prototype.stopChecking = function() { @@ -24,7 +27,7 @@ define(function(require) { delete this._intervalId; }; - OutboxBO.prototype._emptyOutbox = function(callback) { + OutboxBO.prototype._processOutbox = function(callback) { var self = this, emails; @@ -49,19 +52,115 @@ define(function(require) { emails = pending; // sending pending mails - send(); + processMails(); }); } - function send() { - callback(null, emails.length); + function processMails() { + // in the navigation controller, this updates the folder count if (emails.length === 0) { self._outboxBusy = false; + callback(null, 0); return; } + callback(null, emails.length); var email = emails.shift(); + checkReceivers(email); + } + + function checkReceivers(email) { + var unregisteredUsers, receiverChecked; + + unregisteredUsers = []; + receiverChecked = _.after(email.to.length, function() { + if (unregisteredUsers.length > 0) { + invite(unregisteredUsers); + return; + } + + sendEncrypted(email); + }); + + email.to.forEach(function(recipient) { + self._emailDao._keychain.getReceiverPublicKey(recipient.address, function(err, key) { + if (err) { + // stop processing + } + + if (!key) { + unregisteredUsers.push(recipient); + } + + receiverChecked(); + }); + }); + } + + function invite(addresses) { + var sender = self._emailDao._account.emailAddress; + + var invitationFinished = _.after(addresses.length, function() { + // after all of the invitations are checked and sent (if necessary), + // + processMails(); + }); + + // send invite + addresses.forEach(function(recipient) { + var recipientAddress = recipient.address; + self._invitationDao.check({ + recipient: recipientAddress, + sender: sender + }, function(err, status) { + if (status === InvitationDAO.INVITE_PENDING) { + // the recipient is already invited, we're done here. + invitationFinished(); + return; + } + + // the recipient is not yet invited, so let's do that + self._invitationDao.invite({ + recipient: recipientAddress, + sender: sender + }, function(err, status) { + if (err) { + console.error(err.errMsg); + return; + } + if (status !== InvitationDAO.INVITE_SUCCESS) { + console.error('could not successfully invite ' + recipientAddress); + return; + } + + sendInvitationMail(recipient, sender); + }); + + }); + }); + + function sendInvitationMail(recipient, sender) { + var to = (recipient.name || recipient.address).split('@')[0].split('.')[0].split(' ')[0], + invitationMail = { + from: [sender], + to: [recipient], + subject: str.invitationSubject, + body: 'Hi ' + to + ',\n\n' + str.invitationMessage + '\n\n\n' + str.signature + }; + + // send invitation mail + self._emailDao.send(invitationMail, function(err) { + if (err) { + console.error(err.errMsg); + } + invitationFinished(); + }); + } + } + + + function sendEncrypted(email) { self._emailDao.encryptedSend(email, function(err) { if (err) { self._outboxBusy = false; @@ -91,7 +190,7 @@ define(function(require) { return; } - send(); + processMails(); }); } }; diff --git a/src/js/dao/email-dao.js b/src/js/dao/email-dao.js index daa50de..eb85c75 100644 --- a/src/js/dao/email-dao.js +++ b/src/js/dao/email-dao.js @@ -578,8 +578,6 @@ define(function(require) { callback({ errMsg: 'User has no public key yet!' }); - // user hasn't registered a public key yet... invite - //self.encryptForNewUser(email, callback); return; } diff --git a/test/new-unit/outbox-bo-test.js b/test/new-unit/outbox-bo-test.js index db9fedf..9d1ebd1 100644 --- a/test/new-unit/outbox-bo-test.js +++ b/test/new-unit/outbox-bo-test.js @@ -2,17 +2,24 @@ define(function(require) { 'use strict'; var expect = chai.expect, + _ = require('underscore'), OutboxBO = require('js/bo/outbox'), + KeychainDAO = require('js/dao/keychain-dao'), EmailDAO = require('js/dao/email-dao'), DeviceStorageDAO = require('js/dao/devicestorage-dao'), InvitationDAO = require('js/dao/invitation-dao'); describe('Outbox Business Object unit test', function() { - var outbox, emailDaoStub, devicestorageStub, invitationDaoStub; + var outbox, emailDaoStub, devicestorageStub, invitationDaoStub, keychainStub, + dummyUser = 'spiderpig@springfield.com'; beforeEach(function() { emailDaoStub = sinon.createStubInstance(EmailDAO); + emailDaoStub._account = { + emailAddress: dummyUser + }; emailDaoStub._devicestorage = devicestorageStub = sinon.createStubInstance(DeviceStorageDAO); + emailDaoStub._keychain = keychainStub = sinon.createStubInstance(KeychainDAO); invitationDaoStub = sinon.createStubInstance(InvitationDAO); outbox = new OutboxBO(emailDaoStub, invitationDaoStub); }); @@ -42,25 +49,55 @@ define(function(require) { }); }); - describe('empty outbox', function() { + describe('process outbox', function() { it('should work', function(done) { - devicestorageStub.listItems.yields(null, [{ - id: '12345' - }]); - emailDaoStub.encryptedSend.yields(); - devicestorageStub.removeList.yields(); + var dummyMails = [{ + id: '123', + to: [{ + name: 'member', + address: 'member@whiteout.io' + }] + }, { + id: '456', + to: [{ + name: 'invited', + address: 'invited@whiteout.io' + }] + }, { + id: '789', + to: [{ + name: 'notinvited', + address: 'notinvited@whiteout.io' + }] + }]; + + devicestorageStub.listItems.yieldsAsync(null, dummyMails); + emailDaoStub.encryptedSend.yieldsAsync(); + emailDaoStub.send.yieldsAsync(); + devicestorageStub.removeList.yieldsAsync(); + invitationDaoStub.check.withArgs(sinon.match(function(o) { return o.recipient === 'invited@whiteout.io'; })).yieldsAsync(null, InvitationDAO.INVITE_PENDING); + invitationDaoStub.check.withArgs(sinon.match(function(o) { return o.recipient === 'notinvited@whiteout.io'; })).yieldsAsync(null, InvitationDAO.INVITE_MISSING); + invitationDaoStub.invite.withArgs(sinon.match(function(o) { return o.recipient === 'notinvited@whiteout.io'; })).yieldsAsync(null, InvitationDAO.INVITE_SUCCESS); + keychainStub.getReceiverPublicKey.withArgs(sinon.match(function(o) { return o === 'member@whiteout.io'; })).yieldsAsync(null, 'this is not the key you are looking for...'); + keychainStub.getReceiverPublicKey.withArgs(sinon.match(function(o) { return o === 'invited@whiteout.io' || o === 'notinvited@whiteout.io'; })).yieldsAsync(); + + var check = _.after(dummyMails.length + 1, function() { + expect(devicestorageStub.listItems.callCount).to.equal(1); + expect(emailDaoStub.encryptedSend.callCount).to.equal(1); + expect(emailDaoStub.send.callCount).to.equal(1); + expect(devicestorageStub.removeList.callCount).to.equal(1); + expect(invitationDaoStub.check.callCount).to.equal(2); + expect(invitationDaoStub.invite.callCount).to.equal(1); + done(); + }); function onOutboxUpdate(err, count) { expect(err).to.not.exist; - if (count === 0) { - expect(devicestorageStub.listItems.callCount).to.equal(1); - expect(emailDaoStub.encryptedSend.callCount).to.equal(1); - expect(devicestorageStub.removeList.callCount).to.equal(1); - done(); - } + expect(count).to.exist; + check(); } - outbox._emptyOutbox(onOutboxUpdate); + outbox._processOutbox(onOutboxUpdate); }); }); });