diff --git a/internal_packages/composer/lib/participants-text-field.cjsx b/internal_packages/composer/lib/participants-text-field.cjsx index 8f4059b40..22b5e356c 100644 --- a/internal_packages/composer/lib/participants-text-field.cjsx +++ b/internal_packages/composer/lib/participants-text-field.cjsx @@ -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. diff --git a/spec-nylas/stores/draft-store-spec.coffee b/spec-nylas/stores/draft-store-spec.coffee index 48e3bbb85..2f5628b64 100644 --- a/spec-nylas/stores/draft-store-spec.coffee +++ b/spec-nylas/stores/draft-store-spec.coffee @@ -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 ', + 'mailto:Ben Gotow ,Shawn ?subject=Yes this is really valid' + 'mailto:Ben%20Gotow%20,Shawn%20?subject=Yes%20this%20is%20really%20valid' + 'mailto:Reply ?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) diff --git a/src/flux/models/utils.coffee b/src/flux/models/utils.coffee index 836efc331..81ec5c7a1 100644 --- a/src/flux/models/utils.coffee +++ b/src/flux/models/utils.coffee @@ -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("@")) diff --git a/src/flux/stores/contact-store.coffee b/src/flux/stores/contact-store.coffee index 008648178..96add9ceb 100644 --- a/src/flux/stores/contact-store.coffee +++ b/src/flux/stores/contact-store.coffee @@ -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) diff --git a/src/flux/stores/draft-store.coffee b/src/flux/stores/draft-store.coffee index 34924cbf0..6fd786beb 100644 --- a/src/flux/stores/draft-store.coffee +++ b/src/flux/stores/draft-store.coffee @@ -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) diff --git a/src/window-event-handler.coffee b/src/window-event-handler.coffee index 28468ac5f..c8370a644 100644 --- a/src/window-event-handler.coffee +++ b/src/window-event-handler.coffee @@ -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]')