1
0
mirror of https://github.com/moparisthebest/mail synced 2024-11-26 19:02:20 -05:00

Merge pull request #78 from whiteout-io/dev/WO-409

[WO-409] Render images in html mails
This commit is contained in:
Tankred Hase 2014-06-26 13:23:30 +02:00
commit ca0f91af27
8 changed files with 137 additions and 14 deletions

View File

@ -338,17 +338,40 @@ define(function(require) {
ngModule.directive('frameLoad', function($sce, $timeout) { ngModule.directive('frameLoad', function($sce, $timeout) {
return function(scope, elm, attrs) { return function(scope, elm, attrs) {
scope.$watch(attrs.frameLoad, function(value) { scope.$watch(attrs.frameLoad, function(value) {
var html = value;
scope.html = undefined; scope.html = undefined;
if (value) { var iframe = elm[0];
$timeout(function() {
// wrap in html doc with scrollable html tag, since chrome apps does not scroll by default if (!html) {
var prefix = '<!DOCTYPE html><html style="overflow-y: auto"><head></head><body>'; return;
var suffix = '</body></html>';
// open links in new window, otherwise the sandbox with not open them
var clickableHtml = value.replace(/<a /g, '<a target="_blank" ');
scope.html = $sce.trustAsHtml(prefix + clickableHtml + suffix);
});
} }
// if there are image tags in the html?
var hasImages = /<img[^>]+\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
}, '*');
};
}); });
}; };
}); });

View File

@ -639,13 +639,13 @@ define(function(require) {
var localMessage = localMessages[0]; var localMessage = localMessages[0];
// treat attachment and non-attachment body parts separately: // treat attachment and non-attachment body parts separately:
// we need to fetch the content for non-attachment body parts (encrypted, signed, text, html) // we need to fetch the content for non-attachment body parts (encrypted, signed, text, html, resources referenced from the html)
// but we spare the effort and fetch attachment content later upon explicit user request. // but we spare the effort and fetch attachment content later upon explicit user request.
var contentParts = localMessage.bodyParts.filter(function(bodyPart) { var contentParts = localMessage.bodyParts.filter(function(bodyPart) {
return bodyPart.type !== "attachment"; return bodyPart.type !== "attachment" || (bodyPart.type === "attachment" && bodyPart.id);
}); });
var attachmentParts = localMessage.bodyParts.filter(function(bodyPart) { var attachmentParts = localMessage.bodyParts.filter(function(bodyPart) {
return bodyPart.type === "attachment"; return bodyPart.type === "attachment" && !bodyPart.id;
}); });
// do we need to fetch content from the imap server? // do we need to fetch content from the imap server?
@ -747,10 +747,12 @@ define(function(require) {
message.attachments = filterBodyParts(root, 'attachment'); message.attachments = filterBodyParts(root, 'attachment');
message.body = body; message.body = body;
message.html = _.pluck(filterBodyParts(root, 'html'), 'content').join('\n'); message.html = _.pluck(filterBodyParts(root, 'html'), 'content').join('\n');
inlineExternalImages(message);
done(); done();
} }
function done(err) { function done(err) {
message.loadingBody = false; message.loadingBody = false;
callback(err, err ? undefined : message); callback(err, err ? undefined : message);
@ -852,6 +854,7 @@ define(function(require) {
// remove the pgp-signature from the attachments // remove the pgp-signature from the attachments
return attmt.mimeType === "application/pgp-signature"; return attmt.mimeType === "application/pgp-signature";
}); });
inlineExternalImages(message);
message.decrypted = true; message.decrypted = true;
@ -1150,7 +1153,7 @@ define(function(require) {
/** /**
* Updates the folder information from imap (if we're online). Adds/removes folders in account.folders, * Updates the folder information from imap (if we're online). Adds/removes folders in account.folders,
* if we added/removed folder in IMAP. If we have an uninitialized folder that lacks folder.messages, * if we added/removed folder in IMAP. If we have an uninitialized folder that lacks folder.messages,
* all the locally available messages are loaded from memory. * all the locally available messages are loaded from memory.
* *
* @param {Function} callback Invoked when the folders are up to date * @param {Function} callback Invoked when the folders are up to date
@ -1472,5 +1475,36 @@ define(function(require) {
return result; return result;
} }
/**
* Helper function that looks through the HTML content for <img src="cid:..."> and
* inlines the images linked internally. Manipulates message.html as a side-effect.
* If no attachment matching the internal reference is found, or constructing a data
* uri fails, just remove the source.
*
* @param {Object} message DTO
*/
function inlineExternalImages(message) {
message.html = message.html.replace(/(<img[^>]+\bsrc=['"])cid:([^'">]+)(['"])/ig, function(match, prefix, src, suffix) {
var localSource = '',
payload = '';
var internalReference = _.findWhere(message.attachments, {
id: src
});
if (internalReference) {
for (var i = 0; i < internalReference.content.byteLength; i++) {
payload += String.fromCharCode(internalReference.content[i]);
}
try {
localSource = 'data:application/octet-stream;base64,' + btoa(payload); // try to replace the source
} catch (e) {}
}
return prefix + localSource + suffix;
});
}
return EmailDAO; return EmailDAO;
}); });

View File

@ -32,5 +32,9 @@
"background": { "background": {
"scripts": ["background.js"] "scripts": ["background.js"]
} }
},
"sandbox": {
"pages": ["sandbox.html"],
"content_security_policy": "sandbox allow-popups allow-scripts; default-src 'self'; object-src 'none'; style-src 'self' 'unsafe-inline'; img-src *"
} }
} }

11
src/sandbox.html Normal file
View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html style="overflow-y: auto">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="sandbox.js"></script>
</head>
<body></body>
</html>

20
src/sandbox.js Normal file
View File

@ -0,0 +1,20 @@
(function() {
'use strict';
// set listener for event from main window
window.addEventListener('message', function(e) {
var html = e.data.html;
// make links open in a new window
html = html.replace(/<a /g, '<a target="_blank" ');
// remove sources where necessary
if (e.data.removeImages) {
html = html.replace(/(<img[^>]+\b)src=['"][^'">]+['"]/ig, function(match, prefix) {
return prefix;
});
}
document.body.innerHTML = html;
}, false);
})();

View File

@ -114,6 +114,17 @@
} }
} }
.showImages {
flex-shrink: 0;
padding: 1em;
text-align: center;
a {
text-decoration: none;
color: $color-blue;
}
}
iframe { iframe {
flex-grow: 1; flex-grow: 1;
margin-top: 1.75em; margin-top: 1.75em;

View File

@ -53,8 +53,12 @@
</div><!--/.working--> </div><!--/.working-->
</div><!--/.working-wrapper--> </div><!--/.working-wrapper-->
<div class="showImages" ng-show="html && showImageButton">
<a href='#' ng-click="displayImages(); $event.preventDefault()">Display images</a>
</div>
<!-- Render html body in sandboxed iframe --> <!-- Render html body in sandboxed iframe -->
<iframe ng-show="html" sandbox="allow-popups" srcdoc="{{html}}" <iframe ng-show="html" sandbox="allow-popups allow-scripts" src="sandbox.html"
frame-load="state.mailList.selected.html"> frame-load="state.mailList.selected.html">
</iframe> </iframe>

File diff suppressed because one or more lines are too long