Show invite dialog in writer when recipient has no public key

This commit is contained in:
Tankred Hase 2015-01-21 12:04:08 +01:00
parent 9aebecd45f
commit 9bc2bc7912
10 changed files with 270 additions and 25 deletions

View File

@ -12,6 +12,7 @@ module.exports = appCfg;
* Global app configurations * Global app configurations
*/ */
appCfg.config = { appCfg.config = {
pgpComment: 'Whiteout Mail - https://whiteout.io',
keyServerUrl: 'https://keys.whiteout.io', keyServerUrl: 'https://keys.whiteout.io',
hkpUrl: 'http://keyserver.ubuntu.com', hkpUrl: 'http://keyserver.ubuntu.com',
privkeyServerUrl: 'https://keychain.whiteout.io', privkeyServerUrl: 'https://keychain.whiteout.io',

View File

@ -6,8 +6,6 @@
var ReadCtrl = function($scope, $location, $q, email, invitation, outbox, pgp, keychain, appConfig, download, auth, dialog, status) { var ReadCtrl = function($scope, $location, $q, email, invitation, outbox, pgp, keychain, appConfig, download, auth, dialog, status) {
var str = appConfig.string;
// //
// scope state // scope state
// //
@ -158,18 +156,10 @@ var ReadCtrl = function($scope, $location, $q, email, invitation, outbox, pgp, k
}); });
}).then(function() { }).then(function() {
var invitationMail = { var invitationMail = invitation.createMail({
from: [{ sender: sender,
address: sender recipient: recipient
}], });
to: [{
address: recipient
}],
cc: [],
bcc: [],
subject: str.invitationSubject,
body: str.invitationMessage
};
// send invitation mail // send invitation mail
return outbox.put(invitationMail); return outbox.put(invitationMail);

View File

@ -6,7 +6,7 @@ var util = require('crypto-lib').util;
// Controller // Controller
// //
var WriteCtrl = function($scope, $window, $filter, $q, appConfig, auth, keychain, pgp, email, outbox, dialog, axe, status) { var WriteCtrl = function($scope, $window, $filter, $q, appConfig, auth, keychain, pgp, email, outbox, dialog, axe, status, invitation) {
var str = appConfig.string; var str = appConfig.string;
var cfg = appConfig.config; var cfg = appConfig.config;
@ -52,6 +52,8 @@ var WriteCtrl = function($scope, $window, $filter, $q, appConfig, auth, keychain
$scope.body = ''; $scope.body = '';
$scope.attachments = []; $scope.attachments = [];
$scope.addressBookCache = undefined; $scope.addressBookCache = undefined;
$scope.showInvite = undefined;
$scope.invited = [];
} }
function reportBug() { function reportBug() {
@ -248,6 +250,9 @@ var WriteCtrl = function($scope, $window, $filter, $q, appConfig, auth, keychain
recipient.key = key; recipient.key = key;
recipient.secure = true; recipient.secure = true;
} }
} else {
// show invite dialog if no key found
$scope.showInvite = true;
} }
$scope.checkSendStatus(); $scope.checkSendStatus();
@ -286,6 +291,7 @@ var WriteCtrl = function($scope, $window, $filter, $q, appConfig, auth, keychain
// only allow sending if receviers exist // only allow sending if receviers exist
if (numReceivers < 1) { if (numReceivers < 1) {
$scope.showInvite = false;
return; return;
} }
@ -299,6 +305,7 @@ var WriteCtrl = function($scope, $window, $filter, $q, appConfig, auth, keychain
$scope.okToSend = true; $scope.okToSend = true;
$scope.sendBtnText = str.sendBtnSecure; $scope.sendBtnText = str.sendBtnSecure;
$scope.sendBtnSecure = true; $scope.sendBtnSecure = true;
$scope.showInvite = false;
} else { } else {
// send plaintext // send plaintext
$scope.okToSend = true; $scope.okToSend = true;
@ -315,6 +322,56 @@ var WriteCtrl = function($scope, $window, $filter, $q, appConfig, auth, keychain
$scope.attachments.splice($scope.attachments.indexOf(attachment), 1); $scope.attachments.splice($scope.attachments.indexOf(attachment), 1);
}; };
/**
* Invite all users without a public key
*/
$scope.invite = function() {
var sender = auth.emailAddress,
sendJobs = [],
invitees = [];
$scope.showInvite = false;
// get recipients with no keys
$scope.to.forEach(check);
$scope.cc.forEach(check);
$scope.bcc.forEach(check);
function check(recipient) {
if (util.validateEmailAddress(recipient.address) && !recipient.secure && $scope.invited.indexOf(recipient.address) === -1) {
invitees.push(recipient.address);
}
}
return $q(function(resolve) {
resolve();
}).then(function() {
invitees.forEach(function(recipientAddress) {
var invitationMail = invitation.createMail({
sender: sender,
recipient: recipientAddress
});
// send invitation mail
var promise = outbox.put(invitationMail).then(function() {
return invitation.invite({
recipient: recipientAddress,
sender: sender
});
});
sendJobs.push(promise);
// remember already invited users to prevent spamming
$scope.invited.push(recipientAddress);
});
return Promise.all(sendJobs);
}).catch(function(err) {
$scope.showInvite = true;
return dialog.error(err);
});
};
// //
// Editing email body // Editing email body
// //

View File

@ -11,7 +11,7 @@ var util = openpgp.util,
* High level crypto api that handles all calls to OpenPGP.js * High level crypto api that handles all calls to OpenPGP.js
*/ */
function PGP() { function PGP() {
openpgp.config.commentstring = 'Whiteout Mail - https://whiteout.io'; openpgp.config.commentstring = config.pgpComment;
openpgp.config.prefer_hash_algorithm = openpgp.enums.hash.sha256; openpgp.config.prefer_hash_algorithm = openpgp.enums.hash.sha256;
openpgp.initWorker(config.workerPath + '/openpgp.worker.min.js'); openpgp.initWorker(config.workerPath + '/openpgp.worker.min.js');
} }

View File

@ -62,6 +62,12 @@ Outbox.prototype.put = function(mail) {
var self = this, var self = this,
allReaders = mail.from.concat(mail.to.concat(mail.cc.concat(mail.bcc))); // all the users that should be able to read the mail allReaders = mail.from.concat(mail.to.concat(mail.cc.concat(mail.bcc))); // all the users that should be able to read the mail
if (mail.to.concat(mail.cc.concat(mail.bcc)).length === 0) {
return new Promise(function() {
throw new Error('Message has no recipients!');
});
}
mail.publicKeysArmored = []; // gather the public keys mail.publicKeysArmored = []; // gather the public keys
mail.uid = mail.id = util.UUID(); // the mail needs a random id & uid for storage in the database mail.uid = mail.id = util.UUID(); // the mail needs a random id & uid for storage in the database

View File

@ -8,13 +8,33 @@ module.exports = Invitation;
* The Invitation is a high level Data Access Object that access the invitation service REST endpoint. * The Invitation is a high level Data Access Object that access the invitation service REST endpoint.
* @param {Object} restDao The REST Data Access Object abstraction * @param {Object} restDao The REST Data Access Object abstraction
*/ */
function Invitation(invitationRestDao) { function Invitation(invitationRestDao, appConfig) {
this._restDao = invitationRestDao; this._restDao = invitationRestDao;
this._appConfig = appConfig;
} }
// /**
// API * Create the invitation mail object
// * @param {String} options.sender The sender's email address
* @param {String} options.recipient The recipient's email address
* @return {Object} The mail object
*/
Invitation.prototype.createMail = function(options) {
var str = this._appConfig.string;
return {
from: [{
address: options.sender
}],
to: [{
address: options.recipient
}],
cc: [],
bcc: [],
subject: str.invitationSubject,
body: str.invitationMessage
};
};
/** /**
* Notes an invite for the recipient by the sender in the invitation web service * Notes an invite for the recipient by the sender in the invitation web service

View File

@ -21,6 +21,33 @@
margin-top: 0.5em; margin-top: 0.5em;
} }
} }
&__invite {
position: relative;
margin-top: 1.3em;
border: 1px solid $color-red-light;
p {
color: $color-red-light;
margin: 0.7em 1em;
svg {
width: 1em;
height: 1em;
fill: $color-red-light;
// for better valignment
position: relative;
top: 0.15em;
margin-right: 0.3em;
}
}
.btn {
position: absolute;
top: 5px;
right: 5px;
}
}
&__subject { &__subject {
position: relative; position: relative;
margin-top: 1.3em; margin-top: 1.3em;

View File

@ -41,6 +41,18 @@
</tags-input> </tags-input>
</div> </div>
<div class="write__invite" ng-show="showInvite">
<p>
<svg role="presentation"><use xlink:href="#icon-decrypted"/></svg>
<strong>Key not found!</strong>
<span class="u-hidden-sm">Invite user to encrypt.</span>
</p>
<button class="btn btn--light" wo-touch="invite()" title="Invite to Whiteout Mail">
<svg role="presentation"><use xlink:href="#icon-add_contact"/></svg>
Invite
</button>
</div>
<div class="write__subject"> <div class="write__subject">
<input class="input-text" ng-model="subject" spellcheck="true" tabindex="2" placeholder="Subject" ng-change="updatePreview()"> <input class="input-text" ng-model="subject" spellcheck="true" tabindex="2" placeholder="Subject" ng-change="updatePreview()">
<input id="attachment-input" type="file" multiple attachment-input> <input id="attachment-input" type="file" multiple attachment-input>
@ -61,10 +73,10 @@
</header> </header>
<textarea class="write__body" ng-model="body" spellcheck="true" wo-focus-me="state.lightbox === 'write' && writerTitle === 'Reply'" tabindex="3"></textarea> <textarea class="write__body" ng-model="body" spellcheck="true" wo-focus-me="state.lightbox === 'write' && writerTitle === 'Reply'" tabindex="3"></textarea>
</div><!--/write-->
</div>
<footer class="lightbox__controls"> <footer class="lightbox__controls">
<button wo-touch="sendToOutbox()" class="btn" ng-class="{'btn--invalid': sendBtnSecure === false}" <button wo-touch="sendToOutbox()" class="btn" ng-class="{'btn--invalid': sendBtnSecure === false}"
ng-disabled="!okToSend" tabindex="4">{{sendBtnText || 'Send'}}</button> ng-disabled="!okToSend" tabindex="4">{{sendBtnText || 'Send'}}</button>
</footer> </footer>
</div> </div><!--/lightbox__body-->

View File

@ -7,11 +7,12 @@ var WriteCtrl = require('../../../../src/js/controller/app/write'),
Auth = require('../../../../src/js/service/auth'), Auth = require('../../../../src/js/service/auth'),
PGP = require('../../../../src/js/crypto/pgp'), PGP = require('../../../../src/js/crypto/pgp'),
Status = require('../../../../src/js/util/status'), Status = require('../../../../src/js/util/status'),
Dialog = require('../../../../src/js/util/dialog'); Dialog = require('../../../../src/js/util/dialog'),
Invitation = require('../../../../src/js/service/invitation');
describe('Write controller unit test', function() { describe('Write controller unit test', function() {
var ctrl, scope, var ctrl, scope,
authMock, pgpMock, dialogMock, emailMock, keychainMock, outboxMock, statusMock, authMock, pgpMock, dialogMock, emailMock, keychainMock, outboxMock, statusMock, invitationMock,
emailAddress, realname; emailAddress, realname;
beforeEach(function() { beforeEach(function() {
@ -23,6 +24,7 @@ describe('Write controller unit test', function() {
emailMock = sinon.createStubInstance(Email); emailMock = sinon.createStubInstance(Email);
keychainMock = sinon.createStubInstance(Keychain); keychainMock = sinon.createStubInstance(Keychain);
statusMock = sinon.createStubInstance(Status); statusMock = sinon.createStubInstance(Status);
invitationMock = sinon.createStubInstance(Invitation);
emailAddress = 'fred@foo.com'; emailAddress = 'fred@foo.com';
realname = 'Fred Foo'; realname = 'Fred Foo';
@ -43,7 +45,8 @@ describe('Write controller unit test', function() {
email: emailMock, email: emailMock,
outbox: outboxMock, outbox: outboxMock,
dialog: dialogMock, dialog: dialogMock,
status: statusMock status: statusMock,
invitation: invitationMock
}); });
}); });
}); });
@ -205,6 +208,25 @@ describe('Write controller unit test', function() {
}); });
}); });
it('should work for no key in keychain', function(done) {
var recipient = {
address: 'asds@example.com'
};
keychainMock.refreshKeyForUserId.withArgs({
userId: recipient.address
}).returns(resolves());
scope.verify(recipient).then(function() {
expect(recipient.key).to.be.undefined;
expect(recipient.secure).to.be.false;
expect(scope.showInvite).to.be.true;
expect(scope.checkSendStatus.callCount).to.equal(2);
expect(keychainMock.refreshKeyForUserId.calledOnce).to.be.true;
done();
});
});
it('should work for main userId', function(done) { it('should work for main userId', function(done) {
var recipient = { var recipient = {
address: 'asdf@example.com' address: 'asdf@example.com'
@ -226,6 +248,7 @@ describe('Write controller unit test', function() {
userId: 'asdf@example.com' userId: 'asdf@example.com'
}); });
expect(recipient.secure).to.be.true; expect(recipient.secure).to.be.true;
expect(scope.showInvite).to.be.undefined;
expect(scope.checkSendStatus.callCount).to.equal(2); expect(scope.checkSendStatus.callCount).to.equal(2);
expect(keychainMock.refreshKeyForUserId.calledOnce).to.be.true; expect(keychainMock.refreshKeyForUserId.calledOnce).to.be.true;
done(); done();
@ -252,6 +275,7 @@ describe('Write controller unit test', function() {
scope.verify(recipient).then(function() { scope.verify(recipient).then(function() {
expect(recipient.key).to.deep.equal(key); expect(recipient.key).to.deep.equal(key);
expect(recipient.secure).to.be.true; expect(recipient.secure).to.be.true;
expect(scope.showInvite).to.be.undefined;
expect(scope.checkSendStatus.callCount).to.equal(2); expect(scope.checkSendStatus.callCount).to.equal(2);
expect(keychainMock.refreshKeyForUserId.calledOnce).to.be.true; expect(keychainMock.refreshKeyForUserId.calledOnce).to.be.true;
done(); done();
@ -272,6 +296,7 @@ describe('Write controller unit test', function() {
expect(scope.okToSend).to.be.false; expect(scope.okToSend).to.be.false;
expect(scope.sendBtnText).to.be.undefined; expect(scope.sendBtnText).to.be.undefined;
expect(scope.sendBtnSecure).to.be.undefined; expect(scope.sendBtnSecure).to.be.undefined;
expect(scope.showInvite).to.be.false;
}); });
it('should be able to send plaintext', function() { it('should be able to send plaintext', function() {
@ -312,6 +337,95 @@ describe('Write controller unit test', function() {
expect(scope.okToSend).to.be.true; expect(scope.okToSend).to.be.true;
expect(scope.sendBtnText).to.equal('Send securely'); expect(scope.sendBtnText).to.equal('Send securely');
expect(scope.sendBtnSecure).to.be.true; expect(scope.sendBtnSecure).to.be.true;
expect(scope.showInvite).to.be.false;
});
});
describe('invite', function() {
beforeEach(function() {
scope.state.writer.write();
});
afterEach(function() {});
it('should not invite anyone', function(done) {
scope.invite().then(function() {
expect(scope.showInvite).to.be.false;
expect(outboxMock.put.called).to.be.false;
expect(invitationMock.invite.called).to.be.false;
done();
});
});
it('should work', function(done) {
scope.to = [{
address: 'asdf@asdf.de'
}, {
address: 'qwer@asdf.de'
}];
outboxMock.put.returns(resolves());
invitationMock.invite.returns(resolves());
scope.invite().then(function() {
expect(scope.showInvite).to.be.false;
expect(outboxMock.put.callCount).to.equal(2);
expect(invitationMock.invite.callCount).to.equal(2);
done();
});
});
it('should work for one already invited', function(done) {
scope.to = [{
address: 'asdf@asdf.de'
}, {
address: 'qwer@asdf.de'
}];
scope.invited.push('asdf@asdf.de');
outboxMock.put.returns(resolves());
invitationMock.invite.returns(resolves());
scope.invite().then(function() {
expect(scope.showInvite).to.be.false;
expect(outboxMock.put.callCount).to.equal(1);
expect(invitationMock.invite.callCount).to.equal(1);
done();
});
});
it('should fail due to error in outbox.put', function(done) {
scope.to = [{
address: 'asdf@asdf.de'
}];
outboxMock.put.returns(rejects(new Error('Peng')));
invitationMock.invite.returns(resolves());
scope.invite().then(function() {
expect(dialogMock.error.calledOnce).to.be.true;
expect(scope.showInvite).to.be.true;
expect(outboxMock.put.callCount).to.equal(1);
expect(invitationMock.invite.callCount).to.equal(0);
done();
});
});
it('should fail due to error in invitation.invite', function(done) {
scope.to = [{
address: 'asdf@asdf.de'
}];
outboxMock.put.returns(resolves());
invitationMock.invite.returns(rejects(new Error('Peng')));
scope.invite().then(function() {
expect(dialogMock.error.calledOnce).to.be.true;
expect(scope.showInvite).to.be.true;
expect(outboxMock.put.callCount).to.equal(1);
expect(invitationMock.invite.callCount).to.equal(1);
done();
});
}); });
}); });

View File

@ -49,6 +49,24 @@ describe('Outbox unit test', function() {
outbox._processOutbox.restore(); outbox._processOutbox.restore();
}); });
it('should throw error for message without recipients', function(done) {
var mail = {
from: [{
name: 'member',
address: 'member@whiteout.io'
}],
to: [],
cc: [],
bcc: []
};
outbox.put(mail).catch(function(err) {
expect(err).to.exist;
expect(keychainStub.getReceiverPublicKey.called).to.be.false;
done();
});
});
it('should not encrypt and store a mail', function(done) { it('should not encrypt and store a mail', function(done) {
var mail, senderKey, receiverKey; var mail, senderKey, receiverKey;