@salesflare/planer
Version:
Remove reply quotations from emails
369 lines (337 loc) • 14.5 kB
JavaScript
// Generated by CoffeeScript 2.5.1
(function() {
var BREAK_TAG_REGEX, CHECKPOINT_PREFIX, CHECKPOINT_SUFFIX, DOCUMENT_POSITION_FOLLOWING, DOCUMENT_POSITION_PRECEDING, OUTLOOK_SPLITTER_QUERY_SELECTORS, OUTLOOK_SPLITTER_QUOTE_IDS, OUTLOOK_XPATH_SPLITTER_QUERIES, QUOTE_IDS, compareByDomPosition, elementIsAllContent, ensureTextNodeBetweenChildElements, findMicrosoftSplitter, findOutlookSplitterWithQuerySelector, findOutlookSplitterWithQuoteId, findOutlookSplitterWithXpathQuery, findParentDiv, hasTagName, isTextNodeWrappedInSpan, removeNodes;
CHECKPOINT_PREFIX = '#!%!';
CHECKPOINT_SUFFIX = '!%!#';
exports.CHECKPOINT_PATTERN = new RegExp(`${CHECKPOINT_PREFIX}\\d+${CHECKPOINT_SUFFIX}`, 'g');
// HTML quote indicators (tag ids)
QUOTE_IDS = ['OLK_SRC_BODY_SECTION'];
// Create an instance of Document using the message html and the injected base document
exports.createEmailDocument = function(msgBody, dom) {
var emailBodyElement, emailDocument, head, htmlElement;
emailDocument = dom.implementation.createHTMLDocument();
// Write html of email to `html` element
[htmlElement] = emailDocument.getElementsByTagName('html');
htmlElement.innerHTML = msgBody.trim();
if (emailDocument.body == null) {
[emailBodyElement] = emailDocument.getElementsByTagName('body');
emailDocument.body = emailBodyElement;
}
// Remove 'head' element from document
[head] = emailDocument.getElementsByTagName('head');
if (head) {
emailDocument.documentElement.removeChild(head);
}
return emailDocument;
};
// Recursively adds checkpoints to html tree.
exports.addCheckpoints = function(htmlNode, counter) {
var childNode, i, len, ref;
// 3 is a text node
if (htmlNode.nodeType === 3) {
htmlNode.nodeValue = `${htmlNode.nodeValue.trim()}${CHECKPOINT_PREFIX}${counter}${CHECKPOINT_SUFFIX}\n`;
counter++;
}
// 1 is an element
if (htmlNode.nodeType === 1) {
if (!hasTagName(htmlNode, 'body')) {
// Pad with spacing to ensure there are text nodes at the begining and end of non-body elements
htmlNode.innerHTML = ` ${htmlNode.innerHTML} `;
}
// Ensure that there are text nodes between sibling elements
ensureTextNodeBetweenChildElements(htmlNode);
ref = htmlNode.childNodes;
for (i = 0, len = ref.length; i < len; i++) {
childNode = ref[i];
counter = exports.addCheckpoints(childNode, counter);
}
}
return counter;
};
exports.deleteQuotationTags = function(htmlNode, counter, quotationCheckpoints) {
var childNode, childTagInQuotation, i, j, len, len1, quotationChildren, ref, tagInQuotation;
tagInQuotation = true;
// 3 is a text node
if (htmlNode.nodeType === 3) {
if (!quotationCheckpoints[counter]) {
tagInQuotation = false;
}
counter++;
return [counter, tagInQuotation];
}
// 1 is an element
if (htmlNode.nodeType === 1) {
// Collect child nodes that are marked as in the quotation
childTagInQuotation = false;
quotationChildren = [];
if (!hasTagName(htmlNode, 'body')) {
// Pad with spacing to ensure there are text nodes at the begining and end of non-body elements
htmlNode.innerHTML = ` ${htmlNode.innerHTML} `;
}
// Ensure that there are text nodes between sibling elements
ensureTextNodeBetweenChildElements(htmlNode);
ref = htmlNode.childNodes;
for (i = 0, len = ref.length; i < len; i++) {
childNode = ref[i];
[counter, childTagInQuotation] = exports.deleteQuotationTags(childNode, counter, quotationCheckpoints);
// Keep tracking if all children are in the quotation
tagInQuotation = tagInQuotation && childTagInQuotation;
if (childTagInQuotation) {
quotationChildren.push(childNode);
}
}
}
// If all of an element's children are part of a quotation, let parent delete whole element
if (tagInQuotation) {
return [counter, tagInQuotation];
} else {
// Otherwise, delete specific quotation children
for (j = 0, len1 = quotationChildren.length; j < len1; j++) {
childNode = quotationChildren[j];
htmlNode.removeChild(childNode);
}
return [counter, tagInQuotation];
}
};
exports.cutGmailQuote = function(emailDocument) {
var nodesArray;
nodesArray = emailDocument.getElementsByClassName('gmail_quote');
if (!(nodesArray.length > 0)) {
return false;
}
removeNodes(nodesArray);
return true;
};
exports.cutMicrosoftQuote = function(emailDocument) {
var afterSplitter, parentElement, splitterElement;
splitterElement = findMicrosoftSplitter(emailDocument);
if (splitterElement == null) {
return false;
}
parentElement = splitterElement.parentElement;
afterSplitter = splitterElement.nextElementSibling;
while (afterSplitter != null) {
parentElement.removeChild(afterSplitter);
afterSplitter = splitterElement.nextElementSibling;
}
parentElement.removeChild(splitterElement);
return true;
};
// Remove the last non-nested blockquote element
exports.cutBlockQuote = function(emailDocument) {
var blockquoteElement, div, parent, xpathQuery, xpathResult;
xpathQuery = '(.//blockquote)[not(ancestor::blockquote)][last()]';
xpathResult = emailDocument.evaluate(xpathQuery, emailDocument, null, 9, null);
blockquoteElement = xpathResult.singleNodeValue;
if (blockquoteElement == null) {
return false;
}
div = emailDocument.createElement('div');
parent = blockquoteElement.parentElement;
parent.removeChild(blockquoteElement);
return true;
};
exports.cutById = function(emailDocument) {
var found, i, len, quoteElement, quoteId;
found = false;
for (i = 0, len = QUOTE_IDS.length; i < len; i++) {
quoteId = QUOTE_IDS[i];
quoteElement = emailDocument.getElementById(quoteId);
if (quoteElement != null) {
found = true;
quoteElement.parentElement.removeChild(quoteElement);
}
}
return found;
};
exports.cutFromBlock = function(emailDocument) {
var afterSplitter, fromBlock, lastBlock, parentDiv, ref, splitterElement, textNode, xpathQuery, xpathResult;
// Handle case where From: block is enclosed in a tag
xpathQuery = "//*[starts-with(normalize-space(.), 'From:')]|//*[starts-with(normalize-space(.), 'Date:')]";
xpathResult = emailDocument.evaluate(xpathQuery, emailDocument, null, 5, null);
// Find last element in iterator
while (fromBlock = xpathResult.iterateNext()) {
lastBlock = fromBlock;
}
if (lastBlock != null) {
// Find parent div and remove from document
parentDiv = findParentDiv(lastBlock);
if ((parentDiv != null) && !elementIsAllContent(parentDiv)) {
parentDiv.parentElement.removeChild(parentDiv);
return true;
}
}
// Handle the case when From: block goes right after e.g. <hr> and is not enclosed in a tag itself
xpathQuery = "//text()[starts-with(normalize-space(.), 'From:')]|//text()[starts-with(normalize-space(.), 'Date:')]";
xpathResult = emailDocument.evaluate(xpathQuery, emailDocument, null, 9, null);
// The text node that is the result
textNode = xpathResult.singleNodeValue;
if (textNode == null) {
return false;
}
if (isTextNodeWrappedInSpan(textNode)) {
// The text node is wrapped in a span element. All sorts formatting could be happening here.
// Return false and hope plain text algorithm can figure it out.
return false;
}
// The previous sibling stopped the initial xpath query from working, so it is likely a splitter (like an hr)
splitterElement = textNode.previousSibling;
if (splitterElement != null) {
if ((ref = splitterElement.parentElement) != null) {
ref.removeChild(splitterElement);
}
}
// Remove all subsequent siblings of the textNode
afterSplitter = textNode.nextSibling;
while (afterSplitter != null) {
afterSplitter.parentNode.removeChild(afterSplitter);
afterSplitter = textNode.nextSibling;
}
textNode.parentNode.removeChild(textNode);
return true;
};
findParentDiv = function(element) {
while ((element != null) && (element.parentElement != null)) {
if (hasTagName(element, 'div')) {
return element;
} else {
element = element.parentElement;
}
}
return null;
};
elementIsAllContent = function(element) {
var maybeBody;
maybeBody = element.parentElement;
return (maybeBody != null) && hasTagName(maybeBody, 'body') && maybeBody.childNodes.length === 1;
};
isTextNodeWrappedInSpan = function(textNode) {
var parentElement;
parentElement = textNode.parentElement;
return (parentElement != null) && hasTagName(parentElement, 'span') && parentElement.childNodes.length === 1;
};
BREAK_TAG_REGEX = new RegExp('<br\\s*[/]?>', 'gi');
exports.replaceBreakTagsWithLineFeeds = function(emailDocument) {
var currentHtml;
currentHtml = emailDocument.body.innerHTML;
return emailDocument.body.innerHTML = currentHtml.replace(BREAK_TAG_REGEX, "\n");
};
// Queries to find a splitter that's the only child of a single parent div
// Usually represents the dividing line between messages in the Outlook html
OUTLOOK_SPLITTER_QUERY_SELECTORS = {
outlook2007: "div[style='border:none;border-top:solid #B5C4DF 1.0pt;padding:3.0pt 0cm 0cm 0cm']",
outlookForAndroid: "div[style='border:none;border-top:solid #E1E1E1 1.0pt;padding:3.0pt 0cm 0cm 0cm']",
windowsMail: "div[style='padding-top: 5px; border-top-color: rgb(229, 229, 229); border-top-width: 1px; border-top-style: solid;']"
};
// More complicated Xpath queries for versions of Outlook that don't use the dividing lines
OUTLOOK_XPATH_SPLITTER_QUERIES = {
outlook2003: "//div/div[@class='MsoNormal' and @align='center' and @style='text-align:center']/font/span/hr[@size='3' and @width='100%' and @align='center' and @tabindex='-1']"
};
// For more modern versions of Outlook that contain replies in quote block with an id
OUTLOOK_SPLITTER_QUOTE_IDS = {
// There's potentially multiple elements with this id so we need to cut everything after this quote as well
office365: '#divRplyFwdMsg'
};
findMicrosoftSplitter = function(emailDocument) {
var _, possibleSplitterElements, querySelector, quoteId, splitterElement, xpathQuery;
possibleSplitterElements = [];
for (_ in OUTLOOK_SPLITTER_QUERY_SELECTORS) {
querySelector = OUTLOOK_SPLITTER_QUERY_SELECTORS[_];
if ((splitterElement = findOutlookSplitterWithQuerySelector(emailDocument, querySelector))) {
possibleSplitterElements.push(splitterElement);
}
}
for (_ in OUTLOOK_XPATH_SPLITTER_QUERIES) {
xpathQuery = OUTLOOK_XPATH_SPLITTER_QUERIES[_];
if ((splitterElement = findOutlookSplitterWithXpathQuery(emailDocument, xpathQuery))) {
possibleSplitterElements.push(splitterElement);
}
}
for (_ in OUTLOOK_SPLITTER_QUOTE_IDS) {
quoteId = OUTLOOK_SPLITTER_QUOTE_IDS[_];
if ((splitterElement = findOutlookSplitterWithQuoteId(emailDocument, quoteId))) {
possibleSplitterElements.push(splitterElement);
}
}
if (!possibleSplitterElements.length) {
return null;
}
// Find the earliest splitter in the DOM to remove everything after it
return possibleSplitterElements.sort(compareByDomPosition)[0];
};
DOCUMENT_POSITION_PRECEDING = 2;
DOCUMENT_POSITION_FOLLOWING = 4;
compareByDomPosition = function(elementA, elementB) {
var documentPositionComparison;
documentPositionComparison = elementA.compareDocumentPosition(elementB);
if (documentPositionComparison & DOCUMENT_POSITION_PRECEDING) {
return 1;
} else if (documentPositionComparison & DOCUMENT_POSITION_FOLLOWING) {
return -1;
}
return 0;
};
findOutlookSplitterWithXpathQuery = function(emailDocument, xpathQuery) {
var splitterElement, xpathResult;
xpathResult = emailDocument.evaluate(xpathQuery, emailDocument, null, 9, null);
splitterElement = xpathResult.singleNodeValue;
// Go up the tree to find the enclosing div.
if (splitterElement != null) {
splitterElement = splitterElement.parentElement.parentElement;
splitterElement = splitterElement.parentElement.parentElement;
}
return splitterElement;
};
findOutlookSplitterWithQuerySelector = function(emailDocument, query) {
var splitterElement, splitterResult;
splitterResult = emailDocument.querySelectorAll(query);
if (!(splitterResult.length > 1)) {
return;
}
splitterElement = splitterResult[1];
if ((splitterElement.parentElement != null) && splitterElement === splitterElement.parentElement.children[0]) {
splitterElement = splitterElement.parentElement;
}
return splitterElement;
};
findOutlookSplitterWithQuoteId = function(emailDocument, id) {
var splitterResult;
splitterResult = emailDocument.querySelectorAll(id);
if (!splitterResult.length) {
return;
}
return splitterResult[0];
};
removeNodes = function(nodesArray) {
var i, index, node, ref, ref1, results;
results = [];
for (index = i = ref = nodesArray.length - 1; (ref <= 0 ? i <= 0 : i >= 0); index = ref <= 0 ? ++i : --i) {
node = nodesArray[index];
results.push(node != null ? (ref1 = node.parentNode) != null ? ref1.removeChild(node) : void 0 : void 0);
}
return results;
};
ensureTextNodeBetweenChildElements = function(element) {
var currentNode, dom, newTextNode, results;
dom = element.ownerDocument;
currentNode = element.childNodes[0];
if (!currentNode) {
newTextNode = dom.createTextNode(' ');
element.appendChild(newTextNode);
return;
}
results = [];
while (currentNode.nextSibling) {
// An element is followed by an element
if (currentNode.nodeType === 1 && currentNode.nextSibling.nodeType === 1) {
newTextNode = dom.createTextNode(' ');
element.insertBefore(newTextNode, currentNode.nextSibling);
}
results.push(currentNode = currentNode.nextSibling);
}
return results;
};
hasTagName = function(element, tagName) {
return element.tagName.toLowerCase() === tagName;
};
}).call(this);