mail/src/js/controller/write.js

479 lines
15 KiB
JavaScript
Raw Normal View History

define(function(require) {
2013-09-11 16:11:26 -04:00
'use strict';
var angular = require('angular'),
2013-09-15 11:05:37 -04:00
appController = require('js/app-controller'),
aes = require('cryptoLib/aes-cbc'),
util = require('cryptoLib/util'),
2014-01-15 04:57:28 -05:00
str = require('js/app-config').string,
2014-02-24 04:14:07 -05:00
crypto, emailDao, outbox;
//
// Controller
//
2013-09-12 11:22:17 -04:00
2013-10-18 21:32:00 -04:00
var WriteCtrl = function($scope, $filter) {
crypto = appController._crypto;
2014-02-24 04:14:07 -05:00
emailDao = appController._emailDao,
outbox = appController._outboxBo;
// set default value so that the popover height is correct on init
$scope.keyId = 'XXXXXXXX';
2013-10-12 13:39:09 -04:00
//
// Init
//
2013-11-08 15:55:08 -05:00
$scope.state.writer = {
open: false,
write: function(replyTo) {
this.open = true;
$scope.replyTo = replyTo;
2013-11-08 15:55:08 -05:00
resetFields();
// fill fields depending on replyTo
fillFields(replyTo);
$scope.updatePreview();
2014-01-14 10:11:59 -05:00
$scope.verify($scope.to[0]);
2013-11-08 15:55:08 -05:00
},
close: function() {
this.open = false;
2013-10-18 21:32:00 -04:00
}
2013-11-08 15:55:08 -05:00
};
2013-10-18 21:32:00 -04:00
function resetFields() {
$scope.writerTitle = 'New email';
2014-01-10 15:35:34 -05:00
$scope.to = [{
address: ''
}];
$scope.cc = [{
address: ''
}];
$scope.bcc = [{
address: ''
}];
2013-10-21 09:02:54 -04:00
$scope.subject = '';
$scope.body = '';
$scope.ciphertextPreview = '';
$scope.attachments = [];
2013-10-18 21:32:00 -04:00
}
2013-10-12 13:39:09 -04:00
function fillFields(re) {
2013-11-26 13:06:37 -05:00
var from, body;
2013-10-13 07:49:37 -04:00
2013-10-12 13:39:09 -04:00
if (!re) {
return;
}
2013-10-18 21:32:00 -04:00
$scope.writerTitle = 'Reply';
2013-10-12 13:39:09 -04:00
// fill recipient field
$scope.to.unshift({
address: re.from[0].address
});
2013-10-12 13:39:09 -04:00
// fill subject
$scope.subject = 'Re: ' + ((re.subject) ? re.subject.replace('Re: ', '') : '');
// fill text body
2013-10-13 07:49:37 -04:00
from = re.from[0].name || re.from[0].address;
2013-11-26 13:06:37 -05:00
body = '\n\n' + $filter('date')(re.sentDate, 'EEEE, MMM d, yyyy h:mm a') + ' ' + from + ' wrote:\n> ';
2013-11-26 13:06:37 -05:00
// only display non html mails in reply part
if (!re.html) {
body += re.body.trim().split('\n').join('\n> ');
$scope.body = body;
}
2013-10-12 13:39:09 -04:00
}
//
// Editing headers
//
2014-01-14 10:11:59 -05:00
/**
* This event is fired when editing the email address headers. It checks is space is pressed and if so, creates a new address field.
*/
$scope.onAddressUpdate = function(field, index) {
var recipient = field[index];
2014-01-14 10:11:59 -05:00
$scope.verify(recipient);
2014-01-10 15:35:34 -05:00
};
2014-01-14 10:11:59 -05:00
/**
* Verify and email address and fetch its public key
*/
$scope.verify = function(recipient) {
// set display to insecure while fetching keys
2014-01-10 15:35:34 -05:00
recipient.key = undefined;
recipient.secure = false;
$scope.checkSendStatus();
2014-01-10 15:35:34 -05:00
// verify email address
if (!util.validateEmailAddress(recipient.address)) {
recipient.secure = undefined;
2014-01-14 10:11:59 -05:00
$scope.checkSendStatus();
2014-01-10 15:35:34 -05:00
return;
}
// check if to address is contained in known public keys
2014-01-10 15:35:34 -05:00
emailDao._keychain.getReceiverPublicKey(recipient.address, function(err, key) {
if (err) {
$scope.onError(err);
return;
}
// compare again since model could have changed during the roundtrip
2014-01-10 15:35:34 -05:00
if (key && key.userId === recipient.address) {
recipient.key = key;
recipient.secure = true;
}
2014-01-13 17:54:53 -05:00
2014-01-14 10:11:59 -05:00
$scope.checkSendStatus();
2014-01-13 17:54:53 -05:00
$scope.$apply();
});
2014-01-14 10:11:59 -05:00
};
$scope.getKeyId = function(recipient) {
$scope.keyId = 'Key not found for that user.';
if (!recipient.key) {
return;
}
var fpr = crypto.getFingerprint(recipient.key.publicKey);
var formatted = fpr.slice(32);
$scope.keyId = formatted;
};
2014-01-14 10:11:59 -05:00
/**
* Check if it is ok to send an email depending on the invitation state of the addresses
*/
$scope.checkSendStatus = function() {
2014-01-13 17:54:53 -05:00
$scope.okToSend = false;
$scope.sendBtnText = undefined;
2014-01-13 17:54:53 -05:00
$scope.sendBtnSecure = undefined;
2014-01-13 17:54:53 -05:00
var allSecure = true;
var numReceivers = 0;
// count number of receivers and check security
$scope.to.forEach(check);
$scope.cc.forEach(check);
$scope.bcc.forEach(check);
function check(recipient) {
// validate address
if (!util.validateEmailAddress(recipient.address)) {
return;
}
numReceivers++;
if (!recipient.secure) {
allSecure = false;
}
}
// only allow sending if receviers exist
if (numReceivers < 1) {
return;
}
if (allSecure) {
// send encrypted if all secure
2014-01-13 17:54:53 -05:00
$scope.okToSend = true;
2014-01-15 04:57:28 -05:00
$scope.sendBtnText = str.sendBtnSecure;
2014-01-13 17:54:53 -05:00
$scope.sendBtnSecure = true;
} else {
// send plaintext
$scope.okToSend = true;
$scope.sendBtnText = str.sendBtnClear;
$scope.sendBtnSecure = false;
2014-01-13 17:54:53 -05:00
}
2014-01-14 10:11:59 -05:00
};
2014-02-05 18:41:08 -05:00
//
// Editing attachments
//
$scope.remove = function(attachment) {
$scope.attachments.splice($scope.attachments.indexOf(attachment), 1);
};
//
// Editing email body
2013-10-12 13:39:09 -04:00
//
// generate key,iv for encryption preview
var key = util.random(128),
iv = util.random(128);
$scope.updatePreview = function() {
var body = $scope.body.trim();
2013-09-14 08:23:46 -04:00
// Although this does encrypt live using AES, this is just for show. The plaintext is encrypted seperately before sending the email.
$scope.ciphertextPreview = (body) ? aes.encrypt(body, key, iv) : '';
};
2013-09-15 11:05:37 -04:00
$scope.sendToOutbox = function() {
var email;
2013-11-11 10:11:06 -05:00
// build email model for smtp-client
2013-09-15 11:05:37 -04:00
email = {
2014-02-24 04:14:07 -05:00
from: [{
address: emailDao._account.emailAddress
}],
2014-02-25 08:10:55 -05:00
to: $scope.to.filter(filterEmptyAddresses),
cc: $scope.cc.filter(filterEmptyAddresses),
bcc: $scope.bcc.filter(filterEmptyAddresses),
2014-01-16 05:58:39 -05:00
subject: $scope.subject.trim() ? $scope.subject.trim() : str.fallbackSubject, // Subject line, or the fallback subject, if nothing valid was entered
body: $scope.body.trim() + (!$scope.sendBtnSecure ? str.signature : ''), // use parsed plaintext body
2014-03-06 12:02:05 -05:00
attachments: $scope.attachments,
sentDate: new Date()
2013-09-15 11:05:37 -04:00
};
2014-02-25 15:05:59 -05:00
// close the writer
$scope.state.writer.close();
2014-02-24 04:14:07 -05:00
// persist the email to disk for later sending
outbox.put(email, function(err) {
2013-09-15 11:05:37 -04:00
if (err) {
$scope.onError(err);
2013-09-15 11:05:37 -04:00
return;
}
2014-02-24 04:14:07 -05:00
// helper flag to remember if we need to sync back to imap
// in case the replyTo.answered changed
var needsSync = false;
2014-02-24 04:14:07 -05:00
// mark replyTo as answered, if necessary
if ($scope.replyTo && !$scope.replyTo.answered) {
$scope.replyTo.answered = true;
2014-02-25 15:05:59 -05:00
// update the ui
$scope.$apply();
2014-02-24 04:14:07 -05:00
needsSync = true;
}
2014-02-24 04:14:07 -05:00
// if we need to synchronize replyTo.answered, let's do that.
// otherwise, we're done
if (!needsSync) {
return;
}
2014-02-24 04:14:07 -05:00
emailDao.sync({
folder: $scope.state.nav.currentFolder.path
}, function(err) {
if (err && err.code === 42) {
// offline
$scope.onError();
return;
}
$scope.onError(err);
});
});
2014-02-25 08:10:55 -05:00
function filterEmptyAddresses(addr) {
return !!addr.address;
}
2014-02-24 04:14:07 -05:00
};
2013-09-12 11:22:17 -04:00
};
2013-09-11 16:11:26 -04:00
//
// Directives
//
var ngModule = angular.module('write', []);
ngModule.directive('contenteditable', function() {
return {
require: 'ngModel',
link: function(scope, elm, attrs, ctrl) {
// view -> model
2013-10-13 07:49:37 -04:00
elm.on('keyup keydown', function() {
scope.$apply(function() {
2013-11-26 13:06:37 -05:00
// set model
ctrl.$setViewValue(elm[0].innerText);
});
});
// model -> view
2013-10-12 13:39:09 -04:00
ctrl.$render = function() {
2013-11-26 13:06:37 -05:00
elm[0].innerText = ctrl.$viewValue;
};
// load init value from DOM
2013-11-26 13:06:37 -05:00
ctrl.$setViewValue(elm[0].innerText);
}
};
});
2013-10-12 13:39:09 -04:00
2013-10-19 09:06:23 -04:00
ngModule.directive('focusMe', function($timeout, $parse) {
return {
2013-10-19 09:06:23 -04:00
//scope: true, // optionally create a child scope
link: function(scope, element, attrs) {
var model = $parse(attrs.focusMe);
scope.$watch(model, function(value) {
if (value === true) {
$timeout(function() {
element[0].focus();
}, 100);
2013-10-19 09:06:23 -04:00
}
});
}
};
});
ngModule.directive('focusChild', function() {
return {
//scope: true, // optionally create a child scope
link: function(scope, element) {
element.on('click', function() {
element[0].children[0].focus();
});
}
};
});
ngModule.directive('autoSize', function($parse) {
return {
require: 'ngModel',
link: function(scope, elm, attrs) {
// resize text input depending on value length
var model = $parse(attrs.autoSize);
scope.$watch(model, function(value) {
2014-01-10 15:35:34 -05:00
var width;
if (value.length < 12) {
width = (14 * 8) + 'px';
} else {
width = ((value.length + 2) * 8) + 'px';
}
elm.css('width', width);
});
}
};
});
function addInput(field, scope) {
field.push({
address: ''
});
scope.$apply();
}
function checkForEmptyInput(field) {
var emptyFieldExists = false;
field.forEach(function(recipient) {
if (!recipient.address) {
emptyFieldExists = true;
}
});
return emptyFieldExists;
}
ngModule.directive('field', function() {
return {
//scope: true, // optionally create a child scope
link: function(scope, element, attrs) {
element.on('click', function() {
var fieldName = attrs.field;
var field = scope[fieldName];
if (!checkForEmptyInput(field)) {
// create new field input if no empy one exists
addInput(field, scope);
}
// focus on last input when clicking on field
var id = fieldName + (field.length - 1);
document.getElementById(id).focus();
});
}
};
});
ngModule.directive('addressInput', function() {
2014-01-10 15:35:34 -05:00
return {
//scope: true, // optionally create a child scope
link: function(scope, element, attrs) {
// get prefix for id
var fieldName = attrs.addressInput;
var field = scope[fieldName];
var index = parseInt(attrs.id.replace(fieldName, ''), 10);
element.on('click', function(e) {
// focus on this one and dont bubble to field click handler
e.stopPropagation();
});
element.on('blur', function() {
if (!checkForEmptyInput(field)) {
// create new field input
addInput(field, scope);
}
});
element.on('keydown', function(e) {
var code = e.keyCode;
2014-02-25 15:32:41 -05:00
scope.$apply();
if (code === 32 || code === 188 || code === 186) {
// catch space, comma, semicolon
e.preventDefault();
// create new field input
addInput(field, scope);
// find next input and focus
var nextId = fieldName + (index + 1);
document.getElementById(nextId).focus();
} else if ((code === 8 || code === 46) && !field[index].address && field.length > 1) {
// backspace, delete on empty input
// remove input
e.preventDefault();
field.splice(index, 1);
scope.$apply();
// focus on previous id
var previousId = fieldName + (index - 1);
document.getElementById(previousId).focus();
2014-01-10 15:35:34 -05:00
}
});
}
};
});
2014-02-05 18:41:08 -05:00
ngModule.directive('attachmentInput', function() {
return function(scope, elm) {
2014-02-05 18:41:08 -05:00
elm.on('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({
2014-02-06 13:19:00 -05:00
filename: file.name,
mimeType: file.type,
content: new Uint8Array(e.target.result)
});
scope.$apply();
};
reader.readAsArrayBuffer(file);
}
};
});
2014-02-05 18:41:08 -05:00
ngModule.directive('attachmentBtn', function() {
return function(scope, elm) {
elm.on('click touchstart', function(e) {
e.preventDefault();
2014-02-05 18:41:08 -05:00
document.querySelector('#attachment-input').click();
});
};
});
2013-09-11 16:11:26 -04:00
return WriteCtrl;
});