mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-01-01 13:14:16 +08:00
rm(autolinker): Use our own very simple autolinker
Summary: Autolinker is a great open source project but it attempts to parse HTML with regexp, is quite slow, and hangs on specific emails https://github.com/nylas/N1/issues/1540 This is super bad, and also super unnecessary. I think this should do the trick. Note: I changed the urlRegex in our Utils to be much more liberal. It now matches anything that looks like a URL, not just things with the http:// and https:// prefixes. It's used in the LinkEditor and onboarding screen (detecting auth errors with urls) and I think it should be ok? Test Plan: Need to write some tests Reviewers: evan, juan Reviewed By: juan Differential Revision: https://phab.nylas.com/D2725
This commit is contained in:
parent
1e765a6b62
commit
c76582194a
16 changed files with 2853 additions and 160 deletions
|
@ -13,6 +13,7 @@
|
|||
"rules": {
|
||||
"react/prop-types": [2, {"ignore": ["children"]}],
|
||||
"react/no-multi-comp": [0],
|
||||
"react/sort-comp": [2],
|
||||
"eqeqeq": [2, "smart"],
|
||||
"id-length": [0],
|
||||
"object-curly-spacing": [0],
|
||||
|
|
54
internal_packages/message-list/lib/autolinker.es6
Normal file
54
internal_packages/message-list/lib/autolinker.es6
Normal file
|
@ -0,0 +1,54 @@
|
|||
import {RegExpUtils, DOMUtils} from 'nylas-exports';
|
||||
|
||||
function _runOnTextNode(node, matchers) {
|
||||
if (node.parentElement) {
|
||||
const withinScript = node.parentElement.tagName === "SCRIPT";
|
||||
const withinStyle = node.parentElement.tagName === "STYLE";
|
||||
const withinA = (node.parentElement.closest('a') !== null);
|
||||
if (withinScript || withinA || withinStyle) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (node.textContent.trim().length < 4) {
|
||||
return;
|
||||
}
|
||||
for (const [prefix, regex] of matchers) {
|
||||
regex.lastIndex = 0;
|
||||
const match = regex.exec(node.textContent);
|
||||
if (match !== null) {
|
||||
const href = `${prefix}${match[0]}`;
|
||||
const range = document.createRange();
|
||||
range.setStart(node, match.index);
|
||||
range.setEnd(node, match.index + match[0].length);
|
||||
const aTag = DOMUtils.wrap(range, 'A');
|
||||
aTag.href = href;
|
||||
aTag.title = href;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function autolink(doc) {
|
||||
// Traverse the new DOM tree and make things that look like links clickable,
|
||||
// and ensure anything with an href has a title attribute.
|
||||
const textWalker = document.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT);
|
||||
const matchers = [
|
||||
['mailto:', RegExpUtils.emailRegex()],
|
||||
['tel:', RegExpUtils.phoneRegex()],
|
||||
['', RegExpUtils.urlRegex({matchEntireString: false})],
|
||||
];
|
||||
|
||||
while (textWalker.nextNode()) {
|
||||
_runOnTextNode(textWalker.currentNode, matchers);
|
||||
}
|
||||
|
||||
// Traverse the new DOM tree and make sure everything with an href has a title.
|
||||
const aTagWalker = document.createTreeWalker(doc.body, NodeFilter.SHOW_ELEMENT, {
|
||||
acceptNode: (node) =>
|
||||
(node.href && !node.title) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP
|
||||
,
|
||||
});
|
||||
while (aTagWalker.nextNode()) {
|
||||
aTagWalker.currentNode.title = aTagWalker.currentNode.href;
|
||||
}
|
||||
}
|
|
@ -1,89 +0,0 @@
|
|||
React = require 'react'
|
||||
_ = require "underscore"
|
||||
{EventedIFrame} = require 'nylas-component-kit'
|
||||
{Utils, QuotedHTMLTransformer} = require 'nylas-exports'
|
||||
|
||||
EmailFrameStylesStore = require './email-frame-styles-store'
|
||||
|
||||
class EmailFrame extends React.Component
|
||||
@displayName = 'EmailFrame'
|
||||
|
||||
@propTypes:
|
||||
content: React.PropTypes.string.isRequired
|
||||
|
||||
render: =>
|
||||
<EventedIFrame ref="iframe" seamless="seamless" searchable={true}
|
||||
onResize={@_setFrameHeight}/>
|
||||
|
||||
componentDidMount: =>
|
||||
@_mounted = true
|
||||
@_writeContent()
|
||||
@_unlisten = EmailFrameStylesStore.listen(@_writeContent)
|
||||
|
||||
componentWillUnmount: =>
|
||||
@_mounted = false
|
||||
@_unlisten?()
|
||||
|
||||
componentDidUpdate: =>
|
||||
@_writeContent()
|
||||
|
||||
shouldComponentUpdate: (nextProps, nextState) =>
|
||||
not Utils.isEqualReact(nextProps, @props) or
|
||||
not Utils.isEqualReact(nextState, @state)
|
||||
|
||||
_writeContent: =>
|
||||
@_lastComputedHeight = 0
|
||||
domNode = React.findDOMNode(@)
|
||||
doc = domNode.contentDocument
|
||||
return unless doc
|
||||
|
||||
doc.open()
|
||||
|
||||
# NOTE: The iframe must have a modern DOCTYPE. The lack of this line
|
||||
# will cause some bizzare non-standards compliant rendering with the
|
||||
# message bodies. This is particularly felt with <table> elements use
|
||||
# the `border-collapse: collapse` css property while setting a
|
||||
# `padding`.
|
||||
doc.write("<!DOCTYPE html>")
|
||||
styles = EmailFrameStylesStore.styles()
|
||||
if (styles)
|
||||
doc.write("<style>#{styles}</style>")
|
||||
doc.write("<div id='inbox-html-wrapper'>#{@_emailContent()}</div>")
|
||||
doc.close()
|
||||
|
||||
# Notify the EventedIFrame that we've replaced it's document (with `open`)
|
||||
# so it can attach event listeners again.
|
||||
@refs.iframe.documentWasReplaced()
|
||||
domNode.height = '0px'
|
||||
@_setFrameHeight()
|
||||
|
||||
_getFrameHeight: (doc) ->
|
||||
return 0 unless doc
|
||||
return doc.body?.scrollHeight ? doc.documentElement?.scrollHeight ? 0
|
||||
|
||||
_setFrameHeight: =>
|
||||
return unless @_mounted
|
||||
|
||||
domNode = React.findDOMNode(@)
|
||||
height = @_getFrameHeight(domNode.contentDocument)
|
||||
|
||||
# Why 5px? Some emails have elements with a height of 100%, and then put
|
||||
# tracking pixels beneath that. In these scenarios, the scrollHeight of the
|
||||
# message is always <100% + 1px>, which leads us to resize them constantly.
|
||||
# This is a hack, but I'm not sure of a better solution.
|
||||
if Math.abs(height - @_lastComputedHeight) > 5
|
||||
domNode.height = "#{height}px"
|
||||
@_lastComputedHeight = height
|
||||
|
||||
unless domNode.contentDocument?.readyState is 'complete'
|
||||
_.defer => @_setFrameHeight()
|
||||
|
||||
_emailContent: =>
|
||||
# When showing quoted text, always return the pure content
|
||||
if @props.showQuotedText
|
||||
@props.content
|
||||
else
|
||||
QuotedHTMLTransformer.removeQuotedHTML(@props.content, keepIfWholeBodyIsQuote: true)
|
||||
|
||||
|
||||
module.exports = EmailFrame
|
119
internal_packages/message-list/lib/email-frame.jsx
Normal file
119
internal_packages/message-list/lib/email-frame.jsx
Normal file
|
@ -0,0 +1,119 @@
|
|||
import React from 'react';
|
||||
import _ from "underscore";
|
||||
import {EventedIFrame} from 'nylas-component-kit';
|
||||
import {Utils, QuotedHTMLTransformer} from 'nylas-exports';
|
||||
import {autolink} from './autolinker';
|
||||
import EmailFrameStylesStore from './email-frame-styles-store';
|
||||
|
||||
export default class EmailFrame extends React.Component {
|
||||
static displayName = 'EmailFrame';
|
||||
|
||||
static propTypes = {
|
||||
content: React.PropTypes.string.isRequired,
|
||||
showQuotedText: React.PropTypes.bool,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this._mounted = true;
|
||||
this._writeContent();
|
||||
this._unlisten = EmailFrameStylesStore.listen(this._writeContent);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return (!Utils.isEqualReact(nextProps, this.props) ||
|
||||
!Utils.isEqualReact(nextState, this.state));
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this._writeContent();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._mounted = false;
|
||||
if (this._unlisten) {
|
||||
this._unlisten();
|
||||
}
|
||||
}
|
||||
|
||||
_emailContent = () => {
|
||||
// When showing quoted text, always return the pure content
|
||||
if (this.props.showQuotedText) {
|
||||
return this.props.content;
|
||||
}
|
||||
return QuotedHTMLTransformer.removeQuotedHTML(this.props.content, {
|
||||
keepIfWholeBodyIsQuote: true,
|
||||
});
|
||||
}
|
||||
|
||||
_writeContent = () => {
|
||||
this._lastComputedHeight = 0;
|
||||
const domNode = React.findDOMNode(this);
|
||||
const doc = domNode.contentDocument;
|
||||
if (!doc) { return; }
|
||||
doc.open();
|
||||
|
||||
// NOTE: The iframe must have a modern DOCTYPE. The lack of this line
|
||||
// will cause some bizzare non-standards compliant rendering with the
|
||||
// message bodies. This is particularly felt with <table> elements use
|
||||
// the `border-collapse: collapse` css property while setting a
|
||||
// `padding`.
|
||||
doc.write("<!DOCTYPE html>");
|
||||
const styles = EmailFrameStylesStore.styles();
|
||||
if (styles) {
|
||||
doc.write(`<style>${styles}</style>`);
|
||||
}
|
||||
doc.write(`<div id='inbox-html-wrapper'>${this._emailContent()}</div>`);
|
||||
doc.close();
|
||||
|
||||
autolink(doc);
|
||||
|
||||
// Notify the EventedIFrame that we've replaced it's document (with `open`)
|
||||
// so it can attach event listeners again.
|
||||
this.refs.iframe.documentWasReplaced();
|
||||
domNode.height = '0px';
|
||||
this._setFrameHeight();
|
||||
}
|
||||
|
||||
_getFrameHeight = (doc) => {
|
||||
if (doc && doc.body) {
|
||||
return doc.body.scrollHeight;
|
||||
}
|
||||
if (doc && doc.documentElement) {
|
||||
return doc.documentElement.scrollHeight;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
_setFrameHeight = () => {
|
||||
if (!this._mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const domNode = React.findDOMNode(this);
|
||||
const height = this._getFrameHeight(domNode.contentDocument);
|
||||
|
||||
// Why 5px? Some emails have elements with a height of 100%, and then put
|
||||
// tracking pixels beneath that. In these scenarios, the scrollHeight of the
|
||||
// message is always <100% + 1px>, which leads us to resize them constantly.
|
||||
// This is a hack, but I'm not sure of a better solution.
|
||||
if (Math.abs(height - this._lastComputedHeight) > 5) {
|
||||
domNode.height = `${height}px`;
|
||||
this._lastComputedHeight = height;
|
||||
}
|
||||
|
||||
if (domNode.contentDocument.readyState !== 'complete') {
|
||||
_.defer(()=> this._setFrameHeight());
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EventedIFrame
|
||||
ref="iframe"
|
||||
seamless="seamless"
|
||||
searchable
|
||||
onResize={this._setFrameHeight}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -13,7 +13,6 @@ ThreadArchiveButton = require './thread-archive-button'
|
|||
ThreadTrashButton = require './thread-trash-button'
|
||||
ThreadToggleUnreadButton = require './thread-toggle-unread-button'
|
||||
|
||||
AutolinkerExtension = require './plugins/autolinker-extension'
|
||||
TrackingPixelsExtension = require './plugins/tracking-pixels-extension'
|
||||
|
||||
module.exports =
|
||||
|
@ -47,7 +46,6 @@ module.exports =
|
|||
ComponentRegistry.register MessageListHiddenMessagesToggle,
|
||||
role: 'MessageListHeaders'
|
||||
|
||||
ExtensionRegistry.MessageView.register AutolinkerExtension
|
||||
ExtensionRegistry.MessageView.register TrackingPixelsExtension
|
||||
|
||||
deactivate: ->
|
||||
|
@ -59,7 +57,6 @@ module.exports =
|
|||
ComponentRegistry.unregister MessageToolbarItems
|
||||
ComponentRegistry.unregister SidebarPluginContainer
|
||||
ComponentRegistry.unregister SidebarParticipantPicker
|
||||
ExtensionRegistry.MessageView.unregister AutolinkerExtension
|
||||
ExtensionRegistry.MessageView.unregister TrackingPixelsExtension
|
||||
|
||||
serialize: -> @state
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
Autolinker = require 'autolinker'
|
||||
{RegExpUtils, MessageViewExtension} = require 'nylas-exports'
|
||||
|
||||
class AutolinkerExtension extends MessageViewExtension
|
||||
|
||||
@formatMessageBody: ({message}) ->
|
||||
# Apply the autolinker pass to make emails and links clickable
|
||||
message.body = Autolinker.link(message.body, {twitter: false})
|
||||
|
||||
# Ensure that the hrefs in the email always have alt text so you can't hide
|
||||
# the target of links
|
||||
# https://regex101.com/r/cH0qM7/1
|
||||
titleRe = -> /title\s.*?=\s.*?['"](.*)['"]/gi
|
||||
message.body = message.body.replace RegExpUtils.linkTagRegex(), (match, openTagPrefix, aTagHref, openTagSuffix, content, closingTag) =>
|
||||
if not content or not closingTag
|
||||
return match
|
||||
|
||||
openTag = openTagPrefix + aTagHref + openTagSuffix
|
||||
|
||||
if titleRe().test(openTag)
|
||||
oldTitle = titleRe().exec(openTag)[1]
|
||||
title = """ title="#{oldTitle} | #{aTagHref}" """
|
||||
openTag = openTag.replace(titleRe(), title)
|
||||
else
|
||||
title = """ title="#{aTagHref}" """
|
||||
tagLen = openTag.length
|
||||
openTag = openTag.slice(0, tagLen - 1) + title + openTag.slice(tagLen - 1, tagLen)
|
||||
|
||||
return openTag + content + closingTag
|
||||
|
||||
module.exports = AutolinkerExtension
|
|
@ -7,8 +7,5 @@
|
|||
"private": true,
|
||||
"engines": {
|
||||
"nylas": "*"
|
||||
},
|
||||
"dependencies": {
|
||||
"autolinker": "0.18.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
Autolinker = require 'autolinker'
|
||||
AutolinkerExtension = require '../lib/plugins/autolinker-extension'
|
||||
|
||||
describe "AutolinkerExtension", ->
|
||||
beforeEach ->
|
||||
spyOn(Autolinker, 'link').andCallFake (txt) => txt
|
||||
|
||||
it "should call through to Autolinker", ->
|
||||
AutolinkerExtension.formatMessageBody(message: {body:'body'})
|
||||
expect(Autolinker.link).toHaveBeenCalledWith('body', {twitter: false})
|
||||
|
||||
it "should add a title to everything with an href", ->
|
||||
message =
|
||||
body: """
|
||||
<a href="apple.com">hello world!</a>
|
||||
<a href = "http://apple.com">hello world!</a>
|
||||
<a href ='http://apple.com'>hello world!</a>
|
||||
<a href ='mailto://'>hello world!</a>
|
||||
"""
|
||||
expected =
|
||||
body: """
|
||||
<a href="apple.com" title="apple.com" >hello world!</a>
|
||||
<a href = "http://apple.com" title="http://apple.com" >hello world!</a>
|
||||
<a href ='http://apple.com' title="http://apple.com" >hello world!</a>
|
||||
<a href ='mailto://' title="mailto://" >hello world!</a>
|
||||
"""
|
||||
AutolinkerExtension.formatMessageBody({message})
|
||||
expect(message.body).toEqual(expected.body)
|
|
@ -0,0 +1,184 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title></title>
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0;" bgcolor="#FFFFFF">
|
||||
<table width="100%" height="100%" style="min-width: 348px;" border=
|
||||
"0" cellspacing="0" cellpadding="0">
|
||||
<tr height="32px">
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr align="center">
|
||||
<td width="32px"></td>
|
||||
<td>
|
||||
<table border="0" cellspacing="0" cellpadding="0" style=
|
||||
"max-width: 600px;">
|
||||
<tr>
|
||||
<td>
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td align="left"><img width="92px" height="32px" src=
|
||||
"cid:google_logo" style="display: block;"></td>
|
||||
<td align="right"><img width="32px" height="32px" style=
|
||||
"display: block;" src="cid:keyhole"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr height="16">
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<table bgcolor="#4184F3" width="100%" border="0" cellspacing="0"
|
||||
cellpadding="0" style=
|
||||
"min-width: 332px; max-width: 600px; border: 1px solid #E0E0E0; border-bottom: 0; border-top-left-radius: 3px; border-top-right-radius: 3px;">
|
||||
<tr>
|
||||
<td height="72px" colspan="3"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="32px"></td>
|
||||
<td style=
|
||||
"font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 24px; color: #FFFFFF; line-height: 1.25;">
|
||||
New sign-in from Chrome on Mac</td>
|
||||
<td width="32px"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td height="18px" colspan="3"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<table bgcolor="#FAFAFA" width="100%" border="0" cellspacing="0"
|
||||
cellpadding="0" style=
|
||||
"min-width: 332px; max-width: 600px; border: 1px solid #F0F0F0; border-bottom: 1px solid #C0C0C0; border-top: 0; border-bottom-left-radius: 3px; border-bottom-right-radius: 3px;">
|
||||
<tr height="16px">
|
||||
<td width="32px" rowspan="3"></td>
|
||||
<td></td>
|
||||
<td width="32px" rowspan="3"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<table style="min-width: 300px;" border="0" cellspacing="0"
|
||||
cellpadding="0">
|
||||
<tr>
|
||||
<td style=
|
||||
"font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 13px; color: #202020; line-height: 1.5;">
|
||||
Hi Ben,</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style=
|
||||
"font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 13px; color: #202020; line-height: 1.5;">
|
||||
Your Google Account careless@foundry376.com was just used to sign
|
||||
in from <span style="white-space:nowrap;">Chrome</span> on
|
||||
<span style="white-space:nowrap;">Mac</span>.
|
||||
<table border="0" cellspacing="0" cellpadding="0" style=
|
||||
"margin-top: 48px; margin-bottom: 48px;">
|
||||
<tr valign="middle">
|
||||
<td width="32px"></td>
|
||||
<td align="center"><img src="cid:profilephoto" width="48px" height=
|
||||
"48px" style="display: block; border-radius: 50%;"></td>
|
||||
<td width="16px"></td>
|
||||
<td style="line-height: 1;"><span style=
|
||||
"font-family: Roboto-Regular,Helvetica,Arial,sans-serif;font-size: 20px; color: #202020;">
|
||||
Ben Gotow (Careless)</span><br>
|
||||
<span style=
|
||||
"font-family: Roboto-Regular,Helvetica,Arial,sans-serif;font-size: 13px; color: #727272;">
|
||||
careless@foundry376.com</span></td>
|
||||
</tr>
|
||||
<tr valign="middle">
|
||||
<td width="32px" height="24px"></td>
|
||||
<td align="center" height="24px"><img src="cid:down_arrow" width=
|
||||
"4px" height="10px" style="display: block;"></td>
|
||||
</tr>
|
||||
<tr valign="top">
|
||||
<td width="32px"></td>
|
||||
<td align="center"><img src="cid:osx" width="48px" height="48px"
|
||||
style="display: block;"></td>
|
||||
<td width="16px"></td>
|
||||
<td style="line-height: 1.5;"><span style=
|
||||
"font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 16px; color: #202020;">
|
||||
Mac</span><br>
|
||||
<span style=
|
||||
"font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 13px; color: #727272;">
|
||||
Monday, July 13, 2015 3:49 PM (Pacific Daylight Time)<br>
|
||||
San Francisco, CA, USA*<br>
|
||||
Chrome</span></td>
|
||||
</tr>
|
||||
</table>
|
||||
<b>Don't recognize this activity?</b><br>
|
||||
Review your <a href=
|
||||
"https://accounts.google.com/AccountChooser?Email=careless@foundry376.com&am%E2%80%A6//security.google.com/settings/security/activity/nt/1436827773000?rfn%3D31"
|
||||
style="text-decoration: none; color: #4285F4;" target=
|
||||
"_blank">recently used devices</a> now.<br>
|
||||
<br>
|
||||
Why are we sending this? We take security very seriously and we
|
||||
want to keep you in the loop on important actions in your
|
||||
account.<br>
|
||||
We were unable to determine whether you have used this browser or
|
||||
device with your account before. This can happen when you sign in
|
||||
for the first time on a new computer, phone or browser, when you
|
||||
use your browser's incognito or private browsing mode or clear your
|
||||
cookies, or when somebody else is accessing your account.</td>
|
||||
</tr>
|
||||
<tr height="32px">
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style=
|
||||
"font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 13px; color: #202020; line-height: 1.5;">
|
||||
Best,<br>
|
||||
The Google Accounts team</td>
|
||||
</tr>
|
||||
<tr height="16px">
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style=
|
||||
"font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 12px; color: #B9B9B9; line-height: 1.5;">
|
||||
*The location is approximate and determined by the IP address it
|
||||
was coming from.<br>
|
||||
This email can't receive replies. To give us feedback on this
|
||||
alert, <a href=
|
||||
"https://support.google.com/accounts/contact/device_alert_feedback?hl=en"
|
||||
style="text-decoration: none; color: #4285F4;" target=
|
||||
"_blank">click here</a>.<br>
|
||||
For more information, visit the <a href=
|
||||
"https://support.google.com/accounts/answer/2733203" style=
|
||||
"text-decoration: none; color: #4285F4;" target="_blank">Google
|
||||
Accounts Help Center</a>.</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr height="32px">
|
||||
<td></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr height="16">
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style=
|
||||
"max-width: 600px; font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 10px; color: #BCBCBC; line-height: 1.5;">
|
||||
You received this mandatory email service announcement to update
|
||||
you about important changes to your Google product or account.<br>
|
||||
<div style="direction: ltr; text-align: left">© 2015 Google Inc.,
|
||||
1600 Amphitheatre Parkway, Mountain View, CA 94043, USA</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
<td width="32px"></td>
|
||||
</tr>
|
||||
<tr height="32px">
|
||||
<td></td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,153 @@
|
|||
|
||||
|
||||
|
||||
<title></title>
|
||||
|
||||
|
||||
<table width="100%" height="100%" style="min-width: 348px;" border="0" cellspacing="0" cellpadding="0">
|
||||
<tbody><tr height="32px">
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr align="center">
|
||||
<td width="32px"></td>
|
||||
<td>
|
||||
<table border="0" cellspacing="0" cellpadding="0" style="max-width: 600px;">
|
||||
<tbody><tr>
|
||||
<td>
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tbody><tr>
|
||||
<td align="left"><img width="92px" height="32px" src="cid:google_logo" style="display: block;"></td>
|
||||
<td align="right"><img width="32px" height="32px" style="display: block;" src="cid:keyhole"></td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr height="16">
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<table bgcolor="#4184F3" width="100%" border="0" cellspacing="0" cellpadding="0" style="min-width: 332px; max-width: 600px; border: 1px solid #E0E0E0; border-bottom: 0; border-top-left-radius: 3px; border-top-right-radius: 3px;">
|
||||
<tbody><tr>
|
||||
<td height="72px" colspan="3"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="32px"></td>
|
||||
<td style="font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 24px; color: #FFFFFF; line-height: 1.25;">
|
||||
New sign-in from Chrome on Mac</td>
|
||||
<td width="32px"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td height="18px" colspan="3"></td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<table bgcolor="#FAFAFA" width="100%" border="0" cellspacing="0" cellpadding="0" style="min-width: 332px; max-width: 600px; border: 1px solid #F0F0F0; border-bottom: 1px solid #C0C0C0; border-top: 0; border-bottom-left-radius: 3px; border-bottom-right-radius: 3px;">
|
||||
<tbody><tr height="16px">
|
||||
<td width="32px" rowspan="3"></td>
|
||||
<td></td>
|
||||
<td width="32px" rowspan="3"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<table style="min-width: 300px;" border="0" cellspacing="0" cellpadding="0">
|
||||
<tbody><tr>
|
||||
<td style="font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 13px; color: #202020; line-height: 1.5;">
|
||||
Hi Ben,</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 13px; color: #202020; line-height: 1.5;">
|
||||
Your Google Account <a href="mailto:careless@foundry376.com" title="mailto:careless@foundry376.com">careless@foundry376.com</a> was just used to sign
|
||||
in from <span style="white-space:nowrap;">Chrome</span> on
|
||||
<span style="white-space:nowrap;">Mac</span>.
|
||||
<table border="0" cellspacing="0" cellpadding="0" style="margin-top: 48px; margin-bottom: 48px;">
|
||||
<tbody><tr valign="middle">
|
||||
<td width="32px"></td>
|
||||
<td align="center"><img src="cid:profilephoto" width="48px" height="48px" style="display: block; border-radius: 50%;"></td>
|
||||
<td width="16px"></td>
|
||||
<td style="line-height: 1;"><span style="font-family: Roboto-Regular,Helvetica,Arial,sans-serif;font-size: 20px; color: #202020;">
|
||||
Ben Gotow (Careless)</span><br>
|
||||
<span style="font-family: Roboto-Regular,Helvetica,Arial,sans-serif;font-size: 13px; color: #727272;">
|
||||
<a href="mailto:careless@foundry376.com" title="mailto:careless@foundry376.com">careless@foundry376.com</a></span></td>
|
||||
</tr>
|
||||
<tr valign="middle">
|
||||
<td width="32px" height="24px"></td>
|
||||
<td align="center" height="24px"><img src="cid:down_arrow" width="4px" height="10px" style="display: block;"></td>
|
||||
</tr>
|
||||
<tr valign="top">
|
||||
<td width="32px"></td>
|
||||
<td align="center"><img src="cid:osx" width="48px" height="48px" style="display: block;"></td>
|
||||
<td width="16px"></td>
|
||||
<td style="line-height: 1.5;"><span style="font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 16px; color: #202020;">
|
||||
Mac</span><br>
|
||||
<span style="font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 13px; color: #727272;">
|
||||
Monday, July 13, 2015 3:49 PM (Pacific Daylight Time)<br>
|
||||
San Francisco, CA, USA*<br>
|
||||
Chrome</span></td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
<b>Don't recognize this activity?</b><br>
|
||||
Review your <a href="https://accounts.google.com/AccountChooser?Email=careless@foundry376.com&am%E2%80%A6//security.google.com/settings/security/activity/nt/1436827773000?rfn%3D31" style="text-decoration: none; color: #4285F4;" target="_blank" title="https://accounts.google.com/AccountChooser?Email=careless@foundry376.com&am%E2%80%A6//security.google.com/settings/security/activity/nt/1436827773000?rfn%3D31">recently used devices</a> now.<br>
|
||||
<br>
|
||||
Why are we sending this? We take security very seriously and we
|
||||
want to keep you in the loop on important actions in your
|
||||
account.<br>
|
||||
We were unable to determine whether you have used this browser or
|
||||
device with your account before. This can happen when you sign in
|
||||
for the first time on a new computer, phone or browser, when you
|
||||
use your browser's incognito or private browsing mode or clear your
|
||||
cookies, or when somebody else is accessing your account.</td>
|
||||
</tr>
|
||||
<tr height="32px">
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 13px; color: #202020; line-height: 1.5;">
|
||||
Best,<br>
|
||||
The Google Accounts team</td>
|
||||
</tr>
|
||||
<tr height="16px">
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 12px; color: #B9B9B9; line-height: 1.5;">
|
||||
*The location is approximate and determined by the IP address it
|
||||
was coming from.<br>
|
||||
This email can't receive replies. To give us feedback on this
|
||||
alert, <a href="https://support.google.com/accounts/contact/device_alert_feedback?hl=en" style="text-decoration: none; color: #4285F4;" target="_blank" title="https://support.google.com/accounts/contact/device_alert_feedback?hl=en">click here</a>.<br>
|
||||
For more information, visit the <a href="https://support.google.com/accounts/answer/2733203" style="text-decoration: none; color: #4285F4;" target="_blank" title="https://support.google.com/accounts/answer/2733203">Google
|
||||
Accounts Help Center</a>.</td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr height="32px">
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr height="16">
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="max-width: 600px; font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 10px; color: #BCBCBC; line-height: 1.5;">
|
||||
You received this mandatory email service announcement to update
|
||||
you about important changes to your Google product or account.<br>
|
||||
<div style="direction: ltr; text-align: left">© 2015 Google Inc.,
|
||||
1600 Amphitheatre Parkway, Mountain View, CA 94043, USA</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
</td>
|
||||
<td width="32px"></td>
|
||||
</tr>
|
||||
<tr height="32px">
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
|
||||
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,26 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title></title>
|
||||
</head>
|
||||
<body>
|
||||
http://apple.com/
|
||||
<div><br></div>
|
||||
<div>https://dropbox.com/</div>
|
||||
<div><br></div>
|
||||
<div>whatever.com</div>
|
||||
<div><br></div>
|
||||
<div>kinda-looks-like-a-link.com</div>
|
||||
<div><br></div>
|
||||
<div>ftp://helloworld.com/asd</div>
|
||||
<div><br></div>
|
||||
<div>540-250-2334</div>
|
||||
<div><br></div>
|
||||
<div>+1-524-123-3333</div>
|
||||
<div><br></div>
|
||||
<div>550.555.1234</div>
|
||||
<div><br></div>
|
||||
<div>bengotow@gmail.com</div>
|
||||
<div><br></div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,28 @@
|
|||
|
||||
|
||||
|
||||
<title></title>
|
||||
|
||||
<a href="
|
||||
http://apple.com/" title="
|
||||
http://apple.com/">
|
||||
http://apple.com/</a>
|
||||
<div><br></div>
|
||||
<div><a href="https://dropbox.com/" title="https://dropbox.com/">https://dropbox.com/</a></div>
|
||||
<div><br></div>
|
||||
<div><a href="whatever.com" title="whatever.com">whatever.com</a></div>
|
||||
<div><br></div>
|
||||
<div><a href="kinda-looks-like-a-link.com" title="kinda-looks-like-a-link.com">kinda-looks-like-a-link.com</a></div>
|
||||
<div><br></div>
|
||||
<div><a href="ftp://helloworld.com/asd" title="ftp://helloworld.com/asd">ftp://helloworld.com/asd</a></div>
|
||||
<div><br></div>
|
||||
<div><a href="tel:540-250-2334" title="tel:540-250-2334">540-250-2334</a></div>
|
||||
<div><br></div>
|
||||
<div><a href="tel:+1-524-123-3333" title="tel:+1-524-123-3333">+1-524-123-3333</a></div>
|
||||
<div><br></div>
|
||||
<div><a href="550.555.1234" title="550.555.1234">550.555.1234</a></div>
|
||||
<div><br></div>
|
||||
<div><a href="mailto:bengotow@gmail.com" title="mailto:bengotow@gmail.com">bengotow@gmail.com</a></div>
|
||||
<div><br></div>
|
||||
|
||||
|
24
internal_packages/message-list/spec/autolinker-spec.es6
Normal file
24
internal_packages/message-list/spec/autolinker-spec.es6
Normal file
|
@ -0,0 +1,24 @@
|
|||
import {autolink} from '../lib/autolinker';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
describe("autolink", () => {
|
||||
const fixturesDir = path.join(__dirname, 'autolinker-fixtures');
|
||||
fs.readdirSync(fixturesDir).filter(filename =>
|
||||
filename.indexOf('-in.html') !== -1
|
||||
).forEach((filename) => {
|
||||
it(`should properly autolink a variety of email bodies ${filename}`, () => {
|
||||
const div = document.createElement('div');
|
||||
const inputPath = path.join(fixturesDir, filename);
|
||||
const expectedPath = inputPath.replace('-in', '-out');
|
||||
|
||||
const input = fs.readFileSync(inputPath).toString();
|
||||
const expected = fs.readFileSync(expectedPath).toString();
|
||||
|
||||
div.innerHTML = input;
|
||||
autolink({body: div});
|
||||
|
||||
expect(div.innerHTML).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -14,6 +14,9 @@ RegExpUtils =
|
|||
# https://en.wikipedia.org/wiki/Email_address#Local_part
|
||||
emailRegex: -> new RegExp(/([a-z.A-Z0-9!#$%&'*+\-/=?^_`{|}~;:]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,63})/g)
|
||||
|
||||
# http://stackoverflow.com/questions/16631571/javascript-regular-expression-detect-all-the-phone-number-from-the-page-source
|
||||
phoneRegex: -> new RegExp(/(?:\+?(\d{1,3}))?[- (]*(\d{3})[- )]*(\d{3})[- ]*(\d{4})(?: *x(\d+))?\b/g)
|
||||
|
||||
# http://stackoverflow.com/a/16463966
|
||||
# http://www.regexpal.com/?fam=93928
|
||||
# NOTE: This does not match full urls with `http` protocol components.
|
||||
|
@ -22,15 +25,12 @@ RegExpUtils =
|
|||
# https://www.safaribooksonline.com/library/view/regular-expressions-cookbook/9780596802837/ch07s16.html
|
||||
ipAddressRegex: -> new RegExp(/^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/i)
|
||||
|
||||
# Test cases: https://regex101.com/r/pD7iS5/2
|
||||
# http://daringfireball.net/2010/07/improved_regex_for_matching_urls
|
||||
# https://mathiasbynens.be/demo/url-regex
|
||||
# This is the Gruber Regex.
|
||||
# Test cases: https://regex101.com/r/pD7iS5/3
|
||||
urlRegex: ({matchEntireString} = {}) ->
|
||||
if matchEntireString
|
||||
new RegExp(/^\b((?:https?:\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))$/)
|
||||
new RegExp(/^((([A-Za-z]{3,9}:(?:\/\/)?)(?:[\-;:&=\+\$,\w]+@)?[A-Za-z0-9\.\-]+|(?:[\w\-]+\.|[\-;:&=\+\$,\w]+@)[A-Za-z0-9\.\-]+)((?:\/[\+~%\/\.\w\-_]*)?\??(?:[\-\+=&;%@\.\w_]*)#?(?:[\.\!\/\\\w]*))?)/g)
|
||||
else
|
||||
new RegExp(/\b((?:https?:\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))$/)
|
||||
new RegExp(/(?:^|\s)((([A-Za-z]{3,9}:(?:\/\/)?)(?:[\-;:&=\+\$,\w]+@)?[A-Za-z0-9\.\-]+|(?:[\w\-]+\.|[\-;:&=\+\$,\w]+@)[A-Za-z0-9\.\-]+)((?:\/[\+~%\/\.\w\-_]*)?\??(?:[\-\+=&;%@\.\w_]*)#?(?:[\.\!\/\\\w]*))?)/g)
|
||||
|
||||
# Test cases: https://regex101.com/r/jD5zC7/2
|
||||
# Returns the following capturing groups:
|
||||
|
|
Loading…
Reference in a new issue