mirror of
https://github.com/moparisthebest/mail
synced 2024-12-21 23:08:50 -05:00
Merge pull request #299 from whiteout-io/dev/WO-854
Show invite dialog in writer when recipient has no public key
This commit is contained in:
commit
6873fd7f3d
@ -12,6 +12,7 @@ module.exports = appCfg;
|
||||
* Global app configurations
|
||||
*/
|
||||
appCfg.config = {
|
||||
pgpComment: 'Whiteout Mail - https://whiteout.io',
|
||||
keyServerUrl: 'https://keys.whiteout.io',
|
||||
hkpUrl: 'http://keyserver.ubuntu.com',
|
||||
privkeyServerUrl: 'https://keychain.whiteout.io',
|
||||
|
@ -6,8 +6,6 @@
|
||||
|
||||
var ReadCtrl = function($scope, $location, $q, email, invitation, outbox, pgp, keychain, appConfig, download, auth, dialog, status) {
|
||||
|
||||
var str = appConfig.string;
|
||||
|
||||
//
|
||||
// scope state
|
||||
//
|
||||
@ -158,18 +156,10 @@ var ReadCtrl = function($scope, $location, $q, email, invitation, outbox, pgp, k
|
||||
});
|
||||
|
||||
}).then(function() {
|
||||
var invitationMail = {
|
||||
from: [{
|
||||
address: sender
|
||||
}],
|
||||
to: [{
|
||||
address: recipient
|
||||
}],
|
||||
cc: [],
|
||||
bcc: [],
|
||||
subject: str.invitationSubject,
|
||||
body: str.invitationMessage
|
||||
};
|
||||
var invitationMail = invitation.createMail({
|
||||
sender: sender,
|
||||
recipient: recipient
|
||||
});
|
||||
// send invitation mail
|
||||
return outbox.put(invitationMail);
|
||||
|
||||
|
@ -6,7 +6,7 @@ var util = require('crypto-lib').util;
|
||||
// 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 cfg = appConfig.config;
|
||||
@ -52,6 +52,8 @@ var WriteCtrl = function($scope, $window, $filter, $q, appConfig, auth, keychain
|
||||
$scope.body = '';
|
||||
$scope.attachments = [];
|
||||
$scope.addressBookCache = undefined;
|
||||
$scope.showInvite = undefined;
|
||||
$scope.invited = [];
|
||||
}
|
||||
|
||||
function reportBug() {
|
||||
@ -248,6 +250,9 @@ var WriteCtrl = function($scope, $window, $filter, $q, appConfig, auth, keychain
|
||||
recipient.key = key;
|
||||
recipient.secure = true;
|
||||
}
|
||||
} else {
|
||||
// show invite dialog if no key found
|
||||
$scope.showInvite = true;
|
||||
}
|
||||
$scope.checkSendStatus();
|
||||
|
||||
@ -286,6 +291,7 @@ var WriteCtrl = function($scope, $window, $filter, $q, appConfig, auth, keychain
|
||||
|
||||
// only allow sending if receviers exist
|
||||
if (numReceivers < 1) {
|
||||
$scope.showInvite = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@ -299,6 +305,7 @@ var WriteCtrl = function($scope, $window, $filter, $q, appConfig, auth, keychain
|
||||
$scope.okToSend = true;
|
||||
$scope.sendBtnText = str.sendBtnSecure;
|
||||
$scope.sendBtnSecure = true;
|
||||
$scope.showInvite = false;
|
||||
} else {
|
||||
// send plaintext
|
||||
$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);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
//
|
||||
|
@ -11,7 +11,7 @@ var util = openpgp.util,
|
||||
* High level crypto api that handles all calls to OpenPGP.js
|
||||
*/
|
||||
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.initWorker(config.workerPath + '/openpgp.worker.min.js');
|
||||
}
|
||||
|
@ -62,6 +62,12 @@ Outbox.prototype.put = function(mail) {
|
||||
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
|
||||
|
||||
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.uid = mail.id = util.UUID(); // the mail needs a random id & uid for storage in the database
|
||||
|
||||
|
@ -8,13 +8,33 @@ module.exports = Invitation;
|
||||
* 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
|
||||
*/
|
||||
function Invitation(invitationRestDao) {
|
||||
function Invitation(invitationRestDao, appConfig) {
|
||||
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
|
||||
|
@ -21,6 +21,33 @@
|
||||
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 {
|
||||
position: relative;
|
||||
margin-top: 1.3em;
|
||||
|
@ -41,6 +41,18 @@
|
||||
</tags-input>
|
||||
</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">
|
||||
<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>
|
||||
@ -61,10 +73,10 @@
|
||||
</header>
|
||||
|
||||
<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">
|
||||
<button wo-touch="sendToOutbox()" class="btn" ng-class="{'btn--invalid': sendBtnSecure === false}"
|
||||
ng-disabled="!okToSend" tabindex="4">{{sendBtnText || 'Send'}}</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div><!--/lightbox__body-->
|
@ -7,11 +7,12 @@ var WriteCtrl = require('../../../../src/js/controller/app/write'),
|
||||
Auth = require('../../../../src/js/service/auth'),
|
||||
PGP = require('../../../../src/js/crypto/pgp'),
|
||||
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() {
|
||||
var ctrl, scope,
|
||||
authMock, pgpMock, dialogMock, emailMock, keychainMock, outboxMock, statusMock,
|
||||
authMock, pgpMock, dialogMock, emailMock, keychainMock, outboxMock, statusMock, invitationMock,
|
||||
emailAddress, realname;
|
||||
|
||||
beforeEach(function() {
|
||||
@ -23,6 +24,7 @@ describe('Write controller unit test', function() {
|
||||
emailMock = sinon.createStubInstance(Email);
|
||||
keychainMock = sinon.createStubInstance(Keychain);
|
||||
statusMock = sinon.createStubInstance(Status);
|
||||
invitationMock = sinon.createStubInstance(Invitation);
|
||||
|
||||
emailAddress = 'fred@foo.com';
|
||||
realname = 'Fred Foo';
|
||||
@ -43,7 +45,8 @@ describe('Write controller unit test', function() {
|
||||
email: emailMock,
|
||||
outbox: outboxMock,
|
||||
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) {
|
||||
var recipient = {
|
||||
address: 'asdf@example.com'
|
||||
@ -226,6 +248,7 @@ describe('Write controller unit test', function() {
|
||||
userId: 'asdf@example.com'
|
||||
});
|
||||
expect(recipient.secure).to.be.true;
|
||||
expect(scope.showInvite).to.be.undefined;
|
||||
expect(scope.checkSendStatus.callCount).to.equal(2);
|
||||
expect(keychainMock.refreshKeyForUserId.calledOnce).to.be.true;
|
||||
done();
|
||||
@ -252,6 +275,7 @@ describe('Write controller unit test', function() {
|
||||
scope.verify(recipient).then(function() {
|
||||
expect(recipient.key).to.deep.equal(key);
|
||||
expect(recipient.secure).to.be.true;
|
||||
expect(scope.showInvite).to.be.undefined;
|
||||
expect(scope.checkSendStatus.callCount).to.equal(2);
|
||||
expect(keychainMock.refreshKeyForUserId.calledOnce).to.be.true;
|
||||
done();
|
||||
@ -272,6 +296,7 @@ describe('Write controller unit test', function() {
|
||||
expect(scope.okToSend).to.be.false;
|
||||
expect(scope.sendBtnText).to.be.undefined;
|
||||
expect(scope.sendBtnSecure).to.be.undefined;
|
||||
expect(scope.showInvite).to.be.false;
|
||||
});
|
||||
|
||||
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.sendBtnText).to.equal('Send securely');
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -49,6 +49,24 @@ describe('Outbox unit test', function() {
|
||||
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) {
|
||||
var mail, senderKey, receiverKey;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user