diff --git a/examples/N1-Composer-Translate/lib/main.cjsx b/examples/N1-Composer-Translate/lib/main.cjsx
index 882dfe4c1..68bd28641 100644
--- a/examples/N1-Composer-Translate/lib/main.cjsx
+++ b/examples/N1-Composer-Translate/lib/main.cjsx
@@ -9,7 +9,7 @@ request = require 'request'
{React,
ComponentRegistry,
- QuotedHTMLParser,
+ QuotedHTMLTransformer,
DraftStore} = require 'nylas-exports'
{Menu,
RetinaImg,
@@ -83,7 +83,7 @@ class TranslateButton extends React.Component
#
session = DraftStore.sessionForClientId(@props.draftClientId).then (session) =>
draftHtml = session.draft().body
- text = QuotedHTMLParser.removeQuotedHTML(draftHtml)
+ text = QuotedHTMLTransformer.removeQuotedHTML(draftHtml)
query =
key: YandexTranslationKey
@@ -99,7 +99,7 @@ class TranslateButton extends React.Component
# The new text of the draft is our translated response, plus any quoted text
# that we didn't process.
translated = json.text.join('')
- translated = QuotedHTMLParser.appendQuotedHTML(translated, draftHtml)
+ translated = QuotedHTMLTransformer.appendQuotedHTML(translated, draftHtml)
# To update the draft, we add the new body to it's session. The session object
# automatically marshalls changes to the database and ensures that others accessing
diff --git a/examples/N1-Quick-Schedule/docs/main.html b/examples/N1-Quick-Schedule/docs/main.html
index d9bec5d0b..016653652 100644
--- a/examples/N1-Quick-Schedule/docs/main.html
+++ b/examples/N1-Quick-Schedule/docs/main.html
@@ -60,7 +60,7 @@ your availabilities to schedule an appointment with you.
Optio
+Labs Announces the Acquisition of Oculis Labs, and Names Oculis Founder, Dr.
+Bill Anderson, as Chief Product Officer
+
+
+
+
+
+
Baltimore (April 8, 2015) – Optio Labs, which creates
+technology products that make mobile devices more secure, announced
+that it has purchased Maryland-based security company Oculis Labs, and its CEO,
+Dr. Bill Anderson, will be joining the company as Chief Product Officer. Oculis
+is developer of the award-winning products PrivateEye and Chameleon.
+
+
+
+
+
\ No newline at end of file
diff --git a/spec/fixtures/paste/word-paste-out.html b/spec/fixtures/paste/word-paste-out.html
new file mode 100644
index 000000000..419e0dbc8
--- /dev/null
+++ b/spec/fixtures/paste/word-paste-out.html
@@ -0,0 +1,1306 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Optio
+Labs Announces the Acquisition of Oculis Labs, and Names Oculis Founder, Dr.
+Bill Anderson, as Chief Product Officer
+
+
+
+
+
+
Baltimore (April 8, 2015) – Optio Labs, which creates
+technology products that make mobile devices more secure, announced
+that it has purchased Maryland-based security company Oculis Labs, and its CEO,
+Dr. Bill Anderson, will be joining the company as Chief Product Officer. Oculis
+is developer of the award-winning products PrivateEye and Chameleon.
+
+
+
+
+
\ No newline at end of file
diff --git a/spec/quoted-html-parser-spec.coffee b/spec/quoted-html-transformer-spec.coffee
similarity index 90%
rename from spec/quoted-html-parser-spec.coffee
rename to spec/quoted-html-transformer-spec.coffee
index aa7a697a7..ed5070d8a 100644
--- a/spec/quoted-html-parser-spec.coffee
+++ b/spec/quoted-html-transformer-spec.coffee
@@ -1,22 +1,22 @@
_ = require('underscore')
fs = require('fs')
path = require 'path'
-QuotedHTMLParser = require('../src/services/quoted-html-parser')
+QuotedHTMLTransformer = require('../src/services/quoted-html-transformer')
-describe "QuotedHTMLParser", ->
+describe "QuotedHTMLTransformer", ->
readFile = (fname) ->
emailPath = path.resolve(__dirname, 'fixtures', 'emails', fname)
return fs.readFileSync(emailPath, 'utf8')
hideQuotedHTML = (fname) ->
- return QuotedHTMLParser.hideQuotedHTML(readFile(fname))
+ return QuotedHTMLTransformer.hideQuotedHTML(readFile(fname))
removeQuotedHTML = (fname, opts={}) ->
- return QuotedHTMLParser.removeQuotedHTML(readFile(fname), opts)
+ return QuotedHTMLTransformer.removeQuotedHTML(readFile(fname), opts)
numQuotes = (html) ->
- re = new RegExp(QuotedHTMLParser.annotationClass, 'g')
+ re = new RegExp(QuotedHTMLTransformer.annotationClass, 'g')
html.match(re)?.length ? 0
[1..16].forEach (n) ->
@@ -260,23 +260,23 @@ describe "QuotedHTMLParser", ->
it 'works with these manual test cases', ->
for {before, after} in tests
opts = keepIfWholeBodyIsQuote: true
- test = clean(QuotedHTMLParser.removeQuotedHTML(before, opts))
+ test = clean(QuotedHTMLTransformer.removeQuotedHTML(before, opts))
expect(test).toEqual clean(after)
it 'removes all trailing tags except one', ->
input0 = "hello world
foolololol
"
expect0 = "hello world "
- expect(QuotedHTMLParser.removeQuotedHTML(input0)).toEqual expect0
+ expect(QuotedHTMLTransformer.removeQuotedHTML(input0)).toEqual expect0
it 'preserves tags in the middle and only chops off tail', ->
input0 = "hello
world
foolololol
"
expect0 = "hello
world "
- expect(QuotedHTMLParser.removeQuotedHTML(input0)).toEqual expect0
+ expect(QuotedHTMLTransformer.removeQuotedHTML(input0)).toEqual expect0
# We have a little utility method that you can manually uncomment to
- # generate what the current iteration of the QuotedHTMLParser things the
+ # generate what the current iteration of the QuotedHTMLTransformer things the
# `removeQuotedHTML` should look like. These can be manually inspected in
# a browser before getting their filename changed to
# `email_#{n}_stripped.html". The actually tests will run the current
@@ -284,11 +284,10 @@ describe "QuotedHTMLParser", ->
# anything has changed in the parser.
#
# It's inside of the specs here instaed of its own script because the
- # `QuotedHTMLParser` needs Electron booted up in order to work because
+ # `QuotedHTMLTransformer` needs Electron booted up in order to work because
# of the DOMParser.
xit "Run this simple funciton to generate output files", ->
[16].forEach (n) ->
- newHTML = QuotedHTMLParser.removeQuotedHTML(readFile("email_#{n}.html"))
+ newHTML = QuotedHTMLTransformer.removeQuotedHTML(readFile("email_#{n}.html"))
outPath = path.resolve(__dirname, 'fixtures', 'emails', "email_#{n}_raw_stripped.html")
fs.writeFileSync(outPath, newHTML)
-
diff --git a/spec/quoted-plain-text-parser-spec.coffee b/spec/quoted-plain-text-transformer-spec.coffee
similarity index 99%
rename from spec/quoted-plain-text-parser-spec.coffee
rename to spec/quoted-plain-text-transformer-spec.coffee
index 3776eabe1..c69711bcc 100644
--- a/spec/quoted-plain-text-parser-spec.coffee
+++ b/spec/quoted-plain-text-transformer-spec.coffee
@@ -4,7 +4,7 @@
fs = require('fs')
path = require 'path'
_ = require('underscore')
-QuotedPlainTextParser = require('../src/services/quoted-plain-text-parser')
+QuotedPlainTextParser = require('../src/services/quoted-plain-text-transformer')
getParsedEmail = (name, format="plain") ->
data = getRawEmail(name, format)
diff --git a/spec/services/inline-style-transformer-spec.coffee b/spec/services/inline-style-transformer-spec.coffee
new file mode 100644
index 000000000..d64069672
--- /dev/null
+++ b/spec/services/inline-style-transformer-spec.coffee
@@ -0,0 +1,67 @@
+InlineStyleTransformer = require '../../src/services/inline-style-transformer'
+{ipcRenderer} = require 'electron'
+
+describe "InlineStyleTransformer", ->
+ describe "run", ->
+ beforeEach ->
+ spyOn(ipcRenderer, 'send')
+ spyOn(InlineStyleTransformer, '_injectUserAgentStyles').andCallFake (input) =>
+ return input
+ InlineStyleTransformer._inlineStylePromises = {}
+
+ it "should return a Promise", ->
+ expect(InlineStyleTransformer.run("asd") instanceof Promise).toBe(true)
+
+ it "should resolve immediately if the html is empty", ->
+ result = InlineStyleTransformer.run("")
+ expect(result.isResolved()).toBe(true)
+
+ it "should resolve immediately if there is no
+
+ """)
+ expect(ipcRenderer.send.mostRecentCall.args[1].html).toEqual("""
+
+
+ """)
+
+ it "should add user agent styles", ->
+ InlineStyleTransformer.run("""Other content goes here""")
+ expect(InlineStyleTransformer._injectUserAgentStyles).toHaveBeenCalled()
+
+ it "should fire inline-style-parse to the main process", ->
+ InlineStyleTransformer.run("""Other content goes here""")
+ expect(ipcRenderer.send).toHaveBeenCalled()
+ expect(ipcRenderer.send.mostRecentCall.args[0]).toEqual('inline-style-parse')
diff --git a/spec/stores/draft-store-spec.coffee b/spec/stores/draft-store-spec.coffee
index 4b6eee14a..fd628c942 100644
--- a/spec/stores/draft-store-spec.coffee
+++ b/spec/stores/draft-store-spec.coffee
@@ -13,6 +13,9 @@ SoundRegistry = require '../../src/sound-registry'
Actions = require '../../src/flux/actions'
Utils = require '../../src/flux/models/utils'
+InlineStyleTransformer = require '../../src/services/inline-style-transformer'
+SanitizeTransformer = require '../../src/services/sanitize-transformer'
+
{ipcRenderer} = require 'electron'
_ = require 'underscore'
@@ -40,9 +43,8 @@ describe "DraftStore", ->
describe "creating drafts", ->
beforeEach ->
- spyOn(DraftStore, "_sanitizeBody").andCallThrough()
- spyOn(DraftStore, "_onInlineStylesResult").andCallThrough()
- spyOn(DraftStore, "_convertToInlineStyles").andCallThrough()
+ spyOn(DraftStore, "_prepareBodyForQuoting").andCallFake (body) ->
+ Promise.resolve(body)
spyOn(ipcRenderer, "send").andCallFake (message, body) ->
if message is "inline-style-parse"
# There needs to be a defer block in here so the promise
@@ -181,11 +183,7 @@ describe "DraftStore", ->
expect(@model.replyToMessageId).toEqual(fakeMessage1.id)
it "should sanitize the HTML", ->
- expect(DraftStore._sanitizeBody).toHaveBeenCalled()
-
- it "should not call the style inliner when there are no style tags", ->
- expect(DraftStore._convertToInlineStyles).not.toHaveBeenCalled()
- expect(DraftStore._onInlineStylesResult).not.toHaveBeenCalled()
+ expect(DraftStore._prepareBodyForQuoting).toHaveBeenCalled()
describe "onComposeReply", ->
describe "when the message provided as context has one or more 'ReplyTo' recipients", ->
@@ -245,11 +243,7 @@ describe "DraftStore", ->
expect(@model.replyToMessageId).toEqual(fakeMessage1.id)
it "should sanitize the HTML", ->
- expect(DraftStore._sanitizeBody).toHaveBeenCalled()
-
- it "should not call the style inliner when there are no style tags", ->
- expect(DraftStore._convertToInlineStyles).not.toHaveBeenCalled()
- expect(DraftStore._onInlineStylesResult).not.toHaveBeenCalled()
+ expect(DraftStore._prepareBodyForQuoting).toHaveBeenCalled()
describe "onComposeReplyAll", ->
describe "when the message provided as context has one or more 'ReplyTo' recipients", ->
@@ -329,45 +323,7 @@ describe "DraftStore", ->
expect(@model.replyToMessageId).toEqual(undefined)
it "should sanitize the HTML", ->
- expect(DraftStore._sanitizeBody).toHaveBeenCalled()
-
- it "should not call the style inliner when there are no style tags", ->
- expect(DraftStore._convertToInlineStyles).not.toHaveBeenCalled()
- expect(DraftStore._onInlineStylesResult).not.toHaveBeenCalled()
-
- describe "inlining #{body[i..-1]}"
-
- _onInlineStylesResult: (event, {body, clientId}) =>
- delete @_inlineStylePromises[clientId]
- @_inlineStyleResolvers[clientId](body)
- delete @_inlineStyleResolvers[clientId]
- return
-
- _sanitizeBody: (body) ->
- return sanitizeHtml body,
- allowedTags: DOMUtils.permissiveTags()
- allowedAttributes: DOMUtils.permissiveAttributes()
- allowedSchemes: [ 'http', 'https', 'ftp', 'mailto', 'data' ]
+ InlineStyleTransformer.run(body).then (body) =>
+ SanitizeTransformer.run(body, SanitizeTransformer.Preset.UnsafeOnly)
_onPopoutBlankDraft: =>
account = AccountStore.current()
diff --git a/src/flux/stores/message-body-processor.coffee b/src/flux/stores/message-body-processor.coffee
index 5f54628a6..5ba2c17a5 100644
--- a/src/flux/stores/message-body-processor.coffee
+++ b/src/flux/stores/message-body-processor.coffee
@@ -83,4 +83,5 @@ class MessageBodyProcessor
@_recentlyProcessedA.unshift(item)
@_recentlyProcessedD[key] = item
+
module.exports = new MessageBodyProcessor()
diff --git a/src/global/nylas-exports.coffee b/src/global/nylas-exports.coffee
index 890c532df..4572c9d2b 100644
--- a/src/global/nylas-exports.coffee
+++ b/src/global/nylas-exports.coffee
@@ -151,10 +151,15 @@ class NylasExports
# Services
@load "UndoManager", 'undo-manager'
@load "SoundRegistry", 'sound-registry'
- @load "QuotedHTMLParser", 'services/quoted-html-parser'
- @load "QuotedPlainTextParser", 'services/quoted-plain-text-parser'
@load "NativeNotifications", 'native-notifications'
+ @load "QuotedHTMLTransformer", 'services/quoted-html-transformer'
+ @load "QuotedPlainTextTransformer", 'services/quoted-plain-text-transformer'
+ @load "SanitizeTransformer", 'services/sanitize-transformer'
+ @load "InlineStyleTransformer", 'services/inline-style-transformer'
+ @requireDeprecated "QuotedHTMLParser", 'services/quoted-html-transformer',
+ instead: 'QuotedHTMLTransformer'
+
# Errors
@get "APIError", -> require('../flux/errors').APIError
@get "OfflineError", -> require('../flux/errors').OfflineError
diff --git a/src/services/inline-style-transformer.coffee b/src/services/inline-style-transformer.coffee
new file mode 100644
index 000000000..9be9566d6
--- /dev/null
+++ b/src/services/inline-style-transformer.coffee
@@ -0,0 +1,46 @@
+{ipcRenderer} = require "electron"
+RegExpUtils = require '../regexp-utils'
+crypto = require 'crypto'
+_ = require 'underscore'
+
+class InlineStyleTransformer
+ constructor: ->
+ @_inlineStylePromises = {}
+ @_inlineStyleResolvers = {}
+ ipcRenderer.on 'inline-styles-result', @_onInlineStylesResult
+
+ run: (html) =>
+ return Promise.resolve(html) unless html and _.isString(html) and html.length > 0
+ return Promise.resolve(html) unless RegExpUtils.looseStyleTag().test(html)
+
+ key = crypto.createHash('md5').update(html).digest('hex')
+
+ # http://stackoverflow.com/questions/8695031/why-is-there-often-a-inside-the-style-tag
+ # https://regex101.com/r/bZ5tX4/1
+ html = html.replace /
+ @_inlineStyleResolvers[key] = resolve
+ ipcRenderer.send('inline-style-parse', {html, key})
+ return @_inlineStylePromises[key]
+
+ # This will prepend the user agent stylesheet so we can apply it to the
+ # styles properly.
+ _injectUserAgentStyles: (body) ->
+ # No DOM parsing! Just find the first #{body[i..-1]}"
+
+ _onInlineStylesResult: (event, {html, key}) =>
+ delete @_inlineStylePromises[key]
+ @_inlineStyleResolvers[key](html)
+ delete @_inlineStyleResolvers[key]
+ return
+
+module.exports = new InlineStyleTransformer()
diff --git a/src/services/quoted-html-parser.coffee b/src/services/quoted-html-transformer.coffee
similarity index 99%
rename from src/services/quoted-html-parser.coffee
rename to src/services/quoted-html-transformer.coffee
index eb5354fab..e144d397c 100644
--- a/src/services/quoted-html-parser.coffee
+++ b/src/services/quoted-html-transformer.coffee
@@ -2,7 +2,7 @@ _ = require 'underscore'
crypto = require 'crypto'
DOMUtils = require '../dom-utils'
-class QuotedHTMLParser
+class QuotedHTMLTransformer
annotationClass: "nylas-quoted-text-segment"
@@ -198,4 +198,4 @@ class QuotedHTMLParser
return Array::slice.call(doc.querySelectorAll('blockquote'))
-module.exports = new QuotedHTMLParser
+module.exports = new QuotedHTMLTransformer
diff --git a/src/services/quoted-plain-text-parser.coffee b/src/services/quoted-plain-text-transformer.coffee
similarity index 99%
rename from src/services/quoted-plain-text-parser.coffee
rename to src/services/quoted-plain-text-transformer.coffee
index 2d436954f..669e4f9d9 100644
--- a/src/services/quoted-plain-text-parser.coffee
+++ b/src/services/quoted-plain-text-transformer.coffee
@@ -6,7 +6,7 @@ _str = require 'underscore.string'
# For plain text emails we look for lines that look like they're quoted
# text based on common conventions:
#
-# For HTML emails use QuotedHTMLParser
+# For HTML emails use QuotedHTMLTransformer
#
# This is modied from https://github.com/mko/emailreplyparser, which is a
# JS port of GitHub's Ruby https://github.com/github/email_reply_parser
diff --git a/src/services/sanitize-transformer.coffee b/src/services/sanitize-transformer.coffee
new file mode 100644
index 000000000..4bac25a61
--- /dev/null
+++ b/src/services/sanitize-transformer.coffee
@@ -0,0 +1,42 @@
+sanitizeHtml = require 'sanitize-html'
+
+Preset =
+ Strict:
+ allowedTags: ['p', 'b', 'i', 'em', 'strong', 'a', 'br', 'img', 'ul', 'ol', 'li', 'strike', 'table']
+ allowedAttributes:
+ a: ['href', 'name']
+ img: ['src', 'alt']
+ transformTags:
+ h1: "p"
+ h2: "p"
+ h3: "p"
+ h4: "p"
+ h5: "p"
+ h6: "p"
+ div: "p"
+ pre: "p"
+ blockquote: "p"
+
+ Permissive:
+ allowedTags: ['p', 'b', 'i', 'em', 'strong', 'a', 'br', 'img', 'ul', 'ol', 'li', 'strike', 'table', 'tr', 'td', 'th', 'col', 'colgroup']
+ allowedAttributes: [ 'abbr', 'accept', 'acceptcharset', 'accesskey', 'action', 'align', 'alt', 'async', 'autocomplete', 'axis', 'border', 'bgcolor', 'cellpadding', 'cellspacing', 'char', 'charoff', 'charset', 'checked', 'classid', 'classname', 'colspan', 'cols', 'content', 'contextmenu', 'controls', 'coords', 'data', 'datetime', 'defer', 'dir', 'disabled', 'download', 'draggable', 'enctype', 'form', 'formaction', 'formenctype', 'formmethod', 'formnovalidate', 'formtarget', 'frame', 'frameborder', 'headers', 'height', 'hidden', 'high', 'href', 'hreflang', 'htmlfor', 'httpequiv', 'icon', 'id', 'label', 'lang', 'list', 'loop', 'low', 'manifest', 'marginheight', 'marginwidth', 'max', 'maxlength', 'media', 'mediagroup', 'method', 'min', 'multiple', 'muted', 'name', 'novalidate', 'nowrap', 'open', 'optimum', 'pattern', 'placeholder', 'poster', 'preload', 'radiogroup', 'readonly', 'rel', 'required', 'role', 'rowspan', 'rows', 'rules', 'sandbox', 'scope', 'scoped', 'scrolling', 'seamless', 'selected', 'shape', 'size', 'sizes', 'sortable', 'sorted', 'span', 'spellcheck', 'src', 'srcdoc', 'srcset', 'start', 'step', 'style', 'summary', 'tabindex', 'target', 'title', 'translate', 'type', 'usemap', 'valign', 'value', 'width', 'wmode' ]
+
+ UnsafeOnly:
+ allowedTags: ["a", "abbr", "address", "area", "article", "aside", "audio", "b", "bdi", "bdo", "big", "blockquote", "body", "br", "button", "canvas", "caption", "cite", "code", "col", "colgroup", "data", "datalist", "dd", "del", "details", "dfn", "dialog", "div", "dl", "dt", "em", "fieldset", "figcaption", "figure", "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hr", "i", "img", "input", "ins", "kbd", "keygen", "label", "legend", "li", "main", "map", "mark", "menu", "menuitem", "meta", "meter", "nav", "object", "ol", "optgroup", "option", "output", "p", "param", "picture", "pre", "progress", "q", "rp", "rt", "ruby", "s", "samp", "section", "select", "small", "source", "span", "strong", "sub", "summary", "sup", "table", "tbody", "td", "textarea", "tfoot", "th", "thead", "time", "title", "tr", "track", "u", "ul", "var", "video", "wbr"]
+ allowedAttributes: [ 'abbr', 'accept', 'acceptcharset', 'accesskey', 'action', 'align', 'alt', 'async', 'autocomplete', 'axis', 'border', 'bgcolor', 'cellpadding', 'cellspacing', 'char', 'charoff', 'charset', 'checked', 'classid', 'classname', 'colspan', 'cols', 'content', 'contextmenu', 'controls', 'coords', 'data', 'datetime', 'defer', 'dir', 'disabled', 'download', 'draggable', 'enctype', 'form', 'formaction', 'formenctype', 'formmethod', 'formnovalidate', 'formtarget', 'frame', 'frameborder', 'headers', 'height', 'hidden', 'high', 'href', 'hreflang', 'htmlfor', 'httpequiv', 'icon', 'id', 'label', 'lang', 'list', 'loop', 'low', 'manifest', 'marginheight', 'marginwidth', 'max', 'maxlength', 'media', 'mediagroup', 'method', 'min', 'multiple', 'muted', 'name', 'novalidate', 'nowrap', 'open', 'optimum', 'pattern', 'placeholder', 'poster', 'preload', 'radiogroup', 'readonly', 'rel', 'required', 'role', 'rowspan', 'rows', 'rules', 'sandbox', 'scope', 'scoped', 'scrolling', 'seamless', 'selected', 'shape', 'size', 'sizes', 'sortable', 'sorted', 'span', 'spellcheck', 'src', 'srcdoc', 'srcset', 'start', 'step', 'style', 'summary', 'tabindex', 'target', 'title', 'translate', 'type', 'usemap', 'valign', 'value', 'width', 'wmode' ]
+ allowedSchemes: [ 'http', 'https', 'ftp', 'mailto', 'data' ]
+
+
+class SanitizeTransformer
+ Preset: Preset
+
+ run: (body, settings = Preset.Strict) ->
+ if settings.allowedAttributes instanceof Array
+ attrMap = {}
+ for tag in settings.allowedTags
+ attrMap[tag] = settings.allowedAttributes
+ settings.allowedAttributes = attrMap
+
+ return Promise.resolve(sanitizeHtml(body, settings))
+
+module.exports = new SanitizeTransformer()
diff --git a/src/window-event-handler.coffee b/src/window-event-handler.coffee
index ea7b81565..b11906b8e 100644
--- a/src/window-event-handler.coffee
+++ b/src/window-event-handler.coffee
@@ -125,6 +125,7 @@ class WindowEventHandler
bindCommandToAction('core:copy', 'copy')
bindCommandToAction('core:cut', 'cut')
bindCommandToAction('core:paste', 'paste')
+ bindCommandToAction('core:paste-and-match-style', 'pasteAndMatchStyle')
bindCommandToAction('core:undo', 'undo')
bindCommandToAction('core:redo', 'redo')
bindCommandToAction('core:select-all', 'selectAll')