define(function(require) { 'use strict'; var appController = require('js/app-controller'), download = require('js/util/download'), angular = require('angular'), str = require('js/app-config').string, emailDao, invitationDao, outbox, pgp, keychain; // // Controller // var ReadCtrl = function($scope, $timeout) { emailDao = appController._emailDao; invitationDao = appController._invitationDao; outbox = appController._outboxBo; pgp = appController._pgp; keychain = appController._keychain; // set default value so that the popover height is correct on init $scope.keyId = 'No key found.'; $scope.state.read = { open: false, toggle: function(to) { this.open = to; } }; $scope.lineEmpty = function(line) { return line.replace(/>/g, '').trim().length === 0; }; $scope.getKeyId = function(address) { $scope.keyId = 'Searching...'; keychain.getReceiverPublicKey(address, function(err, pubkey) { if (err) { $scope.onError(err); return; } if (!pubkey) { $scope.keyId = 'User has no key. Click to invite.'; $scope.$apply(); return; } var fpr = pgp.getFingerprint(pubkey.publicKey); var formatted = fpr.slice(32); $scope.keyId = 'PGP key: ' + formatted; $scope.$apply(); }); }; $scope.$watch('state.mailList.selected', function(mail) { if (!mail) { return; } // display sender security status mail.from.forEach(checkPublicKey); // display recipient security status mail.to.forEach(checkPublicKey); // display recipient security status Array.isArray(mail.cc) && mail.cc.forEach(checkPublicKey); $scope.node = undefined; }); $scope.$watch('state.mailList.selected.body', function(body) { $scope.node = undefined; // reset model if (!body) { return; } var selected = $scope.state.mailList.selected; if (selected.encrypted && !selected.decrypted) { return; } $timeout(function() { // parse text nodes for rendering $scope.node = $scope.parseConversation({ body: body }); }); }); function checkPublicKey(user) { user.secure = undefined; keychain.getReceiverPublicKey(user.address, function(err, pubkey) { if (err) { $scope.onError(err); return; } if (pubkey && pubkey.publicKey) { user.secure = true; } else { user.secure = false; } $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({ folder: folder, 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); } }; $scope.invite = function(user) { // only invite non-pgp users if (user.secure) { return; } $scope.keyId = 'Sending invitation...'; var sender = emailDao._account.emailAddress, recipient = user.address; invitationDao.invite({ recipient: recipient, sender: sender }, function(err) { if (err) { $scope.onError(err); return; } var invitationMail = { from: [{ address: sender }], to: [{ address: recipient }], cc: [], bcc: [], subject: str.invitationSubject, body: str.invitationMessage }; // send invitation mail outbox.put(invitationMail, $scope.onError); }); }; $scope.parseConversation = function(email) { var nodes; if (!email || !email.body) { return; } function parseLines(body) { var lines = []; body.split('\n').forEach(parseLine); function parseLine(line) { var regex = /^>*/; var result = regex.exec(line); lines.push({ text: line.replace(regex, '').trim(), level: (result && result.length > 0) ? result[0].length : 0 }); } return lines; } function buildTextNodes(lines) { var i, j, root, currentLevel, currentNode, levelDelta; root = new Node(); currentLevel = 0; currentNode = root; // iterate over text lines for (i = 0; i < lines.length; i++) { levelDelta = lines[i].level - currentLevel; if (levelDelta === 0) { // we are at the desired node ... no traversal required } else if (levelDelta > 0) { // traverse to child node(s) for (j = 0; j < levelDelta; j++) { var newChild = new Node(currentNode); // create new child node currentNode.children.push(newChild); // go to last child node currentNode = newChild; // increase current level by one currentLevel++; } } else { // traverse to parent(s) for (j = levelDelta; j < 0; j++) { currentNode = currentNode.parent; currentLevel--; } } // add text to the current node currentNode.addLine(lines[i].text); } return root; } function Node(parent) { this.parent = parent; this.children = []; } Node.prototype.addLine = function(lineText) { var c, l; c = this.children; l = c.length; // append text node to children if last child is not a text node if (l < 1 || typeof c[l - 1] !== 'string') { c[l] = ''; l = c.length; } // append line to last child (add newline between lines) c[l - 1] += lineText + '\n'; }; function removeParentReference(node) { if (!node.children) { // this is a text leaf ... terminate recursion return; } // remove parent node to prevent infinite loop in JSON stringify delete node.parent; for (var i = 0; i < node.children.length; i++) { if (typeof node.children[i] === 'string') { // remove trailing newline in string node.children[i] = node.children[i].replace(/\n$/, ''); } else { // I used recursion ... removeParentReference(node.children[i]); } } } nodes = buildTextNodes(parseLines(email.body.replace(/ >/g, '>'))); removeParentReference(nodes); return nodes; }; }; // // Directives // var ngModule = angular.module('read', []); ngModule.directive('replySelection', function() { return function(scope, elm) { var popover, visible; popover = angular.element(document.querySelector('.reply-selection')); visible = false; elm.on('touchstart click', appear); elm.parent().parent().on('touchstart click', disappear); popover.on('touchstart click', disappear); function appear(e) { e.preventDefault(); e.stopPropagation(); visible = true; // set popover position var top = elm[0].offsetTop; var left = elm[0].offsetLeft; var width = elm[0].offsetWidth; var height = elm[0].offsetHeight; popover[0].style.transition = 'opacity 0.1s linear'; popover[0].style.top = (top + height) + 'px'; popover[0].style.left = (left + width / 2 - popover[0].offsetWidth / 2) + 'px'; popover[0].style.opacity = '1'; } function disappear() { if (!visible) { return; } popover[0].style.transition = 'opacity 0.25s linear, top 0.25s step-end, left 0.25s step-end'; popover[0].style.opacity = '0'; popover[0].style.top = '-9999px'; popover[0].style.left = '-9999px'; visible = false; } }; }); ngModule.directive('frameLoad', function($sce, $timeout) { return function(scope, elm, attrs) { scope.$watch(attrs.frameLoad, function(value) { var html = value; scope.html = undefined; var iframe = elm[0]; if (!html) { return; } // if there are image tags in the html? var hasImages = /]+\bsrc=['"][^'">]+['"]/ig.test(html); scope.showImageButton = hasImages; // inital loading $timeout(function() { scope.html = true; iframe.contentWindow.postMessage({ html: html, removeImages: hasImages // avoids doing unnecessary work on the html }, '*'); }); // no need to add a scope function to reload the html if there are no images if (!hasImages) { return; } // reload WITH images scope.displayImages = function() { scope.showImageButton = false; iframe.contentWindow.postMessage({ html: html, removeImages: false }, '*'); }; }); }; }); ngModule.filter('createAnchors', function($sce) { return function(str) { // replace all urls with anchors return $sce.trustAsHtml(str.replace(/(https?:\/\/[^\s]+)/g, function(url) { return '' + url + ''; })); }; }); return ReadCtrl; });