mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-01-07 08:37:49 +08:00
fix(mailto): Use paste logic to parse fields in mailto links, support poorly encoded URLs
Summary: Fixes T1927 - Allow email addresses to contain `_` - Centralize logic for parsing string into Contact objects into ContactStore - Always decode and then encode mailto links to ensure spaces, special characters are properly encoded and that the URL is valid - Add specs for mailto:// behavior Test Plan: Run wonderful new tests covering mailto:// Reviewers: evan Reviewed By: evan Maniphest Tasks: T1927 Differential Revision: https://phab.nylas.com/D1657
This commit is contained in:
parent
9b6117afdb
commit
0e96dd3372
6 changed files with 134 additions and 46 deletions
|
@ -1,9 +1,7 @@
|
|||
React = require 'react'
|
||||
_ = require 'underscore'
|
||||
|
||||
{Contact,
|
||||
Utils,
|
||||
ContactStore} = require 'nylas-exports'
|
||||
{Contact, ContactStore} = require 'nylas-exports'
|
||||
{TokenizingTextField, Menu} = require 'nylas-component-kit'
|
||||
|
||||
class ParticipantsTextField extends React.Component
|
||||
|
@ -85,32 +83,7 @@ class ParticipantsTextField extends React.Component
|
|||
# parentheses, look for a preceding name, if one exists.
|
||||
|
||||
if _.isString(values)
|
||||
detected = []
|
||||
while (match = Utils.emailRegex.exec(values))
|
||||
email = match[0]
|
||||
name = null
|
||||
|
||||
hasLeadingParen = values[match.index-1] in ['(','<']
|
||||
hasTrailingParen = values[match.index+email.length] in [')','>']
|
||||
|
||||
if hasLeadingParen and hasTrailingParen
|
||||
nameStart = 0
|
||||
for char in ['>', ')', ',', '\n', '\r']
|
||||
i = values.lastIndexOf(char, match.index)
|
||||
nameStart = i+1 if i+1 > nameStart
|
||||
name = values.substr(nameStart, match.index - 1 - nameStart).trim()
|
||||
|
||||
if not name or name.length is 0
|
||||
# Look to see if we can find a name for this email address in the ContactStore.
|
||||
# Otherwise, just populate the name with the email address.
|
||||
existing = ContactStore.searchContacts(email, {limit:1})[0]
|
||||
if existing and existing.name
|
||||
name = existing.name
|
||||
else
|
||||
name = email
|
||||
|
||||
detected.push(new Contact({email, name}))
|
||||
values = detected
|
||||
values = ContactStore.parseContactsInString(values)
|
||||
|
||||
# Safety check: remove anything from the incoming values that isn't
|
||||
# a Contact. We should never receive anything else in the values array.
|
||||
|
|
|
@ -9,6 +9,7 @@ TaskQueue = require '../../src/flux/stores/task-queue'
|
|||
SendDraftTask = require '../../src/flux/tasks/send-draft'
|
||||
DestroyDraftTask = require '../../src/flux/tasks/destroy-draft'
|
||||
Actions = require '../../src/flux/actions'
|
||||
Utils = require '../../src/flux/models/utils'
|
||||
_ = require 'underscore'
|
||||
|
||||
fakeThread = null
|
||||
|
@ -600,3 +601,81 @@ describe "DraftStore", ->
|
|||
|
||||
it "Calls cleanup on the session", ->
|
||||
expect(@draftCleanup).toHaveBeenCalled
|
||||
|
||||
describe "mailto handling", ->
|
||||
it "should correctly instantiate drafts for a wide range of mailto URLs", ->
|
||||
received = null
|
||||
spyOn(DatabaseStore, 'persistModel').andCallFake (draft) ->
|
||||
received = draft
|
||||
Promise.resolve()
|
||||
|
||||
links = [
|
||||
'mailto:'
|
||||
'mailto://bengotow@gmail.com'
|
||||
'mailto:bengotow@gmail.com'
|
||||
'mailto:?subject=%1z2a', # fails uriDecode
|
||||
'mailto:?subject=%52z2a', # passes uriDecode
|
||||
'mailto:?subject=Martha Stewart',
|
||||
'mailto:?subject=Martha Stewart&cc=cc@nylas.com',
|
||||
'mailto:bengotow@gmail.com?subject=Martha%20Stewart&cc=cc@nylas.com&bcc=bcc@nylas.com',
|
||||
'mailto:bengotow@gmail.com?subject=Martha%20Stewart&cc=cc@nylas.com&bcc=Ben <bcc@nylas.com>',
|
||||
'mailto:Ben Gotow <bengotow@gmail.com>,Shawn <shawn@nylas.com>?subject=Yes this is really valid'
|
||||
'mailto:Ben%20Gotow%20<bengotow@gmail.com>,Shawn%20<shawn@nylas.com>?subject=Yes%20this%20is%20really%20valid'
|
||||
'mailto:Reply <d+AORGpRdj0KXKUPBE1LoI0a30F10Ahj3wu3olS-aDk5_7K5Wu6WqqqG8t1HxxhlZ4KEEw3WmrSdtobgUq57SkwsYAH6tG57IrNqcQR0K6XaqLM2nGNZ22D2k@docs.google.com>?subject=Nilas%20Message%20to%20Customers',
|
||||
]
|
||||
expected = [
|
||||
new Message(),
|
||||
new Message(
|
||||
to: [new Contact(name: 'bengotow@gmail.com', email: 'bengotow@gmail.com')]
|
||||
),
|
||||
new Message(
|
||||
to: [new Contact(name: 'bengotow@gmail.com', email: 'bengotow@gmail.com')]
|
||||
),
|
||||
new Message(
|
||||
subject: '%1z2a'
|
||||
),
|
||||
new Message(
|
||||
subject: 'Rz2a'
|
||||
),
|
||||
new Message(
|
||||
subject: 'Martha Stewart'
|
||||
),
|
||||
new Message(
|
||||
cc: [new Contact(name: 'cc@nylas.com', email: 'cc@nylas.com')],
|
||||
subject: 'Martha Stewart'
|
||||
),
|
||||
new Message(
|
||||
to: [new Contact(name: 'bengotow@gmail.com', email: 'bengotow@gmail.com')],
|
||||
cc: [new Contact(name: 'cc@nylas.com', email: 'cc@nylas.com')],
|
||||
bcc: [new Contact(name: 'bcc@nylas.com', email: 'bcc@nylas.com')],
|
||||
subject: 'Martha Stewart'
|
||||
),
|
||||
new Message(
|
||||
to: [new Contact(name: 'bengotow@gmail.com', email: 'bengotow@gmail.com')],
|
||||
cc: [new Contact(name: 'cc@nylas.com', email: 'cc@nylas.com')],
|
||||
bcc: [new Contact(name: 'Ben', email: 'bcc@nylas.com')],
|
||||
subject: 'Martha Stewart'
|
||||
),
|
||||
new Message(
|
||||
to: [new Contact(name: 'Ben Gotow', email: 'bengotow@gmail.com'), new Contact(name: 'Shawn', email: 'shawn@nylas.com')],
|
||||
subject: 'Yes this is really valid'
|
||||
),
|
||||
new Message(
|
||||
to: [new Contact(name: 'Ben Gotow', email: 'bengotow@gmail.com'), new Contact(name: 'Shawn', email: 'shawn@nylas.com')],
|
||||
subject: 'Yes this is really valid'
|
||||
),
|
||||
new Message(
|
||||
to: [new Contact(name: 'Reply', email: 'd+AORGpRdj0KXKUPBE1LoI0a30F10Ahj3wu3olS-aDk5_7K5Wu6WqqqG8t1HxxhlZ4KEEw3WmrSdtobgUq57SkwsYAH6tG57IrNqcQR0K6XaqLM2nGNZ22D2k@docs.google.com')],
|
||||
subject: 'Nilas Message to Customers'
|
||||
)
|
||||
]
|
||||
|
||||
for link, idx in links
|
||||
DraftStore._onHandleMailtoLink(link)
|
||||
expectedDraft = expected[idx]
|
||||
expect(received['subject']).toEqual(expectedDraft['subject'])
|
||||
for attr in ['to', 'cc', 'bcc', 'subject']
|
||||
for contact, jdx in received[attr]
|
||||
expectedContact = expectedDraft[attr][jdx]
|
||||
expect(contact.email).toEqual(expectedContact.email)
|
||||
expect(contact.name).toEqual(expectedContact.name)
|
||||
|
|
|
@ -302,7 +302,7 @@ Utils =
|
|||
toMatch = domains[0]
|
||||
return _.every(domains, (domain) -> domain.length > 0 and toMatch is domain)
|
||||
|
||||
emailRegex: /[a-z.A-Z0-9%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}/g
|
||||
emailRegex: /[a-z.A-Z0-9%+_-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}/g
|
||||
|
||||
emailHasCommonDomain: (email="") ->
|
||||
domain = _.last(email.toLowerCase().trim().split("@"))
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
Reflux = require 'reflux'
|
||||
Actions = require '../actions'
|
||||
Contact = require '../models/contact'
|
||||
Utils = require '../models/utils'
|
||||
DatabaseStore = require './database-store'
|
||||
NamespaceStore = require './namespace-store'
|
||||
_ = require 'underscore'
|
||||
|
@ -81,6 +82,34 @@ class ContactStore
|
|||
|
||||
matches
|
||||
|
||||
parseContactsInString: (str) =>
|
||||
detected = []
|
||||
while (match = Utils.emailRegex.exec(str))
|
||||
email = match[0]
|
||||
name = null
|
||||
|
||||
hasLeadingParen = str[match.index-1] in ['(','<']
|
||||
hasTrailingParen = str[match.index+email.length] in [')','>']
|
||||
|
||||
if hasLeadingParen and hasTrailingParen
|
||||
nameStart = 0
|
||||
for char in ['>', ')', ',', '\n', '\r']
|
||||
i = str.lastIndexOf(char, match.index)
|
||||
nameStart = i+1 if i+1 > nameStart
|
||||
name = str.substr(nameStart, match.index - 1 - nameStart).trim()
|
||||
|
||||
if not name or name.length is 0
|
||||
# Look to see if we can find a name for this email address in the ContactStore.
|
||||
# Otherwise, just populate the name with the email address.
|
||||
existing = @searchContacts(email, {limit:1})[0]
|
||||
if existing and existing.name
|
||||
name = existing.name
|
||||
else
|
||||
name = email
|
||||
|
||||
detected.push(new Contact({email, name}))
|
||||
detected
|
||||
|
||||
__refreshCache: =>
|
||||
new Promise (resolve, reject) =>
|
||||
DatabaseStore.findAll(Contact)
|
||||
|
|
|
@ -352,11 +352,13 @@ class DraftStore
|
|||
namespace = NamespaceStore.current()
|
||||
return unless namespace
|
||||
|
||||
url = require 'url'
|
||||
qs = require 'querystring'
|
||||
parts = url.parse(urlString)
|
||||
query = qs.parse(parts.query)
|
||||
query.to = "#{parts.auth}@#{parts.host}"
|
||||
try
|
||||
urlString = decodeURI(urlString)
|
||||
|
||||
[whole, to, query] = /mailto:[//]?([^\?]*)[\?]?(.*)/.exec(urlString)
|
||||
|
||||
query = require('querystring').parse(query)
|
||||
query.to = to
|
||||
|
||||
draft = new Message
|
||||
body: query.body || ''
|
||||
|
@ -367,13 +369,9 @@ class DraftStore
|
|||
pristine: true
|
||||
namespaceId: namespace.id
|
||||
|
||||
contactForEmail = (email) ->
|
||||
match = ContactStore.searchContacts(email, 1)
|
||||
return match[0] if match[0]
|
||||
return new Contact({email})
|
||||
|
||||
for attr in ['to', 'cc', 'bcc']
|
||||
draft[attr] = query[attr]?.split(',').map(contactForEmail) || []
|
||||
if query[attr]
|
||||
draft[attr] = ContactStore.parseContactsInString(query[attr])
|
||||
|
||||
DatabaseStore.persistModel(draft).then =>
|
||||
DatabaseStore.localIdForModel(draft).then(@_onPopoutDraftLocalId)
|
||||
|
|
|
@ -130,11 +130,20 @@ class WindowEventHandler
|
|||
openLink: ({href, target, currentTarget}) ->
|
||||
if not href
|
||||
href = target?.getAttribute('href') or currentTarget?.getAttribute('href')
|
||||
if href?
|
||||
schema = url.parse(href).protocol
|
||||
if schema? and schema in ['http:', 'https:', 'mailto:', 'tel:']
|
||||
shell.openExternal(href)
|
||||
false
|
||||
|
||||
return unless href
|
||||
schema = url.parse(href).protocol
|
||||
return unless schema
|
||||
|
||||
if schema is 'mailto:'
|
||||
# We sometimes get mailto URIs that are not escaped properly, or have been only partially escaped.
|
||||
# (T1927) Be sure to escape them once, and completely, before we try to open them. This logic
|
||||
# *might* apply to http/https as well but it's unclear.
|
||||
shell.openExternal(encodeURI(decodeURI(href)))
|
||||
else if schema in ['http:', 'https:', 'tel:']
|
||||
shell.openExternal(href)
|
||||
|
||||
return
|
||||
|
||||
eachTabIndexedElement: (callback) ->
|
||||
for element in $('[tabindex]')
|
||||
|
|
Loading…
Reference in a new issue